Skip to main content

rusty_vipe/
cli.rs

1//! Command-line interface stub.
2//!
3//! **STUB** — full implementation pending Phase 3+ (US1) tasks.
4
5use clap::Parser;
6
7#[derive(Parser, Debug)]
8#[command(
9    name = "rusty-vipe",
10    version,
11    about = "Pop $EDITOR mid-pipe; edit the buffered bytes; resume the pipeline.",
12    long_about = "A Rust port of moreutils `vipe`. Buffers stdin to a tempfile, \
13                  spawns $EDITOR on it with cross-platform TTY reattachment, then \
14                  writes the post-edit bytes back to the original stdout sink."
15)]
16pub struct Cli {
17    /// Tempfile filename suffix (default `.txt`). Editors use this as a
18    /// syntax-highlighting hint.
19    #[arg(long = "suffix", value_parser = parse_suffix_arg)]
20    pub suffix: Option<String>,
21
22    /// Explicit editor override (Default mode only). Whitespace-aware
23    /// (`code --wait` parses correctly).
24    #[arg(long = "editor")]
25    pub editor: Option<String>,
26
27    /// Enable strict moreutils-compat mode.
28    #[arg(long, conflicts_with = "no_strict")]
29    pub strict: bool,
30
31    /// Explicitly disable strict mode (overrides env + argv[0]).
32    #[arg(long = "no-strict")]
33    pub no_strict: bool,
34
35    /// Extra positional arguments forwarded to the editor (before the
36    /// tempfile path).
37    #[arg(trailing_var_arg = true)]
38    pub editor_extras: Vec<String>,
39
40    /// Subcommand (currently only `completions`).
41    #[command(subcommand)]
42    pub command: Option<Subcommand>,
43}
44
45#[derive(clap::Subcommand, Debug)]
46pub enum Subcommand {
47    /// Emit shell completion scripts (Default mode only).
48    Completions { shell: clap_complete::Shell },
49}
50
51/// clap value_parser for `--suffix=<ext>`. Delegates to
52/// [`crate::validate_suffix`] so Default and Strict modes share validation.
53fn parse_suffix_arg(value: &str) -> Result<String, String> {
54    crate::validate_suffix(value).map_err(|e| e.to_string())?;
55    Ok(value.to_string())
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use clap::CommandFactory;
62
63    #[test]
64    fn cli_command_factory_compiles() {
65        let cmd = Cli::command();
66        assert_eq!(cmd.get_name(), "rusty-vipe");
67    }
68
69    #[test]
70    fn parse_no_args() {
71        let cli = Cli::try_parse_from(["rusty-vipe"]).unwrap();
72        assert!(cli.suffix.is_none());
73        assert!(cli.editor.is_none());
74        assert!(!cli.strict);
75        assert!(!cli.no_strict);
76    }
77
78    #[test]
79    fn parse_suffix() {
80        let cli = Cli::try_parse_from(["rusty-vipe", "--suffix=.json"]).unwrap();
81        assert_eq!(cli.suffix.as_deref(), Some(".json"));
82    }
83
84    #[test]
85    fn parse_editor_override() {
86        let cli = Cli::try_parse_from(["rusty-vipe", "--editor=code --wait"]).unwrap();
87        assert_eq!(cli.editor.as_deref(), Some("code --wait"));
88    }
89
90    #[test]
91    fn parse_strict_conflicts_with_no_strict() {
92        let result = Cli::try_parse_from(["rusty-vipe", "--strict", "--no-strict"]);
93        assert!(result.is_err());
94    }
95}