Skip to content

Commit 280db1b

Browse files
committed
feat: fish autocomplete
Signed-off-by: loks0n <[email protected]>
1 parent f48f563 commit 280db1b

File tree

3 files changed

+102
-1
lines changed

3 files changed

+102
-1
lines changed

packages/auto-complete/src/commands.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export const installCommand = buildCommand({
2525
optional: true,
2626
placeholder: "command",
2727
},
28+
fish: {
29+
kind: "parsed",
30+
brief: "Command executed by fish to generate completion proposals",
31+
parse: String,
32+
optional: true,
33+
placeholder: "command",
34+
},
2835
},
2936
positional: {
3037
kind: "tuple",
@@ -67,6 +74,11 @@ export const uninstallCommand = buildCommand({
6774
brief: "Uninstall autocompletion for bash",
6875
optional: true,
6976
},
77+
fish: {
78+
kind: "boolean",
79+
brief: "Uninstall autocompletion for fish",
80+
optional: true,
81+
},
7082
},
7183
positional: {
7284
kind: "tuple",

packages/auto-complete/src/impl.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
// Distributed under the terms of the Apache 2.0 license.
33
import type { StricliAutoCompleteContext } from "./context";
44
import { forBash } from "./shells/bash";
5+
import { forFish } from "./shells/fish";
56

67
export interface ShellManager {
78
readonly install: (targetCommand: string, autocompleteCommand: string) => Promise<void>;
89
readonly uninstall: (targetCommand: string) => Promise<void>;
910
}
1011

11-
export type Shell = "bash";
12+
export type Shell = "bash" | "fish";
1213

1314
export type ShellAutoCompleteCommands = Readonly<Partial<Record<Shell, string>>>;
1415

@@ -21,6 +22,10 @@ export async function install(
2122
const bash = await forBash(this);
2223
await bash?.install(targetCommand, flags.bash);
2324
}
25+
if (flags.fish) {
26+
const fish = await forFish(this);
27+
await fish?.install(targetCommand, flags.fish);
28+
}
2429
}
2530

2631
export type ActiveShells = Readonly<Partial<Record<Shell, boolean>>>;
@@ -34,4 +39,8 @@ export async function uninstall(
3439
const shellManager = await forBash(this);
3540
await shellManager?.uninstall(targetCommand);
3641
}
42+
if (flags.fish) {
43+
const shellManager = await forFish(this);
44+
await shellManager?.uninstall(targetCommand);
45+
}
3746
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2024 Bloomberg Finance L.P.
2+
// Distributed under the terms of the Apache 2.0 license.
3+
import type { StricliAutoCompleteContext } from "../context";
4+
import type { ShellManager } from "../impl";
5+
import nodeOs from "node:os";
6+
import nodeFs from "node:fs";
7+
import nodePath from "node:path";
8+
9+
function getFilePath(targetCommand: string, completionsDir: string): string {
10+
return nodePath.join(completionsDir, `${targetCommand}.fish`);
11+
}
12+
13+
const BLOCK_START_LINE = (targetCommand: string) =>
14+
`# @stricli/auto-complete START [${targetCommand}]`;
15+
const BLOCK_END_LINE = "# @stricli/auto-complete END";
16+
17+
export async function forFish(
18+
context: StricliAutoCompleteContext
19+
): Promise<ShellManager | undefined> {
20+
const { os = nodeOs, fs = nodeFs, path = nodePath } = context;
21+
if (!context.process.env["SHELL"]?.includes("fish")) {
22+
context.process.stderr.write(`Skipping fish as shell was not detected.\n`);
23+
return;
24+
}
25+
const home = os.homedir();
26+
const completionsDir = path.join(home, ".config", "fish", "completions");
27+
try {
28+
await fs.promises.mkdir(completionsDir, { recursive: true });
29+
} catch {
30+
context.process.stderr.write(
31+
`Could not create fish completions directory at ${completionsDir}.\n`
32+
);
33+
return;
34+
}
35+
return {
36+
install: async (targetCommand: string, autocompleteCommand: string) => {
37+
const filePath = getFilePath(targetCommand, completionsDir);
38+
const functionName = `__fish_${targetCommand}_complete`;
39+
// Create a fish completions file that defines a helper function and registers it.
40+
const fileContent = [
41+
BLOCK_START_LINE(targetCommand),
42+
`function ${functionName}`,
43+
` # Get the current command line (as a single string)`,
44+
` set -l cmd (commandline -cp)`,
45+
` # Invoke the provided autocomplete command with the command line`,
46+
` set -l completions ( ${autocompleteCommand} $cmd )`,
47+
` for comp in $completions`,
48+
` echo $comp`,
49+
` end`,
50+
`end`,
51+
`complete -c ${targetCommand} -f -a "(${functionName})"`,
52+
BLOCK_END_LINE,
53+
"",
54+
].join("\n");
55+
try {
56+
await fs.promises.writeFile(filePath, fileContent);
57+
context.process.stdout.write(
58+
`Fish completions installed to ${filePath}. Restart fish shell or run 'source ${filePath}' to load changes.\n`
59+
);
60+
} catch {
61+
context.process.stderr.write(
62+
`Failed to write fish completions file at ${filePath}.\n`
63+
);
64+
}
65+
},
66+
uninstall: async (targetCommand: string) => {
67+
const filePath = getFilePath(targetCommand, completionsDir);
68+
try {
69+
await fs.promises.unlink(filePath);
70+
context.process.stdout.write(
71+
`Fish completions removed from ${filePath}. Restart fish shell or run 'source ${filePath}' to update changes.\n`
72+
);
73+
} catch {
74+
context.process.stderr.write(
75+
`Could not remove fish completions file at ${filePath}.\n`
76+
);
77+
}
78+
},
79+
};
80+
}

0 commit comments

Comments
 (0)