Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add autocompletion for fish shell #5

Closed
2 changes: 1 addition & 1 deletion src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export abstract class AutocompleteBase extends Command {
this.error('Missing required argument shell')
}
this.errorIfWindows()
if (!['bash', 'zsh'].includes(shell)) {
if (!['bash', 'fish', 'zsh'].includes(shell)) {
throw new Error(`${shell} is not a supported shell for autocomplete`)
}
}
Expand Down
51 changes: 51 additions & 0 deletions src/commands/autocomplete/create.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as child_process from 'child_process'
import * as fs from 'fs-extra'
import * as path from 'path'

Expand Down Expand Up @@ -39,6 +40,10 @@ export default class Create extends AutocompleteBase {
await fs.writeFile(this.bashCompletionFunctionPath, this.bashCompletionFunction)
await fs.writeFile(this.zshSetupScriptPath, this.zshSetupScript)
await fs.writeFile(this.zshCompletionFunctionPath, this.zshCompletionFunction)
if (this.config.shell === "fish") {
debug(`fish shell detected, writing completion to ${this.fishCompletionFunctionPath}`);
await fs.writeFile(this.fishCompletionFunctionPath, this.fishCompletionFunction)
}
}

private get bashSetupScriptPath(): string {
Expand Down Expand Up @@ -66,6 +71,12 @@ export default class Create extends AutocompleteBase {
return path.join(this.bashFunctionsDir, `${this.cliBin}.bash`)
}

private get fishCompletionFunctionPath(): string {
// dynamically load path to completions file
const dir = child_process.execSync('pkg-config --variable completionsdir fish').toString().trimRight()
return `${dir}/${this.cliBin}.fish`
}

private get zshCompletionFunctionPath(): string {
// <cachedir>/autocomplete/functions/zsh/_<bin>
return path.join(this.zshFunctionsDir, `_${this.cliBin}`)
Expand Down Expand Up @@ -222,6 +233,46 @@ complete -F _${cliBin} ${cliBin}
`
}

private get fishCompletionFunction(): string {
const cliBin = this.cliBin
const completions = []
completions.push(`
function __fish_${cliBin}_needs_command
set cmd (commandline -opc)
if [ (count $cmd) -eq 1 ]
return 0
else
return 1
end
end

function __fish_${cliBin}_using_command
set cmd (commandline -opc)
if [ (count $cmd) -gt 1 ]
if [ $argv[1] = $cmd[2] ]
return 0
end
end
return 1
end`
)

for (const command of this.commands) {
completions.push(`complete -f -c ${cliBin} -n '__fish_${cliBin}_needs_command' -a ${command.id} -d "${command.description}"`)
const flags = command.flags || {}
Object.keys(flags)
.filter(flag => flags[flag] && !flags[flag].hidden)
.forEach(flag => {
const f = flags[flag] || {}
const shortFlag = f.char ? `-s ${f.char}` : ''
const description = f.description ? `-d "${f.description}"` : ''
const options = f.options ? `-r -a "${f.options.join(' ')}"` : ''
completions.push(`complete -f -c ${cliBin} -n ' __fish_${cliBin}_using_command ${command.id}' -l ${flag} ${shortFlag} ${options} ${description}`)
})
}
return completions.join('\n')
}

private get zshCompletionFunction(): string {
const cliBin = this.cliBin
const allCommandsMeta = this.genAllCommandsMetaString
Expand Down
48 changes: 44 additions & 4 deletions src/commands/autocomplete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export default class Index extends AutocompleteBase {
static examples = [
'$ <%= config.bin %> autocomplete',
'$ <%= config.bin %> autocomplete bash',
'$ <%= config.bin %> autocomplete fish',
'$ <%= config.bin %> autocomplete zsh',
'$ <%= config.bin %> autocomplete --refresh-cache'
]
Expand All @@ -33,14 +34,14 @@ export default class Index extends AutocompleteBase {

if (!flags['refresh-cache']) {
const bin = this.config.bin
let tabStr = shell === 'bash' ? '<TAB><TAB>' : '<TAB>'
let note = shell === 'zsh' ? `After sourcing, you can run \`${chalk.cyan('$ compaudit -D')}\` to ensure no permissions conflicts are present` : 'If your terminal starts as a login shell you may need to print the init script into ~/.bash_profile or ~/.profile.'
const tabStr = this.getTabStr(shell)
const setupStep = this.getSetupStep(shell, bin)
const note = this.getNote(shell)

this.log(`
${chalk.bold(`Setup Instructions for ${bin.toUpperCase()} CLI Autocomplete ---`)}

1) Add the autocomplete env var to your ${shell} profile and source it
${chalk.cyan(`$ printf "$(${bin} autocomplete:script ${shell})" >> ~/.${shell}rc; source ~/.${shell}rc`)}
1) ${setupStep}

NOTE: ${note}

Expand All @@ -52,4 +53,43 @@ Enjoy!
`)
}
}

private getSetupStep(shell: string, bin: string): string {
switch (shell) {
case 'bash':
case 'zsh':
return `Add the autocomplete env var to your ${shell} profile and source it
${chalk.cyan(`$ printf "$(${bin} autocomplete:script ${shell})" >> ~/.${shell}rc; source ~/.${shell}rc`)}`
case 'fish':
return `Update your shell to load the new completions
${chalk.cyan('source ~/.config/fish/config.fish')}`
default:
return ''
}
}

private getNote(shell: string): string {
switch (shell) {
case 'bash':
return 'If your terminal starts as a login shell you may need to print the init script into ~/.bash_profile or ~/.profile.'
case 'fish':
return 'This assumes your Fish configuration is stored at ~/.config/fish/config.fish'
case 'zsh':
return `After sourcing, you can run \`${chalk.cyan('$ compaudit -D')}\` to ensure no permissions conflicts are present`
default:
return ''
}
}

private getTabStr(shell: string): string {
switch (shell) {
case 'bash':
return '<TAB><TAB>'
case 'fish':
case 'zsh':
return '<TAB>'
default:
return ''
}
}
}