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.
change opwpwlunoxzptvypvtuprtokmmpypnsp
commit 60877bce54cbf79cc7371344f26490ff446e239d
author Alpha Chen <alpha@kejadlen.dev>
date
parent xotysnuo
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}");
+    }
 }