From 92ff2225efd5321af4ce98cf04bc1c7311e88751 Mon Sep 17 00:00:00 2001 From: Chooooooo Date: Sun, 18 Aug 2024 23:57:34 +0900 Subject: [PATCH 1/6] feat: add completion command --- README.md | 51 ++++---------- bin/completion/bash/tldr | 12 ++++ bin/tldr | 25 ++++++- lib/completion.js | 82 +++++++++++++++++++++++ lib/errors.js | 22 ++++++- test/completion.spec.js | 139 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 292 insertions(+), 39 deletions(-) create mode 100644 lib/completion.js create mode 100644 test/completion.spec.js diff --git a/README.md b/README.md index a06260a..54a6241 100644 --- a/README.md +++ b/README.md @@ -120,55 +120,32 @@ you can set the configuration variable `skipUpdateWhenPageNotFound` to `true` (d ## Command-line Autocompletion -Currently we only support command-line autocompletion for zsh -and bash. Pull requests for other shells are most welcome! +We currently support command-line autocompletion for zsh and bash. +Pull requests for other shells are most welcome! -### zsh - -It's easiest for -[oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) -users, so let's start with that. - -```zsh -mkdir -p $ZSH_CUSTOM/plugins/tldr -ln -s bin/completion/zsh/_tldr $ZSH_CUSTOM/plugins/tldr/_tldr -``` - -Then add tldr to your oh-my-zsh plugins, -usually defined in `~/.zshrc`, -resulting in something looking like this: - -```zsh -plugins=(git tmux tldr) -``` +To enable autocompletion for the tldr command, run: -Alternatively, using [zplug](https://github.com/zplug/zplug) +### zsh ```zsh -zplug "tldr-pages/tldr-node-client", use:bin/completion/zsh +tldr completion zsh +source ~/.zshrc ``` -Fret not regular zsh user! -Copy or symlink `bin/completion/zsh/_tldr` to -`my/completions/_tldr` -(note the filename). -Then add the containing directory to your fpath: +### bash -```zsh -fpath=(my/completions $fpath) +```bash +tldr completion bash +source ~/.bashrc ``` -### Bash +This command will generate the appropriate completion script and append it to your shell's configuration file (`.zshrc` or `.bashrc`). -```bash -ln -s bin/completion/bash/tldr ~/.tldr-completion.bash -``` +After running the completion installation command, restart your shell or reload your configuration file to enable the autocompletion. -Now add the following line to our bashrc file: +You should now have autocompletion enabled for the tldr command. -```bash -source ~/.tldr-completion.bash -``` +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. ## FAQ diff --git a/bin/completion/bash/tldr b/bin/completion/bash/tldr index 860b478..057e3f2 100644 --- a/bin/completion/bash/tldr +++ b/bin/completion/bash/tldr @@ -1,5 +1,17 @@ #!/bin/bash +# tldr bash completion + +# Check if bash-completion is already sourced +if ! type _completion_loader &>/dev/null; then + # If not, try to load it + if [ -f /usr/share/bash-completion/bash_completion ]; then + . /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + . /etc/bash_completion + fi +fi + BUILTIN_THEMES="single base16 ocean" PLATFORM_TYPES="android freebsd linux netbsd openbsd osx sunos windows" diff --git a/bin/tldr b/bin/tldr index 705d6eb..9c14701 100755 --- a/bin/tldr +++ b/bin/tldr @@ -5,10 +5,33 @@ const pkg = require('../package'); const Tldr = require('../lib/tldr'); const config = require('../lib/config'); const platforms = require('../lib/platforms'); +const Completion = require('../lib/completion'); const { TldrError } = require('../lib/errors'); pkg.version = `v${pkg.version}\nClient Specification: 2.0`; +program + .command('completion ') + .description('Output shell completion script') + .action((shell) => { + const completion = new Completion(shell); + completion.getScript() + .then((script) => { + return completion.appendScript(script); + }) + .then(() => { + if (shell === 'zsh') { + console.log('If completions don\'t work, you may need to rebuild your zcompdump:'); + console.log(' rm -f ~/.zcompdump; compinit'); + } + process.exit(0); + }) + .catch((err) => { + console.error(err.message); + process.exit(err.code || 1); + }); + }); + program .version(pkg.version, '-v, --version', 'Display version') .helpOption('-h, --help', 'Show this help message') @@ -104,7 +127,7 @@ if (program.list) { } else if (program.search) { program.args.unshift(program.search); p = tldr.search(program.args); -} else if (program.args.length >= 1) { +} else if (program.args.length >= 1 && program.args[0] !== 'completion') { p = tldr.get(program.args, program); } diff --git a/lib/completion.js b/lib/completion.js new file mode 100644 index 0000000..d8c9eaf --- /dev/null +++ b/lib/completion.js @@ -0,0 +1,82 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const { UnsupportedShellError, CompletionScriptError } = require('./errors'); + +class Completion { + constructor(shell) { + this.supportedShells = ['bash', 'zsh']; + if (!this.supportedShells.includes(shell)) { + throw new UnsupportedShellError(shell, this.supportedShells); + } + this.shell = shell; + this.rcFilename = shell === 'zsh' ? '.zshrc' : '.bashrc'; + } + + getFilePath() { + const homeDir = os.homedir(); + return path.join(homeDir, this.rcFilename); + } + + appendScript(script) { + const rcFilePath = this.getFilePath(); + return new Promise((resolve, reject) => { + fs.appendFile(rcFilePath, `\n${script}\n`, (err) => { + if (err) { + reject((new CompletionScriptError(`Error appending to ${rcFilePath}: ${err.message}`))); + } else { + console.log(`Completion script added to ${rcFilePath}`); + console.log(`Please restart your shell or run 'source ~/${this.rcFilename}' to enable completions`); + resolve(); + } + }); + }); + } + + getScript() { + return new Promise((resolve) => { + if (this.shell === 'zsh') { + resolve(this.getZshScript()); + } else if (this.shell === 'bash') { + resolve(this.getBashScript()); + } + }); + } + + getZshScript() { + const completionDir = path.join(__dirname, '..', 'bin', 'completion', 'zsh'); + return ` +# tldr zsh completion +fpath=(${completionDir} $fpath) + +# You might need to force rebuild zcompdump: +# rm -f ~/.zcompdump; compinit + +# If you're using oh-my-zsh, you can force reload of completions: +# autoload -U compinit && compinit + +# Check if compinit is already loaded, if not, load it +if (( ! $+functions[compinit] )); then + autoload -Uz compinit + compinit -C +fi + `.trim(); + } + + getBashScript() { + return new Promise((resolve, reject) => { + const scriptPath = path.join(__dirname, '..', 'bin', 'completion', 'bash', 'tldr'); + fs.readFile(scriptPath, 'utf8', (err, data) => { + if (err) { + reject(new CompletionScriptError(`Error reading bash completion script: ${err.message}`)); + } else { + resolve(data); + } + }); + }); + } +} + +module.exports = Completion; \ No newline at end of file diff --git a/lib/errors.js b/lib/errors.js index 882c4e5..24a2e28 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -44,11 +44,31 @@ class MissingRenderPathError extends TldrError { } } +class UnsupportedShellError extends TldrError { + constructor(shell, supportedShells) { + super(`Unsupported shell: ${shell}. Supported shells are: ${supportedShells.join(', ')}`); + this.name = 'UnsupportedShellError'; + // eslint-disable-next-line no-magic-numbers + this.code = 5; + } +} + +class CompletionScriptError extends TldrError { + constructor(message) { + super(message); + this.name = 'CompletionScriptError'; + // eslint-disable-next-line no-magic-numbers + this.code = 6; + } +} + module.exports = { TldrError, EmptyCacheError, MissingPageError, - MissingRenderPathError + MissingRenderPathError, + UnsupportedShellError, + CompletionScriptError }; function trim(strings, ...values) { diff --git a/test/completion.spec.js b/test/completion.spec.js new file mode 100644 index 0000000..870b4df --- /dev/null +++ b/test/completion.spec.js @@ -0,0 +1,139 @@ +'use strict'; + +const Completion = require('../lib/completion'); +const { UnsupportedShellError, CompletionScriptError } = require('../lib/errors'); +const sinon = require('sinon'); +const fs = require('fs'); +const os = require('os'); +const should = require('should'); + +describe('Completion', () => { + describe('constructor()', () => { + it('should construct with supported shell', () => { + const completion = new Completion('zsh'); + should.exist(completion); + completion.shell.should.equal('zsh'); + completion.rcFilename.should.equal('.zshrc'); + }); + + it('should throw UnsupportedShellError for unsupported shell', () => { + (() => {return new Completion('fish');}).should.throw(UnsupportedShellError); + }); + }); + + describe('getFilePath()', () => { + let homeStub; + + beforeEach(() => { + homeStub = sinon.stub(os, 'homedir').returns('/home/user'); + }); + + afterEach(() => { + homeStub.restore(); + }); + + it('should return .zshrc path for zsh', () => { + const completion = new Completion('zsh'); + completion.getFilePath().should.equal('/home/user/.zshrc'); + }); + + it('should return .bashrc path for bash', () => { + const completion = new Completion('bash'); + completion.getFilePath().should.equal('/home/user/.bashrc'); + }); + }); + + describe('appendScript()', () => { + let appendFileStub; + let getFilePathStub; + + beforeEach(() => { + appendFileStub = sinon.stub(fs, 'appendFile').yields(null); + getFilePathStub = sinon.stub(Completion.prototype, 'getFilePath').returns('/home/user/.zshrc'); + }); + + afterEach(() => { + appendFileStub.restore(); + getFilePathStub.restore(); + }); + + it('should append script to file', () => { + const completion = new Completion('zsh'); + return completion.appendScript('test script') + .then(() => { + appendFileStub.calledOnce.should.be.true(); + appendFileStub.firstCall.args[0].should.equal('/home/user/.zshrc'); + appendFileStub.firstCall.args[1].should.equal('\ntest script\n'); + }); + }); + + it('should reject with CompletionScriptError on fs error', () => { + const completion = new Completion('zsh'); + appendFileStub.yields(new Error('File write error')); + return completion.appendScript('test script') + .should.be.rejectedWith(CompletionScriptError); + }); + }); + + describe('getScript()', () => { + it('should return zsh script for zsh shell', () => { + const completion = new Completion('zsh'); + return completion.getScript() + .then((script) => { + script.should.containEql('# tldr zsh completion'); + script.should.containEql('fpath=('); + }); + }); + + it('should return bash script for bash shell', () => { + const completion = new Completion('bash'); + const readFileStub = sinon.stub(fs, 'readFile').yields(null, '# bash completion script'); + + return completion.getScript() + .then((script) => { + script.should.equal('# bash completion script'); + readFileStub.restore(); + }); + }); + }); + + describe('getZshScript()', () => { + it('should return zsh completion script', () => { + const completion = new Completion('zsh'); + const script = completion.getZshScript(); + script.should.containEql('# tldr zsh completion'); + script.should.containEql('fpath=('); + script.should.containEql('compinit'); + }); + }); + + describe('getBashScript()', () => { + let readFileStub; + + beforeEach(() => { + readFileStub = sinon.stub(fs, 'readFile'); + }); + + afterEach(() => { + readFileStub.restore(); + }); + + it('should return bash completion script', () => { + const completion = new Completion('bash'); + readFileStub.yields(null, '# bash completion script'); + + return completion.getBashScript() + .then((script) => { + script.should.equal('# bash completion script'); + }); + }); + + it('should reject with CompletionScriptError on fs error', () => { + const completion = new Completion('bash'); + readFileStub.yields(new Error('File read error')); + + return completion.getBashScript() + .should.be.rejectedWith(CompletionScriptError); + }); + }); +}); \ No newline at end of file From 54e853ba631303726581b08100a012303faebd79 Mon Sep 17 00:00:00 2001 From: Chooooooo Date: Mon, 19 Aug 2024 22:45:57 +0900 Subject: [PATCH 2/6] update: completion from command to option and update help messages --- bin/tldr | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/bin/tldr b/bin/tldr index 9c14701..521e5cf 100755 --- a/bin/tldr +++ b/bin/tldr @@ -10,28 +10,6 @@ const { TldrError } = require('../lib/errors'); pkg.version = `v${pkg.version}\nClient Specification: 2.0`; -program - .command('completion ') - .description('Output shell completion script') - .action((shell) => { - const completion = new Completion(shell); - completion.getScript() - .then((script) => { - return completion.appendScript(script); - }) - .then(() => { - if (shell === 'zsh') { - console.log('If completions don\'t work, you may need to rebuild your zcompdump:'); - console.log(' rm -f ~/.zcompdump; compinit'); - } - process.exit(0); - }) - .catch((err) => { - console.error(err.message); - process.exit(err.code || 1); - }); - }); - program .version(pkg.version, '-v, --version', 'Display version') .helpOption('-h, --help', 'Show this help message') @@ -47,7 +25,8 @@ program .option('-e, --random-example', 'Show a random example') .option('-f, --render [file]', 'Render a specific markdown [file]') .option('-m, --markdown', 'Output in markdown format') - .option('-p, --platform [type]', `Override the current platform [${platforms.supportedPlatforms.join(', ')}]`); + .option('-p, --platform [type]', `Override the current platform [${platforms.supportedPlatforms.join(', ')}]`) + .option('completion ', 'Generate and add shell completion script to your shell configuration'); for (const platform of platforms.supportedPlatforms) { program.option(`--${platform}`, `Override the platform with ${platform}`); @@ -81,6 +60,11 @@ const help = ` To render a local file (for testing): $ tldr --render /path/to/file.md + + To add shell completion: + + $ tldr completion bash + $ tldr completion zsh `; program.on('--help', () => { @@ -127,8 +111,21 @@ if (program.list) { } else if (program.search) { program.args.unshift(program.search); p = tldr.search(program.args); -} else if (program.args.length >= 1 && program.args[0] !== 'completion') { - p = tldr.get(program.args, program); +} else if (program.args.length >= 1) { + if (program.args[0] === 'completion') { + const shell = program.args[1]; + const completion = new Completion(shell); + p = completion.getScript() + .then((script) => {return completion.appendScript(script);}) + .then(() => { + if (shell === 'zsh') { + console.log('If completions don\'t work, you may need to rebuild your zcompdump:'); + console.log(' rm -f ~/.zcompdump; compinit'); + } + }); + } else { + p = tldr.get(program.args, program); + } } if (p === null) { From f775586fe158a634dd279387cd159a0d9dfffbe0 Mon Sep 17 00:00:00 2001 From: Chooooooo Date: Wed, 21 Aug 2024 20:11:16 +0900 Subject: [PATCH 3/6] test: Use cross-platform paths in completion tests - Replace hardcoded paths with `os.homedir()` and `path.join()` - Ensure tests run consistently across different operating systems --- test/completion.spec.js | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/test/completion.spec.js b/test/completion.spec.js index 870b4df..98ec4fc 100644 --- a/test/completion.spec.js +++ b/test/completion.spec.js @@ -6,8 +6,12 @@ const sinon = require('sinon'); const fs = require('fs'); const os = require('os'); const should = require('should'); +const path = require('path'); describe('Completion', () => { + const zshrcPath = path.join(os.homedir(), '.zshrc'); + const bashrcPath = path.join(os.homedir(), '.bashrc'); + describe('constructor()', () => { it('should construct with supported shell', () => { const completion = new Completion('zsh'); @@ -22,39 +26,26 @@ describe('Completion', () => { }); describe('getFilePath()', () => { - let homeStub; - - beforeEach(() => { - homeStub = sinon.stub(os, 'homedir').returns('/home/user'); - }); - - afterEach(() => { - homeStub.restore(); - }); - it('should return .zshrc path for zsh', () => { const completion = new Completion('zsh'); - completion.getFilePath().should.equal('/home/user/.zshrc'); + completion.getFilePath().should.equal(zshrcPath); }); it('should return .bashrc path for bash', () => { const completion = new Completion('bash'); - completion.getFilePath().should.equal('/home/user/.bashrc'); + completion.getFilePath().should.equal(bashrcPath); }); }); describe('appendScript()', () => { let appendFileStub; - let getFilePathStub; beforeEach(() => { appendFileStub = sinon.stub(fs, 'appendFile').yields(null); - getFilePathStub = sinon.stub(Completion.prototype, 'getFilePath').returns('/home/user/.zshrc'); }); afterEach(() => { appendFileStub.restore(); - getFilePathStub.restore(); }); it('should append script to file', () => { @@ -62,7 +53,7 @@ describe('Completion', () => { return completion.appendScript('test script') .then(() => { appendFileStub.calledOnce.should.be.true(); - appendFileStub.firstCall.args[0].should.equal('/home/user/.zshrc'); + appendFileStub.firstCall.args[0].should.equal(zshrcPath); appendFileStub.firstCall.args[1].should.equal('\ntest script\n'); }); }); @@ -136,4 +127,4 @@ describe('Completion', () => { .should.be.rejectedWith(CompletionScriptError); }); }); -}); \ No newline at end of file +}); From cb87c0716d467c49c04c8f9c3ef7579ed9cefe89 Mon Sep 17 00:00:00 2001 From: Chooooo Date: Thu, 22 Aug 2024 14:35:00 +0900 Subject: [PATCH 4/6] style: Change completion option argument from to [shell] for consistency Co-authored-by: spageektti --- bin/tldr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/tldr b/bin/tldr index 521e5cf..7f59f32 100755 --- a/bin/tldr +++ b/bin/tldr @@ -26,7 +26,7 @@ program .option('-f, --render [file]', 'Render a specific markdown [file]') .option('-m, --markdown', 'Output in markdown format') .option('-p, --platform [type]', `Override the current platform [${platforms.supportedPlatforms.join(', ')}]`) - .option('completion ', 'Generate and add shell completion script to your shell configuration'); + .option('completion [shell]', 'Generate and add shell completion script to your shell configuration'); for (const platform of platforms.supportedPlatforms) { program.option(`--${platform}`, `Override the platform with ${platform}`); From 5dc0ed1f97ad696f39d2dcc252de5d5de27436a5 Mon Sep 17 00:00:00 2001 From: Chooooooo Date: Fri, 25 Oct 2024 16:50:05 +0900 Subject: [PATCH 5/6] docs: update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 54a6241..0f30cd7 100644 --- a/README.md +++ b/README.md @@ -141,10 +141,6 @@ source ~/.bashrc This command will generate the appropriate completion script and append it to your shell's configuration file (`.zshrc` or `.bashrc`). -After running the completion installation command, restart your shell or reload your configuration file to enable the autocompletion. - -You should now have autocompletion enabled for the tldr command. - 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. ## FAQ From 985fad3ab7b95ab96bb0d8caa214628cb764478f Mon Sep 17 00:00:00 2001 From: Chooooooo Date: Sat, 26 Oct 2024 00:44:45 +0900 Subject: [PATCH 6/6] docs: update README.md for oh-my-zsh users --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 0f30cd7..1a8abc0 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,26 @@ To enable autocompletion for the tldr command, run: ### zsh +It's easiest for +[oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) +users, so let's start with that. + +```zsh +mkdir -p $ZSH_CUSTOM/plugins/tldr +ln -s bin/completion/zsh/_tldr $ZSH_CUSTOM/plugins/tldr/_tldr +``` + +Then add tldr to your oh-my-zsh plugins, +usually defined in `~/.zshrc`, +resulting in something looking like this: + +```zsh +plugins=(git tmux tldr) +``` + +Fret not regular zsh user! +You can also do this: + ```zsh tldr completion zsh source ~/.zshrc