Skip to content

Commit 08d634a

Browse files
committed
docs
1 parent 8caf381 commit 08d634a

File tree

2 files changed

+180
-16
lines changed

2 files changed

+180
-16
lines changed

README.md

Lines changed: 147 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,160 @@
22

33
# tab
44

5-
- [x] zsh test in git
5+
Shell autocompletions are largely missing in the javascript cli ecosystem. This tool is an attempt to make autocompletions come out of the box for any cli tool.
66

7-
```zsh
8-
source <(pnpm tsx demo.ts complete zsh)
7+
Tools like git and their autocompletion experience inspired us to build this tool and make the same ability available for any javascript cli project. Developers love hitting the tab key, hence why they prefer tabs over spaces.
8+
9+
```ts
10+
import { Completion, script } from "@bombsh/tab";
11+
12+
const name = "vite"
13+
const completion = new Completion()
14+
15+
completion.addCommand("start", "Start the application", async (previousArgs, toComplete, endsWithSpace) => {
16+
// suggestions
17+
return [
18+
{ value: "dev", description: "Start in development mode" },
19+
{ value: "prod", description: "Start in production mode" }
20+
]
21+
})
922

10-
vite # rest of the completions
23+
completion.addOption("start", "--port", "Specify the port number", async (previousArgs, toComplete, endsWithSpace) => {
24+
return [
25+
{ value: "3000", description: "Development port" },
26+
{ value: "8080", description: "Production port" }
27+
]
28+
})
1129

12-
pnpm tsx demo.ts complete -- --po
30+
31+
// a way of getting the executable path to pass to the shell autocompletion script
32+
function quoteIfNeeded(path: string) {
33+
return path.includes(" ") ? `'${path}'` : path;
34+
}
35+
const execPath = process.execPath;
36+
const processArgs = process.argv.slice(1);
37+
const quotedExecPath = quoteIfNeeded(execPath);
38+
const quotedProcessArgs = processArgs.map(quoteIfNeeded);
39+
const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded);
40+
const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`;
41+
42+
if (process.argv[2] === "--") {
43+
// autocompletion logic
44+
await completion.parse(process.argv.slice(2), "start") // TODO: remove "start"
45+
} else {
46+
// process.argv[2] can be "zsh", "bash", "fish", "powershell"
47+
script(process.argv[2], name, x)
48+
}
1349
```
1450

15-
- [x] tests vitest (this should mostly test the completions array, e.g. logs)
16-
- [x] powershell completions generation
17-
- [x] citty support `@bomsh/tab/citty`
18-
- [] `@bombsh/tab`
51+
Now your user can run `source <(vite complete zsh)` and they will get completions for the `vite` command using the [autocompletion server](#autocompletion-server).
1952

20-
- [] fish
21-
- [] bash
53+
## Adapters
54+
55+
Since we are heavy users of tools like `cac` and `citty`, we have created adapters for both of them. Ideally, tab would be integrated internally into these tools, but for now, this is a good compromise.
56+
57+
### `@bombsh/tab/cac`
2258

2359
```ts
24-
const completion = new Completion()
25-
completion.addCommand()
26-
completion.addOption()
60+
import cac from "cac";
61+
import tab from "@bombsh/tab/cac";
62+
63+
const cli = cac("vite");
64+
65+
cli
66+
.command("dev", "Start dev server")
67+
.option("--port <port>", "Specify port");
68+
69+
const completion = tab(cli);
70+
71+
// Get the dev command completion handler
72+
const devCommandCompletion = completion.commands.get("dev");
73+
74+
// Get and configure the port option completion handler
75+
const portOptionCompletion = devCommandCompletion.options.get("--port");
76+
portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => {
77+
return [
78+
{ value: "3000", description: "Development port" },
79+
{ value: "8080", description: "Production port" }
80+
]
81+
}
82+
83+
cli.parse();
84+
```
85+
Now autocompletion will be available for any specified command and option in your cac instance. If your user writes `vite dev --po`, they will get suggestions for the `--port` option. Or if they write `vite d` they will get suggestions for the `dev` command.
86+
87+
Suggestions are missing in the adapters since yet cac or citty do not have a way to provide suggestions (tab just came out!), we'd have to provide them manually. Mutations do not hurt in this situation.
88+
89+
### `@bombsh/tab/citty`
90+
91+
```ts
92+
import citty, { defineCommand, createMain } from "citty";
93+
import tab from "@bombsh/tab/citty";
94+
95+
const main = defineCommand({
96+
meta: {
97+
name: "vite",
98+
description: "Vite CLI tool",
99+
},
100+
});
101+
102+
const devCommand = defineCommand({
103+
meta: {
104+
name: "dev",
105+
description: "Start dev server",
106+
},
107+
args: {
108+
port: { type: "string", description: "Specify port" },
109+
}
110+
});
111+
112+
main.subCommands = {
113+
dev: devCommand
114+
};
115+
116+
const completion = await tab(main);
117+
118+
// TODO: addHandler function to export
119+
const devCommandCompletion = completion.commands.get("dev");
27120

28-
// better name
29-
completion.parse()
121+
const portOptionCompletion = devCommandCompletion.options.get("--port");
122+
123+
portOptionCompletion.handler = async (previousArgs, toComplete, endsWithSpace) => {
124+
return [
125+
{ value: "3000", description: "Development port" },
126+
{ value: "8080", description: "Production port" }
127+
]
128+
}
129+
130+
const cli = createMain(main);
131+
cli();
30132
```
133+
134+
## Autocompletion Server
135+
136+
By integrating tab into your cli, your cli would have a new command called `complete`. This is where all the magic happens. And the shell would contact this command to get completions. That's why we call it the autocompletion server.
137+
138+
```zsh
139+
vite complete -- --po
140+
--port Specify the port number
141+
:0
142+
```
143+
144+
The autocompletion server can be a standard to identify whether a package provides autocompletions. Whether running `tool complete --` would result in an output that ends with `:{Number}` (matching the pattern `/:\d+$/`).
145+
146+
In situations like `vite dev --po` you'd have autocompletions! But in the case of `pnpm vite dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm.
147+
148+
Since pnpm already has its own autocompletion [script](https://pnpm.io/completion), this provides the opportunity to check whether a package provides autocompletions and use those autocompletions if available.
149+
150+
This would also have users avoid injecting autocompletions in their shell config for any tool that provides its own autocompletion script, since pnpm would already support proxying the autocompletions out of the box.
151+
152+
Other package managers like `npm` and `yarn` can decide whether to support this or not too for more universal support.
153+
154+
## Inspiration
155+
- git
156+
- [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go)
157+
158+
159+
## TODO
160+
- [] fish
161+
- [] bash

src/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import * as zsh from "./zsh";
2+
import * as bash from "./bash";
3+
import * as fish from "./fish";
4+
import * as powershell from "./powershell";
5+
16
// ShellCompRequestCmd is the name of the hidden command that is used to request
27
// completion results from the program. It is used by the shell completion scripts.
38
export const ShellCompRequestCmd: string = "__complete";
@@ -208,3 +213,31 @@ export class Completion {
208213
console.log(`:${directive}`);
209214
}
210215
}
216+
217+
export function script(shell: "zsh" | "bash" | "fish" | "powershell", name: string, x: string) {
218+
switch (shell) {
219+
case "zsh": {
220+
const script = zsh.generate(name, x);
221+
console.log(script);
222+
break;
223+
}
224+
case "bash": {
225+
const script = bash.generate(name, x);
226+
console.log(script);
227+
break;
228+
}
229+
case "fish": {
230+
const script = fish.generate(name, x);
231+
console.log(script);
232+
break;
233+
}
234+
case "powershell": {
235+
const script = powershell.generate(name, x);
236+
console.log(script);
237+
break;
238+
}
239+
default: {
240+
throw new Error(`Unsupported shell: ${shell}`);
241+
}
242+
}
243+
}

0 commit comments

Comments
 (0)