diff --git a/CHANGELOG.md b/CHANGELOG.md index 6417d8a..7093459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased - [ReleaseDate] +### Added + +- Add shell completions, accessed by enabling the `COMPLETE` environment variable [#6](https://github.com/LucasPickering/env-select/issues/6) + - For example, adding `COMPLETE=fish es | source` to your `fish.config` will enable completions for fish + - [See docs](https://env-select.lucaspickering.me/book/user_guide/shell_completions.html) for more info and a list of supported shells + ### Changed - Upgrade Rust version to 1.80.0 diff --git a/Cargo.lock b/Cargo.lock index cd0caa4..86d42a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,23 +43,24 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", - "anstyle-wincon 2.1.0", + "anstyle-wincon 3.0.4", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.2" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" @@ -91,12 +92,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -346,32 +347,43 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.4.0" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d5f1946157a96594eb2d2c10eb7ad9a2b27518cb3000209dec700c35df9197d" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.4.0" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78116e32a042dd73c2901f0dc30790d20ff3447f3e3472fad359e8c3d282bcd6" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" dependencies = [ - "anstream 0.5.0", + "anstream 0.6.15", "anstyle", "clap_lex", "strsim", ] +[[package]] +name = "clap_complete" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" +dependencies = [ + "clap", + "clap_lex", + "is_executable", + "shlex", +] + [[package]] name = "clap_derive" -version = "4.4.0" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -381,9 +393,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" @@ -534,6 +546,7 @@ dependencies = [ "anyhow", "assert_cmd", "clap", + "clap_complete", "ctrlc", "derive_more", "dialoguer", @@ -752,9 +765,9 @@ checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -784,6 +797,21 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is_executable" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba3d8548b8b04dafdf2f4cc6f5e379db766d0a6d9aac233ad4c9a92ea892233" +dependencies = [ + "winapi", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -992,9 +1020,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1201,6 +1229,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1244,9 +1278,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" diff --git a/Cargo.toml b/Cargo.toml index f1ad3b1..3794517 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,8 @@ path = "src/main.rs" [dependencies] anyhow = {version = "^1.0.65", features = ["backtrace"]} -clap = {version = "^4.4.0", features = ["derive"]} +clap = {version = "^4.5.19", features = ["derive"]} +clap_complete = {version = "4.5.32", features = ["unstable-dynamic"]} ctrlc = "^3.2.3" derive_more = "^0.99.17" dialoguer = {version = "^0.10.2", default-features = false} diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index e5f35a3..1502663 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -15,6 +15,7 @@ - [Inheritance & Cascading Configs](./user_guide/inheritance.md) - [Side Effects](./user_guide/side_effects.md) - [`es run` and Shell Interactions](./user_guide/run_advanced.md) +- [Shell Completions](./user_guide/shell_completions.md) # API Reference diff --git a/docs/src/user_guide/shell_completions.md b/docs/src/user_guide/shell_completions.md new file mode 100644 index 0000000..e90417f --- /dev/null +++ b/docs/src/user_guide/shell_completions.md @@ -0,0 +1,43 @@ +# Shell Completions + +Env-select provides tab completions for most shells. For the full list of supported shells, [see the clap docs](https://docs.rs/clap_complete/latest/clap_complete/aot/enum.Shell.html). + +> Note: Env-select uses clap's native shell completions, which are still experimental. [This issue](https://github.com/clap-rs/clap/issues/3166) outlines the remaining work to be done. + +To source your completions: + +**WARNING:** We recommend re-sourcing your completions on upgrade. +These completions work by generating shell code that calls into `your_program` while completing. That interface is unstable and a mismatch between the shell code and `your_program` may result in either invalid completions or no completions being generated. + +For this reason, we recommend generating the shell code anew on shell startup so that it is "self-correcting" on shell launch, rather than writing the generated completions to a file. + +## Bash + +```bash +echo "source <(COMPLETE=bash es)" >> ~/.bashrc +``` + +## Elvish + +```elvish +echo "eval (E:COMPLETE=elvish es | slurp)" >> ~/.elvish/rc.elv +``` + +## Fish + +```fish +echo "source (COMPLETE=fish es | psub)" >> ~/.config/fish/config.fish +``` + +## Powershell + +```powershell +echo "COMPLETE=powershell es | Invoke-Expression" >> $PROFILE +``` + +## Zsh + +````zsh +echo "source <(COMPLETE=zsh es)" >> ~/.zshrc +``` +```` diff --git a/src/commands/mod.rs b/src/commands/mod.rs index eb6fba3..0f2b129 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,6 +6,7 @@ use crate::{ commands::{ init::InitCommand, run::RunCommand, set::SetCommand, show::ShowCommand, }, + completions::{complete_application, complete_profile}, config::{Config, Name, Profile}, console::prompt_options, environment::Environment, @@ -14,6 +15,7 @@ use crate::{ GlobalArgs, }; use clap::Subcommand; +use clap_complete::ArgValueCompleter; use smol::lock::OnceCell; use std::path::PathBuf; @@ -58,10 +60,12 @@ trait SubcommandTrait { pub struct Selection { /// Application to select a profile for. If omitted, an interactive prompt /// will be shown to select between possible options + #[clap(add = ArgValueCompleter::new(complete_application))] pub application: Option, /// Profile to select. If omitted, an interactive prompt will be shown to /// select between possible options. + #[clap(add = ArgValueCompleter::new(complete_profile))] pub profile: Option, } diff --git a/src/commands/show.rs b/src/commands/show.rs index 3d98f6e..b46282e 100644 --- a/src/commands/show.rs +++ b/src/commands/show.rs @@ -1,8 +1,10 @@ use crate::{ commands::{CommandContext, SubcommandTrait}, + completions::{complete_application, complete_profile}, config::{MapExt, Name}, }; use clap::{Parser, Subcommand}; +use clap_complete::ArgValueCompleter; /// Print configuration and meta information #[derive(Clone, Debug, Parser)] @@ -19,9 +21,11 @@ enum ShowSubcommand { // are incorrect for this use case /// Application to show configuration for. If omitted, show all /// applications. + #[clap(add = ArgValueCompleter::new(complete_application))] application: Option, /// Profile to show configuration for. If omitted, show all profiles /// for the selected application. + #[clap(add = ArgValueCompleter::new(complete_profile))] profile: Option, }, /// Print the name or path to the shell in use diff --git a/src/completions.rs b/src/completions.rs new file mode 100644 index 0000000..72a6dd4 --- /dev/null +++ b/src/completions.rs @@ -0,0 +1,44 @@ +use crate::config::{Config, Name}; +use clap_complete::CompletionCandidate; +use std::ffi::OsStr; + +/// Provide completions for application names +pub fn complete_application(current: &OsStr) -> Vec { + let Ok(config) = Config::load() else { + return Vec::new(); + }; + + get_candidates(config.applications.keys().map(Name::as_str), current) +} + +/// Provide completions for profile names +pub fn complete_profile(current: &OsStr) -> Vec { + let Ok(config) = Config::load() else { + return Vec::new(); + }; + + // Suggest all profiles for all applications. Ideally we could grab the + // prior argument to tell us what application we're in, but I'm not sure if + // clap exposes that at all + get_candidates( + config + .applications + .values() + .flat_map(|application| application.profiles.keys()) + .map(Name::as_str), + current, + ) +} + +fn get_candidates<'a>( + iter: impl Iterator, + current: &OsStr, +) -> Vec { + let Some(current) = current.to_str() else { + return Vec::new(); + }; + // Only include IDs prefixed by the input we've gotten so far + iter.filter(|value| value.starts_with(current)) + .map(CompletionCandidate::new) + .collect() +} diff --git a/src/config/mod.rs b/src/config/mod.rs index fdb804b..97d2811 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -229,6 +229,12 @@ impl Config { } } +impl Name { + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + // Validate application/profile name. We do a bit of sanity checking here to // prevent stuff that might be confusing, or collide with env-select features impl FromStr for Name { diff --git a/src/main.rs b/src/main.rs index 198072f..c89dd24 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod commands; +mod completions; mod config; mod console; mod environment; @@ -10,9 +11,10 @@ mod test_util; mod shell; use crate::{commands::Commands, error::ExitCodeError, shell::ShellKind}; -use clap::Parser; +use clap::{CommandFactory, Parser}; use log::{error, LevelFilter}; // https://github.com/la10736/rstest/tree/master/rstest_reuse#cavelets +use clap_complete::CompleteEnv; #[cfg(test)] #[allow(clippy::single_component_path_imports)] use rstest_reuse; @@ -51,6 +53,8 @@ pub struct GlobalArgs { } fn main() -> ExitCode { + // If COMPLETE var is enabled, process will stop after completions + CompleteEnv::with_factory(Args::command).complete(); let args = Args::parse(); env_logger::Builder::new() .format_timestamp(None)