git_worktree_manager/prompt_source.rs
1//! Resolve the three mutually-exclusive `gw new` prompt sources
2//! (`--prompt`, `--prompt-file`, `--prompt-stdin`) into a single optional string.
3//!
4//! Mutual exclusion is enforced at parse time by `clap` (`ArgGroup` in `cli.rs`),
5//! so this helper assumes at most one source is active.
6
7use std::path::Path;
8
9use crate::error::{CwError, Result};
10
11/// Collapse the three prompt sources into a single optional string.
12///
13/// `stdin_reader` is injected so tests can drive the stdin path without touching
14/// the real stdin. In production `main` passes a closure that reads from `std::io::stdin()`.
15///
16/// Trailing newline characters (`\r`, `\n`, or any mix thereof) are stripped
17/// from the end of the resolved string — editors and heredocs routinely append
18/// one or more, and the AI tool doesn't want them.
19/// If the resolved string is empty or whitespace-only after stripping, `None`
20/// is returned so downstream code behaves as if no prompt was given (avoids
21/// passing an empty argv entry like `claude ""`).
22pub fn resolve_prompt(
23 inline: Option<String>,
24 file: Option<&Path>,
25 stdin: bool,
26 stdin_reader: impl FnOnce() -> std::io::Result<String>,
27) -> Result<Option<String>> {
28 let raw: Option<String> = if let Some(s) = inline {
29 Some(s)
30 } else if let Some(p) = file {
31 Some(std::fs::read_to_string(p).map_err(|e| {
32 CwError::Other(format!(
33 "failed to read --prompt-file '{}': {e}",
34 p.display()
35 ))
36 })?)
37 } else if stdin {
38 Some(
39 stdin_reader()
40 .map_err(|e| CwError::Other(format!("failed to read --prompt-stdin: {e}")))?,
41 )
42 } else {
43 None
44 };
45
46 Ok(raw.and_then(|s| {
47 let trimmed = s.trim_end_matches(['\n', '\r']);
48 if trimmed.trim().is_empty() {
49 None
50 } else {
51 Some(trimmed.to_string())
52 }
53 }))
54}