Skip to content

Instantly share code, notes, and snippets.

@heaths
Last active June 8, 2026 05:22
Show Gist options
  • Select an option

  • Save heaths/333d00c1ddddbdc5ebb5dcb7e6e74c9f to your computer and use it in GitHub Desktop.

Select an option

Save heaths/333d00c1ddddbdc5ebb5dcb7e6e74c9f to your computer and use it in GitHub Desktop.
Cargo script example using dynamic completion with clap
#!/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