diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14c04ba..1f6383d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,28 @@ jobs: ls -l $SYNC_DIR if [[ ! -x $SYNC_DIR/rg ]]; then echo "error on: rg"; false; fi + - if: matrix.os == 'ubuntu-latest' + name: "Integration test: [ubuntu-latest] [check bash completion]" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cargo run -- completion bash --rename test-name > /dev/null 2> log + + cat log | tail -n +5 > logg + + diff logg tests/bash-completion + if [[ $? == 1 ]]; then logg test/bash-completion; false; fi + - if: matrix.os == 'macos-latest' + name: "Integration test: [macos-latest] [check zsh completion]" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cargo run -- completion zsh --rename test-name > /dev/null 2> log + + cat log | tail -n +5 > logg + diff logg tests/zsh-completion + if [[ $? == 1 ]]; then logg test/zsh-completion; false; fi - if: matrix.os != 'windows-latest' name: "Integration test: [unix] [sync-ripgrep]" env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 133233d..35da73c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ available [on GitHub][2]. Adds new error message RepoError::NotFound when fetch_release_info returns 404 (by [@crudiedo][crudiedo]) +* [#133](https://github.com/chshersh/tool-sync/issues/133) + Added shell completion + (by [@MitchellBerend][MitchellBerend]) + ### Fixed diff --git a/Cargo.lock b/Cargo.lock index dcc269c..90778f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "clap_complete" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11cba7abac9b56dfe2f035098cdb3a43946f276e6db83b72c4e692343f9aab9a" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.0.13" @@ -620,6 +629,7 @@ name = "tool-sync" version = "0.2.0" dependencies = [ "clap", + "clap_complete", "console", "dirs", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 87203c6..ecc3be1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ serde = { version = "1.0", features = ["derive"] } ureq = { version = "2.5.0", features = ["json"] } zip = { version = "0.6.3", default-features = false, features = ["deflate"] } +clap_complete = "4.0.2" console = "0.15.2" dirs = "4.0.0" flate2 = "1.0" diff --git a/README.md b/README.md index b7685c6..c455b5d 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,31 @@ By default `tool-sync` reads the configuration from from the `$HOME/.tool.toml` path. But you can specify a custom location via the `--config=/path/to/my/config` flag. +### Autocomplete + +`tool-sync` can generate it's own autocomplete scripts for some common shell +emulators. you can run `tool completion --help` to see the supported shells. + +If you decide to rename the `tool` binary a `--rename` flag will help make sure +the autocompletion will keep working. + + +The following example will assume you have renamed the binary to `random-name` +and you are using bash. You can run the following command to get completion for +the current session of your shell. + +```shell +random-name completion bash --rename random-name +``` + +If you want to have these autocomplete functions in the next (and the ones after +that) as well you can add the following to your `~/.bashrc`: + +```shell +eval "$(random-name completion bash --rename random-name)" +``` + + ### Quick start `tool-sync` has a command to generate a default configuration with examples and diff --git a/src/completion.rs b/src/completion.rs new file mode 100644 index 0000000..c0c4a27 --- /dev/null +++ b/src/completion.rs @@ -0,0 +1,86 @@ +//! This file contains all logic revolving the generation of the shell completion script + +use clap_complete::Shell; + +// This function can break when clap_complete adds support for a new shell type +pub fn rename_completion_suggestion(shell: &Shell, bin_name: &str) -> Result<(), RenameError> { + let completion_str: String = match shell { + Shell::Zsh => zsh_completion_help(bin_name), + Shell::Bash => bash_completion_help(bin_name), + Shell::Fish => fish_completion_help(bin_name), + Shell::Elvish => elvish_completion_help(bin_name), + Shell::PowerShell => powershell_completion_help(bin_name), + _ => return Err(RenameError::NewShellFound(shell.to_owned())), + }; + + eprintln!( + "\n\n############################\n{}\n############################", + completion_str + ); + + Ok(()) +} + +pub enum RenameError { + NewShellFound(Shell), +} + +impl std::fmt::Display for RenameError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + RenameError::NewShellFound(shell) => write!(f, "[Rename error]: {}", shell), + } + } +} + +//##################// +// Helper functions // +//##################// + +fn zsh_completion_help(bin_name: &str) -> String { + format!( + r##"Generate a `_{bin_name}` completion script and put it somewhere in your `$fpath`: +`{bin_name} completion zsh --rename {bin_name} > /usr/local/share/zsh/site-functions/_{bin_name}` + +Ensure that the following is present in your `~/.zshrc`: + +`autoload -U compinit` + +`compinit -i`"## + ) +} + +fn bash_completion_help(bin_name: &str) -> String { + format!( + r##"First, ensure that you install `bash-completion` using your package manager. + +After, add this to your `~/.bash_profile`: + +`eval "$({bin_name} completion bash --rename {bin_name})"`"## + ) +} + +fn fish_completion_help(bin_name: &str) -> String { + format!( + r##"Generate a `tool.fish` completion script: + +`{bin_name} completion fish --rename {bin_name} > ~/.config/fish/completions/{bin_name}.fish`"## + ) +} + +fn elvish_completion_help(_bin_name: &str) -> String { + r##"This suggestion is missing, if you use this and know how to implement this please file an issue over at https://github.com/chshersh/tool-sync/issues"##.into() +} + +fn powershell_completion_help(bin_name: &str) -> String { + format!( + r##"Open your profile script with: + +`mkdir -Path (Split-Path -Parent $profile) -ErrorAction SilentlyContinue` +`notepad $profile` + +Add the line and save the file: + +`Invoke-Expression -Command $({bin_name} completion powershell --rename {bin_name} | Out-String)`"## + ) +} diff --git a/src/config/cli.rs b/src/config/cli.rs index 2cf91f0..3061af2 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -18,6 +18,58 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Command { + /// Generate shell completion scripts for GitHub CLI commands. + /// + /// You will need to set up completions manually, follow the instructions below. The exact + /// config file locations might vary based on your system. Make sure to restart your + /// shell before testing whether completions are working. + /// + /// ### bash + /// + /// First, ensure that you install `bash-completion` using your package manager. + /// + /// After, add this to your `~/.bash_profile`: + /// + /// `eval "$(tool completion bash)"` + /// + /// ### zsh + /// + /// Generate a `_tool` completion script and put it somewhere in your `$fpath`: + /// + /// `tool completion zsh > /usr/local/share/zsh/site-functions/_tool` + /// + /// Ensure that the following is present in your `~/.zshrc`: + /// + /// `autoload -U compinit` + /// `compinit -i` + /// + /// Zsh version 5.7 or later is recommended. + /// + /// ### fish + /// + /// Generate a `tool.fish` completion script: + /// + /// `tool completion fish > ~/.config/fish/completions/tool.fish` + /// + /// ### PowerShell + /// + /// Open your profile script with: + /// + /// `mkdir -Path (Split-Path -Parent $profile) -ErrorAction SilentlyContinue` + /// `notepad $profile` + /// + /// Add the line and save the file: + /// + /// `Invoke-Expression -Command $(tool completion powershell | Out-String)` + Completion { + shell: clap_complete::Shell, + + /// If you want to rename the binary this option should be filled with the name you choose + /// to rename `tool` to. + #[arg(long, value_name = "string")] + rename: Option, + }, + /// Sync all tools specified in configuration file or the only one specified in the command line Sync { tool: Option }, diff --git a/src/lib.rs b/src/lib.rs index 9f2259d..f13631c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,21 @@ +mod completion; mod config; mod infra; mod install; mod model; mod sync; -use clap::Parser; +use clap::{CommandFactory, Parser}; +use clap_complete::generate; + use std::path::PathBuf; +use crate::completion::rename_completion_suggestion; use crate::config::cli::{Cli, Command}; use crate::infra::err; +const DEFAULT_CONFIG_PATH: &str = ".tool.toml"; + pub fn run() { let cli = Cli::parse(); @@ -18,6 +24,9 @@ pub fn run() { let config_path = resolve_config_path(cli.config); match cli.command { + Command::Completion { shell, rename } => { + generate_completion(shell, rename); + } Command::DefaultConfig { path } => match path { true => print_default_path(), false => config::template::generate_default_config(), @@ -27,7 +36,20 @@ pub fn run() { } } -const DEFAULT_CONFIG_PATH: &str = ".tool.toml"; +fn generate_completion(shell: clap_complete::Shell, rename: Option) { + let mut cmd: clap::Command = Cli::command(); + match rename { + Some(cmd_name) => { + generate(shell, &mut cmd, &cmd_name, &mut std::io::stdout()); + rename_completion_suggestion(&shell, &cmd_name) + .unwrap_or_else(|e| err::abort_suggest_issue(e)); + } + None => { + let cmd_name: String = cmd.get_name().into(); + generate(shell, &mut cmd, cmd_name, &mut std::io::stdout()); + } + }; +} fn resolve_config_path(config_path: Option) -> PathBuf { match config_path { diff --git a/tests/bash-completion b/tests/bash-completion new file mode 100644 index 0000000..c6a8fd7 --- /dev/null +++ b/tests/bash-completion @@ -0,0 +1,7 @@ +############################ +First, ensure that you install `bash-completion` using your package manager. + +After, add this to your `~/.bash_profile`: + +`eval "$(test-name completion bash --rename test-name)"` +############################ diff --git a/tests/zsh-completion b/tests/zsh-completion new file mode 100644 index 0000000..4e00e83 --- /dev/null +++ b/tests/zsh-completion @@ -0,0 +1,10 @@ +############################ +Generate a `_test-name` completion script and put it somewhere in your `$fpath`: +`test-name completion zsh --rename test-name > /usr/local/share/zsh/site-functions/_test-name` + +Ensure that the following is present in your `~/.zshrc`: + +`autoload -U compinit` + +`compinit -i` +############################