Last active
June 8, 2026 05:22
-
-
Save heaths/333d00c1ddddbdc5ebb5dcb7e6e74c9f to your computer and use it in GitHub Desktop.
Cargo script example using dynamic completion with clap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env -S cargo +nightly -q -Zscript | |
| --- | |
| [package] | |
| edition = "2024" | |
| description = "Cargo script example using dynamic completion with clap" | |
| [dependencies] | |
| clap = { version = "4.6.1", features = ["derive"] } | |
| clap_complete = { version = "4.6.5", features = ["unstable-dynamic"] } | |
| kdl = { version = "6.7.1", features = ["serde", "v1-fallback"] } | |
| serde = { version = "1.0.228", features = ["derive"] } | |
| --- | |
| //! Cargo script example using dynamic completion with clap. | |
| //! | |
| //! # Setup | |
| //! | |
| //! To register completions e.g., in bash: | |
| //! | |
| //! ```bash | |
| //! # Use actual name in lieu of `dynamic.rs`. | |
| //! source <(COMPLETE=bash ./dynamic.rs) | |
| //! ``` | |
| //! | |
| //! You need to do this every time after changing this script. | |
| //! | |
| //! # Caveats | |
| //! | |
| //! Programs have to be in the `$PATH` for completions to work. | |
| use clap::{CommandFactory, Parser}; | |
| use clap_complete::engine::{ArgValueCompleter, CompletionCandidate, PathCompleter}; | |
| use std::{ | |
| env, | |
| ffi::OsStr, | |
| fs, io, | |
| path::{Path, PathBuf}, | |
| }; | |
| fn main() -> Result<(), Box<dyn std::error::Error>> { | |
| clap_complete::CompleteEnv::with_factory(Args::command).complete(); | |
| let args = Args::parse(); | |
| let config = config::load()?; | |
| if let Some(layout) = args.layout.as_deref() { | |
| let layout_path = PathBuf::from(layout); | |
| if layout_exists(&layout_path)? { | |
| println!("layout = \"{}\"", layout_path.display()); | |
| return Ok(()); | |
| } | |
| if let Some(layout_dir) = config.layout_dir().as_deref() { | |
| let layout_path = layout_dir.join(format!("{layout}.kdl")); | |
| if layout_exists(&layout_path)? { | |
| println!("layout = \"{}\"", layout_path.display()); | |
| return Ok(()); | |
| } | |
| } | |
| return Err(io::Error::new(io::ErrorKind::NotFound, "layout not found").into()); | |
| } | |
| Ok(()) | |
| } | |
| fn layout_completer(current: &OsStr) -> Vec<CompletionCandidate> { | |
| use clap_complete::engine::ValueCompleter; | |
| // Consider files. | |
| let file_completer = PathCompleter::file(); | |
| let mut file_completions = file_completer | |
| .complete(current) | |
| .into_iter() | |
| .map(|c| c.tag(Some("files".into()))) | |
| .collect(); | |
| // Include file basenames in layout_dir. | |
| let Some(current) = current.to_str() else { | |
| return file_completions; | |
| }; | |
| let Ok(config) = config::load() else { | |
| return file_completions; | |
| }; | |
| let Some(layout_dir) = config.layout_dir() else { | |
| return file_completions; | |
| }; | |
| if !layout_dir.exists() { | |
| return file_completions; | |
| } | |
| let Ok(entries) = fs::read_dir(layout_dir) else { | |
| return file_completions; | |
| }; | |
| let mut completions = Vec::new(); | |
| for entry in entries.flatten() { | |
| let path = entry.path(); | |
| if path.extension() != Some(OsStr::new("kdl")) { | |
| continue; | |
| } | |
| let Some(name) = path.file_name().and_then(OsStr::to_str) else { | |
| continue; | |
| }; | |
| let Some(name) = name.strip_suffix(".kdl") else { | |
| continue; | |
| }; | |
| if name.starts_with(current) { | |
| completions.push(CompletionCandidate::new(name.to_owned()).tag(Some("layout".into()))); | |
| } | |
| } | |
| // List layouts first. | |
| completions.append(&mut file_completions); | |
| completions | |
| } | |
| fn layout_exists(path: &Path) -> Result<bool, io::Error> { | |
| match fs::metadata(path) { | |
| Ok(metadata) => Ok(metadata.is_file()), | |
| Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false), | |
| Err(err) => Err(err), | |
| } | |
| } | |
| #[derive(Parser)] | |
| // Called as a cargo script despite binary lacking extension. | |
| #[command(name = concat!(env!("CARGO_BIN_NAME"), ".rs"))] | |
| struct Args { | |
| /// Name or path to a layout. | |
| #[arg(long, add = ArgValueCompleter::new(layout_completer))] | |
| layout: Option<String>, | |
| } | |
| mod config { | |
| use serde::Deserialize; | |
| use std::{env, fs, io, path::PathBuf}; | |
| #[derive(Debug, Default, Deserialize)] | |
| pub struct Config { | |
| layout_dir: Option<PathBuf>, | |
| } | |
| impl Config { | |
| pub fn layout_dir(&self) -> Option<PathBuf> { | |
| self.layout_dir | |
| .clone() | |
| .or_else(|| Some(dir()?.join("layouts"))) | |
| } | |
| } | |
| pub fn dir() -> Option<PathBuf> { | |
| env::var_os("XDG_CONFIG_HOME") | |
| .map(PathBuf::from) | |
| .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".config"))) | |
| .map(|config_home| config_home.join("zellij")) | |
| } | |
| pub fn load() -> Result<Config, io::Error> { | |
| let Some(config_dir) = dir() else { | |
| return Ok(Config::default()); | |
| }; | |
| if !config_dir.try_exists()? { | |
| return Ok(Config::default()); | |
| } | |
| let config_path = config_dir.join("config.kdl"); | |
| if config_path.try_exists()? { | |
| let content = fs::read_to_string(config_path)?; | |
| return kdl::de::from_str(&content) | |
| .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)); | |
| } | |
| Ok(Config::default()) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment