From 689e39b3f6beba4d2f95f6bd54325ed52732692f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C3=85kesson?= Date: Wed, 8 Apr 2026 00:25:11 +0200 Subject: [PATCH] Add shell-completions for `dx` (#5232) * add naive shell completion using clap_complete * first pass completions * simpler toggle --------- Co-authored-by: Jonathan Kelley --- Cargo.lock | 22 ++++++++++++++ packages/cli/Cargo.toml | 1 + packages/cli/src/cli/mod.rs | 5 +++ packages/cli/src/cli/platform_override.rs | 13 +++++--- packages/cli/src/cli/shell_completions.rs | 33 ++++++++++++++++++++ packages/cli/src/logging.rs | 37 ++++++++++++++--------- packages/cli/src/main.rs | 1 + 7 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 packages/cli/src/cli/shell_completions.rs diff --git a/Cargo.lock b/Cargo.lock index 3d5aeba1c..6b1ed54b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3452,6 +3452,18 @@ dependencies = [ "terminal_size", ] +[[package]] +name = "clap_complete" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" +dependencies = [ + "clap", + "clap_lex", + "is_executable", + "shlex", +] + [[package]] name = "clap_derive" version = "4.5.61" @@ -4855,6 +4867,7 @@ dependencies = [ "cargo_toml", "chrono", "clap", + "clap_complete", "codemap", "console 0.16.3", "console-subscriber", @@ -9142,6 +9155,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_executable" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index f1086e9a1..230fb89c7 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -169,6 +169,7 @@ nucleo = "0.5.0" # 0.5.12 rust-version is too high home = "=0.5.11" +clap_complete = { version = "4.5.65", features = ["unstable-dynamic"] } [target.'cfg(target_env = "musl")'.dependencies] git2 = { version = "0.20.2", features = ["vendored-libgit2"] } diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index c1c11f57e..f8be0dfce 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod platform_override; pub(crate) mod print; pub(crate) mod run; pub(crate) mod serve; +pub(crate) mod shell_completions; pub(crate) mod target; pub(crate) mod translate; pub(crate) mod update; @@ -84,6 +85,10 @@ pub(crate) enum Commands { #[clap(name = "doctor")] Doctor(doctor::Doctor), + /// Print shell completions for the given shell. + #[clap(name = "completions")] + ShellCompletions(shell_completions::ShellCompletions), + /// Print project information in a structured format, like cargo args, linker args, and other /// flags DX sets that might be useful in third-party tools. #[clap(name = "print")] diff --git a/packages/cli/src/cli/platform_override.rs b/packages/cli/src/cli/platform_override.rs index 826e712b6..37a23e6c9 100644 --- a/packages/cli/src/cli/platform_override.rs +++ b/packages/cli/src/cli/platform_override.rs @@ -1,5 +1,5 @@ #![allow(dead_code)] -use crate::Anonymized; +use crate::{shell_completions::GENERATING_COMPLETIONS, Anonymized}; use clap::parser::ValueSource; use clap::{ArgMatches, Args, CommandFactory, FromArgMatches, Parser, Subcommand}; use serde_json::{json, Value}; @@ -159,9 +159,14 @@ where U: Subcommand, { fn augment_args(cmd: clap::Command) -> clap::Command { - // We use the special `defer` method which lets us recursively call `augment_args` on the inner command - // and thus `from_arg_matches` - T::augment_args(cmd).defer(|cmd| U::augment_subcommands(cmd.disable_help_subcommand(true))) + let cmd = T::augment_args(cmd).disable_help_subcommand(true); + if GENERATING_COMPLETIONS.load(std::sync::atomic::Ordering::Relaxed) { + cmd + } else { + // We use the special `defer` method which lets us recursively call `augment_args` on the inner command + // and thus `from_arg_matches` + cmd.defer(|cmd| U::augment_subcommands(cmd)) + } } fn augment_args_for_update(_cmd: clap::Command) -> clap::Command { diff --git a/packages/cli/src/cli/shell_completions.rs b/packages/cli/src/cli/shell_completions.rs new file mode 100644 index 000000000..cc5fdea45 --- /dev/null +++ b/packages/cli/src/cli/shell_completions.rs @@ -0,0 +1,33 @@ +use super::*; +use crate::{cli, Result}; +use clap::CommandFactory; +use clap_complete::{generate, Shell}; +use std::sync::atomic::AtomicBool; + +/// Generate shell completions for the specified shell. +#[derive(Clone, Debug, Parser)] +pub(crate) struct ShellCompletions { + /// The shell to generate completions for. + #[clap(value_enum)] + pub shell: Shell, +} + +/// An annoying, code smelly, way of setting the CLI recursive depth parser to limit early, useful for +/// generating completions here. +/// +/// This should be set before calling clap_complete's generation function +pub static GENERATING_COMPLETIONS: AtomicBool = AtomicBool::new(false); + +impl ShellCompletions { + pub fn generate_and_print(self) -> Result { + let mut cmd = cli::Cli::command(); + GENERATING_COMPLETIONS.swap(true, std::sync::atomic::Ordering::Relaxed); + generate( + self.shell, + &mut cmd, + env!("CARGO_BIN_NAME"), + &mut std::io::stdout(), + ); + Ok(StructuredOutput::Success) + } +} diff --git a/packages/cli/src/logging.rs b/packages/cli/src/logging.rs index 932eb78bc..8a2f649b5 100644 --- a/packages/cli/src/logging.rs +++ b/packages/cli/src/logging.rs @@ -73,21 +73,6 @@ pub fn verbosity_or_default() -> Verbosity { crate::VERBOSITY.get().cloned().unwrap_or_default() } -fn reset_cursor() { - use std::io::IsTerminal; - - // if we are running in a terminal, reset the cursor. The tui_active flag is not set for - // the cargo generate TUI, but we still want to reset the cursor. - if std::io::stdout().is_terminal() { - _ = console::Term::stdout().show_cursor(); - } -} - -/// A trait that emits an anonymous JSON representation of the object, suitable for telemetry. -pub(crate) trait Anonymized { - fn anonymized(&self) -> serde_json::Value; -} - /// A custom layer that wraps our special interception logic based on the mode of the CLI and its verbosity. /// /// Redirects TUI logs, writes to files, and queues telemetry events. @@ -114,6 +99,7 @@ struct CapturedPanicError { thread_name: Option, location: Option, } + impl std::fmt::Display for CapturedPanicError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -132,6 +118,11 @@ struct SavedLocation { column: u32, } +/// A trait that emits an anonymous JSON representation of the object, suitable for telemetry. +pub(crate) trait Anonymized { + fn anonymized(&self) -> serde_json::Value; +} + impl TraceController { /// Initialize the CLI and set up the tracing infrastructure /// @@ -840,6 +831,12 @@ impl TraceController { }), ), Commands::Doctor(_cmd) => ("doctor".to_string(), json!({})), + Commands::ShellCompletions(cmd) => ( + "completions".to_string(), + json!({ + "shell": cmd.shell.to_string(), + }), + ), Commands::Translate(cmd) => ( "translate".to_string(), json!({ @@ -1437,3 +1434,13 @@ impl PosthogEvent { Ok(()) } } + +fn reset_cursor() { + use std::io::IsTerminal; + + // if we are running in a terminal, reset the cursor. The tui_active flag is not set for + // the cargo generate TUI, but we still want to reset the cursor. + if std::io::stdout().is_terminal() { + _ = console::Term::stdout().show_cursor(); + } +} diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index dce3197e5..eee5833ca 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -75,6 +75,7 @@ async fn main() -> ExitCode { Commands::Doctor(opts) => opts.doctor().await, Commands::Print(opts) => opts.print().await, Commands::Components(opts) => opts.run().await, + Commands::ShellCompletions(opts) => opts.generate_and_print(), } }) });