From 007d7937ecb32d15adb3178befcc7284001745eb Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Wed, 20 May 2026 20:46:18 +0100 Subject: [PATCH] Move flyline comp-spec-synth to own crate --- Cargo.lock | 11 +++++ Cargo.toml | 4 ++ README.md | 5 +-- docker/builder.Dockerfile | 4 ++ flycomp/Cargo.toml | 18 +++++++++ .../src/lib.rs | 40 +++++++++++++------ flycomp/src/main.rs | 19 +++++++++ src/cli.rs | 40 +++++-------------- src/lib.rs | 1 - 9 files changed, 96 insertions(+), 46 deletions(-) create mode 100644 flycomp/Cargo.toml rename src/comp_spec_synthesis.rs => flycomp/src/lib.rs (98%) create mode 100644 flycomp/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 5b7443c4..c6f9373c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -596,6 +596,16 @@ dependencies = [ "atty", ] +[[package]] +name = "flycomp" +version = "1.0.0" +dependencies = [ + "anyhow", + "clap", + "clap_complete", + "log", +] + [[package]] name = "flyline" version = "1.0.0" @@ -609,6 +619,7 @@ dependencies = [ "ctor", "easing-function", "flash", + "flycomp", "glob", "itertools", "libc", diff --git a/Cargo.toml b/Cargo.toml index e4a6978d..25713985 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ name = "flyline" version = "1.0.0" edition = "2024" +[workspace] +members = ["flycomp"] + [lib] crate-type = ["cdylib"] @@ -39,6 +42,7 @@ parse-style = { version = "0.4.0" } easing-function = "0.1.1" ctor = "0.10.0" strum = { version = "0.28.0", features = ["derive"] } +flycomp = { path = "flycomp" } [features] pre_bash_4_4 = [] diff --git a/README.md b/README.md index 91841ed3..a2e366a3 100644 --- a/README.md +++ b/README.md @@ -389,7 +389,8 @@ Descriptions for files are the time since last modified. ### Automatically complete based on `--help` Coming soon: Automatically generate a completion spec for commands without one. -For now, you can manually generate a Bash completion script with `flyline comp-spec-synthesis your_command`. +For now, you can manually generate a completion script with `flycomp your_command`. +Use `--shell zsh` for zsh output (defaults to bash). ### `LS_COLORS` styling Flyline styles your filename tab completion results according to `$LS_COLORS`: @@ -470,8 +471,6 @@ Commands: log Logging commands: dump, configure level, or stream logs. run-tutorial Run the interactive tutorial for first-time users. editor Configure the inline editor. - comp-spec-synthesis Run a command with --help, parse the output, and print a Bash completion - script to stdout. help Print this message or the help of the given subcommand(s) Options: diff --git a/docker/builder.Dockerfile b/docker/builder.Dockerfile index fa3af39e..cd5dec82 100644 --- a/docker/builder.Dockerfile +++ b/docker/builder.Dockerfile @@ -29,7 +29,9 @@ RUN cargo install cargo-chef --locked # Stage 2: Planner FROM chef AS planner COPY Cargo.toml Cargo.lock build.rs ./ +COPY flycomp/Cargo.toml ./flycomp/Cargo.toml COPY src ./src +COPY flycomp/src ./flycomp/src COPY examples ./examples RUN cargo chef prepare --recipe-path recipe.json @@ -40,7 +42,9 @@ ARG CARGO_FEATURES COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release ${CARGO_FEATURES:+--features $CARGO_FEATURES} --recipe-path recipe.json COPY Cargo.toml Cargo.lock build.rs ./ +COPY flycomp/Cargo.toml ./flycomp/Cargo.toml COPY src ./src +COPY flycomp/src ./flycomp/src COPY examples ./examples COPY tests ./tests RUN cargo build --release ${CARGO_FEATURES:+--features $CARGO_FEATURES} diff --git a/flycomp/Cargo.toml b/flycomp/Cargo.toml new file mode 100644 index 00000000..53567450 --- /dev/null +++ b/flycomp/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "flycomp" +version = "1.0.0" +edition = "2024" + +[lib] +name = "flycomp" +path = "src/lib.rs" + +[[bin]] +name = "flycomp" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.102" +clap = { version = "4.6.1", features = ["derive"] } +clap_complete = { version = "4.6.3", features = ["unstable-dynamic"] } +log = "0.4" diff --git a/src/comp_spec_synthesis.rs b/flycomp/src/lib.rs similarity index 98% rename from src/comp_spec_synthesis.rs rename to flycomp/src/lib.rs index c921c958..cb1d5e1c 100644 --- a/src/comp_spec_synthesis.rs +++ b/flycomp/src/lib.rs @@ -608,8 +608,8 @@ pub fn parse_help_generic(help: &str) -> Command { /// This is used when building a dynamic [`clap::Command`] structure at runtime, /// because clap 4.x's builder methods (`.long()`, `.about()`, `.help()`, /// `.value_name()`) require `&'static str` references. The leak is intentional -/// and acceptable because `to_clap_command` is only called from the one-shot -/// `comp-spec-synthesis` subcommand. +/// and acceptable because `to_clap_command` is only called in short-lived +/// completion synthesis runs. fn leak_string(s: String) -> &'static str { Box::leak(s.into_boxed_str()) } @@ -648,10 +648,7 @@ pub fn to_clap_command(cmd: &Command) -> clap::Command { if let Some(long) = &long_bare { if !used_long_flags.insert(long.clone()) { - log::debug!( - "comp-spec-synthesis: dropping duplicate long flag '--{}'", - long - ); + log::debug!("flycomp: dropping duplicate long flag '--{}'", long); continue; } } @@ -688,7 +685,7 @@ pub fn to_clap_command(cmd: &Command) -> clap::Command { clap_arg = clap_arg.short(c); } else { log::debug!( - "comp-spec-synthesis: dropping duplicate short flag '-{}' for arg {:?}", + "flycomp: dropping duplicate short flag '-{}' for arg {:?}", c, id ); @@ -797,11 +794,7 @@ where Ok(s) if !s.trim().is_empty() => s, Ok(_) => continue, Err(e) => { - log::debug!( - "comp-spec-synthesis: skipping '{}': {}", - path_strs.join(" "), - e - ); + log::debug!("flycomp: skipping '{}': {}", path_strs.join(" "), e); continue; } }; @@ -868,6 +861,29 @@ pub(crate) fn run_help(command_path: &str, extra_args: &[&str]) -> anyhow::Resul }) } +/// Run `command_path --help`, synthesize its completion model, and render a +/// shell completion script. +pub fn generate_completion_script( + command_path: &str, + shell: clap_complete::Shell, +) -> anyhow::Result { + let parsed_cmd = synthesize_completion(command_path, |args| run_help(command_path, args))?; + let cmd_name = std::path::Path::new(command_path) + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or(command_path) + .to_string(); + + let mut clap_cmd = to_clap_command(&parsed_cmd); + let mut output = Vec::new(); + clap_complete::generate(shell, &mut clap_cmd, &cmd_name, &mut output); + + let script = std::str::from_utf8(&output) + .map_err(|e| anyhow::anyhow!("failed to encode completion script: {}", e))? + .to_string(); + Ok(script) +} + #[cfg(test)] mod tests { use super::*; diff --git a/flycomp/src/main.rs b/flycomp/src/main.rs new file mode 100644 index 00000000..6decbbe1 --- /dev/null +++ b/flycomp/src/main.rs @@ -0,0 +1,19 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "flycomp")] +#[command(about = "Generate shell completions from COMMAND --help output")] +struct CliArgs { + /// Command name or path to synthesize completions for. + command: String, + /// Output shell type (defaults to bash). + #[arg(long, value_enum, default_value_t = clap_complete::Shell::Bash)] + shell: clap_complete::Shell, +} + +fn main() -> anyhow::Result<()> { + let args = CliArgs::parse(); + let script = flycomp::generate_completion_script(&args.command, args.shell)?; + print!("{}", script); + Ok(()) +} diff --git a/src/cli.rs b/src/cli.rs index 1fb9824e..f78f9959 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,16 +1,18 @@ use clap::{CommandFactory, Parser, Subcommand, error::ErrorKind}; -use clap_complete::{ArgValueCompleter, CompletionCandidate, Shell, generate}; +use clap_complete::{ArgValueCompleter, CompletionCandidate}; use libc::c_int; use strum::VariantArray; use crate::{ Flyline, app::actions::{self}, - bash_funcs, bash_symbols, comp_spec_synthesis, content_utils, + bash_funcs, bash_symbols, content_utils, cursor::{self, CursorStyleConfig}, dparser, logging, palette, settings, tutorial, }; +use flycomp::generate_completion_script; + fn get_styles() -> clap::builder::Styles { clap::builder::Styles::styled() .header( @@ -1154,34 +1156,12 @@ impl Flyline { } } Some(Commands::CompSpecSynthesis { command }) => { - match comp_spec_synthesis::synthesize_completion(&command, |args| { - let prev_sigchld = - unsafe { libc::signal(libc::SIGCHLD, libc::SIG_DFL) }; - let ret = comp_spec_synthesis::run_help(&command, args); - unsafe { libc::signal(libc::SIGCHLD, prev_sigchld) }; - ret - }) { - Ok(parsed_cmd) => { - let cmd_name = std::path::Path::new(&command) - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or(&command) - .to_string(); - let mut clap_cmd = - comp_spec_synthesis::to_clap_command(&parsed_cmd); - let mut output = Vec::new(); - generate(Shell::Bash, &mut clap_cmd, &cmd_name, &mut output); - match std::str::from_utf8(&output) { - Ok(s) => print!("{}", s), - Err(e) => { - log::error!( - "flyline comp-spec-synthesis: failed to encode output: {}", - e - ); - return bash_symbols::BuiltinExitCode::Usage as c_int; - } - } - } + let prev_sigchld = unsafe { libc::signal(libc::SIGCHLD, libc::SIG_DFL) }; + let result = generate_completion_script(&command, clap_complete::Shell::Bash); + unsafe { libc::signal(libc::SIGCHLD, prev_sigchld) }; + + match result { + Ok(script) => print!("{}", script), Err(e) => { return_usage_error!("flyline comp-spec-synthesis: {}", e); } diff --git a/src/lib.rs b/src/lib.rs index ef9e4f3a..b42572d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,6 @@ mod bash_funcs; mod bash_symbols; mod cli; mod command_acceptance; -mod comp_spec_synthesis; mod content_builder; mod content_utils; mod cursor;