From 64e6131828134c6c876187d62cc50e6895f89a00 Mon Sep 17 00:00:00 2001 From: Ralf Grubenmann Date: Thu, 18 Dec 2025 15:32:07 +0100 Subject: [PATCH] add shell completions --- Cargo.lock | 10 ++++ README.md | 23 +++++++++ coman/Cargo.toml | 2 + coman/src/cli.rs | 117 ++++++++++++++++++++++++++++++---------------- coman/src/main.rs | 8 +++- 5 files changed, 118 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38a3899..bc17fe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -936,6 +936,15 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "clap_complete" +version = "4.5.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.47" @@ -1024,6 +1033,7 @@ dependencies = [ "chrono", "claim", "clap", + "clap_complete", "cli-log", "color-eyre", "config", diff --git a/README.md b/README.md index fe0b3c8..280307e 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,29 @@ Upload a file: coman cscs file upload /my/local/file /capstor/scratch/cscs/your_user/your_file ``` +You can set up shell completions as follows: + +```shell +# Bash +mkdir -p ~/.local/share/bash-completion/completions +coman completions bash > ~/.local/share/bash-completion/completions/coman + +# Bash (macOS/Homebrew) +mkdir -p $(brew --prefix)/etc/bash_completion.d/ +coman completions bash > $(brew --prefix)/etc/bash_completion.d/coman.bash-completion + +# Fish +mkdir -p ~/.config/fish/completions +coman completions fish > ~/.config/fish/completions/coman.fish + +# Zsh +mkdir ~/.zfunc +# Then add the following lines to your `.zshrc` just before +# `compinit`: +# +# fpath+=~/.zfunc +coman completions zsh > ~/.zfunc/_coman +``` ### TUI To run the TUI, simply run `coman` without any arguments: diff --git a/coman/Cargo.toml b/coman/Cargo.toml index bcb50c5..cd446ef 100644 --- a/coman/Cargo.toml +++ b/coman/Cargo.toml @@ -73,9 +73,11 @@ openssl = { version = "0.10.75", features = ["vendored"] } tui-realm-treeview = "3.0.0" aws-sdk-s3 = "1.115.0" toml_edit = "0.23.9" +clap_complete = "4.5.61" [build-dependencies] anyhow = "1.0.90" + vergen-gix = { version = "1.0.2", features = ["build", "cargo"] } [dev-dependencies] diff --git a/coman/src/cli.rs b/coman/src/cli.rs index 79b4f35..6382741 100644 --- a/coman/src/cli.rs +++ b/coman/src/cli.rs @@ -1,6 +1,7 @@ use std::{error::Error, path::PathBuf}; -use clap::{Args, Parser, Subcommand, builder::TypedValueParser}; +use clap::{Args, Command, Parser, Subcommand, ValueHint, builder::TypedValueParser}; +use clap_complete::{Generator, Shell, generate}; use color_eyre::Result; use strum::VariantNames; @@ -10,6 +11,17 @@ use crate::{ util::types::DockerImageUrl, }; +#[derive(Parser, Debug)] +#[command(author, version = version(), about)] +pub struct Cli { + /// Tick rate, i.e. number of ticks per second + #[arg(short, long, value_name = "FLOAT", default_value_t = 4.0)] + pub tick_rate: f64, + + #[command(subcommand)] + pub command: Option, +} + #[derive(Subcommand, Debug)] #[allow(clippy::large_enum_variant)] pub enum CliCommands { @@ -19,18 +31,25 @@ pub enum CliCommands { Cscs { #[command(subcommand)] command: CscsCommands, - #[clap(short, long, help = "override compute system (e.g. 'eiger', 'daint')")] + #[clap(short, long, help = "override compute system (e.g. 'eiger', 'daint')", value_hint=ValueHint::Other)] system: Option, - #[clap(short, long, ignore_case=true, value_parser=clap::builder::PossibleValuesParser::new(ComputePlatform::VARIANTS).map(|s|s.parse::().unwrap()),help = "override compute platform (one of 'hpc', 'ml' or 'cw')")] + #[clap( + short, + long, + ignore_case=true, + value_parser=clap::builder::PossibleValuesParser::new(ComputePlatform::VARIANTS).map( + |s|s.parse::().unwrap()), + help = "override compute platform (one of 'hpc', 'ml' or 'cw')", + value_hint=ValueHint::Other)] platform: Option, - #[clap(short, long, help = "override compute account to use (project or user)")] + #[clap(short, long, help = "override compute account to use (project or user)",value_hint=ValueHint::Other)] account: Option, }, #[clap(about = "Create a new project configuration file")] Init { - #[clap(help = "destination folder to create config in (default = current directory)")] + #[clap(help = "destination folder to create config in (default = current directory)",value_hint=ValueHint::DirPath)] destination: Option, - #[clap(help = "project name to use")] + #[clap(help = "project name to use", value_hint=ValueHint::Other)] name: Option, }, #[clap(about = "Manage configuration")] @@ -38,6 +57,12 @@ pub enum CliCommands { #[command(subcommand)] command: ConfigCommands, }, + #[clap(about = "Generate shell completions")] + Completions { + /// generate shell completions + #[clap(value_enum)] + generator: Shell, + }, } #[derive(Subcommand, Debug)] @@ -51,13 +76,13 @@ pub enum ConfigCommands { help = "whether to change the global config or the project local one" )] global: bool, - #[clap(help = "Config key path, e.g. `cscs.current_system`")] + #[clap(help = "Config key path, e.g. `cscs.current_system`", value_hint=ValueHint::Other)] key_path: String, - #[clap(help = "Value to set", value_parser = parse_toml_value)] + #[clap(help = "Value to set", value_parser = parse_toml_value, value_hint=ValueHint::Other)] value: toml_edit::Value, }, Get { - #[clap(help = "Config key path, e.g. `cscs.current_system`")] + #[clap(help = "Config key path, e.g. `cscs.current_system`", value_hint=ValueHint::Other)] key_path: String, }, } @@ -95,9 +120,9 @@ pub struct ScriptSpec { help = "generate and upload script file based on template (on by default unless `--local` or `--remote` are passed)" )] generate_script: bool, - #[arg(long, value_name = "PATH", help = "upload local script file")] + #[arg(long, value_name = "PATH", help = "upload local script file", value_hint=ValueHint::FilePath)] local_script: Option, - #[arg(long, value_name = "PATH", help = "use script file already present on remote")] + #[arg(long, value_name = "PATH", help = "use script file already present on remote", value_hint=ValueHint::Other)] remote_script: Option, } impl Default for ScriptSpec { @@ -130,9 +155,9 @@ pub struct EdfSpec { help = "generate and upload edf file based on template (on by default unless `--local` or `--remote` are passed)" )] generate_edf: bool, - #[arg(long, value_name = "PATH", help = "upload local edf file")] + #[arg(long, value_name = "PATH", help = "upload local edf file", value_hint=ValueHint::FilePath)] local_edf: Option, - #[arg(long, value_name = "PATH", help = "use edf file already present on remote")] + #[arg(long, value_name = "PATH", help = "use edf file already present on remote", value_hint=ValueHint::Other)] remote_edf: Option, } @@ -164,65 +189,84 @@ pub enum CscsJobCommands { #[clap(alias("ls"), about = "List all jobs [aliases: ls]")] List, #[clap(alias("g"), about = "Get metadata for a specific job [aliases: g]")] - Get { job_id: i64 }, + Get { + #[arg(help="id of the job", value_hint=ValueHint::Other)] + job_id: i64, + }, #[clap(about = "Get the stdout of a job")] Log { #[clap(short, long, action, help = "whether to get stderr instead of stdout")] stderr: bool, + #[arg(help="id of the job", value_hint=ValueHint::Other)] job_id: i64, }, #[clap(alias("s"), about = "Submit a new compute job [aliases: s]")] Submit { - #[clap(short, long, help = "name of the job")] + #[clap(short, long, help = "name of the job", value_hint=ValueHint::Other)] name: Option, #[clap( short, long, - help = "the working directory path inside the container (note this is different from the working directory that the srun command is executed from)" + help = "the working directory path inside the container (note this is different from the working directory that the srun command is executed from)", + value_hint=ValueHint::Other )] workdir: Option, - #[clap(short='E', value_name="KEY=VALUE", value_parser=parse_key_val::, help="Environment variables to set in the container")] + #[clap(short='E', + value_name="KEY=VALUE", + value_parser=parse_key_val::, + help="Environment variables to set in the container", + value_hint=ValueHint::Other)] env: Vec<(String, String)>, - #[clap(short='M', value_name="PATH:CONTAINER_PATH", value_parser=parse_key_val_colon::, help="Paths to mount inside container")] + #[clap(short='M', + value_name="PATH:CONTAINER_PATH", + value_parser=parse_key_val_colon::, + help="Paths to mount inside container", + value_hint=ValueHint::Other)] mount: Vec<(String, String)>, - #[clap(short, long, help = "The docker image to use")] + #[clap(short, long, help = "The docker image to use", value_hint=ValueHint::Other)] image: Option, - #[clap(long, help = "Path where stdout of the job gets written to")] + #[clap(long, help = "Path where stdout of the job gets written to", value_hint=ValueHint::Other)] stdout: Option, - #[clap(long, help = "Path where stderr of the job gets written to")] + #[clap(long, help = "Path where stderr of the job gets written to", value_hint=ValueHint::Other)] stderr: Option, #[command(flatten)] edf_spec: Option, #[command(flatten)] script_spec: Option, - #[clap(trailing_var_arg = true, help = "The command to run in the container")] + #[clap(trailing_var_arg = true, help = "The command to run in the container", value_hint=ValueHint::Other)] command: Option>, }, #[clap( alias("c"), about = "Cancel a running job, fails if the job isn't running [aliases: c]" )] - Cancel { job_id: i64 }, + Cancel { + #[clap(help="id of the job", value_hint=ValueHint::Other)] + job_id: i64, + }, } #[derive(Subcommand, Debug)] pub enum CscsFileCommands { #[clap(alias("ls"), about = "List folders and files in a remote path [aliases: ls]")] - List { path: PathBuf }, + List { + #[arg(help ="remote path to list", value_hint=ValueHint::Other)] + path: PathBuf, + }, #[clap(alias("dl"), about = "Download a remote file [aliases: dl]")] Download { - #[clap(help = "The path in the cluster to download")] + #[clap(help = "The path in the cluster to download", value_hint=ValueHint::Other)] remote: PathBuf, - #[clap(help = "The local path to download the file to")] + #[clap(help = "The local path to download the file to", value_hint=ValueHint::AnyPath)] local: PathBuf, }, #[clap(alias("ul"), about = "Upload a file to remote storage [aliases: ul]")] Upload { - #[clap(help = "The local path to upload to the cluster")] + #[clap(help = "The local path to upload to the cluster", value_hint=ValueHint::AnyPath)] local: PathBuf, - #[clap(help = "the path in the cluster to upload to")] + #[clap(help = "the path in the cluster to upload to", value_hint=ValueHint::Other)] remote: PathBuf, }, } @@ -238,22 +282,11 @@ pub enum CscsSystemCommands { Set { #[clap(short, long, action, help = "set in global config instead of project-local one")] global: bool, - #[clap(help = "System name to use")] + #[clap(help = "System name to use", value_hint=ValueHint::Other)] system_name: String, }, } -#[derive(Parser, Debug)] -#[command(author, version = version(), about)] -pub struct Cli { - /// Tick rate, i.e. number of ticks per second - #[arg(short, long, value_name = "FLOAT", default_value_t = 4.0)] - pub tick_rate: f64, - - #[command(subcommand)] - pub command: Option, -} - const VERSION_MESSAGE: &str = concat!( env!("CARGO_PKG_VERSION"), "-", @@ -335,3 +368,7 @@ fn is_bare_string(value_str: &str) -> bool { true // empty or whitespace only } } + +pub fn print_completions(generator: G, cmd: &mut Command) { + generate(generator, cmd, cmd.get_name().to_string(), &mut std::io::stdout()); +} diff --git a/coman/src/main.rs b/coman/src/main.rs index 5357eed..722eccf 100644 --- a/coman/src/main.rs +++ b/coman/src/main.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use clap::Parser; +use clap::{CommandFactory, Parser}; use color_eyre::Result; use keyring::set_global_service_name; use tokio::{runtime::Handle, sync::mpsc}; @@ -17,7 +17,7 @@ use crate::{ model::Model, user_events::{CscsEvent, FileEvent, StatusEvent, UserEvent}, }, - cli::{Cli, get_config, set_config, version}, + cli::{Cli, get_config, print_completions, set_config, version}, components::{ file_tree::FileTree, global_listener::GlobalListener, status_bar::StatusBar, toolbar::Toolbar, workload_list::WorkloadList, @@ -58,6 +58,10 @@ async fn main() -> Result<()> { match args.command { Some(command) => match command { cli::CliCommands::Version => println!("{}", version()), + cli::CliCommands::Completions { generator } => { + let mut cmd = Cli::command(); + print_completions(generator, &mut cmd); + } cli::CliCommands::Config { command: config_command, } => match config_command {