From a19de00300348abeea6a3fd4a033982425705b9f Mon Sep 17 00:00:00 2001 From: David Henning Date: Tue, 7 Jul 2026 18:46:43 +0200 Subject: [PATCH 1/6] docs: design for prefill query parameter --- .../specs/2026-07-07-prefill-query-design.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-07-prefill-query-design.md diff --git a/docs/superpowers/specs/2026-07-07-prefill-query-design.md b/docs/superpowers/specs/2026-07-07-prefill-query-design.md new file mode 100644 index 0000000..11d1353 --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-prefill-query-design.md @@ -0,0 +1,76 @@ +# Prefill Query Parameter — Design + +## Summary + +Add a command-line flag that prefills the interactive search query so `srchr` +opens with a query already entered and results already displayed. This turns +`srchr` into a usable launch target for shell aliases and editor integrations +that want to jump straight into a search. + +## Motivation + +Today the query always starts empty (`app.rs`), and argument parsing is a single +`std::env::args().nth(1)` call that treats the first positional argument as the +search root (`main.rs`). There is no way to open the tool with a query already +populated. A prefill flag lets callers seed the search without simulating +keystrokes. + +## CLI + +Argument parsing moves to [`clap`](https://docs.rs/clap) (derive API). + +- New dependency in `rust/Cargo.toml`: + `clap = { version = "4", features = ["derive"] }`. +- A `Cli` struct defined in `main.rs`: + - `path: PathBuf` — positional, defaults to `.`. Replaces the current + `args().nth(1)` logic. + - `query: Option` — `-q` / `--query`, optional. +- Help text uses clap's minimal derive defaults: name and version inferred from + `Cargo.toml`, plus a short `about` string. `--help`/`-h` and argument error + messages come for free. + +Examples: + +```sh +srchr # search '.', empty query (unchanged behavior) +srchr src # search 'src', empty query (unchanged behavior) +srchr -q TODO # search '.', query prefilled with "TODO" +srchr src --query fn # search 'src', query prefilled with "fn" +``` + +## Prefill Behavior + +- `App` gains `App::with_query(String)`. `App::new()` delegates to + `App::with_query(String::new())` so existing callers are unaffected. +- `run()` takes the resolved `path` and `Option` query, and seeds the + app via `App::with_query`. +- If the prefilled query is non-empty, the search runs immediately on startup: + `run()` seeds `pending_query = Some(query)` with `pending_at` set far enough in + the past that the debounce fires on the first loop iteration, and sets + `status = "searching..."`. Results appear as soon as the TUI opens. +- The prefilled query is fully editable — backspace and further typing behave + exactly as if the user had typed it. + +## Edge Cases + +- `-q ""` (explicit empty string) behaves identically to no `-q`: empty query, + no startup search, empty status. +- Omitting `-q` is byte-for-byte the current behavior. + +## Testing + +- Unit test: `App::with_query("foo")` yields `query == "foo"`; `App::new()` + yields an empty query. +- Clap sanity test: `Cli::command().debug_assert()` in a `#[test]` catches + malformed argument definitions at test time. +- The startup-search wiring lives inside the TTY event loop and cannot be fully + exercised headlessly; it is covered by the manual smoke test per `AGENTS.md`. + The smoke test should confirm that `srchr -q ` opens with the query + shown and results already populated, and that editing the prefilled query + works. + +## Non-Goals + +- No change to search semantics, preview, or editor handoff. +- No reading the query from stdin, a file, or environment variables. +- No multi-query or history support. From 7dbc2b8f76b43309775125144536567bfa44d00a Mon Sep 17 00:00:00 2001 From: David Henning Date: Tue, 7 Jul 2026 18:49:49 +0200 Subject: [PATCH 2/6] docs: implementation plan for prefill query parameter --- .../plans/2026-07-07-prefill-query.md | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-07-prefill-query.md diff --git a/docs/superpowers/plans/2026-07-07-prefill-query.md b/docs/superpowers/plans/2026-07-07-prefill-query.md new file mode 100644 index 0000000..ac2f785 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-prefill-query.md @@ -0,0 +1,288 @@ +# Prefill Query Parameter Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `-q`/`--query` flag that prefills the interactive query and runs the search immediately on startup. + +**Architecture:** Replace the hand-rolled `env::args().nth(1)` parsing in `main.rs` with a clap derive `Cli` struct (positional `path`, optional `--query`). Add `App::with_query` so the app can start with a seeded query. In `run()`, seed the query and, when non-empty, pre-arm the debounce so the first event-loop iteration launches a search. + +**Tech Stack:** Rust, clap 4 (derive), ratatui/crossterm (existing TUI), existing `search`/`preview`/`editor` modules. + +--- + +### Task 1: Add `App::with_query` constructor + +**Files:** +- Modify: `rust/src/app.rs:11-19` (impl `App`) +- Test: `rust/src/app.rs` (existing `#[cfg(test)] mod tests`) + +- [ ] **Step 1: Write the failing test** + +Add these two tests inside `mod tests` in `rust/src/app.rs` (after the existing `typing_and_backspace_edit_query` test): + +```rust + #[test] + fn with_query_seeds_the_query() { + let app = App::with_query("foo".to_string()); + assert_eq!(app.query, "foo"); + } + + #[test] + fn new_starts_with_empty_query() { + let app = App::new(); + assert_eq!(app.query, ""); + } +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cargo test --manifest-path rust/Cargo.toml with_query_seeds_the_query` +Expected: FAIL to compile with "no function or associated item named `with_query`". + +- [ ] **Step 3: Write minimal implementation** + +Replace the `new` method in `rust/src/app.rs` (lines 12-19) with `with_query` plus a delegating `new`: + +```rust + pub fn new() -> Self { + App::with_query(String::new()) + } + + pub fn with_query(query: String) -> Self { + App { + query, + results: Vec::new(), + selected: 0, + status: String::new(), + } + } +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cargo test --manifest-path rust/Cargo.toml --lib` +Expected: PASS, including `with_query_seeds_the_query` and `new_starts_with_empty_query`. + +- [ ] **Step 5: Commit** + +```bash +git add rust/src/app.rs +git commit -m "feat: add App::with_query constructor" +``` + +--- + +### Task 2: Add clap dependency and `Cli` struct + +**Files:** +- Modify: `rust/Cargo.toml:14-22` (`[dependencies]`) +- Modify: `rust/src/main.rs:1-47` (imports, add `Cli`, rewrite `main`) +- Test: `rust/src/main.rs` (existing `#[cfg(test)] mod tests`) + +- [ ] **Step 1: Add the clap dependency** + +Add this line to the `[dependencies]` table in `rust/Cargo.toml` (after the `once_cell` line): + +```toml +clap = { version = "4", features = ["derive"] } +``` + +- [ ] **Step 2: Write the failing test** + +Add this test inside `mod tests` in `rust/src/main.rs` (after the existing tests): + +```rust + #[test] + fn cli_definition_is_valid() { + use clap::CommandFactory; + Cli::command().debug_assert(); + } + + #[test] + fn cli_defaults_path_to_dot_and_no_query() { + use clap::Parser; + let cli = Cli::parse_from(["srchr"]); + assert_eq!(cli.path, PathBuf::from(".")); + assert_eq!(cli.query, None); + } + + #[test] + fn cli_parses_path_and_query() { + use clap::Parser; + let cli = Cli::parse_from(["srchr", "src", "-q", "fn"]); + assert_eq!(cli.path, PathBuf::from("src")); + assert_eq!(cli.query.as_deref(), Some("fn")); + } +``` + +- [ ] **Step 3: Run test to verify it fails** + +Run: `cargo test --manifest-path rust/Cargo.toml cli_definition_is_valid` +Expected: FAIL to compile with "cannot find type `Cli`". + +- [ ] **Step 4: Add the `Cli` struct and wire up `main`** + +In `rust/src/main.rs`, add the clap import near the other `use` lines (after line 15): + +```rust +use clap::Parser; +``` + +Add the `Cli` struct definition immediately above `fn main()` (before line 32): + +```rust +/// Live-grep file search with fuzzy selection and syntax preview. +#[derive(Parser, Debug)] +#[command(version, about)] +struct Cli { + /// Directory to search (defaults to the current directory). + #[arg(default_value = ".")] + path: PathBuf, + + /// Prefill the search query and run it immediately on startup. + #[arg(short, long)] + query: Option, +} +``` + +Replace the body of `fn main()` (lines 33-46) so it parses via clap. Keep calling `run` with only `path` for now (Task 3 updates `run` to take the query); this keeps the crate compiling after this task: + +```rust +fn main() { + if !io::stdin().is_terminal() || !io::stdout().is_terminal() { + eprintln!("srchr: not a terminal (this is an interactive tool)"); + std::process::exit(2); + } + + let cli = Cli::parse(); + + if let Err(e) = run(cli.path) { + eprintln!("srchr: {e}"); + std::process::exit(1); + } +} +``` + +Note: `cli.query` is intentionally unused in this task. Prefix it in the struct is not needed; clap keeps the field. If clippy flags an unused field here, ignore it — Task 3 consumes `cli.query`. (Run clippy only in Task 4, after Task 3.) + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `cargo test --manifest-path rust/Cargo.toml` +Expected: The three new `cli_*` tests PASS and the crate compiles (`run` still takes one argument). + +- [ ] **Step 6: Commit** + +```bash +git add rust/Cargo.toml rust/Cargo.lock rust/src/main.rs +git commit -m "feat: parse args with clap and add --query flag" +``` + +--- + +### Task 3: Seed the query and search immediately in `run` + +**Files:** +- Modify: `rust/src/main.rs:49-64` (`run` signature and app setup) + +- [ ] **Step 1: Update the `main` call site** + +In `rust/src/main.rs`, change the call in `main` from: + +```rust + if let Err(e) = run(cli.path) { +``` + +to: + +```rust + if let Err(e) = run(cli.path, cli.query) { +``` + +- [ ] **Step 2: Update the `run` signature and seed the app** + +In `rust/src/main.rs`, change the `run` function signature from: + +```rust +fn run(root: PathBuf) -> io::Result<()> { +``` + +to: + +```rust +fn run(root: PathBuf, initial_query: Option) -> io::Result<()> { +``` + +Then replace the app construction and the pending-query initialization. Change: + +```rust + let mut app = App::new(); + let (result_tx, result_rx): (Sender, Receiver) = mpsc::channel(); + let mut pending_query: Option = None; + let mut pending_at = Instant::now(); +``` + +to: + +```rust + let seed = initial_query.unwrap_or_default(); + let mut app = App::with_query(seed.clone()); + let (result_tx, result_rx): (Sender, Receiver) = mpsc::channel(); + let mut pending_query: Option = None; + let mut pending_at = Instant::now(); + + if !seed.is_empty() { + pending_query = Some(seed); + pending_at = Instant::now() - DEBOUNCE; + app.status = "searching...".to_string(); + } +``` + +- [ ] **Step 3: Verify the project builds and all tests pass** + +Run: `cargo test --manifest-path rust/Cargo.toml` +Expected: PASS, all tests including Task 1 and Task 2 additions. No `run` arity errors. + +- [ ] **Step 4: Commit** + +```bash +git add rust/src/main.rs +git commit -m "feat: prefill and run search on startup when --query is given" +``` + +--- + +### Task 4: Verification gates and manual smoke test + +**Files:** none (verification only) + +- [ ] **Step 1: Format check** + +Run: `cargo fmt --manifest-path rust/Cargo.toml -- --check` +Expected: no output (clean). If it fails, run `cargo fmt --manifest-path rust/Cargo.toml` and re-commit. + +- [ ] **Step 2: Clippy** + +Run: `cargo clippy --manifest-path rust/Cargo.toml -- -D warnings` +Expected: no warnings/errors. + +- [ ] **Step 3: Full test run** + +Run: `cargo test --manifest-path rust/Cargo.toml` +Expected: all tests PASS. + +- [ ] **Step 4: Ask the user for a manual TUI smoke test** + +The interactive TUI needs a TTY and cannot be exercised headlessly (per `AGENTS.md`). Ask the user to run: + +```sh +cargo run --manifest-path rust/Cargo.toml -- . -q TODO +``` + +Confirm with the user that: the TUI opens with `TODO` shown in the query input, results are already populated (or a valid empty/status state), the prefilled query is editable via backspace/typing, and `--help` (`cargo run --manifest-path rust/Cargo.toml -- --help`) shows the `-q/--query` and path arguments. + +--- + +## Notes + +- `Cargo.lock` should be committed alongside `Cargo.toml` in Task 2 since adding clap changes it. +- `-q ""` yields an empty seed, so `seed.is_empty()` is true and no startup search runs — matching the spec edge case. From 75959340c504997434bdbaa4bb317f271f3303a0 Mon Sep 17 00:00:00 2001 From: David Henning Date: Tue, 7 Jul 2026 18:51:53 +0200 Subject: [PATCH 3/6] feat: add App::with_query constructor --- rust/src/app.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rust/src/app.rs b/rust/src/app.rs index 63040bc..b8d1e61 100644 --- a/rust/src/app.rs +++ b/rust/src/app.rs @@ -10,8 +10,12 @@ pub struct App { impl App { pub fn new() -> Self { + App::with_query(String::new()) + } + + pub fn with_query(query: String) -> Self { App { - query: String::new(), + query, results: Vec::new(), selected: 0, status: String::new(), @@ -77,6 +81,18 @@ mod tests { assert_eq!(app.query, "a"); } + #[test] + fn with_query_seeds_the_query() { + let app = App::with_query("foo".to_string()); + assert_eq!(app.query, "foo"); + } + + #[test] + fn new_starts_with_empty_query() { + let app = App::new(); + assert_eq!(app.query, ""); + } + #[test] fn set_results_clamps_selection() { let mut app = App::new(); From 7bd9fc54264dd6f60e4054dde564bb03fe7246c7 Mon Sep 17 00:00:00 2001 From: David Henning Date: Tue, 7 Jul 2026 18:54:17 +0200 Subject: [PATCH 4/6] feat: parse args with clap and add --query flag --- rust/Cargo.lock | 115 +++++++++++++++++++++++++++++++++++++++++++++++ rust/Cargo.toml | 1 + rust/src/main.rs | 44 +++++++++++++++--- 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 3e474a2..10f049d 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -23,6 +23,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "base64" version = "0.22.1" @@ -86,6 +136,52 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "compact_str" version = "0.8.2" @@ -411,6 +507,12 @@ dependencies = [ "syn", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -523,6 +625,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "onig" version = "6.5.3" @@ -839,6 +947,7 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" name = "srchr" version = "0.1.0" dependencies = [ + "clap", "crossterm", "grep-regex", "grep-searcher", @@ -1014,6 +1123,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 3f9b133..94e232c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -20,6 +20,7 @@ ratatui = "0.29" crossterm = "0.28" syntect = "5" once_cell = "1" +clap = { version = "4", features = ["derive"] } [dev-dependencies] tempfile = "3" diff --git a/rust/src/main.rs b/rust/src/main.rs index 4e543a9..633a233 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -14,6 +14,8 @@ use crossterm::terminal::{ use ratatui::backend::CrosstermBackend; use ratatui::Terminal; +use clap::Parser; + use srchr::app::App; use srchr::editor; use srchr::preview::{build_preview_safe, style_preview, PreviewData, StyledPreview}; @@ -29,18 +31,28 @@ struct SearchResult { error: Option, } +/// Live-grep file search with fuzzy selection and syntax preview. +#[derive(Parser, Debug)] +#[command(version, about)] +struct Cli { + /// Directory to search (defaults to the current directory). + #[arg(default_value = ".")] + path: PathBuf, + + /// Prefill the search query and run it immediately on startup. + #[arg(short, long)] + query: Option, +} + fn main() { if !io::stdin().is_terminal() || !io::stdout().is_terminal() { eprintln!("srchr: not a terminal (this is an interactive tool)"); std::process::exit(2); } - let root = std::env::args() - .nth(1) - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(".")); + let cli = Cli::parse(); - if let Err(e) = run(root) { + if let Err(e) = run(cli.path) { eprintln!("srchr: {e}"); std::process::exit(1); } @@ -266,4 +278,26 @@ mod tests { assert_eq!(action, Action::None); assert_eq!(app.selected, 0); } + + #[test] + fn cli_definition_is_valid() { + use clap::CommandFactory; + Cli::command().debug_assert(); + } + + #[test] + fn cli_defaults_path_to_dot_and_no_query() { + use clap::Parser; + let cli = Cli::parse_from(["srchr"]); + assert_eq!(cli.path, PathBuf::from(".")); + assert_eq!(cli.query, None); + } + + #[test] + fn cli_parses_path_and_query() { + use clap::Parser; + let cli = Cli::parse_from(["srchr", "src", "-q", "fn"]); + assert_eq!(cli.path, PathBuf::from("src")); + assert_eq!(cli.query.as_deref(), Some("fn")); + } } From b40e45a477f2cb26e8834d57a59b4bc06bd26255 Mon Sep 17 00:00:00 2001 From: David Henning Date: Tue, 7 Jul 2026 18:56:33 +0200 Subject: [PATCH 5/6] feat: prefill and run search on startup when --query is given --- rust/src/main.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/rust/src/main.rs b/rust/src/main.rs index 633a233..31c2130 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -52,23 +52,30 @@ fn main() { let cli = Cli::parse(); - if let Err(e) = run(cli.path) { + if let Err(e) = run(cli.path, cli.query) { eprintln!("srchr: {e}"); std::process::exit(1); } } -fn run(root: PathBuf) -> io::Result<()> { +fn run(root: PathBuf, initial_query: Option) -> io::Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let mut app = App::new(); + let seed = initial_query.unwrap_or_default(); + let mut app = App::with_query(seed.clone()); let (result_tx, result_rx): (Sender, Receiver) = mpsc::channel(); let mut pending_query: Option = None; let mut pending_at = Instant::now(); + + if !seed.is_empty() { + pending_query = Some(seed); + pending_at = Instant::now() - DEBOUNCE; + app.status = "searching...".to_string(); + } let mut current_cancel: Option> = None; let mut launch_target: Option<(String, Option)> = None; let mut preview_key: Option<(PathBuf, Option)> = None; From b73527ad0af708d46032eb7c232b89411653261f Mon Sep 17 00:00:00 2001 From: David Henning Date: Tue, 7 Jul 2026 19:01:48 +0200 Subject: [PATCH 6/6] fix: allow help before tty check --- rust/src/main.rs | 4 ++-- rust/tests/cli_tests.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 rust/tests/cli_tests.rs diff --git a/rust/src/main.rs b/rust/src/main.rs index 31c2130..ff86ce0 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -45,13 +45,13 @@ struct Cli { } fn main() { + let cli = Cli::parse(); + if !io::stdin().is_terminal() || !io::stdout().is_terminal() { eprintln!("srchr: not a terminal (this is an interactive tool)"); std::process::exit(2); } - let cli = Cli::parse(); - if let Err(e) = run(cli.path, cli.query) { eprintln!("srchr: {e}"); std::process::exit(1); diff --git a/rust/tests/cli_tests.rs b/rust/tests/cli_tests.rs new file mode 100644 index 0000000..16088bb --- /dev/null +++ b/rust/tests/cli_tests.rs @@ -0,0 +1,27 @@ +use std::process::Command; + +fn srchr() -> Command { + Command::new(env!("CARGO_BIN_EXE_srchr")) +} + +#[test] +fn help_does_not_require_tty() { + let output = srchr().arg("--help").output().expect("run srchr --help"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("stdout is utf-8"); + assert!(stdout.contains("Usage:")); + assert!(stdout.contains("--query")); +} + +#[test] +fn version_does_not_require_tty() { + let output = srchr() + .arg("--version") + .output() + .expect("run srchr --version"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("stdout is utf-8"); + assert!(stdout.contains("srchr")); +}