├── .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 |
7 | - SHELL (bash, zsh or fish)
8 | - Path to shell script (with sensible defaults)
9 |
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 |
46 | - Writes to SHELL config file, adding a new line to tabtab internal script.
47 | - Creates or edit tabtab internal script
48 | - Creates the actual completion script for this package.
49 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------