feat: add shell autocompletion support
Add 'ranger completions <shell>' subcommand using clap_complete.
Supports bash, zsh, fish, elvish, and powershell.
Completions are generated without requiring a database connection.
diff --git a/Cargo.lock b/Cargo.lock
index 1d344ea..7d1fb72 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -282,6 +282,15 @@ dependencies = [
"strsim",
]
+[[package]]
+name = "clap_complete"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
+dependencies = [
+ "clap",
+]
+
[[package]]
name = "clap_derive"
version = "4.5.55"
@@ -1428,6 +1437,7 @@ dependencies = [
"assert_cmd",
"axum",
"clap",
+ "clap_complete",
"color-eyre",
"jiff",
"predicates",
diff --git a/Cargo.toml b/Cargo.toml
index fcff23b..30c0a53 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,7 @@ color-eyre = "*"
tracing-subscriber = { version = "*", features = ["env-filter"] }
jiff = { version = "*", features = ["serde"] }
clap = { version = "*", features = ["derive", "env"] }
+clap_complete = "*"
rand = "*"
serde = { version = "*", features = ["derive"] }
serde_json = "*"
diff --git a/src/bin/ranger/commands/serve.rs b/src/bin/ranger/commands/serve.rs
index 88f1d06..636c8b9 100644
--- a/src/bin/ranger/commands/serve.rs
+++ b/src/bin/ranger/commands/serve.rs
@@ -139,7 +139,9 @@ fn render_backlog_panel(in_progress: &[TaskView], queued: &[TaskView]) -> String
html.push_str(r#"<div class="empty">No active tasks</div>"#);
} else {
if !in_progress.is_empty() {
- html.push_str(r#"<div class="section-label section-label-in-progress">In Progress</div>"#);
+ html.push_str(
+ r#"<div class="section-label section-label-in-progress">In Progress</div>"#,
+ );
html.push_str(r#"<div class="state-in-progress">"#);
for task in in_progress {
html.push_str(&render_task(task));
@@ -217,10 +219,7 @@ fn render_task(task: &TaskView) -> String {
));
html.push_str("</div>");
if let Some(desc) = &task.description {
- html.push_str(&format!(
- r#"<div class="desc">{}</div>"#,
- html_escape(desc)
- ));
+ html.push_str(&format!(r#"<div class="desc">{}</div>"#, html_escape(desc)));
}
if task.has_subtasks {
html.push_str(&format!(
diff --git a/src/bin/ranger/main.rs b/src/bin/ranger/main.rs
index 11f4036..eb9772c 100644
--- a/src/bin/ranger/main.rs
+++ b/src/bin/ranger/main.rs
@@ -1,7 +1,8 @@
mod commands;
mod output;
-use clap::{Parser, Subcommand};
+use clap::{CommandFactory, Parser, Subcommand};
+use clap_complete::Shell;
use std::path::PathBuf;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
@@ -40,6 +41,11 @@ enum Commands {
#[command(subcommand)]
command: commands::comment::CommentCommands,
},
+ /// Generate shell completions
+ Completions {
+ /// Shell to generate completions for
+ shell: Shell,
+ },
/// Start the web server
Serve {
/// Port to listen on
@@ -69,6 +75,14 @@ async fn main() -> color_eyre::Result<()> {
.init();
let cli = Cli::parse();
+
+ // Handle completions before DB connection — no database needed
+ if let Some(Commands::Completions { shell }) = &cli.command {
+ let mut cmd = Cli::command();
+ clap_complete::generate(*shell, &mut cmd, "ranger", &mut std::io::stdout());
+ return Ok(());
+ }
+
let db_path = resolve_db_path(cli.db);
let pool = ranger::db::connect(&db_path).await?;
@@ -82,6 +96,7 @@ async fn main() -> color_eyre::Result<()> {
Some(Commands::Comment { command }) => {
commands::comment::run(&pool, command, cli.json).await?;
}
+ Some(Commands::Completions { .. }) => unreachable!(),
Some(Commands::Serve { port, backlog }) => {
commands::serve::run(&pool, port, backlog).await?;
}
diff --git a/tests/cli.rs b/tests/cli.rs
index 3ba3d85..09d3d5d 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -277,4 +277,15 @@ fn full_workflow() {
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Ranger"));
+
+ // Shell completions (no DB needed, but pass one anyway for the helper)
+ for shell in ["bash", "zsh", "fish", "elvish", "powershell"] {
+ let output = ranger(db_path)
+ .args(["completions", shell])
+ .output()
+ .unwrap();
+ assert!(output.status.success(), "completions failed for {shell}");
+ let stdout = String::from_utf8(output.stdout).unwrap();
+ assert!(!stdout.is_empty(), "completions empty for {shell}");
+ }
}