Skip to content

Commit 92ff222

Browse files
committed
feat: add completion command
1 parent 7a64fb8 commit 92ff222

File tree

6 files changed

+292
-39
lines changed

6 files changed

+292
-39
lines changed

README.md

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -120,55 +120,32 @@ you can set the configuration variable `skipUpdateWhenPageNotFound` to `true` (d
120120

121121
## Command-line Autocompletion
122122

123-
Currently we only support command-line autocompletion for zsh
124-
and bash. Pull requests for other shells are most welcome!
123+
We currently support command-line autocompletion for zsh and bash.
124+
Pull requests for other shells are most welcome!
125125

126-
### zsh
127-
128-
It's easiest for
129-
[oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh)
130-
users, so let's start with that.
131-
132-
```zsh
133-
mkdir -p $ZSH_CUSTOM/plugins/tldr
134-
ln -s bin/completion/zsh/_tldr $ZSH_CUSTOM/plugins/tldr/_tldr
135-
```
136-
137-
Then add tldr to your oh-my-zsh plugins,
138-
usually defined in `~/.zshrc`,
139-
resulting in something looking like this:
140-
141-
```zsh
142-
plugins=(git tmux tldr)
143-
```
126+
To enable autocompletion for the tldr command, run:
144127

145-
Alternatively, using [zplug](https://github.com/zplug/zplug)
128+
### zsh
146129

147130
```zsh
148-
zplug "tldr-pages/tldr-node-client", use:bin/completion/zsh
131+
tldr completion zsh
132+
source ~/.zshrc
149133
```
150134

151-
Fret not regular zsh user!
152-
Copy or symlink `bin/completion/zsh/_tldr` to
153-
`my/completions/_tldr`
154-
(note the filename).
155-
Then add the containing directory to your fpath:
135+
### bash
156136

157-
```zsh
158-
fpath=(my/completions $fpath)
137+
```bash
138+
tldr completion bash
139+
source ~/.bashrc
159140
```
160141

161-
### Bash
142+
This command will generate the appropriate completion script and append it to your shell's configuration file (`.zshrc` or `.bashrc`).
162143

163-
```bash
164-
ln -s bin/completion/bash/tldr ~/.tldr-completion.bash
165-
```
144+
After running the completion installation command, restart your shell or reload your configuration file to enable the autocompletion.
166145

167-
Now add the following line to our bashrc file:
146+
You should now have autocompletion enabled for the tldr command.
168147

169-
```bash
170-
source ~/.tldr-completion.bash
171-
```
148+
If you encounter any issues or need more information about the autocompletion setup, please refer to the [completion.js](https://github.com/tldr-pages/tldr-node-client/blob/master/lib/completion.js) file in the repository.
172149

173150
## FAQ
174151

bin/completion/bash/tldr

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
#!/bin/bash
22

3+
# tldr bash completion
4+
5+
# Check if bash-completion is already sourced
6+
if ! type _completion_loader &>/dev/null; then
7+
# If not, try to load it
8+
if [ -f /usr/share/bash-completion/bash_completion ]; then
9+
. /usr/share/bash-completion/bash_completion
10+
elif [ -f /etc/bash_completion ]; then
11+
. /etc/bash_completion
12+
fi
13+
fi
14+
315
BUILTIN_THEMES="single base16 ocean"
416

517
PLATFORM_TYPES="android freebsd linux netbsd openbsd osx sunos windows"

bin/tldr

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,33 @@ const pkg = require('../package');
55
const Tldr = require('../lib/tldr');
66
const config = require('../lib/config');
77
const platforms = require('../lib/platforms');
8+
const Completion = require('../lib/completion');
89
const { TldrError } = require('../lib/errors');
910

1011
pkg.version = `v${pkg.version}\nClient Specification: 2.0`;
1112

13+
program
14+
.command('completion <shell>')
15+
.description('Output shell completion script')
16+
.action((shell) => {
17+
const completion = new Completion(shell);
18+
completion.getScript()
19+
.then((script) => {
20+
return completion.appendScript(script);
21+
})
22+
.then(() => {
23+
if (shell === 'zsh') {
24+
console.log('If completions don\'t work, you may need to rebuild your zcompdump:');
25+
console.log(' rm -f ~/.zcompdump; compinit');
26+
}
27+
process.exit(0);
28+
})
29+
.catch((err) => {
30+
console.error(err.message);
31+
process.exit(err.code || 1);
32+
});
33+
});
34+
1235
program
1336
.version(pkg.version, '-v, --version', 'Display version')
1437
.helpOption('-h, --help', 'Show this help message')
@@ -104,7 +127,7 @@ if (program.list) {
104127
} else if (program.search) {
105128
program.args.unshift(program.search);
106129
p = tldr.search(program.args);
107-
} else if (program.args.length >= 1) {
130+
} else if (program.args.length >= 1 && program.args[0] !== 'completion') {
108131
p = tldr.get(program.args, program);
109132
}
110133

lib/completion.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const os = require('os');
6+
const { UnsupportedShellError, CompletionScriptError } = require('./errors');
7+
8+
class Completion {
9+
constructor(shell) {
10+
this.supportedShells = ['bash', 'zsh'];
11+
if (!this.supportedShells.includes(shell)) {
12+
throw new UnsupportedShellError(shell, this.supportedShells);
13+
}
14+
this.shell = shell;
15+
this.rcFilename = shell === 'zsh' ? '.zshrc' : '.bashrc';
16+
}
17+
18+
getFilePath() {
19+
const homeDir = os.homedir();
20+
return path.join(homeDir, this.rcFilename);
21+
}
22+
23+
appendScript(script) {
24+
const rcFilePath = this.getFilePath();
25+
return new Promise((resolve, reject) => {
26+
fs.appendFile(rcFilePath, `\n${script}\n`, (err) => {
27+
if (err) {
28+
reject((new CompletionScriptError(`Error appending to ${rcFilePath}: ${err.message}`)));
29+
} else {
30+
console.log(`Completion script added to ${rcFilePath}`);
31+
console.log(`Please restart your shell or run 'source ~/${this.rcFilename}' to enable completions`);
32+
resolve();
33+
}
34+
});
35+
});
36+
}
37+
38+
getScript() {
39+
return new Promise((resolve) => {
40+
if (this.shell === 'zsh') {
41+
resolve(this.getZshScript());
42+
} else if (this.shell === 'bash') {
43+
resolve(this.getBashScript());
44+
}
45+
});
46+
}
47+
48+
getZshScript() {
49+
const completionDir = path.join(__dirname, '..', 'bin', 'completion', 'zsh');
50+
return `
51+
# tldr zsh completion
52+
fpath=(${completionDir} $fpath)
53+
54+
# You might need to force rebuild zcompdump:
55+
# rm -f ~/.zcompdump; compinit
56+
57+
# If you're using oh-my-zsh, you can force reload of completions:
58+
# autoload -U compinit && compinit
59+
60+
# Check if compinit is already loaded, if not, load it
61+
if (( ! $+functions[compinit] )); then
62+
autoload -Uz compinit
63+
compinit -C
64+
fi
65+
`.trim();
66+
}
67+
68+
getBashScript() {
69+
return new Promise((resolve, reject) => {
70+
const scriptPath = path.join(__dirname, '..', 'bin', 'completion', 'bash', 'tldr');
71+
fs.readFile(scriptPath, 'utf8', (err, data) => {
72+
if (err) {
73+
reject(new CompletionScriptError(`Error reading bash completion script: ${err.message}`));
74+
} else {
75+
resolve(data);
76+
}
77+
});
78+
});
79+
}
80+
}
81+
82+
module.exports = Completion;

lib/errors.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,31 @@ class MissingRenderPathError extends TldrError {
4444
}
4545
}
4646

47+
class UnsupportedShellError extends TldrError {
48+
constructor(shell, supportedShells) {
49+
super(`Unsupported shell: ${shell}. Supported shells are: ${supportedShells.join(', ')}`);
50+
this.name = 'UnsupportedShellError';
51+
// eslint-disable-next-line no-magic-numbers
52+
this.code = 5;
53+
}
54+
}
55+
56+
class CompletionScriptError extends TldrError {
57+
constructor(message) {
58+
super(message);
59+
this.name = 'CompletionScriptError';
60+
// eslint-disable-next-line no-magic-numbers
61+
this.code = 6;
62+
}
63+
}
64+
4765
module.exports = {
4866
TldrError,
4967
EmptyCacheError,
5068
MissingPageError,
51-
MissingRenderPathError
69+
MissingRenderPathError,
70+
UnsupportedShellError,
71+
CompletionScriptError
5272
};
5373

5474
function trim(strings, ...values) {

test/completion.spec.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
'use strict';
2+
3+
const Completion = require('../lib/completion');
4+
const { UnsupportedShellError, CompletionScriptError } = require('../lib/errors');
5+
const sinon = require('sinon');
6+
const fs = require('fs');
7+
const os = require('os');
8+
const should = require('should');
9+
10+
describe('Completion', () => {
11+
describe('constructor()', () => {
12+
it('should construct with supported shell', () => {
13+
const completion = new Completion('zsh');
14+
should.exist(completion);
15+
completion.shell.should.equal('zsh');
16+
completion.rcFilename.should.equal('.zshrc');
17+
});
18+
19+
it('should throw UnsupportedShellError for unsupported shell', () => {
20+
(() => {return new Completion('fish');}).should.throw(UnsupportedShellError);
21+
});
22+
});
23+
24+
describe('getFilePath()', () => {
25+
let homeStub;
26+
27+
beforeEach(() => {
28+
homeStub = sinon.stub(os, 'homedir').returns('/home/user');
29+
});
30+
31+
afterEach(() => {
32+
homeStub.restore();
33+
});
34+
35+
it('should return .zshrc path for zsh', () => {
36+
const completion = new Completion('zsh');
37+
completion.getFilePath().should.equal('/home/user/.zshrc');
38+
});
39+
40+
it('should return .bashrc path for bash', () => {
41+
const completion = new Completion('bash');
42+
completion.getFilePath().should.equal('/home/user/.bashrc');
43+
});
44+
});
45+
46+
describe('appendScript()', () => {
47+
let appendFileStub;
48+
let getFilePathStub;
49+
50+
beforeEach(() => {
51+
appendFileStub = sinon.stub(fs, 'appendFile').yields(null);
52+
getFilePathStub = sinon.stub(Completion.prototype, 'getFilePath').returns('/home/user/.zshrc');
53+
});
54+
55+
afterEach(() => {
56+
appendFileStub.restore();
57+
getFilePathStub.restore();
58+
});
59+
60+
it('should append script to file', () => {
61+
const completion = new Completion('zsh');
62+
return completion.appendScript('test script')
63+
.then(() => {
64+
appendFileStub.calledOnce.should.be.true();
65+
appendFileStub.firstCall.args[0].should.equal('/home/user/.zshrc');
66+
appendFileStub.firstCall.args[1].should.equal('\ntest script\n');
67+
});
68+
});
69+
70+
it('should reject with CompletionScriptError on fs error', () => {
71+
const completion = new Completion('zsh');
72+
appendFileStub.yields(new Error('File write error'));
73+
return completion.appendScript('test script')
74+
.should.be.rejectedWith(CompletionScriptError);
75+
});
76+
});
77+
78+
describe('getScript()', () => {
79+
it('should return zsh script for zsh shell', () => {
80+
const completion = new Completion('zsh');
81+
return completion.getScript()
82+
.then((script) => {
83+
script.should.containEql('# tldr zsh completion');
84+
script.should.containEql('fpath=(');
85+
});
86+
});
87+
88+
it('should return bash script for bash shell', () => {
89+
const completion = new Completion('bash');
90+
const readFileStub = sinon.stub(fs, 'readFile').yields(null, '# bash completion script');
91+
92+
return completion.getScript()
93+
.then((script) => {
94+
script.should.equal('# bash completion script');
95+
readFileStub.restore();
96+
});
97+
});
98+
});
99+
100+
describe('getZshScript()', () => {
101+
it('should return zsh completion script', () => {
102+
const completion = new Completion('zsh');
103+
const script = completion.getZshScript();
104+
script.should.containEql('# tldr zsh completion');
105+
script.should.containEql('fpath=(');
106+
script.should.containEql('compinit');
107+
});
108+
});
109+
110+
describe('getBashScript()', () => {
111+
let readFileStub;
112+
113+
beforeEach(() => {
114+
readFileStub = sinon.stub(fs, 'readFile');
115+
});
116+
117+
afterEach(() => {
118+
readFileStub.restore();
119+
});
120+
121+
it('should return bash completion script', () => {
122+
const completion = new Completion('bash');
123+
readFileStub.yields(null, '# bash completion script');
124+
125+
return completion.getBashScript()
126+
.then((script) => {
127+
script.should.equal('# bash completion script');
128+
});
129+
});
130+
131+
it('should reject with CompletionScriptError on fs error', () => {
132+
const completion = new Completion('bash');
133+
readFileStub.yields(new Error('File read error'));
134+
135+
return completion.getBashScript()
136+
.should.be.rejectedWith(CompletionScriptError);
137+
});
138+
});
139+
});

0 commit comments

Comments
 (0)