Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name = "flyline"
version = "1.0.0"
edition = "2024"

[workspace]
members = ["flycomp"]

[lib]
crate-type = ["cdylib"]

Expand Down Expand Up @@ -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 = []
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions docker/builder.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}
Expand Down
18 changes: 18 additions & 0 deletions flycomp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
40 changes: 28 additions & 12 deletions src/comp_spec_synthesis.rs → flycomp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -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;
}
};
Expand Down Expand Up @@ -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<String> {
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::*;
Expand Down
19 changes: 19 additions & 0 deletions flycomp/src/main.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
40 changes: 10 additions & 30 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading