├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── api ├── index.js.md ├── installer.js.md └── prompt.js.md ├── examples └── tabtab-test-complete │ ├── .gitignore │ ├── index.js │ ├── package-lock.json │ ├── package.json │ └── readme.md ├── lib ├── constants.js ├── filename.js ├── index.js ├── installer.js ├── prompt.js ├── templates │ ├── completion.bash │ ├── completion.fish │ ├── completion.ps1 │ └── completion.zsh ├── tsconfig.json └── utils │ ├── exists.js │ ├── index.js │ └── tabtabDebug.js ├── package.json ├── patches └── untildify@4.0.0.patch ├── pnpm-lock.yaml ├── readme.md ├── test ├── fixtures │ └── tabtab-install.js ├── getCompletionScript.js ├── getShellFromEnv.js ├── installer.js ├── isShellSupported.js ├── logCompletion.js ├── parse-env.js ├── tabtab-install.js ├── tsconfig.json └── utils │ └── index.js └── tsconfig.common.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "mklabs", 3 | 4 | "rules": { 5 | "no-param-reassign": "off", 6 | "no-console": "off", 7 | "consistent-return": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | include: 11 | - node: '18' 12 | platform: ubuntu-latest 13 | - node: '20' 14 | platform: ubuntu-latest 15 | - node: '22' 16 | platform: ubuntu-latest 17 | - node: '22' 18 | platform: windows-latest 19 | - node: '22' 20 | platform: macos-latest 21 | 22 | name: '${{matrix.platform}} / Node.js ${{ matrix.node }}' 23 | runs-on: ${{matrix.platform}} 24 | 25 | steps: 26 | - name: Checkout Commit 27 | uses: actions/checkout@v1 28 | - name: Setup Node 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.node }} 32 | - name: install pnpm 33 | run: | 34 | npm install pnpm -g 35 | pnpm --version 36 | - name: pnpm install 37 | run: pnpm install 38 | - name: run tests 39 | run: pnpm test 40 | 41 | typecheck: 42 | name: Type Check 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - name: Checkout Commit 47 | uses: actions/checkout@v1 48 | - name: Setup Node 49 | uses: actions/setup-node@v1 50 | with: 51 | node-version: '20' 52 | - name: install pnpm 53 | run: | 54 | npm install pnpm -g 55 | pnpm --version 56 | - name: pnpm install 57 | run: pnpm install 58 | - name: type check 59 | run: pnpm run typecheck 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .completions/tt 2 | .tern-port 3 | src/ 4 | .completions/ 5 | node_modules/ 6 | .nyc_output/ 7 | 8 | note.txt 9 | quick-test.js 10 | coverage/ 11 | tabtab/ 12 | test/tabtab.log 13 | /types 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | note.txt 3 | .nyc_output/ 4 | quick-test.js 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 9 | 10 | ## [Unreleased](https://github.com/mklabs/tabtab/compare/v3.0.1-beta...HEAD) 11 | 12 | ### Commits 13 | 14 | - feat: add tabtab.uninstall() [`23907cd`](https://github.com/mklabs/tabtab/commit/23907cdfbe16e17aefa171a750550c6fc7af42db) 15 | 16 | ## [v3.0.1-beta](https://github.com/mklabs/tabtab/compare/v3.0.0-beta...v3.0.1-beta) - 2018-10-05 17 | 18 | ### Fixed 19 | 20 | - Fixing check of filename when installing [`#47`](https://github.com/mklabs/tabtab/issues/47) 21 | 22 | ### Commits 23 | 24 | - Begin to write uninstall method [`e143f80`](https://github.com/mklabs/tabtab/commit/e143f80909cfc06ab223c41a41ae4bc7ec1f9e1a) 25 | - npm: remove assert-rejects [`399a3da`](https://github.com/mklabs/tabtab/commit/399a3da9216d565d61c101f3d97f082d83809228) 26 | - example: adding uninstall-completion to example [`c48223a`](https://github.com/mklabs/tabtab/commit/c48223ae3ca204f322b523c14b887689e6ea23ee) 27 | 28 | ## [v3.0.0-beta](https://github.com/mklabs/tabtab/compare/v2.2.2...v3.0.0-beta) - 2018-10-04 29 | 30 | ### Merged 31 | 32 | - installer: ensure successful status/$? [`#38`](https://github.com/mklabs/tabtab/pull/38) 33 | - Remove `npmlog`. [`#31`](https://github.com/mklabs/tabtab/pull/31) 34 | - Fix wrong shell reference [`#35`](https://github.com/mklabs/tabtab/pull/35) 35 | 36 | ### Commits 37 | 38 | - npm: setup nyc [`1f86b2a`](https://github.com/mklabs/tabtab/commit/1f86b2a0a57a136de6299a472336e7d05ecf8004) 39 | - first cleanup [`c89f237`](https://github.com/mklabs/tabtab/commit/c89f237f2a7539a9a205bf0a23cc817c2ea8e4d8) 40 | - setup npm-watch [`a47d73a`](https://github.com/mklabs/tabtab/commit/a47d73ad4be0c215be20389a7c42a975702ccd2c) 41 | - npm: remove reactify, reset babel [`da65059`](https://github.com/mklabs/tabtab/commit/da65059e87bbda4b1feefbb94820e71d373a6d73) 42 | - npm: update dependencies [`dd19437`](https://github.com/mklabs/tabtab/commit/dd1943774f36b8d9910f9bef4b161beff6a44f43) 43 | - eslint: setup eslint and config [`64f3364`](https://github.com/mklabs/tabtab/commit/64f33648ac9131b26ff3d404501ff0b1807984b9) 44 | - npm: update dependencies [`63cd043`](https://github.com/mklabs/tabtab/commit/63cd043b02797ace9022276917f9dc63587c3831) 45 | - ci: setup nyc and coverage scripts [`e6fe55a`](https://github.com/mklabs/tabtab/commit/e6fe55ab9a7438341cd074262c1732bce0bcc9d7) 46 | - feat: tabtab.install() with prompt (wip) [`f599724`](https://github.com/mklabs/tabtab/commit/f5997246c19e042e9d30654662df53a73882df0d) 47 | - ci: disable nyc for now [`c1e1806`](https://github.com/mklabs/tabtab/commit/c1e180654579890d09e5352522526af4b0992492) 48 | - log: better handling of descriptions [`12d5897`](https://github.com/mklabs/tabtab/commit/12d5897e38958a64f4c05b0ffa4a36210b4efdc8) 49 | - es6: replace vars by const/let [`71ad75a`](https://github.com/mklabs/tabtab/commit/71ad75a882e7fd4ee5bc834fe809192ce0ecfa22) 50 | - feat: avoid adding multiple lines to SHELL scripts [`036d9c0`](https://github.com/mklabs/tabtab/commit/036d9c036b77c0b47469ae38960bfe77401c04de) 51 | - feat: add necessary completion lines in shell config [`2030676`](https://github.com/mklabs/tabtab/commit/20306764122aa7534e26094adaad8049b38d22f7) 52 | - log: fix TABTAB_DEBUG and use it in tests [`dbd6ffb`](https://github.com/mklabs/tabtab/commit/dbd6ffb4e97a0db6eac40b646b93e567996d0e5d) 53 | - examples: add tabtab-test-complete to serve as completion tests [`c0423dc`](https://github.com/mklabs/tabtab/commit/c0423dca0556e1fe6b724ba9806ff9185c650acc) 54 | - cli: test out completions [`c861535`](https://github.com/mklabs/tabtab/commit/c8615352664e19b0f8966cfcd38ee844927f1d51) 55 | - eslint: fix all eslint errros and update config [`bbf2a5b`](https://github.com/mklabs/tabtab/commit/bbf2a5bbdfc2ff317debe93485478f86867098a1) 56 | - fix: ensure fileformat is unix and remove code related to bash-completion [`9b83d76`](https://github.com/mklabs/tabtab/commit/9b83d765c8cb10102bac19113786c0cb9fca2751) 57 | - npm: setup prettier [`a2cd163`](https://github.com/mklabs/tabtab/commit/a2cd163ae19fd465816654ac61f071ddf8744e66) 58 | - node: support for version 10, 8 and 7 [`c01b443`](https://github.com/mklabs/tabtab/commit/c01b44301de30f1be0c0ff7322c5b3357dadd8f2) 59 | - example: remove completion script, all in one [`fae0553`](https://github.com/mklabs/tabtab/commit/fae05531d957c454de732f2de1567731e93783bb) 60 | - completion: now emits the whole line (breaking change) [`75c7b6a`](https://github.com/mklabs/tabtab/commit/75c7b6ac5b96bb85c0ac7047ba6d0a3f82e3e0dd) 61 | - log: slight change to tabtab.log [`1aa7d1a`](https://github.com/mklabs/tabtab/commit/1aa7d1a8146a29f4a0fd454ac6df12ac8f7c8e63) 62 | - eslint: include test dir as well [`345d191`](https://github.com/mklabs/tabtab/commit/345d191a9046a514f52b4af7652abc60dd1477ae) 63 | - babel: remove babel continuation [`6f279be`](https://github.com/mklabs/tabtab/commit/6f279be38b197f3f9e9fcf13feceefb48f9638a9) 64 | - npm: remove prettier / es6-promisify [`b5ab126`](https://github.com/mklabs/tabtab/commit/b5ab126d95d4706a7c4a4da63f031e17b40cfdc8) 65 | - babel: start to remove babel [`7f19043`](https://github.com/mklabs/tabtab/commit/7f19043c2be3461f0d98434a428c46a74a0da186) 66 | - bash: handle semicolon [`51f1de7`](https://github.com/mklabs/tabtab/commit/51f1de7f215dacba5b321daf615ad31d1579eadb) 67 | - fix: a bit more debugging and fix entry point [`4875c90`](https://github.com/mklabs/tabtab/commit/4875c906ea3efa8eb7ccdffcb6a807e0a96f28a7) 68 | - debug: use tabtab debug in all files [`e81aa93`](https://github.com/mklabs/tabtab/commit/e81aa931c8a4996d015de54e287dd3117e2e676b) 69 | - Add .babelrc with preset env [`5f0eaf1`](https://github.com/mklabs/tabtab/commit/5f0eaf1fab2515f26cd9002d8d9552ddbc3843b9) 70 | - complete: always trigger original event based on options.name [`ca0ab39`](https://github.com/mklabs/tabtab/commit/ca0ab3953733e560f4993f26ba2bece54b3516ee) 71 | - debug: dont log into console [`db192d7`](https://github.com/mklabs/tabtab/commit/db192d76419c5773ba899970d0b3c63a1bf413f8) 72 | - parseEnv: change signature to only take environment [`3e60094`](https://github.com/mklabs/tabtab/commit/3e60094f00e6f357da627b7aa13f8dfd055a936d) 73 | - ci: remove npm prune [`7a319e2`](https://github.com/mklabs/tabtab/commit/7a319e22c1f9b83c8e876b21095980b7513ab560) 74 | - debug: change debug for parseEnv, too verbose [`627ef79`](https://github.com/mklabs/tabtab/commit/627ef79636acc116c1da065720b907e5644a3cbc) 75 | - ci(package.json): run nyc on tests [`eff6e95`](https://github.com/mklabs/tabtab/commit/eff6e9552cf75232d04445474f975f5b2a1b0b10) 76 | - mocha: increase timeout to avoid failure on node 6 [`fc97626`](https://github.com/mklabs/tabtab/commit/fc976262a37d6da3ef86eea7ff39883c6c8872fe) 77 | - Do NOT test with cache for the moment [`3cf9115`](https://github.com/mklabs/tabtab/commit/3cf911529cbe32dd32300a008f605b723869fb03) 78 | - ci: forget about node 6 for the moment [`aa7ade3`](https://github.com/mklabs/tabtab/commit/aa7ade38aefd2cd1cf2a06a0544e37d8820d7833) 79 | - commands: remove options.auto mandatory in uninstall [`36dce25`](https://github.com/mklabs/tabtab/commit/36dce25198e582b629a779d8e4e8a0cd37ac7df9) 80 | - uninstall: use default options on uninstall [`7e179b6`](https://github.com/mklabs/tabtab/commit/7e179b6fa3e743e514fcbda9171c059530de9aa6) 81 | - fix: package.json syntax [`0a0238b`](https://github.com/mklabs/tabtab/commit/0a0238bac7d56d0bfbe229a0adcce862c4bb52cf) 82 | - Update package.json [`f3a9580`](https://github.com/mklabs/tabtab/commit/f3a95802b777a5cbc94ea144a5cdee9f168f265c) 83 | - add .gitattributes to force unix line endings [`9a7440e`](https://github.com/mklabs/tabtab/commit/9a7440edeaaef13b04cfba2af67365cf2a90c6bb) 84 | 85 | ## [v2.2.2](https://github.com/mklabs/tabtab/compare/v2.2.1...v2.2.2) - 2017-01-06 86 | 87 | ### Merged 88 | 89 | - fix(win32): fix usage of SHELL environment variable when it is not set [`#30`](https://github.com/mklabs/tabtab/pull/30) 90 | 91 | ## [v2.2.1](https://github.com/mklabs/tabtab/compare/v2.2.0...v2.2.1) - 2016-10-13 92 | 93 | ### Commits 94 | 95 | - fix: create duplicate-free version of completion items accross evt listeners [`dc8b587`](https://github.com/mklabs/tabtab/commit/dc8b58795d57a010800e1f858580217d141aef8e) 96 | 97 | ## [v2.2.0](https://github.com/mklabs/tabtab/compare/v2.1.1...v2.2.0) - 2016-10-11 98 | 99 | ### Commits 100 | 101 | - feat(fish): handle description by adding a tab character between name and description [`9290dcc`](https://github.com/mklabs/tabtab/commit/9290dcc042db1bf960ba0b91dd55696660cb9970) 102 | 103 | ## [v2.1.1](https://github.com/mklabs/tabtab/compare/v2.1.0...v2.1.1) - 2016-10-09 104 | 105 | ### Commits 106 | 107 | - fix(zsh): fix uninstall typo in zshrc (instead of zshhrc) [`3d29317`](https://github.com/mklabs/tabtab/commit/3d293170db57a4c74d8ced7a919e527178bfb2fc) 108 | 109 | ## [v2.1.0](https://github.com/mklabs/tabtab/compare/v2.0.2...v2.1.0) - 2016-10-09 110 | 111 | ### Commits 112 | 113 | - fix(fish): Disable description in fish completion per command / options [`1f04613`](https://github.com/mklabs/tabtab/commit/1f04613f5db70e0aaa265df7f8397fcd7f962a76) 114 | - fix(fish): fix COMP_LINE by appending a space so that prev is correctly positioned [`861f8ef`](https://github.com/mklabs/tabtab/commit/861f8ef2290b2cf61d238a8ff1b648d2712fb359) 115 | - feat(fish): prevent filenames from being completed [`282b941`](https://github.com/mklabs/tabtab/commit/282b94122938c0e26db8a54f30c0e94a1fb2e694) 116 | 117 | ## [v2.0.2](https://github.com/mklabs/tabtab/compare/v2.0.1...v2.0.2) - 2016-10-06 118 | 119 | ### Commits 120 | 121 | - fix: have output done after recv to handle async completion handler [`d8596ed`](https://github.com/mklabs/tabtab/commit/d8596edb4e1c8be8cd66c22c350e87b49f00aa95) 122 | - Remove bake from package.json [`c306bce`](https://github.com/mklabs/tabtab/commit/c306bcefc044e488ba3b99df968b5f291001fb2f) 123 | 124 | ## [v2.0.1](https://github.com/mklabs/tabtab/compare/v2.0.0...v2.0.1) - 2016-10-06 125 | 126 | ### Commits 127 | 128 | - Remove src/ folder and babel compiled files [`8531a62`](https://github.com/mklabs/tabtab/commit/8531a62fa8b35a3ffedcde58f1838420fdbd238a) 129 | - rm Makefile [`d717594`](https://github.com/mklabs/tabtab/commit/d717594ef80d3e74e4b6968ff65b8ffdfbd37ebd) 130 | - fix: have uninstall command working as expected by fixing regexp [`21e2de6`](https://github.com/mklabs/tabtab/commit/21e2de6b95688b72b5cddbf549c119b62770c96a) 131 | 132 | ## [v2.0.0](https://github.com/mklabs/tabtab/compare/v1.4.3...v2.0.0) - 2016-09-30 133 | 134 | ## [v1.4.3](https://github.com/mklabs/tabtab/compare/v1.4.2...v1.4.3) - 2016-09-30 135 | 136 | ### Merged 137 | 138 | - allow installing on a `windows` system when running in a `git bash` [`#27`](https://github.com/mklabs/tabtab/pull/27) 139 | - add $CURSOR for position in zsh.sh script [`#24`](https://github.com/mklabs/tabtab/pull/24) 140 | 141 | ### Fixed 142 | 143 | - add $cursor for position in zsh.sh script [`#23`](https://github.com/mklabs/tabtab/issues/23) 144 | 145 | ### Commits 146 | 147 | - add babel'ed files to `src` folder so that you can directly install it from github [`4cd37ec`](https://github.com/mklabs/tabtab/commit/4cd37ecc2772964a79209500a2a60de03a14b2ec) 148 | - src: update build [`1c5619d`](https://github.com/mklabs/tabtab/commit/1c5619db8348dca61833620ee46c142a880bff36) 149 | 150 | ## [v1.4.2](https://github.com/mklabs/tabtab/compare/v1.4.1...v1.4.2) - 2016-05-21 151 | 152 | ### Commits 153 | 154 | - fix(babel): remove transform-runtime plugin [`845eb54`](https://github.com/mklabs/tabtab/commit/845eb54c2c31ed28a3e0dfd668341831fbb86d5a) 155 | 156 | ## [v1.4.1](https://github.com/mklabs/tabtab/compare/v1.4.0...v1.4.1) - 2016-05-21 157 | 158 | ## [v1.4.0](https://github.com/mklabs/tabtab/compare/v1.3.0...v1.4.0) - 2016-05-21 159 | 160 | ### Fixed 161 | 162 | - feat(description): Handle zsh description using _describe fn [`#19`](https://github.com/mklabs/tabtab/issues/19) 163 | 164 | ### Commits 165 | 166 | - rework cache, fix bash completion handling [`b7cecf7`](https://github.com/mklabs/tabtab/commit/b7cecf7bd7e7ff8325a3f7643d4faf9fce2d4e77) 167 | - feat(uninstall): Implement uninstall command and --auto flag [`de37993`](https://github.com/mklabs/tabtab/commit/de3799343d5d30835383250887531c29c11f91ae) 168 | - fix(completion): gather results and write only once to STDOUT [`b928bc9`](https://github.com/mklabs/tabtab/commit/b928bc987e2f9116325040deb4dbe65e203868e8) 169 | - Fix zsh template script [`a22e6b0`](https://github.com/mklabs/tabtab/commit/a22e6b049b0ad67874a7b79010c111f0e96a0bec) 170 | - zsh: check for compdef [`f216888`](https://github.com/mklabs/tabtab/commit/f216888de7200ca7155c0de0ae0f238c993a81ed) 171 | - fix: Skip completion install for win32 platform or unknown shell [`c4f6073`](https://github.com/mklabs/tabtab/commit/c4f6073c1a25413eecbc2519712ab20ca05d6fdd) 172 | - babel: add plugin default transform [`1dcc302`](https://github.com/mklabs/tabtab/commit/1dcc3024edc44eb8f5edaa189a24f6c4faec3466) 173 | - fix(bash): Silently fail if pkg-config bash-completion exists with non 0 [`0765749`](https://github.com/mklabs/tabtab/commit/07657490f381d147072064adb64edc30c5541acf) 174 | - feat(debug): automatically JSON.stringify non string objects [`e4423f8`](https://github.com/mklabs/tabtab/commit/e4423f81f85ae74ea348c5f325e4fe7eff3b6cdf) 175 | 176 | ## [v1.3.0](https://github.com/mklabs/tabtab/compare/v1.2.1...v1.3.0) - 2016-05-08 177 | 178 | ### Fixed 179 | 180 | - feat(cache): Implement cache TTL (default: 5 min) [`#20`](https://github.com/mklabs/tabtab/issues/20) 181 | - feat(cache): Add option to enable / disable cache [`#20`](https://github.com/mklabs/tabtab/issues/20) 182 | 183 | ## [v1.2.1](https://github.com/mklabs/tabtab/compare/v1.2.0...v1.2.1) - 2016-05-08 184 | 185 | ## [v1.2.0](https://github.com/mklabs/tabtab/compare/v1.1.1...v1.2.0) - 2016-05-08 186 | 187 | ### Commits 188 | 189 | - feat: implement a basic cache mechanism [`bb4216c`](https://github.com/mklabs/tabtab/commit/bb4216c5b4abe5236ed5c03d96575e8d678c13d0) 190 | - fix: Use Object.assign polyfill to run on older version of node [`157057a`](https://github.com/mklabs/tabtab/commit/157057a0f25bca2a6ab9ee5f3a2b0d6005c1f724) 191 | 192 | ## [v1.1.1](https://github.com/mklabs/tabtab/compare/v1.1.0...v1.1.1) - 2016-05-01 193 | 194 | ### Commits 195 | 196 | - fix: more generic assert on prompt [`bbcd350`](https://github.com/mklabs/tabtab/commit/bbcd350379a1d5bf40fe2381d5fc7544b054d58b) 197 | 198 | ## [v1.1.0](https://github.com/mklabs/tabtab/compare/v1.0.5...v1.1.0) - 2016-05-01 199 | 200 | ### Commits 201 | 202 | - example: have yo-complete based on yeoman-environment and parse-help [`115fdae`](https://github.com/mklabs/tabtab/commit/115fdaec737ea64e3016b113b5b77360c38637fb) 203 | - Add notes on debug and log output [`e42149a`](https://github.com/mklabs/tabtab/commit/e42149a1349331ab6bea22497731b2e8a55e8d37) 204 | - feat(completion): Enhance package.json completion to support last word [`ce794d4`](https://github.com/mklabs/tabtab/commit/ce794d4a3a14e6f7519e027c240b9b7c0f536a96) 205 | - feat(completion): Emit completion events along package.json results [`2ed8ef5`](https://github.com/mklabs/tabtab/commit/2ed8ef560610db0c1da05b14f6a0e79197333858) 206 | - fish - set default description to package name [`9f8e934`](https://github.com/mklabs/tabtab/commit/9f8e9345657fc99655ddef6eef40c16a38c848c8) 207 | - fix(fish): Better handling of description [`779a188`](https://github.com/mklabs/tabtab/commit/779a188d5dbc3dd591938a56e2c9babf28249be3) 208 | 209 | ## [v1.0.5](https://github.com/mklabs/tabtab/compare/v1.0.4...v1.0.5) - 2016-04-30 210 | 211 | ### Commits 212 | 213 | - release: git push tags && npm publish [`29035d8`](https://github.com/mklabs/tabtab/commit/29035d8d5ddc0c57c75b101ce0dd827e543bb1ef) 214 | 215 | ## [v1.0.4](https://github.com/mklabs/tabtab/compare/v1.0.3...v1.0.4) - 2016-04-30 216 | 217 | ### Commits 218 | 219 | - Change standard-version msg [`d1d19f6`](https://github.com/mklabs/tabtab/commit/d1d19f6b15705bbc50a8f953f2e8a0738b2159fa) 220 | 221 | ## [v1.0.3](https://github.com/mklabs/tabtab/compare/v1.0.1...v1.0.3) - 2016-04-30 222 | 223 | ### Commits 224 | 225 | - chore(release): 1.0.3 [`dbbdcac`](https://github.com/mklabs/tabtab/commit/dbbdcac74412ccc5ef00d42dd3544d9b01f5bfcc) 226 | - fix(babel): Add babel as prepublish step [`97fc9ce`](https://github.com/mklabs/tabtab/commit/97fc9ceec13931d8b40aeb15297af9edeb6db6a9) 227 | 228 | ## [v1.0.1](https://github.com/mklabs/tabtab/compare/v1.0.0...v1.0.1) - 2016-04-29 229 | 230 | ### Merged 231 | 232 | - fix: zsh (on osx anyway) seems to require a space before the ]] [`#16`](https://github.com/mklabs/tabtab/pull/16) 233 | 234 | ### Commits 235 | 236 | - examples: add yo-complete example [`1a18381`](https://github.com/mklabs/tabtab/commit/1a1838146065230a488e2e1de3deedef224f448b) 237 | - fix: fix fish shell script to properly escape variables [`6f9664e`](https://github.com/mklabs/tabtab/commit/6f9664e49cd4626409f87c009103f9bc23ae5c70) 238 | - bash: apply same spacing before closing ] [`50f0340`](https://github.com/mklabs/tabtab/commit/50f034057d14cc5535804afc9d968cc813490087) 239 | - zsh (on osx anyway seems to require a space before the ]] [`1f9f983`](https://github.com/mklabs/tabtab/commit/1f9f983194a664cb385bbd1b0ba55b833b7c3249) 240 | 241 | ## [v1.0.0](https://github.com/mklabs/tabtab/compare/v1.0.0-pre...v1.0.0) - 2016-04-26 242 | 243 | ### Commits 244 | 245 | - Check in examples [`82de5ef`](https://github.com/mklabs/tabtab/commit/82de5ef2cd8bab7c2b5ebe30cce90a786e268ccb) 246 | - fix: check in example and fix bower-complete package.json [`c46185f`](https://github.com/mklabs/tabtab/commit/c46185ffd6663dc3fd9508a1c86583e5788f6477) 247 | 248 | ## [v1.0.0-pre](https://github.com/mklabs/tabtab/compare/v0.0.4...v1.0.0-pre) - 2016-04-26 249 | 250 | ### Commits 251 | 252 | - Move old stuff [`a53de4a`](https://github.com/mklabs/tabtab/commit/a53de4a509f653fb361d961ea3fc98e174ddc81c) 253 | - Move old stuff to .old [`94369a0`](https://github.com/mklabs/tabtab/commit/94369a065d98d52ed4b1b4daef52157112084ee8) 254 | - Main API and plumbing system done [`c3cba1d`](https://github.com/mklabs/tabtab/commit/c3cba1d0cccb98717340e1f594c21a09a97747b9) 255 | - Cleanup old dir [`45b09af`](https://github.com/mklabs/tabtab/commit/45b09af17c7897573097bb3de774d731c948f1df) 256 | - Prompt user for completion script installation method [`73f6090`](https://github.com/mklabs/tabtab/commit/73f60907f17dfb2d66d7c26fba8ac9d3f9b7ff88) 257 | - rm old completion file [`cef3c00`](https://github.com/mklabs/tabtab/commit/cef3c003b1ee85ca1cbc991b63587547de4ed3a8) 258 | - Update docs, less verbose debug output [`927e08c`](https://github.com/mklabs/tabtab/commit/927e08c39d0a76191dac37b7433b39da4fa80974) 259 | - Init v1 [`3314024`](https://github.com/mklabs/tabtab/commit/331402462a639f2111c2df3112d6ad3b29a8b5d3) 260 | - TomDocify [`9587418`](https://github.com/mklabs/tabtab/commit/95874188b01421e1850fc344c70a91c50755cb82) 261 | - Init command plumbing system [`0361905`](https://github.com/mklabs/tabtab/commit/0361905bf777b9014003b012387193b8dfdd4d97) 262 | - Implement fish bridge, template system depending on $SHELL [`1823230`](https://github.com/mklabs/tabtab/commit/18232300a0bb80d2c715a9b87f4eab69a239f744) 263 | - Shell adapters, handle bash / zsh / fish [`ab90a1a`](https://github.com/mklabs/tabtab/commit/ab90a1ae2920eb2a15913da85bfb9225194af7aa) 264 | - More docs [`a483822`](https://github.com/mklabs/tabtab/commit/a483822ab72a20dfc1783a432d4490f4cb897b43) 265 | - install - check for existing content before writing [`2250e08`](https://github.com/mklabs/tabtab/commit/2250e0830943d59ea0a3d490aaa49f3454aec088) 266 | - More docs [`731222e`](https://github.com/mklabs/tabtab/commit/731222e68dbe912cfd85366093c16c251f04c875) 267 | - Implement json based completion results [`5421395`](https://github.com/mklabs/tabtab/commit/542139513e576c79f48acfcbc8ffff013dcad148) 268 | - Support completion item description for fish, still need work to do on zsh [`5dfc6f0`](https://github.com/mklabs/tabtab/commit/5dfc6f031ae6f18caa90f3d15e3e9b6346a6704d) 269 | - wip install / uninstall [`16cdf73`](https://github.com/mklabs/tabtab/commit/16cdf7390e42ccab025563df427b4dc03e0dd890) 270 | - Handle permission issue [`c44ef31`](https://github.com/mklabs/tabtab/commit/c44ef315f7c10278bd5bd39bf56fbfff27e48f75) 271 | - Ensure directory exists before writing [`bed76b3`](https://github.com/mklabs/tabtab/commit/bed76b35a0795230b15d6df08396cf2e9f6e0fc0) 272 | - Event chaining, walking up the line untill it find a listener [`3c4241c`](https://github.com/mklabs/tabtab/commit/3c4241c51d68a33b52c287818d98c5d88ff93c91) 273 | - API example [`4c4d86c`](https://github.com/mklabs/tabtab/commit/4c4d86c876bdf47b286d3eaf196a51a4aabe342c) 274 | - docs task [`305b0b4`](https://github.com/mklabs/tabtab/commit/305b0b4f0015a39cbc71843d2207f4b02bd0517a) 275 | - ghpages task [`62c4362`](https://github.com/mklabs/tabtab/commit/62c4362434a76fd1657059ccc2d0c7968ecd858d) 276 | - travis [`5fe6b73`](https://github.com/mklabs/tabtab/commit/5fe6b73e0a970cb31e962f8c1a350ec5e9495095) 277 | - doc -wrong prefix [`2c8b91a`](https://github.com/mklabs/tabtab/commit/2c8b91a817044328c598f0b004d96388369565f4) 278 | - badge version [`751af46`](https://github.com/mklabs/tabtab/commit/751af468b68d9140e3fd1144ec0139427e37c076) 279 | - travis - run mocha with babel-node [`962127c`](https://github.com/mklabs/tabtab/commit/962127c3824615bca48cf008c6d73559c08610a4) 280 | - travis - babel compile before test [`ddac422`](https://github.com/mklabs/tabtab/commit/ddac4220c10f255ba82562f79d78964dbea5162c) 281 | - travis - add babelrc file [`8a2a29b`](https://github.com/mklabs/tabtab/commit/8a2a29b4a2f4a9b0f100ec3d87e4b7a08f943f4b) 282 | - Check in screenshots [`b7e3724`](https://github.com/mklabs/tabtab/commit/b7e37248108f24aba9d9e41becdadc80e1db72c8) 283 | - init completion directory registry [`3f92281`](https://github.com/mklabs/tabtab/commit/3f92281dff40f56364bf3dec070d179452ed1839) 284 | 285 | ## [v0.0.4](https://github.com/mklabs/tabtab/compare/v0.0.3...v0.0.4) - 2015-06-06 286 | 287 | ### Merged 288 | 289 | - Issues with tabtab in zsh. [`#10`](https://github.com/mklabs/tabtab/pull/10) 290 | - Fix typo [`#11`](https://github.com/mklabs/tabtab/pull/11) 291 | 292 | ### Commits 293 | 294 | - Updated the completion script to match current npm output. [`be1c512`](https://github.com/mklabs/tabtab/commit/be1c512fde5d7c64e9725e3cdf89e343ac8945b7) 295 | - Added default filesystem matching. [`f57a254`](https://github.com/mklabs/tabtab/commit/f57a2545ed45b2ceaef74d9f559e5588fce7d585) 296 | - :book: Fix typo [`45c6ead`](https://github.com/mklabs/tabtab/commit/45c6eadc3eeadaea4994a66272210e81ec9e17a6) 297 | - Didn't realize the line had {completer} before. Changing back. [`10f3472`](https://github.com/mklabs/tabtab/commit/10f3472f1886ac3a4a6c9929a3ceefcb6223d242) 298 | - Added back new line. [`c74f7ab`](https://github.com/mklabs/tabtab/commit/c74f7ab23bc37818d997578c7ba607c2f8c00a86) 299 | 300 | ## [v0.0.3](https://github.com/mklabs/tabtab/compare/v0.0.2...v0.0.3) - 2015-01-26 301 | 302 | ### Merged 303 | 304 | - Allow completing long options [`#5`](https://github.com/mklabs/tabtab/pull/5) 305 | - Catching EPIPE error caused by `source` closing file descriptor before reading it [`#4`](https://github.com/mklabs/tabtab/pull/4) 306 | 307 | ### Fixed 308 | 309 | - Fix #3 - Add license info [`#3`](https://github.com/mklabs/tabtab/issues/3) 310 | 311 | ### Commits 312 | 313 | - rm old .pkgrc file [`42bcf50`](https://github.com/mklabs/tabtab/commit/42bcf50dbf2b4d6a6533c08f56534e08f17847f7) 314 | - Catching error caused by `source` closing file argument before reading from it. [`4fca6aa`](https://github.com/mklabs/tabtab/commit/4fca6aaf04b30b04e3c66e46dd87b90c43b49bbc) 315 | - travis - node 0.10 [`e13de5b`](https://github.com/mklabs/tabtab/commit/e13de5b9ab83e480ba1c77a2fa7e9aeb57df3cdb) 316 | 317 | ## [v0.0.2](https://github.com/mklabs/tabtab/compare/v0.0.1...v0.0.2) - 2012-02-08 318 | 319 | ### Commits 320 | 321 | - tidy up the whole mess. remove unused / unnecessary code [`6a1e9c3`](https://github.com/mklabs/tabtab/commit/6a1e9c3879a454b1db4f277e26c1e4555390516a) 322 | - add missing devDependency [`fab4faf`](https://github.com/mklabs/tabtab/commit/fab4faf8115416902c64539472881c18d86d47eb) 323 | - bumping version [`cd56910`](https://github.com/mklabs/tabtab/commit/cd56910a847e3d77a5b3a8ed168ff81659f8bccd) 324 | - correct abbrev with `-` in it [`0b51ad8`](https://github.com/mklabs/tabtab/commit/0b51ad8140f152cdbbd15ed9bdbab46309cb8b82) 325 | 326 | ## v0.0.1 - 2011-11-11 327 | 328 | ### Commits 329 | 330 | - edit package.json [`9be6eba`](https://github.com/mklabs/tabtab/commit/9be6eba26133bed9d21bd0b5329dcc39b00d2449) 331 | - return warn messages as state [`8da7d5b`](https://github.com/mklabs/tabtab/commit/8da7d5bc2dc6cf781e7065790964f259c214db36) 332 | - warn without exiting with error, and ensure numbers on parsed env [`34a2ede`](https://github.com/mklabs/tabtab/commit/34a2ede7ebb5d0f21ad8021c712adaf87dc056a8) 333 | - rm gendoc script [`06d3a7a`](https://github.com/mklabs/tabtab/commit/06d3a7a4772edbc684d4777c88b9b84ef882dd0c) 334 | - add gendoc script [`dbd4739`](https://github.com/mklabs/tabtab/commit/dbd4739c965529be5f71e6fa30b2765e5efc2ea5) 335 | - package.json: specify directories for the docs task [`08a25ef`](https://github.com/mklabs/tabtab/commit/08a25ef1f829612fd8cf96b16e545bad42d82f49) 336 | - add some completion install/uninstall docs [`46d324a`](https://github.com/mklabs/tabtab/commit/46d324a9d72ecb9bcaa42d37d24c006b7b41e189) 337 | - rename to tabtab and edit test assert to use dynamic path [`061a357`](https://github.com/mklabs/tabtab/commit/061a357ae5af36541a52bf205b610aba0700ba01) 338 | - add vows test suite for completion output and install/uninstall cmd [`029de43`](https://github.com/mklabs/tabtab/commit/029de431ac136a92cf8498011c6937e08feb9da0) 339 | - edit docs.js comments and rm lib/cli.js (was empty anyway) [`4abc675`](https://github.com/mklabs/tabtab/commit/4abc675573a6b9107be8eb6caa2636cb400c46aa) 340 | - add pkgrc help command [`fff228f`](https://github.com/mklabs/tabtab/commit/fff228f68060ba567a463c1445c5f31c1654dd3b) 341 | - add install/uninstall helper [`6cfb0ee`](https://github.com/mklabs/tabtab/commit/6cfb0ee6a40684e918463b30b15240c939c132a3) 342 | - some docs, have more to write [`9ccd0d7`](https://github.com/mklabs/tabtab/commit/9ccd0d7841fad68552bee1d638b3fb2a51ac260d) 343 | - add play-complete script, completion from `play help` output [`f8347bb`](https://github.com/mklabs/tabtab/commit/f8347bb7d4d9949b758eb1a0b7b4ebf800f3bd9d) 344 | - Use readline's default filename completion if no matches. [`5ea2d4c`](https://github.com/mklabs/tabtab/commit/5ea2d4cb8a3159551e508906019ef698dcab1469) 345 | - log instruction on examples when not called within completion context [`bfc6ad0`](https://github.com/mklabs/tabtab/commit/bfc6ad064152268c23ec6557073e0ab84894224b) 346 | - parse ``` and ~~~~ special code marker in markdowns [`31ee00f`](https://github.com/mklabs/tabtab/commit/31ee00fad380e363fe9767df2d778326bbf0f846) 347 | - add help module, takes a file input (md, js or cs) and man a generated manpage [`11d5d70`](https://github.com/mklabs/tabtab/commit/11d5d70559205d16fd792f10c644f3c3d91ce779) 348 | - add basic script for vagrant completion [`5a8fd4d`](https://github.com/mklabs/tabtab/commit/5a8fd4dce74ed6275f1a4ecf56e7c0473a88fc31) 349 | - move helper functions to completion module [`5fc9fa0`](https://github.com/mklabs/tabtab/commit/5fc9fa058f37b8b8d5a7028c13ea1ad814c9de5b) 350 | - add cake/rake completion, very similar [`92f125f`](https://github.com/mklabs/tabtab/commit/92f125f4bcc4dc7adc3e245754b656e104355c60) 351 | - add completer options, decouple completed process from completer process [`c864c9d`](https://github.com/mklabs/tabtab/commit/c864c9d66e7900a179f00195f43d9bcb4ccada49) 352 | - completion: add cakefile completion, testing options/tasks completion [`33c272b`](https://github.com/mklabs/tabtab/commit/33c272b5be8641c7be1ffd447aa616dba9e9d00c) 353 | - completion: add optimist completion, have to parse out the help output [`6c1b1bb`](https://github.com/mklabs/tabtab/commit/6c1b1bb49cfc9ad641e350a09b3fcb1fb240a20d) 354 | - completion: add basic abbrev support and test with nopt/commander opt [`a857dd2`](https://github.com/mklabs/tabtab/commit/a857dd28b167d15b2c8ef45baaf3e3d02e23046a) 355 | - played a little with nopt/commander options and basic completion [`c6fa6de`](https://github.com/mklabs/tabtab/commit/c6fa6de2860e050dde8b02e8cff71f17d5f041d4) 356 | - add prev to options parsed from compgen [`cfb2894`](https://github.com/mklabs/tabtab/commit/cfb2894f5c4cd8c0f56dad31e7662fbf6c2bae87) 357 | - add some commander/optimist/nopt examples script [`22e0681`](https://github.com/mklabs/tabtab/commit/22e06814744b52c7d3b4450ea52c1cd5e1ab7f0d) 358 | - completion - install instruction and simple line parsing/callback api [`ce1f1f3`](https://github.com/mklabs/tabtab/commit/ce1f1f3960939b0a50c2806feddf8640893d69cd) 359 | - completion start [`94b103f`](https://github.com/mklabs/tabtab/commit/94b103f086f9d22d4a77a7de450976349a2e2a52) 360 | - initial config work, merge of global/local rc file [`64a0f7a`](https://github.com/mklabs/tabtab/commit/64a0f7a268398ddea17163f9edae4e64cb51fbc6) 361 | - a start [`a46ca29`](https://github.com/mklabs/tabtab/commit/a46ca2996264c6c4b2bf300855bdd11f3f4dadb1) 362 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT 2011-2018 License (MIT) 2 | 3 | Copyright (c) Mickael Daniel 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /api/index.js.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | 3 |
4 |
install(Options)
5 |

Install and enable completion on user system. It'll ask for:

6 | 10 |
11 |
parseEnv()
12 |

Public: Main utility to extract information from command line arguments and 13 | Environment variables, namely COMP args in "plumbing" mode.

14 |

options - The options hash as parsed by minimist, plus an env property 15 | representing user environment (default: { env: process.env }) 16 | :_ - The arguments Array parsed by minimist (positional arguments) 17 | :env - The environment Object that holds COMP args (default: process.env)

18 |

Examples

19 |

const env = tabtab.parseEnv(); 20 | // env: 21 | // complete A Boolean indicating whether we act in "plumbing mode" or not 22 | // words The Number of words in the completed line 23 | // point A Number indicating cursor position 24 | // line The String input line 25 | // partial The String part of line preceding cursor position 26 | // last The last String word of the line 27 | // lastPartial The last word String of partial 28 | // prev The String word preceding last

29 |

Returns the data env object.

30 |
31 |
completionItem(item)
32 |

Helper to normalize String and Objects with { name, description } when logging out.

33 |
34 |
log(Arguments)
35 |

Main logging utility to pass completion items.

36 |

This is simply an helper to log to stdout with each item separated by a new 37 | line.

38 |

Bash needs in addition to filter out the args for the completion to work 39 | (zsh, fish don't need this).

40 |
41 |
42 | 43 | 44 | 45 | ## install(Options) 46 | Install and enable completion on user system. It'll ask for: 47 | 48 | - SHELL (bash, zsh or fish) 49 | - Path to shell script (with sensible defaults) 50 | 51 | **Kind**: global function 52 | 53 | | Param | Type | Description | 54 | | --- | --- | --- | 55 | | Options | Object | to use with namely `name` and `completer` | 56 | 57 | 58 | 59 | ## parseEnv() 60 | Public: Main utility to extract information from command line arguments and 61 | Environment variables, namely COMP args in "plumbing" mode. 62 | 63 | options - The options hash as parsed by minimist, plus an env property 64 | representing user environment (default: { env: process.env }) 65 | :_ - The arguments Array parsed by minimist (positional arguments) 66 | :env - The environment Object that holds COMP args (default: process.env) 67 | 68 | Examples 69 | 70 | const env = tabtab.parseEnv(); 71 | // env: 72 | // complete A Boolean indicating whether we act in "plumbing mode" or not 73 | // words The Number of words in the completed line 74 | // point A Number indicating cursor position 75 | // line The String input line 76 | // partial The String part of line preceding cursor position 77 | // last The last String word of the line 78 | // lastPartial The last word String of partial 79 | // prev The String word preceding last 80 | 81 | Returns the data env object. 82 | 83 | **Kind**: global function 84 | 85 | 86 | ## completionItem(item) 87 | Helper to normalize String and Objects with { name, description } when logging out. 88 | 89 | **Kind**: global function 90 | 91 | | Param | Type | Description | 92 | | --- | --- | --- | 93 | | item | String \| Object | Item to normalize | 94 | 95 | 96 | 97 | ## log(Arguments) 98 | Main logging utility to pass completion items. 99 | 100 | This is simply an helper to log to stdout with each item separated by a new 101 | line. 102 | 103 | Bash needs in addition to filter out the args for the completion to work 104 | (zsh, fish don't need this). 105 | 106 | **Kind**: global function 107 | 108 | | Param | Type | Description | 109 | | --- | --- | --- | 110 | | Arguments | Array | to log, Strings or Objects with name and description property. | 111 | 112 | -------------------------------------------------------------------------------- /api/installer.js.md: -------------------------------------------------------------------------------- 1 | ## Functions 2 | 3 |
4 |
shellExtension()
5 |

Little helper to return the correct file extension based on the SHELL value.

6 |
7 |
scriptFromShell(shell)
8 |

Helper to return the correct script template based on the SHELL provided

9 |
10 |
locationFromShell(shell)String
11 |

Helper to return the expected location for SHELL config file, based on the 12 | provided shell value.

13 |
14 |
sourceLineForShell(scriptname, shell)
15 |

Helper to return the source line to add depending on the SHELL provided or detected.

16 |

If the provided SHELL is not known, it returns the source line for a Bash shell.

17 |
18 |
isInShellConfig(filename)Boolean
19 |

Helper to check if a filename is one of the SHELL config we expect

20 |
21 |
checkFilenameForLine(filename, line)Boolean
22 |

Checks a given file for the existence of a specific line. Used to prevent 23 | adding multiple completion source to SHELL scripts.

24 |
25 |
writeLineToFilename(options)
26 |

Opens a file for modification adding a new source line for the given 27 | SHELL. Used for both SHELL script and tabtab internal one.

28 |
29 |
writeToShellConfig(options)
30 |

Writes to SHELL config file adding a new line, but only one, to the SHELL 31 | config script. This enables tabtab to work for the given SHELL.

32 |
33 |
writeToTabtabScript(options)
34 |

Writes to tabtab internal script that acts as a frontend router for the 35 | completion mechanism, in the internal ~/.config/tabtab directory. Every 36 | completion is added to this file.

37 |
38 |
writeToCompletionScript(options)
39 |

This writes a new completion script in the internal ~/.config/tabtab 40 | directory. Depending on the SHELL used, a different script is created for 41 | the given SHELL.

42 |
43 |
install(options)
44 |

Top level install method. Does three things:

45 | 50 |
51 |
removeLinesFromFilename(filename, name)
52 |

Removes the 3 relevant lines from provided filename, based on the package 53 | name passed in.

54 |
55 |
uninstall(options)
56 |

Here the idea is to uninstall a given package completion from internal 57 | tabtab scripts and / or the SHELL config.

58 |

It also removes the relevant scripts if no more completion are installed on 59 | the system.

60 |
61 |
62 | 63 | 64 | 65 | ## shellExtension() ⇒ 66 | Little helper to return the correct file extension based on the SHELL value. 67 | 68 | **Kind**: global function 69 | **Returns**: The correct file extension for the given SHELL script location 70 | 71 | 72 | ## scriptFromShell(shell) ⇒ 73 | Helper to return the correct script template based on the SHELL provided 74 | 75 | **Kind**: global function 76 | **Returns**: The template script content, defaults to Bash for shell we don't know yet 77 | 78 | | Param | Type | Description | 79 | | --- | --- | --- | 80 | | shell | String | Shell to base the check on, defaults to system shell. | 81 | 82 | 83 | 84 | ## locationFromShell(shell) ⇒ String 85 | Helper to return the expected location for SHELL config file, based on the 86 | provided shell value. 87 | 88 | **Kind**: global function 89 | **Returns**: String - Either ~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish, 90 | untildified. Defaults to ~/.bashrc if provided SHELL is not valid. 91 | 92 | | Param | Type | Description | 93 | | --- | --- | --- | 94 | | shell | String | Shell value to test against | 95 | 96 | 97 | 98 | ## sourceLineForShell(scriptname, shell) 99 | Helper to return the source line to add depending on the SHELL provided or detected. 100 | 101 | If the provided SHELL is not known, it returns the source line for a Bash shell. 102 | 103 | **Kind**: global function 104 | 105 | | Param | Type | Description | 106 | | --- | --- | --- | 107 | | scriptname | String | The script to source | 108 | | shell | String | Shell to base the check on, defaults to system shell. | 109 | 110 | 111 | 112 | ## isInShellConfig(filename) ⇒ Boolean 113 | Helper to check if a filename is one of the SHELL config we expect 114 | 115 | **Kind**: global function 116 | **Returns**: Boolean - Either true or false 117 | 118 | | Param | Type | Description | 119 | | --- | --- | --- | 120 | | filename | String | Filename to check against | 121 | 122 | 123 | 124 | ## checkFilenameForLine(filename, line) ⇒ Boolean 125 | Checks a given file for the existence of a specific line. Used to prevent 126 | adding multiple completion source to SHELL scripts. 127 | 128 | **Kind**: global function 129 | **Returns**: Boolean - true or false, false if the line is not present. 130 | 131 | | Param | Type | Description | 132 | | --- | --- | --- | 133 | | filename | String | The filename to check against | 134 | | line | String | The line to look for | 135 | 136 | 137 | 138 | ## writeLineToFilename(options) 139 | Opens a file for modification adding a new `source` line for the given 140 | SHELL. Used for both SHELL script and tabtab internal one. 141 | 142 | **Kind**: global function 143 | 144 | | Param | Type | Description | 145 | | --- | --- | --- | 146 | | options | Object | Options with - filename: The file to modify - scriptname: The line to add sourcing this file - name: The package being configured | 147 | 148 | 149 | 150 | ## writeToShellConfig(options) 151 | Writes to SHELL config file adding a new line, but only one, to the SHELL 152 | config script. This enables tabtab to work for the given SHELL. 153 | 154 | **Kind**: global function 155 | 156 | | Param | Type | Description | 157 | | --- | --- | --- | 158 | | options | Object | Options object with - location: The SHELL script location (~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish) - name: The package configured for completion | 159 | 160 | 161 | 162 | ## writeToTabtabScript(options) 163 | Writes to tabtab internal script that acts as a frontend router for the 164 | completion mechanism, in the internal ~/.config/tabtab directory. Every 165 | completion is added to this file. 166 | 167 | **Kind**: global function 168 | 169 | | Param | Type | Description | 170 | | --- | --- | --- | 171 | | options | Object | Options object with - name: The package configured for completion | 172 | 173 | 174 | 175 | ## writeToCompletionScript(options) 176 | This writes a new completion script in the internal `~/.config/tabtab` 177 | directory. Depending on the SHELL used, a different script is created for 178 | the given SHELL. 179 | 180 | **Kind**: global function 181 | 182 | | Param | Type | Description | 183 | | --- | --- | --- | 184 | | options | Object | Options object with - name: The package configured for completion - completer: The binary that will act as the completer for `name` program | 185 | 186 | 187 | 188 | ## install(options) 189 | Top level install method. Does three things: 190 | 191 | - Writes to SHELL config file, adding a new line to tabtab internal script. 192 | - Creates or edit tabtab internal script 193 | - Creates the actual completion script for this package. 194 | 195 | **Kind**: global function 196 | 197 | | Param | Type | Description | 198 | | --- | --- | --- | 199 | | options | Object | Options object with - name: The program name to complete - completer: The actual program or binary that will act as the completer for `name` program. Can be the same. - location: The SHELL script config location (~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish) - shell: the target shell language | 200 | 201 | 202 | 203 | ## removeLinesFromFilename(filename, name) 204 | Removes the 3 relevant lines from provided filename, based on the package 205 | name passed in. 206 | 207 | **Kind**: global function 208 | 209 | | Param | Type | Description | 210 | | --- | --- | --- | 211 | | filename | String | The filename to operate on | 212 | | name | String | The package name to look for | 213 | 214 | 215 | 216 | ## uninstall(options) 217 | Here the idea is to uninstall a given package completion from internal 218 | tabtab scripts and / or the SHELL config. 219 | 220 | It also removes the relevant scripts if no more completion are installed on 221 | the system. 222 | 223 | **Kind**: global function 224 | 225 | | Param | Type | Description | 226 | | --- | --- | --- | 227 | | options | Object | Options object with - name: The package name to look for - shell: the target shell language | 228 | 229 | -------------------------------------------------------------------------------- /api/prompt.js.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## prompt() 4 | Asks user about SHELL and desired location. 5 | 6 | It is too difficult to check spawned SHELL, the user has to use chsh before 7 | it is reflected in process.env.SHELL 8 | 9 | **Kind**: global function 10 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const opts = require('minimist')(process.argv.slice(2), { 4 | string: ['foo', 'bar'], 5 | boolean: ['help', 'version', 'loglevel'] 6 | }); 7 | 8 | const tabtab = require('../..'); 9 | 10 | const args = opts._; 11 | 12 | const completion = env => { 13 | const shell = tabtab.getShellFromEnv(env); 14 | 15 | if (!env.complete) return; 16 | 17 | if (env.prev === 'someCommand') { 18 | return tabtab.log(['is', 'this', 'the', 'real', 'life'], shell, console.log); 19 | } 20 | 21 | if (env.prev === 'anotherOne') { 22 | return tabtab.log(['is', 'this', 'just', 'fantasy'], shell, console.log); 23 | } 24 | 25 | if (env.prev === '--loglevel') { 26 | return tabtab.log(['error', 'warn', 'info', 'notice', 'verbose'], shell, console.log); 27 | } 28 | 29 | return tabtab.log([ 30 | '--help', 31 | '--version', 32 | '--loglevel', 33 | 'foo', 34 | 'bar', 35 | 'generate-completion', 36 | 'install-completion', 37 | 'uninstall-completion', 38 | 'completion-server', 39 | 'someCommand:someCommand is a some kind of command with a description', 40 | { 41 | name: 'someOtherCommand:hey', 42 | description: 'You must add a description for items with ":" in them' 43 | }, 44 | 'anotherOne' 45 | ], shell, console.log); 46 | }; 47 | 48 | const init = async () => { 49 | const cmd = args[0]; 50 | 51 | if (opts.help) { 52 | return console.log('Output help here'); 53 | } 54 | 55 | if (opts.version) { 56 | return console.log('Output version here'); 57 | } 58 | 59 | if (opts.loglevel) { 60 | return console.log('Output version here'); 61 | } 62 | 63 | if (cmd === 'foo') { 64 | return console.log('foobar'); 65 | } 66 | 67 | if (cmd === 'bar') { 68 | return console.log('barbar'); 69 | } 70 | 71 | if (cmd === 'someCommand') { 72 | return console.log('is this the real life ?'); 73 | } 74 | 75 | if (cmd === 'anotherOne') { 76 | return console.log('is this just fantasy ?'); 77 | } 78 | 79 | if (cmd === 'generate-completion') { 80 | const shell = args[1]; 81 | if (!shell) { 82 | console.error('shell argument is required'); 83 | return; 84 | } 85 | const completion = await tabtab.getCompletionScript({ 86 | name: 'tabtab-test', 87 | completer: 'tabtab-test', 88 | shell, 89 | }); 90 | console.log(completion); 91 | return; 92 | } 93 | 94 | if (cmd === 'install-completion') { 95 | const shell = args[1]; 96 | if (!tabtab.isShellSupported(shell)) { 97 | throw new Error(`${shell} is not supported`); 98 | } 99 | 100 | // Here we install for the program `tabtab-test` (this file), with 101 | // completer being the same program. Sometimes, you want to complete 102 | // another program that's where the `completer` option might come handy. 103 | await tabtab 104 | .install({ 105 | name: 'tabtab-test', 106 | completer: 'tabtab-test', 107 | shell, 108 | }) 109 | .catch(err => console.error('INSTALL ERROR', err)); 110 | 111 | return; 112 | } 113 | 114 | if (cmd === 'uninstall-completion') { 115 | // Here we uninstall for the program `tabtab-test` (this file). 116 | await tabtab 117 | .uninstall({ 118 | name: 'tabtab-test' 119 | }) 120 | .catch(err => console.error('UNINSTALL ERROR', err)); 121 | 122 | return; 123 | } 124 | 125 | if (cmd === 'completion-server') { 126 | const env = tabtab.parseEnv(process.env); 127 | return completion(env); 128 | } 129 | }; 130 | 131 | init(); 132 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabtab-test-complete", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "debug": { 8 | "version": "4.0.1", 9 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.0.1.tgz", 10 | "integrity": "sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw==", 11 | "requires": { 12 | "ms": "^2.1.1" 13 | } 14 | }, 15 | "minimist": { 16 | "version": "1.2.0", 17 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 18 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 19 | }, 20 | "ms": { 21 | "version": "2.1.1", 22 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 23 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabtab-test-complete", 3 | "version": "1.0.0", 4 | "bin": { 5 | "tabtab-test": "index.js" 6 | }, 7 | "description": "Basic test package for tabtab completion", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "debug": "^4.0.1", 15 | "minimist": "^1.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/tabtab-test-complete/readme.md: -------------------------------------------------------------------------------- 1 | # tabtab-test-complete 2 | 3 | A simple package to test out tabtab against real completions. 4 | 5 | To install, simply run `npm link` 6 | 7 | npm link 8 | 9 | It'll install the following binary system-wide: 10 | 11 | - tabtab-test: The actual binary being completed 12 | 13 | ## Shell notes 14 | 15 | To test against **bash**, make sure to have `$SHELL` set to either `bash` or `/bin/bash` or similar. 16 | 17 | To test against **zsh**, make sure to have zsh installed, and then, if you use bash 18 | as your standard SHELL, type `zsh`. It'll spawn a new zsh session. Within this, 19 | run `SHELL=zsh` to set the environment accordingly so that tabtab understands 20 | the current shell used is actually zsh. 21 | 22 | Similarly, to test against **fish**, make sure to have fish installed, and then 23 | the same steps to reproduce. This time, make sure to type `fish` and run `set 24 | SHELL fish`. This is required for tabtab to understand the shell being used is 25 | actually fish. 26 | 27 | Those steps are not required if testing against your system shell (possibly using `chsh`). 28 | 29 | ## Completion install 30 | 31 | In this example package, simply run: 32 | 33 | tabtab-test install-completion 34 | 35 | You'll need to do this for each and every shell you're testing against. Follow 36 | the `Shell notes` described above for details. 37 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | const COMPLETION_DIR = '~/.config/tabtab'; 2 | 3 | const SUPPORTED_SHELLS = /** @type {const} */ (['bash', 'fish', 'pwsh', 'zsh']); 4 | 5 | /** 6 | * @typedef {typeof SUPPORTED_SHELLS[number]} SupportedShell 7 | */ 8 | 9 | /** @satisfies {Record.} */ 10 | const SHELL_LOCATIONS = /** @type {const} */ ({ 11 | bash: '~/.bashrc', 12 | zsh: '~/.zshrc', 13 | fish: '~/.config/fish/config.fish', 14 | pwsh: '~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1' 15 | }); 16 | 17 | /** @satisfies {Record.} */ 18 | const COMPLETION_FILE_EXT = /** @type {const} */ ({ 19 | bash: 'bash', 20 | fish: 'fish', 21 | pwsh: 'ps1', 22 | zsh: 'zsh', 23 | }); 24 | 25 | module.exports = { 26 | COMPLETION_DIR, 27 | SUPPORTED_SHELLS, 28 | SHELL_LOCATIONS, 29 | COMPLETION_FILE_EXT, 30 | }; 31 | -------------------------------------------------------------------------------- /lib/filename.js: -------------------------------------------------------------------------------- 1 | const { COMPLETION_FILE_EXT } = require('./constants'); 2 | 3 | /** 4 | * Get a template file name for the SHELL provided. 5 | * @param {import('./constants').SupportedShell} shell 6 | * @returns {String} 7 | */ 8 | const templateFileName = shell => { 9 | const ext = COMPLETION_FILE_EXT[shell]; 10 | if (!ext) { 11 | throw new Error(`Unsupported shell: ${shell}`); 12 | } 13 | return `completion.${ext}`; 14 | }; 15 | 16 | /** 17 | * Get a extension for the completion file of the SHELL (without the leading period). 18 | * @param {String} name 19 | * @param {import('./constants').SupportedShell} shell 20 | * @returns {String} 21 | */ 22 | const completionFileName = (name, shell) => { 23 | const ext = COMPLETION_FILE_EXT[shell]; 24 | if (!ext) { 25 | throw new Error(`Unsupported shell: ${shell}`); 26 | } 27 | return `${name}.${ext}`; 28 | }; 29 | 30 | /** 31 | * Get a tabtab file name for the SHELL provided. 32 | * @param {import('./constants').SupportedShell} shell 33 | * @returns {String} 34 | */ 35 | const tabtabFileName = shell => { 36 | const ext = COMPLETION_FILE_EXT[shell]; 37 | if (!ext) { 38 | throw new Error(`Unsupported shell: ${shell}`); 39 | } 40 | return `__tabtab.${ext}`; 41 | }; 42 | 43 | module.exports = { 44 | templateFileName, 45 | completionFileName, 46 | tabtabFileName, 47 | }; 48 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { SUPPORTED_SHELLS, SHELL_LOCATIONS } = require('./constants'); 3 | const prompt = require('./prompt'); 4 | const installer = require('./installer'); 5 | const { tabtabDebug } = require('./utils'); 6 | 7 | /** 8 | * @typedef {import('./constants').SupportedShell} SupportedShell 9 | */ 10 | 11 | // If TABTAB_DEBUG env is set, make it so that debug statements are also log to 12 | // TABTAB_DEBUG file provided. 13 | const debug = tabtabDebug('tabtab'); 14 | 15 | /** 16 | * Check if a shell is supported. 17 | * @param {String} shell - Shell to check. 18 | * @returns {shell is SupportedShell} 19 | */ 20 | const isShellSupported = shell => (/** @type {ReadonlyArray.} */ (SUPPORTED_SHELLS)).includes(shell); 21 | 22 | /** 23 | * This function is to be used inside a completer. 24 | * 25 | * An environment variable named `SHELL` shall be explicitly set 26 | * by the completion script when it invokes the completer. 27 | * 28 | * The value of `SHELL` is expected to be one of the supported shells. 29 | * If this expectation isn't met, it will result in an error. 30 | * 31 | * @example 32 | * const shell = getShellFromEnv(process.env) 33 | * 34 | * @param {Readonly.>} env - Env objects that may contain `SHELL`, usually `process.env`. 35 | * @returns {SupportedShell} 36 | */ 37 | const getShellFromEnv = env => { 38 | if (!env.SHELL) { 39 | throw new TypeError('SHELL cannot be empty'); 40 | } 41 | // some shell env (such as mingw64) would change SHELL into an absolute path even if it was manually set to just a name 42 | const shell = path.basename(env.SHELL) 43 | if (!isShellSupported(shell)) { 44 | const supportedValues = SUPPORTED_SHELLS.map(x => `'${x}'`).join(', '); 45 | throw new TypeError(`SHELL was set to an invalid value (${env.SHELL}). Supported values are: ${supportedValues}`); 46 | } 47 | return shell; 48 | } 49 | 50 | /** 51 | * Construct a completion script. 52 | * @param {Object} options - Options object. 53 | * @param {String} options.name - The package configured for completion 54 | * @param {String} options.completer - The program the will act as the completer for the `name` program 55 | * @param {SupportedShell} options.shell 56 | * @returns {Promise.} 57 | */ 58 | const getCompletionScript = async ({ name, completer, shell }) => { 59 | if (!name) throw new TypeError('options.name is required'); 60 | if (!completer) throw new TypeError('options.completer is required'); 61 | if (!shell) throw new TypeError('options.shell is required'); 62 | const completionScriptContent = await installer.getCompletionScript({ name, completer, shell }); 63 | return completionScriptContent 64 | } 65 | 66 | /** 67 | * Install and enable completion on user system. 68 | * 69 | * @param {Object} options 70 | * @param {String} options.name - Name of the program whose completion needs to be installed. 71 | * @param {String} options.completer - Name of the program that provides completion service. 72 | * @param {SupportedShell} [options.shell] - Name of the target shell. If not specified, it'll prompt the user. 73 | */ 74 | const install = async (options) => { 75 | const { name, completer } = options; 76 | if (!name) throw new TypeError('options.name is required'); 77 | if (!completer) throw new TypeError('options.completer is required'); 78 | 79 | if (options.shell) { 80 | const location = SHELL_LOCATIONS[options.shell]; 81 | if (!location) { 82 | throw new Error(`Couldn't find shell location for ${options.shell}`); 83 | } 84 | await installer.install({ 85 | name, 86 | completer, 87 | location, 88 | shell: options.shell 89 | }); 90 | return; 91 | } 92 | 93 | const { location, shell } = await prompt(); 94 | 95 | await installer.install({ 96 | name, 97 | completer, 98 | location, 99 | shell 100 | }); 101 | }; 102 | 103 | /** 104 | * Uninstall shell completion for one program from one or all supported shells. 105 | * 106 | * It also removes the relevant scripts if no more completion are installed on 107 | * the system. 108 | * 109 | * @param {Object} options 110 | * @param {String} options.name - Name of the target program. 111 | * @param {SupportedShell} [options.shell] - The target shell language. If not specified, target all supported shells. 112 | */ 113 | const uninstall = async options => { 114 | const { name, shell } = options; 115 | if (!name) throw new TypeError('options.name is required'); 116 | 117 | try { 118 | await installer.uninstall({ name, shell }); 119 | } catch (err) { 120 | console.error('ERROR while uninstalling', err); 121 | } 122 | }; 123 | 124 | /** 125 | * @typedef {Object} ParseEnvResult 126 | * @property {Boolean} complete Whether we act in "plumbing mode" or not 127 | * @property {Number} words Number of words in the completed line 128 | * @property {Number} point Cursor position 129 | * @property {String} line Input line 130 | * @property {String} partial Part of line preceding cursor position 131 | * @property {String} last The last word of the line 132 | * @property {String} lastPartial The last word of partial 133 | * @property {String} prev The word preceding last 134 | */ 135 | 136 | /** 137 | * Main utility to extract information from command line arguments and 138 | * Environment variables, namely COMP args in "plumbing" mode. 139 | * 140 | * @param {Record.} env - The environment Object that holds COMP args (usually `process.env`). 141 | * 142 | * @returns {ParseEnvResult} Extracted information. 143 | */ 144 | const parseEnv = env => { 145 | if (!env) { 146 | throw new Error('parseEnv: You must pass in an environment object.'); 147 | } 148 | 149 | debug( 150 | 'Parsing env. CWORD: %s, COMP_POINT: %s, COMP_LINE: %s', 151 | env.COMP_CWORD, 152 | env.COMP_POINT, 153 | env.COMP_LINE 154 | ); 155 | 156 | let cword = Number(env.COMP_CWORD); 157 | let point = Number(env.COMP_POINT); 158 | const line = env.COMP_LINE || ''; 159 | 160 | if (Number.isNaN(cword)) cword = 0; 161 | if (Number.isNaN(point)) point = 0; 162 | 163 | const partial = line.slice(0, point); 164 | 165 | const parts = line.split(' '); 166 | const prev = parts.slice(0, -1).slice(-1)[0]; 167 | 168 | const last = parts.slice(-1).join(''); 169 | const lastPartial = partial 170 | .split(' ') 171 | .slice(-1) 172 | .join(''); 173 | 174 | let complete = true; 175 | if (!env.COMP_CWORD || !env.COMP_POINT || !env.COMP_LINE) { 176 | complete = false; 177 | } 178 | 179 | return { 180 | complete, 181 | words: cword, 182 | point, 183 | line, 184 | partial, 185 | last, 186 | lastPartial, 187 | prev 188 | }; 189 | }; 190 | 191 | /** 192 | * @typedef {Object} CompletionItem 193 | * @property {String} name 194 | * @property {String} [description] 195 | */ 196 | 197 | /** 198 | * Helper to normalize String and Objects with { name, description } when logging out. 199 | * 200 | * @param {String | CompletionItem} item - Item to normalize 201 | * @param {SupportedShell} shell 202 | * @returns {CompletionItem} normalized items 203 | */ 204 | const completionItem = (item, shell) => { 205 | debug('completion item', item); 206 | 207 | if (typeof item === 'object') return item 208 | 209 | let name = item; 210 | let description = ''; 211 | const matching = /^(.*?)(\\)?:(.*)$/.exec(item); 212 | if (matching) { 213 | [, name, , description] = matching; 214 | } 215 | 216 | if (shell === 'zsh' && /\\/.test(item)) { 217 | name += '\\'; 218 | } 219 | 220 | return { 221 | name, 222 | description 223 | }; 224 | }; 225 | 226 | /** 227 | * Main logging utility to pass completion items. 228 | * 229 | * This is simply an helper to log to stdout with each item separated by a new 230 | * line. 231 | * 232 | * Bash needs in addition to filter out the args for the completion to work 233 | * (zsh, fish don't need this). 234 | * 235 | * @param {Array.} args - to log, Strings or Objects with name and 236 | * description property. 237 | * @param {SupportedShell} shell 238 | * @param {(message: String) => void} logToConsole - Function to actually log to the console, usually `console.log` 239 | */ 240 | const log = (args, shell, logToConsole = console.log) => { 241 | if (!Array.isArray(args)) { 242 | throw new Error('log: Invalid arguments, must be an array'); 243 | } 244 | 245 | // Normalize arguments if there are some Objects { name, description } in them. 246 | let lines = args.map(item => completionItem(item, shell)).map(item => { 247 | const { name: rawName, description: rawDescription } = item; 248 | 249 | const name = shell === 'zsh' ? rawName?.replaceAll(':', '\\:') : rawName; 250 | const description = 251 | shell === 'zsh' ? rawDescription?.replaceAll(':', '\\:') : rawDescription; 252 | let str = name; 253 | 254 | if (shell === 'zsh' && description) { 255 | str = `${name}:${description}`; 256 | } else if ((shell === 'fish' || shell === 'pwsh') && description) { 257 | str = `${name}\t${description}`; 258 | } 259 | 260 | return str; 261 | }); 262 | 263 | if (shell === 'bash') { 264 | const env = parseEnv(process.env); 265 | lines = lines.filter(arg => arg.indexOf(env.last) === 0); 266 | } 267 | 268 | for (const line of lines) { 269 | logToConsole(`${line}`); 270 | } 271 | }; 272 | 273 | /** 274 | * Logging utility to trigger the filesystem autocomplete. 275 | * 276 | * This function just returns a constant string that is then interpreted by the 277 | * completion scripts as an instruction to trigger the built-in filesystem 278 | * completion. 279 | */ 280 | const logFiles = () => { 281 | console.log('__tabtab_complete_files__'); 282 | }; 283 | 284 | module.exports = { 285 | SUPPORTED_SHELLS, 286 | getShellFromEnv, 287 | isShellSupported, 288 | getCompletionScript, 289 | install, 290 | uninstall, 291 | parseEnv, 292 | log, 293 | logFiles 294 | }; 295 | -------------------------------------------------------------------------------- /lib/installer.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const untildify = require('untildify'); 4 | const { promisify } = require('util'); 5 | const { tabtabDebug, exists } = require('./utils'); 6 | const { SUPPORTED_SHELLS } = require('./constants') 7 | 8 | const debug = tabtabDebug('tabtab:installer'); 9 | 10 | const readFile = promisify(fs.readFile); 11 | const writeFile = promisify(fs.writeFile); 12 | const unlink = promisify(fs.unlink); 13 | const mkdir = promisify(fs.mkdir); 14 | 15 | const { 16 | SHELL_LOCATIONS, 17 | COMPLETION_DIR, 18 | } = require('./constants'); 19 | 20 | const { 21 | templateFileName, 22 | completionFileName, 23 | tabtabFileName, 24 | } = require('./filename'); 25 | 26 | /** 27 | * @typedef {import('./constants').SupportedShell} SupportedShell 28 | */ 29 | 30 | /** 31 | * Helper to return the correct script template based on the SHELL provided 32 | * 33 | * @param {SupportedShell} shell - Shell to base the check on, defaults to system shell. 34 | * @returns {String} The template script content, defaults to Bash for shell we don't know yet 35 | */ 36 | const scriptFromShell = shell => path.join(__dirname, 'templates', templateFileName(shell)); 37 | 38 | /** 39 | * Helper to return the expected location for SHELL config file, based on the 40 | * provided shell value. 41 | * 42 | * @param {SupportedShell} shell - Shell value to test against 43 | * @returns {String} Either ~/.bashrc, ~/.zshrc or ~/.config/fish/config.fish, 44 | * untildified. Defaults to ~/.bashrc if provided SHELL is not valid. 45 | */ 46 | const locationFromShell = shell => { 47 | const location = SHELL_LOCATIONS[shell]; 48 | if (!location) { 49 | throw new Error(`Unsupported shell: ${shell}`); 50 | } 51 | return untildify(location); 52 | }; 53 | 54 | /** 55 | * Helper to return the source line to add depending on the SHELL provided or detected. 56 | * 57 | * If the provided SHELL is not known, it returns the source line for a Bash shell. 58 | * 59 | * @param {String} scriptname - The script to source 60 | * @param {SupportedShell} shell - Shell to base the check on 61 | */ 62 | const sourceLineForShell = (scriptname, shell) => { 63 | // Windows naturally uses `\` as path separator, which would be misinterpreted by the 64 | // shell interpreters. 65 | scriptname = scriptname.replaceAll('\\', '/'); 66 | 67 | if (shell === 'fish') { 68 | return `[ -f ${scriptname} ]; and . ${scriptname}; or true`; 69 | } 70 | 71 | if (shell === 'zsh') { 72 | return `[[ -f ${scriptname} ]] && . ${scriptname} || true`; 73 | } 74 | 75 | if (shell === 'pwsh') { 76 | return `if (Test-Path ${scriptname}) { . ${scriptname} }`; 77 | } 78 | 79 | if (shell === 'bash') { 80 | return `[ -f ${scriptname} ] && . ${scriptname} || true`; 81 | } 82 | 83 | throw new Error(`Unsupported shell: ${shell}`); 84 | }; 85 | 86 | /** 87 | * Helper to check if a filename is one of the SHELL config we expect 88 | * 89 | * @param {String} filename - Filename to check against 90 | * @returns {Boolean} Either true or false 91 | */ 92 | const isInShellConfig = filename => 93 | [ 94 | SHELL_LOCATIONS.bash, 95 | SHELL_LOCATIONS.zsh, 96 | SHELL_LOCATIONS.fish, 97 | SHELL_LOCATIONS.pwsh, 98 | untildify(SHELL_LOCATIONS.bash), 99 | untildify(SHELL_LOCATIONS.zsh), 100 | untildify(SHELL_LOCATIONS.fish), 101 | untildify(SHELL_LOCATIONS.pwsh), 102 | ].includes(filename); 103 | 104 | /** 105 | * Checks a given file for the existence of a specific line. Used to prevent 106 | * adding multiple completion source to SHELL scripts. 107 | * 108 | * @param {String} filename - The filename to check against 109 | * @param {String} line - The line to look for 110 | * @returns {Promise.} true or false, false if the line is not present. 111 | */ 112 | const checkFilenameForLine = async (filename, line) => { 113 | debug('Check filename (%s) for "%s"', filename, line); 114 | 115 | let filecontent = ''; 116 | try { 117 | filecontent = await readFile(untildify(filename), 'utf8'); 118 | } catch (/** @type {any} */ err) { 119 | if (err.code !== 'ENOENT') { 120 | console.error( 121 | 'Got an error while trying to read from %s file', 122 | filename, 123 | err 124 | ); 125 | return false; 126 | } 127 | } 128 | 129 | return !!filecontent.match(`${line}`); 130 | }; 131 | 132 | /** 133 | * Opens a file for modification adding a new `source` line for the given 134 | * SHELL. Used for both SHELL script and tabtab internal one. 135 | * 136 | * @param {Object} options - Options. 137 | * @param {String} options.filename - The file to modify. 138 | * @param {String} options.scriptname - The line to add sourcing this file. 139 | * @param {String} options.name - The package being configured. 140 | * @param {SupportedShell} options.shell 141 | * @returns {Promise.} 142 | */ 143 | const writeLineToFilename = ({ filename, scriptname, name, shell }) => new Promise(( 144 | resolve, 145 | reject 146 | ) => { 147 | const filepath = untildify(filename); 148 | 149 | debug('Creating directory for %s file', filepath); 150 | mkdir(path.dirname(filepath), { recursive: true }) 151 | .then(() => { 152 | const stream = fs.createWriteStream(filepath, { flags: 'a' }); 153 | stream.on('error', reject); 154 | stream.on('finish', () => resolve()); 155 | 156 | debug('Writing to shell configuration file (%s)', filename); 157 | debug('scriptname:', scriptname); 158 | 159 | const inShellConfig = isInShellConfig(filename); 160 | if (inShellConfig) { 161 | stream.write(`\n# tabtab source for packages`); 162 | } else { 163 | stream.write(`\n# tabtab source for ${name} package`); 164 | } 165 | 166 | stream.write('\n# uninstall by removing these lines'); 167 | stream.write(`\n${sourceLineForShell(scriptname, shell)}`); 168 | stream.end('\n'); 169 | 170 | console.log('=> Added tabtab source line in "%s" file', filename); 171 | }) 172 | .catch(err => { 173 | console.error('mkdirp ERROR', err); 174 | reject(err); 175 | }); 176 | }); 177 | 178 | /** 179 | * Writes to SHELL config file adding a new line, but only one, to the SHELL 180 | * config script. This enables tabtab to work for the given SHELL. 181 | * 182 | * @param {Object} options - Options object with 183 | * @param {String} options.location - The SHELL script location (~/.bashrc, ~/.zshrc or 184 | * ~/.config/fish/config.fish) 185 | * @param {String} options.name - The package configured for completion 186 | * @param {SupportedShell} options.shell options.shell 187 | */ 188 | const writeToShellConfig = async ({ location, name, shell }) => { 189 | const scriptname = path.join( 190 | COMPLETION_DIR, 191 | shell, 192 | tabtabFileName(shell), 193 | ); 194 | 195 | const filename = location; 196 | 197 | // Check if SHELL script already has a line for tabtab 198 | const existing = await checkFilenameForLine(filename, scriptname); 199 | if (existing) { 200 | return console.log('=> Tabtab line already exists in %s file', filename); 201 | } 202 | 203 | return writeLineToFilename({ 204 | filename, 205 | scriptname, 206 | name, 207 | shell, 208 | }); 209 | }; 210 | 211 | /** 212 | * Writes to tabtab internal script that acts as a frontend router for the 213 | * completion mechanism, in the internal ~/.config/tabtab directory. Every 214 | * completion is added to this file. 215 | * 216 | * @param {Object} options - Options object with 217 | * @param {String} options.name - The package configured for completion 218 | * @param {SupportedShell} options.shell 219 | */ 220 | const writeToTabtabScript = async ({ name, shell }) => { 221 | const filename = path.join( 222 | COMPLETION_DIR, 223 | shell, 224 | tabtabFileName(shell), 225 | ); 226 | 227 | const scriptname = path.join( 228 | COMPLETION_DIR, 229 | shell, 230 | completionFileName(name, shell), 231 | ); 232 | 233 | // Check if tabtab completion file already has this line in it 234 | const existing = await checkFilenameForLine(filename, scriptname); 235 | if (existing) { 236 | return console.log('=> Tabtab line already exists in %s file', filename); 237 | } 238 | 239 | return writeLineToFilename({ filename, scriptname, name, shell }); 240 | }; 241 | 242 | /** 243 | * Construct a completion script 244 | * @param {Object} options - Options object 245 | * @param {String} options.name - The package configured for completion 246 | * @param {String} options.completer - The program the will act as the completer for the `name` program 247 | * @param {SupportedShell} options.shell 248 | * @returns {Promise.} 249 | */ 250 | const getCompletionScript = async ({ name, completer, shell }) => { 251 | const templatePath = scriptFromShell(shell); 252 | const templateContent = await readFile(templatePath, 'utf8'); 253 | const scriptContent = templateContent 254 | .replaceAll('{pkgname}', name) 255 | .replaceAll('{completer}', completer) 256 | // on Bash on windows, we need to make sure to remove any \r 257 | .replaceAll(/\r?\n/g, '\n'); 258 | return scriptContent 259 | }; 260 | 261 | /** 262 | * This writes a new completion script in the internal `~/.config/tabtab` 263 | * directory. Depending on the SHELL used, a different script is created for 264 | * the given SHELL. 265 | * 266 | * @param {Object} options - Options object with 267 | * @param {String} options.name - The package configured for completion 268 | * @param {String} options.completer - The binary that will act as the completer for `name` program 269 | * @param {SupportedShell} options.shell 270 | */ 271 | const writeToCompletionScript = async ({ name, completer, shell }) => { 272 | const filename = untildify( 273 | path.join(COMPLETION_DIR, shell, completionFileName(name, shell)) 274 | ); 275 | 276 | try { 277 | const filecontent = await getCompletionScript({ name, completer, shell }) 278 | debug('Writing completion script to', filename); 279 | await mkdir(path.dirname(filename), { recursive: true }); 280 | await writeFile(filename, filecontent); 281 | console.log('=> Wrote completion script to %s file', filename); 282 | } catch (err) { 283 | console.error('ERROR:', err); 284 | } 285 | }; 286 | 287 | /** 288 | * Top level install method. Does three things: 289 | * 290 | * - Writes to SHELL config file, adding a new line to tabtab internal script. 291 | * - Creates or edit tabtab internal script 292 | * - Creates the actual completion script for this package. 293 | * 294 | * @param {Object} options - Options object with 295 | * @param {String} options.name - The program name to complete 296 | * @param {String} options.completer - The actual program or binary that will act as the completer 297 | * for `name` program. Can be the same. 298 | * @param {String} options.location - The SHELL script config location (~/.bashrc, ~/.zshrc or 299 | * ~/.config/fish/config.fish) 300 | * @param {SupportedShell} options.shell - the target shell language 301 | */ 302 | const install = async options => { 303 | debug('Install with options', options); 304 | if (!options) { 305 | throw new Error('options is required'); 306 | } 307 | 308 | if (!options.name) { 309 | throw new Error('options.name is required'); 310 | } 311 | 312 | if (!options.completer) { 313 | throw new Error('options.completer is required'); 314 | } 315 | 316 | if (!options.location) { 317 | throw new Error('options.location is required'); 318 | } 319 | 320 | await Promise.all([ 321 | writeToShellConfig(options), 322 | writeToTabtabScript(options), 323 | writeToCompletionScript(options) 324 | ]); 325 | const { location, name } = options; 326 | console.log(` 327 | => Tabtab source line added to ${location} for ${name} package. 328 | 329 | Make sure to reload your SHELL. 330 | `); 331 | }; 332 | 333 | /** 334 | * Removes the 3 relevant lines from provided filename, based on the package 335 | * name passed in. 336 | * 337 | * @param {String} filename - The filename to operate on 338 | * @param {String} name - The package name to look for 339 | */ 340 | const removeLinesFromFilename = async (filename, name) => { 341 | /* eslint-disable no-unused-vars */ 342 | debug('Removing lines from %s file, looking for %s package', filename, name); 343 | if (!(await exists(filename))) { 344 | return debug('File %s does not exist', filename); 345 | } 346 | 347 | const filecontent = await readFile(filename, 'utf8'); 348 | const lines = filecontent.split(/\r?\n/); 349 | 350 | const sourceLine1 = `# tabtab source for packages`; 351 | const sourceLine2 = `# tabtab source for ${name} package`; 352 | 353 | const hasLine1 = filecontent.includes(sourceLine1); 354 | if (!hasLine1) { 355 | debug('File %s does not include the line: %s', filename, sourceLine1); 356 | } 357 | const hasLine2 = filecontent.includes(sourceLine2); 358 | if (!hasLine2) { 359 | debug('File %s does not include the line: %s', filename, sourceLine2); 360 | } 361 | const hasLine = hasLine1 || hasLine2; 362 | if (!hasLine) { 363 | return debug('File %s does not include either line', filename); 364 | } 365 | 366 | let lineIndex = -1; 367 | const buffer = lines 368 | // Build up the new buffer, removing the 3 lines following the sourceline 369 | .map((line, index) => { 370 | const match = line.match(sourceLine1) ?? line.match(sourceLine2); 371 | if (match) { 372 | lineIndex = index; 373 | } else if (lineIndex + 3 <= index) { 374 | lineIndex = -1; 375 | } 376 | 377 | return lineIndex === -1 ? line : ''; 378 | }) 379 | // Remove any double empty lines from this file 380 | .map((line, index, array) => { 381 | const next = array[index + 1]; 382 | if (line === '' && next === '') { 383 | return; 384 | } 385 | 386 | return line; 387 | }) 388 | // Remove any undefined value from there 389 | .filter(line => line !== undefined) 390 | .join('\n') 391 | .trim(); 392 | 393 | await writeFile(filename, buffer); 394 | console.log('=> Removed tabtab source lines from %s file', filename); 395 | }; 396 | 397 | /** 398 | * Uninstall shell completion for one program from one or all supported shells. 399 | * 400 | * It also removes the relevant scripts if no more completion are installed on 401 | * the system. 402 | * 403 | * @param {Object} options 404 | * @param {String} options.name - Name of the target program. 405 | * @param {SupportedShell} [options.shell] - The target shell language. If not specified, target all supported shells. 406 | */ 407 | const uninstall = async options => { 408 | debug('Uninstall with options', options); 409 | if (!options) { 410 | throw new Error('options is required'); 411 | } 412 | 413 | const { name, shell } = options; 414 | 415 | if (!name) { 416 | throw new Error('Unable to uninstall if options.name is missing'); 417 | } 418 | 419 | if (!shell) { 420 | await Promise.all(SUPPORTED_SHELLS.map(shell => uninstall({ name, shell }))); 421 | return; 422 | } 423 | 424 | const completionScript = untildify( 425 | path.join(COMPLETION_DIR, shell, completionFileName(name, shell)) 426 | ); 427 | 428 | // First, lets remove the completion script itself 429 | if (await exists(completionScript)) { 430 | await unlink(completionScript); 431 | console.log('=> Removed completion script (%s)', completionScript); 432 | } 433 | 434 | // Then the lines in ~/.config/tabtab/__tabtab.shell 435 | const tabtabScript = untildify( 436 | path.join( 437 | COMPLETION_DIR, 438 | shell, 439 | tabtabFileName(shell), 440 | ) 441 | ); 442 | await removeLinesFromFilename(tabtabScript, name); 443 | 444 | // Then, check if __tabtab.shell is empty, if so remove the last source line in SHELL config 445 | const isEmpty = (await readFile(tabtabScript, 'utf8')).trim() === ''; 446 | if (isEmpty) { 447 | const shellScript = locationFromShell(shell); 448 | debug( 449 | 'File %s is empty. Removing source line from %s file', 450 | tabtabScript, 451 | shellScript 452 | ); 453 | await removeLinesFromFilename(shellScript, name); 454 | } 455 | 456 | console.log('=> Uninstalled completion for %s package', name); 457 | }; 458 | 459 | module.exports = { 460 | install, 461 | uninstall, 462 | checkFilenameForLine, 463 | getCompletionScript, 464 | writeToShellConfig, 465 | writeToTabtabScript, 466 | writeToCompletionScript, 467 | writeLineToFilename 468 | }; 469 | -------------------------------------------------------------------------------- /lib/prompt.js: -------------------------------------------------------------------------------- 1 | const enquirer = require('enquirer'); 2 | const path = require('path'); 3 | const { SUPPORTED_SHELLS, SHELL_LOCATIONS } = require('./constants'); 4 | const debug = require('./utils/tabtabDebug')('tabtab:prompt'); 5 | 6 | /** 7 | * @typedef {import('./constants').SupportedShell} SupportedShell 8 | */ 9 | 10 | /** 11 | * @typedef {Object} PromptAnswer 12 | * @property {SupportedShell} shell 13 | * @property {String} location 14 | */ 15 | 16 | /** 17 | * Asks user about SHELL and desired location. 18 | * 19 | * It is too difficult to check spawned SHELL, the user has to use chsh before 20 | * it is reflected in process.env.SHELL 21 | * 22 | * @returns {Promise.} 23 | */ 24 | const prompt = async () => { 25 | const questions = [ 26 | { 27 | type: 'select', 28 | name: 'shell', 29 | message: 'Which Shell do you use ?', 30 | choices: SUPPORTED_SHELLS, 31 | default: 'bash' 32 | } 33 | ]; 34 | 35 | const { shell } = /** @type {{ shell: SupportedShell }} */ (await enquirer.prompt(questions)); 36 | debug('answers', shell); 37 | 38 | if (!(shell in SHELL_LOCATIONS)) { 39 | throw new Error(`Unsupported shell: ${shell}`); 40 | } 41 | 42 | const location = SHELL_LOCATIONS[/** @type {SupportedShell} */ (shell)]; 43 | debug(`Will install completion to ${location}`); 44 | 45 | const initialAnswer = { location, shell }; 46 | 47 | const { locationOK } = /** @type {{ locationOK: Boolean }} */ (await enquirer.prompt({ 48 | type: 'confirm', 49 | name: 'locationOK', 50 | message: `We will install completion to ${location}, is it ok ?` 51 | })); 52 | 53 | if (locationOK) { 54 | debug('location is ok, return', initialAnswer); 55 | return initialAnswer; 56 | } 57 | 58 | // otherwise, ask for specific **absolute** path 59 | const { userLocation } = /** @type {{ userLocation: String }} */ (await enquirer.prompt({ 60 | name: 'userLocation', 61 | message: 'Which path then ? Must be absolute.', 62 | type: 'input', 63 | validate: input => { 64 | debug('Validating input', input); 65 | return path.isAbsolute(input); 66 | } 67 | })); 68 | console.log(`Very well, we will install using ${userLocation}`); 69 | 70 | return { shell, location: userLocation }; 71 | }; 72 | 73 | module.exports = prompt; 74 | -------------------------------------------------------------------------------- /lib/templates/completion.bash: -------------------------------------------------------------------------------- 1 | ###-begin-{pkgname}-completion-### 2 | if type complete &>/dev/null; then 3 | _{pkgname}_completion () { 4 | local words cword 5 | if type _get_comp_words_by_ref &>/dev/null; then 6 | _get_comp_words_by_ref -n = -n @ -n : -w words -i cword 7 | else 8 | cword="$COMP_CWORD" 9 | words=("${COMP_WORDS[@]}") 10 | fi 11 | 12 | local si="$IFS" 13 | IFS=$'\n' COMPREPLY=($(COMP_CWORD="$cword" \ 14 | COMP_LINE="$COMP_LINE" \ 15 | COMP_POINT="$COMP_POINT" \ 16 | SHELL=bash \ 17 | {completer} completion-server -- "${words[@]}" \ 18 | 2>/dev/null)) || return $? 19 | IFS="$si" 20 | 21 | if [ "$COMPREPLY" = "__tabtab_complete_files__" ]; then 22 | COMPREPLY=($(compgen -f -- "$cword")) 23 | fi 24 | 25 | if type __ltrim_colon_completions &>/dev/null; then 26 | __ltrim_colon_completions "${words[cword]}" 27 | fi 28 | } 29 | complete -o default -F _{pkgname}_completion {pkgname} 30 | fi 31 | ###-end-{pkgname}-completion-### 32 | -------------------------------------------------------------------------------- /lib/templates/completion.fish: -------------------------------------------------------------------------------- 1 | ###-begin-{pkgname}-completion-### 2 | function _{pkgname}_completion 3 | set cmd (commandline -o) 4 | set cursor (commandline -C) 5 | set words (count $cmd) 6 | 7 | set completions (eval env DEBUG=\"" \"" COMP_CWORD=\""$words\"" COMP_LINE=\""$cmd \"" COMP_POINT=\""$cursor\"" SHELL=fish {completer} completion-server -- $cmd) 8 | 9 | if [ "$completions" = "__tabtab_complete_files__" ] 10 | set -l matches (commandline -ct)* 11 | if [ -n "$matches" ] 12 | __fish_complete_path (commandline -ct) 13 | end 14 | else 15 | for completion in $completions 16 | echo -e $completion 17 | end 18 | end 19 | end 20 | 21 | complete -f -d '{pkgname}' -c {pkgname} -a "(_{pkgname}_completion)" 22 | ###-end-{pkgname}-completion-### 23 | -------------------------------------------------------------------------------- /lib/templates/completion.ps1: -------------------------------------------------------------------------------- 1 | ###-begin-{pkgname}-completion-### 2 | 3 | Register-ArgumentCompleter -CommandName '{pkgname}' -ScriptBlock { 4 | param( 5 | $WordToComplete, 6 | $CommandAst, 7 | $CursorPosition 8 | ) 9 | 10 | function __{pkgname}_debug { 11 | if ($env:BASH_COMP_DEBUG_FILE) { 12 | "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE" 13 | } 14 | } 15 | 16 | filter __{pkgname}_escapeStringWithSpecialChars { 17 | $_ -replace '\s|#|@|\$|;|,|''|\{|\}|\(|\)|"|`|\||<|>|&','`$&' 18 | } 19 | 20 | # Get the current command line and convert into a string 21 | $Command = $CommandAst.CommandElements 22 | $Command = "$Command" 23 | 24 | __{pkgname}_debug "" 25 | __{pkgname}_debug "========= starting completion logic ==========" 26 | __{pkgname}_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition" 27 | 28 | # The user could have moved the cursor backwards on the command-line. 29 | # We need to trigger completion from the $CursorPosition location, so we need 30 | # to truncate the command-line ($Command) up to the $CursorPosition location. 31 | # Make sure the $Command is longer then the $CursorPosition before we truncate. 32 | # This happens because the $Command does not include the last space. 33 | if ($Command.Length -gt $CursorPosition) { 34 | $Command=$Command.Substring(0,$CursorPosition) 35 | } 36 | __{pkgname}_debug "Truncated command: $Command" 37 | 38 | # Prepare the command to request completions for the program. 39 | # Split the command at the first space to separate the program and arguments. 40 | $Program,$Arguments = $Command.Split(" ",2) 41 | $RequestComp="$Program completion-server" 42 | __{pkgname}_debug "RequestComp: $RequestComp" 43 | 44 | # we cannot use $WordToComplete because it 45 | # has the wrong values if the cursor was moved 46 | # so use the last argument 47 | if ($WordToComplete -ne "" ) { 48 | $WordToComplete = $Arguments.Split(" ")[-1] 49 | } 50 | __{pkgname}_debug "New WordToComplete: $WordToComplete" 51 | 52 | 53 | # Check for flag with equal sign 54 | $IsEqualFlag = ($WordToComplete -Like "--*=*" ) 55 | if ( $IsEqualFlag ) { 56 | __{pkgname}_debug "Completing equal sign flag" 57 | # Remove the flag part 58 | $Flag,$WordToComplete = $WordToComplete.Split("=",2) 59 | } 60 | 61 | if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) { 62 | # If the last parameter is complete (there is a space following it) 63 | # We add an extra empty parameter so we can indicate this to the go method. 64 | __{pkgname}_debug "Adding extra empty parameter" 65 | # We need to use `"`" to pass an empty argument a "" or '' does not work!!! 66 | $Command="$Command" + ' `"`"' 67 | } 68 | 69 | __{pkgname}_debug "Calling $RequestComp" 70 | 71 | $oldenv = ($env:SHELL, $env:COMP_CWORD, $env:COMP_LINE, $env:COMP_POINT) 72 | $env:SHELL = "pwsh" 73 | $env:COMP_CWORD = $Command.Split(" ").Count - 1 74 | $env:COMP_POINT = $CursorPosition 75 | $env:COMP_LINE = $Command 76 | 77 | try { 78 | #call the command store the output in $out and redirect stderr and stdout to null 79 | # $Out is an array contains each line per element 80 | Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null 81 | } finally { 82 | ($env:SHELL, $env:COMP_CWORD, $env:COMP_LINE, $env:COMP_POINT) = $oldenv 83 | } 84 | 85 | __{pkgname}_debug "The completions are: $Out" 86 | 87 | $Longest = 0 88 | $Values = $Out | ForEach-Object { 89 | #Split the output in name and description 90 | $Name, $Description = $_.Split("`t",2) 91 | __{pkgname}_debug "Name: $Name Description: $Description" 92 | 93 | # Look for the longest completion so that we can format things nicely 94 | if ($Longest -lt $Name.Length) { 95 | $Longest = $Name.Length 96 | } 97 | 98 | # Set the description to a one space string if there is none set. 99 | # This is needed because the CompletionResult does not accept an empty string as argument 100 | if (-Not $Description) { 101 | $Description = " " 102 | } 103 | @{Name="$Name";Description="$Description"} 104 | } 105 | 106 | 107 | $Space = " " 108 | $Values = $Values | Where-Object { 109 | # filter the result 110 | if (-not $WordToComplete.StartsWith("-") -and $_.Name.StartsWith("-")) { 111 | # skip flag completions unless a dash is present 112 | return 113 | } else { 114 | $_.Name -like "$WordToComplete*" 115 | } 116 | 117 | # Join the flag back if we have an equal sign flag 118 | if ( $IsEqualFlag ) { 119 | __{pkgname}_debug "Join the equal sign flag back to the completion value" 120 | $_.Name = $Flag + "=" + $_.Name 121 | } 122 | } 123 | 124 | # Get the current mode 125 | $Mode = (Get-PSReadLineKeyHandler | Where-Object {$_.Key -eq "Tab" }).Function 126 | __{pkgname}_debug "Mode: $Mode" 127 | 128 | $Values | ForEach-Object { 129 | 130 | # store temporary because switch will overwrite $_ 131 | $comp = $_ 132 | 133 | # PowerShell supports three different completion modes 134 | # - TabCompleteNext (default windows style - on each key press the next option is displayed) 135 | # - Complete (works like bash) 136 | # - MenuComplete (works like zsh) 137 | # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function 138 | 139 | # CompletionResult Arguments: 140 | # 1) CompletionText text to be used as the auto completion result 141 | # 2) ListItemText text to be displayed in the suggestion list 142 | # 3) ResultType type of completion result 143 | # 4) ToolTip text for the tooltip with details about the object 144 | 145 | switch ($Mode) { 146 | 147 | # bash like 148 | "Complete" { 149 | 150 | if ($Values.Length -eq 1) { 151 | __{pkgname}_debug "Only one completion left" 152 | 153 | # insert space after value 154 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __{pkgname}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 155 | 156 | } else { 157 | # Add the proper number of spaces to align the descriptions 158 | while($comp.Name.Length -lt $Longest) { 159 | $comp.Name = $comp.Name + " " 160 | } 161 | 162 | # Check for empty description and only add parentheses if needed 163 | if ($($comp.Description) -eq " " ) { 164 | $Description = "" 165 | } else { 166 | $Description = " ($($comp.Description))" 167 | } 168 | 169 | [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)") 170 | } 171 | } 172 | 173 | # zsh like 174 | "MenuComplete" { 175 | # insert space after value 176 | # MenuComplete will automatically show the ToolTip of 177 | # the highlighted value at the bottom of the suggestions. 178 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __{pkgname}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 179 | } 180 | 181 | # TabCompleteNext and in case we get something unknown 182 | Default { 183 | # Like MenuComplete but we don't want to add a space here because 184 | # the user need to press space anyway to get the completion. 185 | # Description will not be shown because that's not possible with TabCompleteNext 186 | [System.Management.Automation.CompletionResult]::new($($comp.Name | __{pkgname}_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)") 187 | } 188 | } 189 | 190 | } 191 | } 192 | 193 | ###-end-{pkgname}-completion-### 194 | -------------------------------------------------------------------------------- /lib/templates/completion.zsh: -------------------------------------------------------------------------------- 1 | #compdef {pkgname} 2 | ###-begin-{pkgname}-completion-### 3 | if type compdef &>/dev/null; then 4 | _{pkgname}_completion () { 5 | local reply 6 | local si=$IFS 7 | 8 | IFS=$'\n' reply=($(COMP_CWORD="$((CURRENT-1))" COMP_LINE="$BUFFER" COMP_POINT="$CURSOR" SHELL=zsh {completer} completion-server -- "${words[@]}")) 9 | IFS=$si 10 | 11 | if [ "$reply" = "__tabtab_complete_files__" ]; then 12 | _files 13 | else 14 | _describe 'values' reply 15 | fi 16 | } 17 | # When called by the Zsh completion system, this will end with 18 | # "loadautofunc" when initially autoloaded and "shfunc" later on, otherwise, 19 | # the script was "eval"-ed so use "compdef" to register it with the 20 | # completion system 21 | if [[ $zsh_eval_context == *func ]]; then 22 | _{pkgname}_completion "$@" 23 | else 24 | compdef _{pkgname}_completion {pkgname} 25 | fi 26 | fi 27 | ###-end-{pkgname}-completion-### 28 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.common.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "declarationDir": "../types", 7 | "strict": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/exists.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const untildify = require('untildify'); 3 | const { promisify } = require('util'); 4 | 5 | const readFile = promisify(fs.readFile); 6 | 7 | /** 8 | * @param {String} file 9 | */ 10 | module.exports = async file => { 11 | let fileExists; 12 | try { 13 | await readFile(untildify(file)); 14 | fileExists = true; 15 | } catch (err) { 16 | fileExists = false; 17 | } 18 | 19 | return fileExists; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | const tabtabDebug = require('./tabtabDebug'); 2 | const exists = require('./exists'); 3 | 4 | module.exports = { 5 | tabtabDebug, 6 | exists 7 | }; 8 | -------------------------------------------------------------------------------- /lib/utils/tabtabDebug.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | 4 | /** 5 | * If TABTAB_DEBUG env is set, make it so that debug statements are also log to 6 | * TABTAB_DEBUG file provided. 7 | * @param {String} name 8 | */ 9 | const tabtabDebug = name => { 10 | /* eslint-disable global-require */ 11 | // @ts-ignore 12 | let debug = require('debug')(name); 13 | 14 | if (process.env.TABTAB_DEBUG) { 15 | const file = process.env.TABTAB_DEBUG; 16 | const stream = fs.createWriteStream(file, { 17 | flags: 'a+' 18 | }); 19 | 20 | /** 21 | * @param {...any} args 22 | */ 23 | const log = (...args) => { 24 | args = args.map(arg => { 25 | if (typeof arg === 'string') return arg; 26 | return JSON.stringify(arg); 27 | }); 28 | 29 | const str = `${util.format(...args)}\n`; 30 | stream.write(str); 31 | }; 32 | 33 | if (process.env.COMP_LINE) { 34 | debug = log; 35 | } else { 36 | debug.log = log; 37 | } 38 | } 39 | 40 | return debug; 41 | }; 42 | 43 | module.exports = tabtabDebug; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "mklabs", 3 | "name": "@pnpm/tabtab", 4 | "description": "tab completion helpers, for node cli programs. Inspired by npm completion.", 5 | "main": "lib/index.js", 6 | "types": "types/index.d.ts", 7 | "engines": { 8 | "node": ">=18" 9 | }, 10 | "scripts": { 11 | "test": "c8 mocha --timeout 5000", 12 | "typecheck": "pnpm run build && tsc -p test --noEmit", 13 | "build": "tsc -p lib", 14 | "prepublishOnly": "pnpm run build", 15 | "mocha": "mocha --timeout 5000", 16 | "coverage": "c8 report --reporter=text-lcov | coveralls", 17 | "coverage-html": "npm run mocha && c8 report --reporter=html && serve coverage", 18 | "eslint": "eslint lib/ test/", 19 | "watch": "npm-watch", 20 | "readme": "remark readme.md --use toc --output", 21 | "changelog": "auto-changelog", 22 | "api": "for file in `echo index.js installer.js prompt.js`; do jsdoc2md lib/$file > api/$file.md; done", 23 | "docs": "npm run api && npm run readme && npm run changelog" 24 | }, 25 | "watch": { 26 | "test": "{lib,test}/**/*.js" 27 | }, 28 | "devDependencies": { 29 | "auto-changelog": "^1.16.4", 30 | "c8": "^3.5.0", 31 | "coveralls": "^3.1.0", 32 | "eslint": "^6.8.0", 33 | "eslint-config-mklabs": "^1.0.9", 34 | "inquirer-test": "^2.0.1", 35 | "jsdoc-to-markdown": "^4.0.1", 36 | "mocha": "^7.2.0", 37 | "npm-watch": "^0.4.0", 38 | "remark-cli": "^5.0.0", 39 | "remark-toc": "^5.1.1", 40 | "serve": "^10.1.2", 41 | "typescript": "^5.3.3", 42 | "@types/mocha": "^7.0.0", 43 | "@types/node": "^20.11.13" 44 | }, 45 | "license": "MIT", 46 | "keywords": [ 47 | "terminal", 48 | "tab", 49 | "unix", 50 | "console", 51 | "complete", 52 | "completion" 53 | ], 54 | "repository": { 55 | "type": "git", 56 | "url": "https://github.com/pnpm/tabtab.git" 57 | }, 58 | "dependencies": { 59 | "debug": "^4.3.1", 60 | "enquirer": "^2.3.6", 61 | "minimist": "^1.2.5", 62 | "untildify": "^4.0.0" 63 | }, 64 | "auto-changelog": { 65 | "template": "keepachangelog", 66 | "unreleased": true, 67 | "commitLimit": false, 68 | "ignoreCommitPattern": "changelog|readme|^test" 69 | }, 70 | "version": "0.5.4", 71 | "pnpm": { 72 | "patchedDependencies": { 73 | "untildify@4.0.0": "patches/untildify@4.0.0.patch" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /patches/untildify@4.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/index.js b/index.js 2 | index c82d3c1e6bee37dc41cc7dd03fdde32e90ca5b3d..c05461308fcb60212a13c3f2fcd6580f78318117 100644 3 | --- a/index.js 4 | +++ b/index.js 5 | @@ -1,7 +1,23 @@ 6 | 'use strict'; 7 | const os = require('os'); 8 | +const path = require('path'); 9 | +const fs = require('fs'); 10 | 11 | -const homeDirectory = os.homedir(); 12 | +const homeDirectory = path.join(os.tmpdir(), 'untildify-fake-home-dir'); 13 | +if (!fs.existsSync(homeDirectory)) { 14 | + const touch = suffix => { 15 | + const fullPath = path.join(homeDirectory, suffix); 16 | + const parentDir = path.dirname(fullPath); 17 | + if (!fs.existsSync(parentDir)) { 18 | + fs.mkdirSync(parentDir, { recursive: true }); 19 | + } 20 | + fs.writeFileSync(fullPath, ''); 21 | + }; 22 | + fs.mkdirSync(homeDirectory); 23 | + touch('.bashrc'); 24 | + touch('.zshrc'); 25 | + touch('.config/fish/config.fish'); 26 | +} 27 | 28 | module.exports = pathWithTilde => { 29 | if (typeof pathWithTilde !== 'string') { 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # @pnpm/tabtab 2 | 3 | ![CI](https://github.com/pnpm/tabtab/workflows/CI/badge.svg) 4 | 5 | A node package to do some custom command line `` completion for any 6 | system command, for Bash, Zsh, and Fish shells. 7 | 8 | Made possible using the same technique as npm (whose completion is quite 9 | awesome) relying on a shell script bridge to do the actual completion from 10 | node's land. 11 | 12 | ![tabtab](https://user-images.githubusercontent.com/113832/46506243-deb39b00-c833-11e8-8f5f-7136987e7341.gif) 13 | 14 | **Warning / Breaking changes** 15 | 16 | - Windows is not supported 17 | - Cache has been removed 18 | 19 | ## Goal of this 3.0.0 version 20 | 21 | Simplify everything, major overhaul, rewrite from scratch. 22 | 23 | Functional, less abstraction, clearer documentation, good test coverage, 24 | support for node 10 without babel. 25 | 26 | Up to date dependencies, easier to debug, easier to test. 27 | 28 | Should still support bash, zsh and fish but bash is the primary focus of this 29 | alpha version. 30 | 31 | No binary file anymore, just a library (still debating with myself) 32 | 33 | The goal of this rewrite is two-folded: 34 | 35 | - Integrate nicely with [yo](https://github.com/yeoman/yo) (Yeoman) 36 | - Have a robust and fast enough library for [yarn-completions](https://github.com/mklabs/yarn-completions) 37 | 38 | ## Installation 39 | 40 | ``` 41 | pnpm add @pnpm/tabtab 42 | ``` 43 | 44 | ## Usage 45 | 46 | Writing completion is a two-step process: Installation and Logging. Tabtab 47 | provides just that. 48 | 49 | Here is a basic example using 50 | [minimist](https://www.npmjs.com/package/minimist) to parse arguments. 51 | 52 | ```js 53 | #! /usr/bin/env node 54 | 55 | const tabtab = require('@pnpm/tabtab'); 56 | const opts = require('minimist')(process.argv.slice(2), { 57 | string: ['foo', 'bar'], 58 | boolean: ['help', 'version', 'loglevel'] 59 | }); 60 | 61 | const args = opts._; 62 | const completion = env => { 63 | const shell = tabtab.getShellFromEnv(env); 64 | 65 | if (!env.complete) return; 66 | 67 | // Write your completions there 68 | 69 | if (env.prev === 'foo') { 70 | return tabtab.log(['is', 'this', 'the', 'real', 'life'], shell, console.log); 71 | } 72 | 73 | if (env.prev === 'bar') { 74 | return tabtab.log(['is', 'this', 'just', 'fantasy'], shell, console.log); 75 | } 76 | 77 | if (env.prev === '--loglevel') { 78 | return tabtab.log(['error', 'warn', 'info', 'notice', 'verbose'], shell, console.log); 79 | } 80 | 81 | return tabtab.log([ 82 | '--help', 83 | '--version', 84 | '--loglevel', 85 | 'foo', 86 | 'bar', 87 | 'install-completion', 88 | 'generate-completion', 89 | 'completion-server', 90 | 'someCommand:someCommand is some kind of command with a description', 91 | { 92 | name: 'someOtherCommand:hey', 93 | description: 'You must add a description for items with ":" in them' 94 | }, 95 | 'anotherOne' 96 | ], shell, console.log); 97 | }; 98 | 99 | const run = async () => { 100 | const cmd = args[0]; 101 | 102 | // Write your CLI there 103 | 104 | // Here we install for the program `tabtab-test` (this file), with 105 | // completer being the same program. Sometimes, you want to complete 106 | // another program that's where the `completer` option might come handy. 107 | if (cmd === 'install-completion') { 108 | const shell = args[1]; 109 | if (!tabtab.isShellSupported(shell)) { 110 | throw new Error(`${shell} is not supported`); 111 | } 112 | 113 | await tabtab 114 | .install({ 115 | name: 'tabtab-test', 116 | completer: 'tabtab-test', 117 | shell, 118 | }) 119 | .catch(err => console.error('INSTALL ERROR', err)); 120 | 121 | return; 122 | } 123 | 124 | if (cmd === 'uninstall-completion') { 125 | // Here we uninstall for the program `tabtab-test` (this file). 126 | await tabtab 127 | .uninstall({ 128 | name: 'tabtab-test' 129 | }) 130 | .catch(err => console.error('UNINSTALL ERROR', err)); 131 | 132 | return; 133 | } 134 | 135 | /// Here we print the code for bash completion to stdout. 136 | if (cmd === 'generate-completion') { 137 | const completion = await tabtab 138 | .getCompletionScript({ 139 | name: 'tabtab-test', 140 | completer: 'tabtab-test', 141 | shell: 'bash', 142 | }) 143 | .catch(err => console.error('GENERATE ERROR', err)); 144 | console.log(completion); 145 | 146 | return; 147 | } 148 | 149 | // The `completion-server` command is added automatically by tabtab when the program 150 | // is completed. 151 | if (cmd === 'completion-server') { 152 | const env = tabtab.parseEnv(process.env); 153 | return completion(env); 154 | } 155 | }; 156 | 157 | run(); 158 | ``` 159 | 160 | Please refer to the 161 | [examples/tabtab-test-complete](./examples/tabtab-test-complete) package for a 162 | working example. The following usage documentation is based on it. 163 | 164 | ### 1. Install completion 165 | 166 | To enable completion for a given program or package, you must enable the 167 | completion on your or user's system. This is done by calling `tabtab.install()` 168 | usually behind a `program install-completion` command or something similar. 169 | 170 | ```js 171 | // Here we install for the program `tabtab-test`, with completer being the same 172 | // program. Sometimes, you want to complete another program that's where the 173 | // `completer` option might come handy. 174 | tabtab.install({ 175 | name: 'tabtab-test', 176 | completer: 'tabtab-test' 177 | }) 178 | .then(() => console.log('Completion installed')) 179 | .catch(err => console.error(err)) 180 | ``` 181 | 182 | The method returns a promise, so `await / async` usage is possible. It takes an 183 | `options` parameter, with: 184 | 185 | - `name`: The program to complete 186 | - `completer`: The program that does the completion (can be the same program). 187 | - `shell`: Optional. The shell for which the autocompletion script needs to be generated. If not provided, a prompt will ask about which shell should be used. 188 | 189 | `tabtab.install()` will ask the user which SHELL to use, and optionally a path 190 | to write to. This will add a new line to either `~/.bashrc`, `~/.zshrc` or 191 | `~/.config/fish/config.fish` file to source tabtab completion script. 192 | 193 | Only one line will be added, even if it is called multiple times. 194 | 195 | ### 2. Log completion 196 | 197 | Once the completion is enabled and active, you can write completions for the 198 | program (here, in this example `tabtab-test`). Briefly, adding completions is 199 | as simple as logging output to `stdout`, with a few particularities (namely on 200 | Bash, and for descriptions), but this is taken care of by `tabtab.log()`. 201 | 202 | ```js 203 | tabtab.log([ 204 | '--help', 205 | '--version', 206 | 'command' 207 | 'command-two' 208 | ]); 209 | ``` 210 | 211 | This is the simplest way of adding completions. You can also use an object, 212 | instead of a simple string, with `{ name, description }` property if you want 213 | to add descriptions for each completion item, for the shells that support them 214 | (like Zsh or Fish). Or use the simpler `name:description` form. 215 | 216 | ```js 217 | tabtab.log([ 218 | { name: 'command', description: 'Description for command' }, 219 | 'command-two:Description for command-two' 220 | ]); 221 | ``` 222 | 223 | The `{ name, description }` approach is preferable in case you have completion 224 | items with `:` in them. 225 | 226 | Note that you can call `tabtab.log()` multiple times if you prefer to do so, it 227 | simply logs to the console in sequence. 228 | 229 | #### Filesystem completion 230 | 231 | If you have a parameter that expects a path to some file, you could want to let 232 | the shell use its native autocompletion. This saves you the work of writing 233 | custom filesystem autocomplete logic. Plus, the native autocomplete has a better 234 | handling of things like dircolors or hidden files. 235 | 236 | To trigger the filesystem completion, use `tabtab.logFiles()` without any 237 | argument. 238 | 239 | ```js 240 | if (previousFlag === '--file') { 241 | tabtab.logFiles(); 242 | } 243 | ``` 244 | 245 | ### 3. Parsing env 246 | 247 | If you ever want to add more intelligent completion, you'll need to check and 248 | see what is the last or previous word in the completed line, so that you can 249 | add options for a specific command or flag (such as loglevels for `--loglevel` 250 | for instance). 251 | 252 | Tabtab adds a few environment variables for you to inspect and use, this is 253 | done by calling `tabtab.parseEnv()` method. 254 | 255 | ```js 256 | const env = tabtab.parseEnv(process.env); 257 | // env: 258 | // 259 | // - complete A Boolean indicating whether we act in "plumbing mode" or not 260 | // - words The Number of words in the completed line 261 | // - point A Number indicating cursor position 262 | // - line The String input line 263 | // - partial The String part of line preceding cursor position 264 | // - last The last String word of the line 265 | // - lastPartial The last word String of partial 266 | // - prev The String word preceding last 267 | ``` 268 | 269 | Usually, you'll want to check against `env.last` or `env.prev`. 270 | 271 | ```js 272 | if (env.prev === '--loglevel') { 273 | tabtab.log(['error', 'warn', 'info', 'notice', 'verbose']); 274 | } 275 | ``` 276 | 277 | ## Completion mechanism 278 | 279 | Feel free to browse the [examples](./examples) directory to inspect the various 280 | template files used when creating a completion with `tabtab.install()`. 281 | 282 | Here is a Bash completion snippet created by tabtab. 283 | 284 | ```bash 285 | ###-begin-tabtab-test-completion-### 286 | if type complete &>/dev/null; then 287 | _tabtab-test_completion () { 288 | local words cword 289 | if type _get_comp_words_by_ref &>/dev/null; then 290 | _get_comp_words_by_ref -n = -n @ -n : -w words -i cword 291 | else 292 | cword="$COMP_CWORD" 293 | words=("${COMP_WORDS[@]}") 294 | fi 295 | 296 | local si="$IFS" 297 | IFS=$'\n' COMPREPLY=($(COMP_CWORD="$cword" \ 298 | COMP_LINE="$COMP_LINE" \ 299 | COMP_POINT="$COMP_POINT" \ 300 | SHELL=bash \ 301 | tabtab-test completion-server -- "${words[@]}" \ 302 | 2>/dev/null)) || return $? 303 | IFS="$si" 304 | 305 | if [ "$COMPREPLY" = "__tabtab_complete_files__" ]; then 306 | COMPREPLY=($(compgen -f -- "$cword")) 307 | fi 308 | 309 | if type __ltrim_colon_completions &>/dev/null; then 310 | __ltrim_colon_completions "${words[cword]}" 311 | fi 312 | } 313 | complete -o default -F _tabtab-test_completion tabtab-test 314 | fi 315 | ###-end-tabtab-test-completion-### 316 | ``` 317 | 318 | The system is quite simple (though hard to nail it down, thank you npm). A new 319 | Bash function is created, which is invoked whenever `tabtab-test` is tab 320 | completed. This function then invokes the completer `tabtab-test completion-server` 321 | with `COMP_CWORD`, `COMP_LINE` and `COMP_POINT` environment variables (which is 322 | parsed by `tabtab.parseEnv()`). 323 | 324 | The same mechanism can be applied to Zsh and Fish. 325 | 326 | ### Completion install 327 | 328 | As described in the [`Usage > Install Completion`](#1-install-completion) 329 | section, installing a completion involves adding a new line to source in either 330 | `~/.bashrc`, `~/.zshrc` or `~/.config/fish/config.fish` file. 331 | 332 | In the `3.0.0` version, it has been improved to only add a single line instead 333 | of multiple ones, one for each completion package installed on the system. 334 | 335 | This way, a single line is added to enable the completion of for various 336 | programs without cluttering the Shell configuration file. 337 | 338 | Example for `~/.bashrc` 339 | 340 | ```bash 341 | # tabtab source for packages 342 | # uninstall by removing these lines 343 | [ -f ~/.config/tabtab/__tabtab.bash ] && . ~/.config/tabtab/__tabtab.bash || true 344 | ``` 345 | 346 | It'll load a file `__tabtab.bash`, created in the `~/.config/tabtab` directory, 347 | which will hold all the source lines for each tabtab packages defining a 348 | completion. 349 | 350 | ```bash 351 | # tabtab source for foo package 352 | # uninstall by removing these lines 353 | [ -f ~/.config/tabtab/foo.bash ] && . ~/.config/tabtab/foo.bash || true 354 | 355 | # tabtab source for tabtab-test package 356 | # uninstall by removing these lines 357 | [ -f ~/.config/tabtab/tabtab-test.bash ] && . ~/.config/tabtab/tabtab-test.bash || true 358 | ``` 359 | 360 | ### Completion uninstall 361 | 362 | You can follow the file added in your SHELL configuration file and disable a 363 | completion by removing the above lines. 364 | 365 | Or simply disable tabtab by removing the line in your SHELL configuration file. 366 | 367 | Or, you can use `tabtab.uninstall()` to do this for you. 368 | 369 | ```js 370 | if (cmd === 'uninstall-completion') { 371 | // Here we uninstall for the program `tabtab-test` 372 | await tabtab 373 | .uninstall({ 374 | name: 'tabtab-test' 375 | }) 376 | .catch(err => console.error('UNINSTALL ERROR', err)); 377 | 378 | return; 379 | } 380 | ``` 381 | 382 | ## Debugging 383 | 384 | tabtab internally logs a lot of things, using the 385 | [debug](https://www.npmjs.com/package/debug) package. 386 | 387 | When testing a completion, it can be useful to see those logs, but writing to 388 | `stdout` or `stderr` while completing something can be troublesome. 389 | 390 | You can use the `TABTAB_DEBUG` environment variable to specify a file to log to 391 | instead. 392 | 393 | export TABTAB_DEBUG="/tmp/tabtab.log" 394 | tail -f /tmp/tabtab.log 395 | 396 | # in another shell 397 | tabtab-test 398 | 399 | See [tabtabDebug.js](./lib/utils/tabtabDebug.js) file for details. 400 | 401 | ## API Documentation 402 | 403 | Please refer to [api](./api) directory to see generated documentation (using 404 | [jsdoc2md](https://github.com/jsdoc2md/jsdoc-to-markdown)) 405 | 406 | ## Changelog 407 | 408 | Please refer to [CHANGELOG](./CHANGELOG.md) file to see all possible changes to this project. 409 | 410 | ## Credits 411 | 412 | npm does pretty amazing stuff with its completion feature. bash and zsh 413 | provides command tab-completion, which allow you to complete the names 414 | of commands in your $path. usually these functions means bash 415 | scripting, and in the case of npm, it is partially true. 416 | 417 | there is a special `npm completion` command you may want to look around, 418 | if not already. 419 | 420 | npm completion 421 | 422 | running this should dump [this 423 | script](https://raw.github.com/isaacs/npm/caafb7323708e113d100e3e8145b949ed7a16c22/lib/utils/completion.sh) 424 | to the console. this script works with both bash/zsh and map the correct 425 | completion functions to the npm executable. these functions takes care 426 | of parsing the `comp_*` variables available when hitting tab to complete 427 | a command, set them up as environment variables and run the `npm 428 | completion` command followed by `-- words` where words match value of 429 | the command being completed. 430 | 431 | this means that using this technique npm manage to perform bash/zsh 432 | completion using node and javascript. actually, the comprehensiveness of npm 433 | completion is quite amazing. 434 | 435 | this whole package/module is based entirely on npm's code and @isaacs 436 | work. 437 | 438 | * * * 439 | 440 | > [mit](./LICENSE)  ·  > [mklabs.github.io](https://mklabs.github.io)  ·  > [@mklabs](https://github.com/mklabs) 441 | -------------------------------------------------------------------------------- /test/fixtures/tabtab-install.js: -------------------------------------------------------------------------------- 1 | const tabtab = require('../..'); 2 | 3 | (async () => { 4 | await tabtab.install({ 5 | name: 'foo', 6 | completer: 'foo-complete', 7 | shell: 'bash', 8 | }); 9 | })(); 10 | -------------------------------------------------------------------------------- /test/getCompletionScript.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const { getCompletionScript, SUPPORTED_SHELLS } = require('..'); 4 | const { COMPLETION_FILE_EXT } = require('../lib/constants'); 5 | 6 | describe('getCompletionScript gets the right completion script for', () => { 7 | for (const shell of SUPPORTED_SHELLS) { 8 | it(shell, async () => { 9 | const received = await getCompletionScript({ 10 | name: 'foo', 11 | completer: 'foo-complete', 12 | shell 13 | }); 14 | const expected = fs.readFileSync(require.resolve(`../lib/templates/completion.${COMPLETION_FILE_EXT[shell]}`), 'utf8') 15 | .replaceAll('{pkgname}', 'foo') 16 | .replaceAll('{completer}', 'foo-complete') 17 | .replaceAll(/\r?\n/g, '\n'); 18 | assert.equal(received, expected); 19 | }); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /test/getShellFromEnv.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const assert = require('assert'); 3 | const { SUPPORTED_SHELLS, getShellFromEnv } = require('..'); 4 | 5 | describe('getShellFromEnv', () => { 6 | it('errors when env lacks SHELL', () => { 7 | assert.throws( 8 | () => getShellFromEnv({}), 9 | { 10 | message: 'SHELL cannot be empty', 11 | }, 12 | ) 13 | }); 14 | 15 | it('errors on unsupported shells', () => { 16 | assert.throws( 17 | () => getShellFromEnv({ SHELL: 'unknown' }), 18 | { 19 | message: "SHELL was set to an invalid value (unknown). Supported values are: 'bash', 'fish', 'pwsh', 'zsh'", 20 | }, 21 | ); 22 | }) 23 | 24 | it('returns supported shells', () => { 25 | assert.deepStrictEqual( 26 | SUPPORTED_SHELLS.map(SHELL => getShellFromEnv({ SHELL })), 27 | SUPPORTED_SHELLS, 28 | ); 29 | }); 30 | 31 | it('handles absolute paths', () => { 32 | assert.deepStrictEqual( 33 | SUPPORTED_SHELLS.map(shell => path.resolve('bin', shell)).map(SHELL => getShellFromEnv({ SHELL })), 34 | SUPPORTED_SHELLS, 35 | ) 36 | }) 37 | }); 38 | -------------------------------------------------------------------------------- /test/installer.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const untildify = require('untildify'); 5 | const { promisify } = require('util'); 6 | const { 7 | install, 8 | uninstall, 9 | writeToShellConfig, 10 | writeToCompletionScript 11 | } = require('../lib/installer'); 12 | const { COMPLETION_DIR } = require('../lib/constants'); 13 | const { tabtabFileName } = require('../lib/filename'); 14 | const { rejects, setupSuiteForInstall } = require('./utils'); 15 | 16 | // For node 7 / 8 17 | assert.rejects = rejects; 18 | 19 | const readFile = promisify(fs.readFile); 20 | const writeFile = promisify(fs.writeFile); 21 | const mkdir = promisify(fs.mkdir); 22 | 23 | describe('installer', () => { 24 | it('has install / uninstall functions', () => { 25 | assert.equal(typeof install, 'function'); 26 | assert.equal(typeof uninstall, 'function'); 27 | }); 28 | 29 | it('install rejects on missing options', async () => { 30 | // @ts-ignore 31 | await assert.rejects(async () => install(), /options is required/); 32 | // @ts-ignore 33 | await assert.rejects(async () => install({}), /options.name is required/); 34 | await assert.rejects( 35 | // @ts-ignore 36 | async () => install({ name: 'foo ' }), 37 | /options.completer is required/ 38 | ); 39 | 40 | await assert.rejects( 41 | // @ts-ignore 42 | async () => install({ name: 'foo ', completer: 'foo-complete' }), 43 | /options.location is required/ 44 | ); 45 | }); 46 | 47 | it('uninstall rejects on missing options', async () => { 48 | await assert.rejects( 49 | // @ts-ignore 50 | async () => uninstall(), 51 | /options is required/, 52 | 'Uninstall should throw the expected message when name is missing' 53 | ); 54 | await assert.rejects( 55 | // @ts-ignore 56 | async () => uninstall({}), 57 | /Unable to uninstall if options.name is missing/, 58 | 'Uninstall should throw the expected message when name is missing' 59 | ); 60 | }); 61 | 62 | it('has writeToShellConfig / writeToCompletionScript functions', () => { 63 | assert.equal(typeof writeToShellConfig, 'function'); 64 | assert.equal(typeof writeToCompletionScript, 'function'); 65 | }); 66 | 67 | describe('installer on ~/.bashrc', () => { 68 | setupSuiteForInstall(true); 69 | 70 | before(async () => { 71 | const bashDir = untildify(path.join(COMPLETION_DIR, 'bash')); 72 | await mkdir(bashDir, { recursive: true }); 73 | // Make sure __tabtab.bash starts with empty content, it'll be restored by setupSuiteForInstall 74 | await writeFile(path.join(bashDir, tabtabFileName('bash')), ''); 75 | }); 76 | 77 | it('installs the necessary line into ~/.bashrc', () => 78 | install({ 79 | name: 'foo', 80 | completer: 'foo-complete', 81 | location: '~/.bashrc', 82 | shell: 'bash' 83 | }) 84 | .then(() => readFile(untildify('~/.bashrc'), 'utf8')) 85 | .then(filecontent => { 86 | assert.ok(/tabtab source for packages/.test(filecontent)); 87 | assert.ok(/uninstall by removing these lines/.test(filecontent)); 88 | assert.ok( 89 | filecontent.match( 90 | `. ${path.join(COMPLETION_DIR, 'bash/__tabtab.bash').replaceAll('\\', '/')}` 91 | ) 92 | ); 93 | }) 94 | .then(() => 95 | readFile( 96 | untildify(path.join(COMPLETION_DIR, 'bash/__tabtab.bash')), 97 | 'utf8' 98 | ) 99 | ) 100 | .then(filecontent => { 101 | assert.ok(/tabtab source for foo/.test(filecontent)); 102 | assert.ok( 103 | filecontent.match(`. ${path.join(COMPLETION_DIR, 'bash/foo.bash').replaceAll('\\', '/')}`) 104 | ); 105 | })); 106 | 107 | it('uninstalls the necessary line from ~/.bashrc and completion scripts', () => 108 | uninstall({ 109 | name: 'foo', 110 | shell: 'bash' 111 | }) 112 | .then(() => readFile(untildify('~/.bashrc'), 'utf8')) 113 | .then(filecontent => { 114 | assert.ok(!/tabtab source for packages/.test(filecontent)); 115 | assert.ok(!/uninstall by removing these lines/.test(filecontent)); 116 | assert.ok( 117 | !filecontent.match( 118 | `. ${path.join(COMPLETION_DIR, 'bash/__tabtab.bash')}` 119 | ) 120 | ); 121 | }) 122 | .then(() => 123 | readFile( 124 | untildify(path.join(COMPLETION_DIR, 'bash/__tabtab.bash')), 125 | 'utf8' 126 | ) 127 | ) 128 | .then(filecontent => { 129 | assert.ok(!/tabtab source for foo/.test(filecontent)); 130 | assert.ok( 131 | !filecontent.match( 132 | `. ${path.join(COMPLETION_DIR, 'bash/foo.bash')}` 133 | ) 134 | ); 135 | })); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/isShellSupported.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { isShellSupported, SUPPORTED_SHELLS } = require('..'); 3 | 4 | describe('isShellSupported', () => { 5 | it('returns true for supported shells', () => { 6 | assert.deepStrictEqual( 7 | SUPPORTED_SHELLS.filter(shell => isShellSupported(shell)), 8 | SUPPORTED_SHELLS, 9 | ); 10 | }) 11 | 12 | it('returns false for unsupported shells', () => { 13 | assert.strictEqual(isShellSupported('unknown'), false); 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/logCompletion.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const tabtab = require('..'); 3 | 4 | describe('tabtab.log', () => { 5 | it('tabtab.log throws an Error in case args is not an Array', () => { 6 | assert.throws(() => { 7 | // @ts-ignore 8 | tabtab.log('foo', 'bar'); 9 | }, /^Error: log: Invalid arguments, must be an array$/); 10 | }); 11 | 12 | const logTestHelper = (items, shell) => { 13 | const logs = []; 14 | const log = message => { 15 | logs.push(message); 16 | } 17 | tabtab.log(items, shell, log); 18 | return logs; 19 | }; 20 | 21 | it('tabtab.log logs item to the console', () => { 22 | assert.equal(typeof tabtab.log, 'function'); 23 | 24 | const logs = logTestHelper(['--foo', '--bar'], 'bash'); 25 | 26 | assert.equal(logs.length, 2); 27 | assert.deepStrictEqual(logs, ['--foo', '--bar']); 28 | }); 29 | 30 | it('tabtab.log accepts { name, description }', () => { 31 | const logs = logTestHelper([ 32 | { name: '--foo', description: 'Foo options' }, 33 | { name: '--bar', description: 'Bar options' } 34 | ], 'zsh'); 35 | 36 | assert.deepStrictEqual(logs, [ 37 | '--foo:Foo options', 38 | '--bar:Bar options', 39 | ]); 40 | }); 41 | 42 | it('tabtab.log normalize String and Objects', () => { 43 | const logs = logTestHelper([ 44 | { name: '--foo', description: 'Foo options' }, 45 | { name: '--bar', description: 'Bar options' }, 46 | 'foobar' 47 | ], 'zsh'); 48 | 49 | assert.deepStrictEqual(logs, [ 50 | '--foo:Foo options', 51 | '--bar:Bar options', 52 | 'foobar', 53 | ]); 54 | }); 55 | 56 | it('tabtab.log normalize String and Objects, with description stripped out on Bash', () => { 57 | const logs = logTestHelper([ 58 | { name: '--foo', description: 'Foo options' }, 59 | { name: '--bar', description: 'Bar option' }, 60 | 'foobar', 61 | 'barfoo:barfoo is not foobar' 62 | ], 'bash'); 63 | 64 | assert.equal(logs.length, 4); 65 | assert.deepStrictEqual(logs, ['--foo', '--bar', 'foobar', 'barfoo']); 66 | }); 67 | 68 | it('tabtab.log with description NOT stripped out on Zsh', () => { 69 | const logs = logTestHelper([ 70 | { name: '--foo', description: 'Foo option' }, 71 | { name: '--bar', description: 'Bar option' }, 72 | 'foobar', 73 | 'barfoo:barfoo is not foobar' 74 | ], 'zsh'); 75 | 76 | assert.equal(logs.length, 4); 77 | assert.deepStrictEqual(logs, [ 78 | '--foo:Foo option', 79 | '--bar:Bar option', 80 | 'foobar', 81 | 'barfoo:barfoo is not foobar' 82 | ]); 83 | }); 84 | 85 | it('tabtab.log with description NOT stripped out on fish', () => { 86 | const logs = logTestHelper([ 87 | { name: '--foo', description: 'Foo option' }, 88 | { name: '--bar', description: 'Bar option' }, 89 | 'foobar', 90 | 'barfoo:barfoo is not foobar' 91 | ], 'fish'); 92 | 93 | assert.equal(logs.length, 4); 94 | assert.deepStrictEqual(logs, [ 95 | '--foo\tFoo option', 96 | '--bar\tBar option', 97 | 'foobar', 98 | 'barfoo\tbarfoo is not foobar' 99 | ]); 100 | }); 101 | 102 | it('tabtab.log could use {name, description} for completions with ":" in them', () => { 103 | const logs = logTestHelper([ 104 | { name: '--foo:bar', description: 'Foo option' }, 105 | { name: '--bar:foo', description: 'Bar option' }, 106 | 'foobar', 107 | 'barfoo:barfoo is not foobar' 108 | ], 'zsh'); 109 | 110 | assert.equal(logs.length, 4); 111 | assert.deepStrictEqual(logs, [ 112 | '--foo\\:bar:Foo option', 113 | '--bar\\:foo:Bar option', 114 | 'foobar', 115 | 'barfoo:barfoo is not foobar' 116 | ]); 117 | }); 118 | 119 | it('tabtab.log should escape ":" when name is given as an object without description', () => { 120 | const logs = logTestHelper([ 121 | 'foo:bar', 122 | { name: 'foo:bar' }, 123 | { name: 'foo:bar', description: 'A command' }, 124 | { name: 'foo:bar', description: 'The foo:bar command' } 125 | ], 'zsh'); 126 | 127 | assert.deepStrictEqual(logs, [ 128 | 'foo:bar', 129 | 'foo\\:bar', 130 | 'foo\\:bar:A command', 131 | 'foo\\:bar:The foo\\:bar command' 132 | ]); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /test/parse-env.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const tabtab = require('..'); 3 | 4 | describe('tabtab.parseEnv()', () => { 5 | it('parseEnv with COMP stuff', () => { 6 | assert.equal(typeof tabtab.parseEnv, 'function'); 7 | 8 | const result = tabtab.parseEnv( 9 | Object.assign({}, process.env, { 10 | COMP_CWORD: 3, 11 | COMP_LINE: 'foo bar baz', 12 | COMP_POINT: 11 13 | }) 14 | ); 15 | 16 | assert.deepEqual(result, { 17 | complete: true, 18 | words: 3, 19 | point: 11, 20 | line: 'foo bar baz', 21 | partial: 'foo bar baz', 22 | last: 'baz', 23 | lastPartial: 'baz', 24 | prev: 'bar' 25 | }); 26 | }); 27 | 28 | it('parseEnv without COMP stuff', () => { 29 | const result = tabtab.parseEnv(Object.assign({}, process.env)); 30 | assert.equal(result.complete, false); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/tabtab-install.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const run = require('inquirer-test'); 3 | const debug = require('debug')('tabtab:test:install'); 4 | const untildify = require('untildify'); 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const { promisify } = require('util'); 8 | const tabtab = require('..'); 9 | const { COMPLETION_DIR } = require('../lib/constants'); 10 | const { tabtabFileName } = require('../lib/filename'); 11 | const { rejects, setupSuiteForInstall } = require('./utils'); 12 | 13 | const readFile = promisify(fs.readFile); 14 | const writeFile = promisify(fs.writeFile); 15 | const mkdir = promisify(fs.mkdir); 16 | 17 | // For node 7 / 8 18 | assert.rejects = rejects; 19 | 20 | // inquirer-test needs a little bit more time, or my setup 21 | const TIMEOUT = 500; 22 | const { ENTER } = run; 23 | 24 | describe('tabtab.install()', () => { 25 | it('is a function', () => { 26 | assert.equal(typeof tabtab.install, 'function'); 27 | }); 28 | 29 | it('rejects on missing options', async () => { 30 | // @ts-ignore 31 | await assert.rejects(async () => tabtab.install(), TypeError); 32 | }); 33 | 34 | it('rejects on missing name options', async () => { 35 | await assert.rejects( 36 | // @ts-ignore 37 | async () => tabtab.install({}), 38 | /options\.name is required/ 39 | ); 40 | }); 41 | 42 | it('rejects on missing completer options', async () => { 43 | await assert.rejects( 44 | // @ts-ignore 45 | async () => tabtab.install({ name: 'foo' }), 46 | /options\.completer is required/ 47 | ); 48 | }); 49 | 50 | it('rejects on unknown shell target', async () => { 51 | await assert.rejects( 52 | async () => 53 | tabtab.install({ name: 'foo', completer: 'foo', shell: /** @type {any} */ ('unknown') }), 54 | /Couldn't find shell location for unknown/ 55 | ); 56 | }); 57 | 58 | it('installs to the passed in shell', async () => { 59 | const bashDir = untildify(path.join(COMPLETION_DIR, 'bash')); 60 | await mkdir(bashDir, { recursive: true }); 61 | // Make sure __tabtab.bash starts with empty content, it'll be restored by setupSuiteForInstall 62 | await writeFile(path.join(bashDir, tabtabFileName('bash')), ''); 63 | 64 | await tabtab.install({ name: 'foo', completer: 'foo', shell: 'bash' }); 65 | 66 | const filecontent = await readFile( 67 | untildify(path.join(COMPLETION_DIR, 'bash/__tabtab.bash')), 68 | 'utf8' 69 | ); 70 | assert.ok(/tabtab source for foo/.test(filecontent)); 71 | assert.ok( 72 | filecontent.match(`. ${path.join(COMPLETION_DIR, 'bash/foo.bash').replaceAll('\\', '/')}`) 73 | ); 74 | }); 75 | 76 | describe.skip('tabtab.install() on ~/.bashrc', () => { 77 | setupSuiteForInstall(); 78 | 79 | it('asks about shell (bash) with custom location', () => { 80 | const cliPath = path.join(__dirname, 'fixtures/tabtab-install.js'); 81 | 82 | return run( 83 | [cliPath], 84 | [ENTER, 'n', ENTER, '/tmp/foo', ENTER], 85 | TIMEOUT 86 | ).then(result => { 87 | debug('Test result', result); 88 | 89 | assert.ok(/Which Shell do you use \? bash/.test(result)); 90 | assert.ok( 91 | /We will install completion to ~\/\.bashrc, is it ok \?/.test(result) 92 | ); 93 | assert.ok(/Which path then \? Must be absolute/.test(result)); 94 | assert.ok(/Very well, we will install using \/tmp\/foo/.test(result)); 95 | }); 96 | }); 97 | 98 | it('asks about shell (bash) with default location', () => { 99 | const cliPath = path.join(__dirname, 'fixtures/tabtab-install.js'); 100 | 101 | return run([cliPath], [ENTER, ENTER], TIMEOUT) 102 | .then(result => { 103 | debug('Test result', result); 104 | 105 | assert.ok(/Which Shell do you use \? bash/.test(result)); 106 | assert.ok( 107 | /install completion to ~\/\.bashrc, is it ok \? Yes/.test(result) 108 | ); 109 | }) 110 | .then(() => readFile(untildify('~/.bashrc'), 'utf8')) 111 | .then(filecontent => { 112 | assert.ok(/tabtab source for packages/.test(filecontent)); 113 | assert.ok(/uninstall by removing these lines/.test(filecontent)); 114 | assert.ok( 115 | filecontent.match( 116 | `. ${path.join(COMPLETION_DIR, 'bash/__tabtab.bash')}` 117 | ) 118 | ); 119 | }); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.common.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const untildify = require('untildify'); 4 | const { promisify } = require('util'); 5 | const { COMPLETION_DIR } = require('../../lib/constants'); 6 | const { tabtabFileName } = require('../../lib/filename'); 7 | 8 | const { exists } = require('../../lib/utils'); 9 | 10 | const writeFile = promisify(fs.writeFile); 11 | const readFile = promisify(fs.readFile); 12 | 13 | /** 14 | * Returns both { exists, content } 15 | * 16 | * @param {String} filename - The file to check and read 17 | */ 18 | const readIfExists = async filename => { 19 | /* eslint-disable no-return-await */ 20 | const filepath = untildify(filename); 21 | const fileExists = await exists(filepath); 22 | const content = fileExists ? await readFile(filepath, 'utf8') : ''; 23 | 24 | return { 25 | exists: fileExists, 26 | content 27 | }; 28 | }; 29 | 30 | const afterWrites = (prevBashrc, prevScript) => async () => { 31 | const bashrc = untildify('~/.bashrc'); 32 | const tabtabScript = untildify( 33 | path.join(COMPLETION_DIR, tabtabFileName('bash')) 34 | ); 35 | 36 | await writeFile(bashrc, prevBashrc); 37 | await writeFile(tabtabScript, prevScript); 38 | }; 39 | 40 | /** This simply setup a suite with after hook for tabtab.install. 41 | * 42 | * Defaults to afterEach, pass in true to make it so that it uses "after" 43 | * instead. 44 | * 45 | * @param {Boolean} shouldUseAfter - True to use after instead of afterEach 46 | */ 47 | const setupSuiteForInstall = async (shouldUseAfter = false) => { 48 | const files = {}; 49 | const hook = shouldUseAfter ? after : afterEach; 50 | const tabtabScript = path.join(COMPLETION_DIR, tabtabFileName('bash')); 51 | 52 | before(async () => { 53 | const { exists: bashrcExists, content: bashrcContent } = await readIfExists( 54 | '~/.bashrc' 55 | ); 56 | 57 | const { 58 | exists: tabtabScriptExists, 59 | content: tabtabScriptContent 60 | } = await readIfExists(tabtabScript); 61 | 62 | files.bashrcExists = bashrcExists; 63 | files.bashrcContent = bashrcContent; 64 | files.tabtabScriptExists = tabtabScriptExists; 65 | files.tabtabScriptContent = tabtabScriptContent; 66 | }); 67 | 68 | hook(async () => { 69 | const { 70 | bashrcExists, 71 | bashrcContent, 72 | tabtabScriptExists, 73 | tabtabScriptContent 74 | } = files; 75 | 76 | if (bashrcExists) { 77 | await writeFile(untildify('~/.bashrc'), bashrcContent); 78 | } 79 | 80 | if (tabtabScriptExists) { 81 | await writeFile(untildify(tabtabScript), tabtabScriptContent); 82 | } 83 | }); 84 | }; 85 | 86 | // For node 7 / 8 87 | const rejects = async (promise, error, message = '') => { 88 | let toThrow; 89 | await promise().catch(err => { 90 | if (error instanceof RegExp) { 91 | const ok = error.test(err.message); 92 | if (!ok) { 93 | toThrow = new Error( 94 | `AssertionError: ${error} is not validated. Got ${err.message} 95 | ${message}` 96 | ); 97 | } 98 | } else { 99 | const ok = err instanceof error; 100 | if (!ok) { 101 | toThrow = new Error( 102 | `AssertionError: ${err.name} is not an instanceof ${error.name} 103 | ${message}` 104 | ); 105 | } 106 | } 107 | }); 108 | 109 | if (toThrow) { 110 | throw toThrow; 111 | } 112 | }; 113 | 114 | module.exports = { 115 | readIfExists, 116 | rejects, 117 | afterWrites, 118 | setupSuiteForInstall 119 | }; 120 | -------------------------------------------------------------------------------- /tsconfig.common.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "module": "Node16", 6 | "strictNullChecks": true, 7 | "target": "ES2022", 8 | } 9 | } 10 | --------------------------------------------------------------------------------