From 7b8041348922289282532d2a007bb0f53cdc5a7c Mon Sep 17 00:00:00 2001 From: Tobias Langendorf Date: Wed, 13 Nov 2024 10:59:21 +0100 Subject: [PATCH] Add dynamic shell completions More of an experiment, but I stumbled upon this when trying to make shell completions suggest existing marks, and I just had to add it --- Cargo.lock | 99 ++++++++++++++++++++++++++++++++++++++++++---------- Cargo.toml | 2 +- README.md | 17 +++++++++ flake.nix | 6 ++++ src/cli.rs | 24 +------------ src/main.rs | 20 ++++++++++- src/marks.rs | 15 ++++++++ 7 files changed, 140 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e93d45..a8ede3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611cc2ae7d2e242c457e4be7f97036b8ad9ca152b499f53faf99b1ed8fc2553f" +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "anstream" @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.37" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8" dependencies = [ "jobserver", "libc", @@ -175,6 +175,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11611dca53440593f38e6b25ec629de50b14cdfa63adc0fb856115a2c6d97595" dependencies = [ "clap", + "clap_lex", + "is_executable", + "shlex", ] [[package]] @@ -278,6 +281,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "diff" version = "0.1.13" @@ -375,6 +413,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.3" @@ -557,6 +601,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -596,14 +646,27 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "instability" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" +checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e" dependencies = [ + "darling", + "indoc", + "pretty_assertions", + "proc-macro2", "quote", "syn", ] +[[package]] +name = "is_executable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +dependencies = [ + "winapi", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1019,9 +1082,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1045,9 +1108,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.39" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ "bitflags", "errno", @@ -1082,18 +1145,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -1249,18 +1312,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 02d7aee..38848e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ exclude = ["images/*"] git2 = { version= "0.19", features = [ "vendored-openssl" ] } clap = { version = "4.5", features = ["cargo", "derive"] } +clap_complete = { version = "4.5", features = [ "unstable-dynamic" ] } serde_derive = "1.0" serde = "1.0" error-stack = "0.5" @@ -31,7 +32,6 @@ dirs = "5.0" nucleo = "0.5.0" ratatui = { version = "0.29", features = ["serde"] } crossterm = "0.28" -clap_complete = "4.5" [lib] name = "tms" diff --git a/README.md b/README.md index b2d3200..9f26cc1 100644 --- a/README.md +++ b/README.md @@ -209,3 +209,20 @@ session switch. This can be configured with settings like this. bind -r '(' switch-client -p\; refresh-client -S bind -r ')' switch-client -n\; refresh-client -S ``` + +## Shell completions + +### Bash +```bash +echo "source <(COMPLETE=bash tms)" >> ~/.bashrc +``` + +### Zsh +```zsh +echo "source <(COMPLETE=zsh tms) >> ~/.zshrc" +``` + +### Fish +```fish +echo "COMPLETE=fish tms | source" >> ~/.config/fish/config.fish +``` diff --git a/flake.nix b/flake.nix index 13f4745..1ef7a42 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,12 @@ cargoDeps = final.rustPlatform.importCargoLock { lockFile = self + "/Cargo.lock"; }; + postInstall = final.lib.optionalString (final.stdenv.buildPlatform.canExecute final.stdenv.hostPlatform) '' + installShellCompletion --cmd tms \ + --bash <(COMPLETE=bash $out/bin/tms) \ + --fish <(COMPLETE=fish $out/bin/tms) \ + --zsh <(COMPLETE=zsh $out/bin/tms) + ''; }); }; diff --git a/src/cli.rs b/src/cli.rs index 1a9baa9..70a2cf9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -15,8 +15,7 @@ use crate::{ tmux::Tmux, Result, TmsError, }; -use clap::{Args, Command, CommandFactory, Parser, Subcommand}; -use clap_complete::{generate, Generator, Shell}; +use clap::{Args, Parser, Subcommand}; use error_stack::ResultExt; use git2::{build::RepoBuilder, FetchOptions, RemoteCallbacks, Repository}; use ratatui::style::Color; @@ -25,8 +24,6 @@ use ratatui::style::Color; #[command(author, version)] ///Scan for all git folders in specified directorires, select one and open it as a new tmux session pub struct Cli { - #[arg(long = "generate", value_enum)] - generator: Option, #[command(subcommand)] command: Option, } @@ -154,12 +151,6 @@ pub struct OpenSessionCommand { impl Cli { pub fn handle_sub_commands(&self, tmux: &Tmux) -> Result { - if let Some(generator) = self.generator { - let mut cmd = Cli::command(); - print_completions(generator, &mut cmd); - return Ok(SubCommandGiven::Yes); - } - // Get the configuration from the config file let config = Config::new().change_context(TmsError::ConfigError)?; @@ -771,19 +762,6 @@ fn open_session_command(args: &OpenSessionCommand, config: Config, tmux: &Tmux) } } -fn print_completions(gen: G, cmd: &mut Command) { - let name = if let Ok(exe) = std::env::current_exe() { - if let Some(exe) = exe.file_name() { - exe.to_string_lossy().to_string() - } else { - cmd.get_name().to_string() - } - } else { - cmd.get_name().to_string() - }; - generate(gen, cmd, name, &mut std::io::stdout()); -} - pub enum SubCommandGiven { Yes, No(Box), diff --git a/src/main.rs b/src/main.rs index a79791a..713928c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ -use clap::Parser; +use std::env; + +use clap::{CommandFactory, Parser}; +use clap_complete::CompleteEnv; use error_stack::Report; use tms::{ @@ -18,6 +21,21 @@ fn main() -> Result<()> { #[cfg(any(not(debug_assertions), test))] Report::install_debug_hook::(|_value, _context| {}); + let bin_name = std::env::current_exe() + .ok() + .and_then(|exe| exe.file_name().map(|exe| exe.to_string_lossy().to_string())) + .unwrap_or("tms".into()); + match CompleteEnv::with_factory(Cli::command) + .bin(bin_name) + .try_complete(env::args_os(), None) + { + Ok(true) => return Ok(()), + Err(e) => { + panic!("failed to generate completions: {e}"); + } + Ok(false) => {} + }; + // Use CLAP to parse the command line arguments let cli_args = Cli::parse(); diff --git a/src/marks.rs b/src/marks.rs index 79d691f..cbcc7e1 100644 --- a/src/marks.rs +++ b/src/marks.rs @@ -1,6 +1,7 @@ use std::{env::current_dir, path::PathBuf}; use clap::{Args, Subcommand}; +use clap_complete::{ArgValueCandidates, CompletionCandidate}; use error_stack::ResultExt; use crate::{ @@ -14,6 +15,7 @@ use crate::{ #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true)] pub struct MarksCommand { + #[arg(add = ArgValueCandidates::new(get_completion_candidates))] /// The index of the mark to open index: Option, #[command(subcommand)] @@ -43,6 +45,7 @@ pub struct MarksSetCommand { #[derive(Debug, Args)] pub struct MarksOpenCommand { + #[arg(add = ArgValueCandidates::new(get_completion_candidates))] /// The index of the mark to open index: usize, } @@ -50,6 +53,7 @@ pub struct MarksOpenCommand { #[derive(Debug, Args)] #[group(required = true, multiple = false)] pub struct MarksDeleteCommand { + #[arg(add = ArgValueCandidates::new(get_completion_candidates))] /// Index of mark to delete index: Option, #[arg(long, short)] @@ -57,6 +61,17 @@ pub struct MarksDeleteCommand { all: bool, } +fn get_completion_candidates() -> Vec { + let config = Config::new().unwrap_or_default(); + let marks = get_marks(&config).unwrap_or_default(); + marks + .iter() + .map(|(index, session)| { + CompletionCandidate::new(index.to_string()).help(Some(session.name.clone().into())) + }) + .collect::>() +} + pub fn marks_command(args: &MarksCommand, config: Config, tmux: &Tmux) -> Result<()> { match (&args.cmd, args.index) { (None, None) => list(config),