Skip to main content

snapper_fmt/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Parser, Subcommand, ValueEnum};
4
5#[derive(Debug, Clone, Copy, ValueEnum)]
6pub enum FormatArg {
7    Org,
8    Latex,
9    Markdown,
10    Rst,
11    Plaintext,
12}
13
14#[derive(Debug, Clone, Copy, ValueEnum)]
15pub enum OutputFormat {
16    Text,
17    Json,
18    Sarif,
19}
20
21#[derive(Debug, Parser)]
22#[command(name = "snapper", version, about = "Semantic line break formatter")]
23pub struct Cli {
24    #[command(subcommand)]
25    pub command: Option<Commands>,
26
27    /// Input files. Reads stdin if omitted.
28    #[arg()]
29    pub files: Vec<PathBuf>,
30
31    /// Input format (auto-detected from extension if omitted).
32    #[arg(short, long)]
33    pub format: Option<FormatArg>,
34
35    /// Assume this filename when reading stdin (for format auto-detection).
36    #[arg(long)]
37    pub stdin_filepath: Option<PathBuf>,
38
39    /// Output file (stdout if omitted).
40    #[arg(short, long)]
41    pub output: Option<PathBuf>,
42
43    /// Modify files in place.
44    #[arg(short, long)]
45    pub in_place: bool,
46
47    /// Maximum line width (0 = unlimited).
48    #[arg(short = 'w', long, default_value_t = 0)]
49    pub max_width: usize,
50
51    /// Use neural sentence detection (nnsplit LSTM model).
52    #[arg(long)]
53    pub neural: bool,
54
55    /// Language for neural sentence detection (default: en).
56    /// Available: en, de, fr, no, sv, zh, tr, ru, uk.
57    #[arg(long, default_value = "en")]
58    pub lang: String,
59
60    /// Path to custom ONNX model file for neural detection.
61    #[arg(long)]
62    pub model_path: Option<PathBuf>,
63
64    /// Use pandoc as parser backend (universal format support).
65    #[arg(long)]
66    pub use_pandoc: bool,
67
68    /// Exit with code 1 if any file would change.
69    #[arg(long)]
70    pub check: bool,
71
72    /// Show a unified diff of what would change.
73    #[arg(long)]
74    pub diff: bool,
75
76    /// Path to config file (default: .snapperrc.toml in current or parent dirs).
77    #[arg(long)]
78    pub config: Option<PathBuf>,
79
80    /// Only format lines in this range (1-indexed, inclusive). Format: START:END.
81    #[arg(long)]
82    pub range: Option<String>,
83
84    /// Output format for --check mode.
85    #[arg(long, default_value = "text")]
86    pub output_format: OutputFormat,
87}
88
89#[derive(Debug, Subcommand)]
90pub enum Commands {
91    /// Initialize snapper for a project (generate config, pre-commit, gitattributes).
92    Init {
93        /// Preview what would be generated without writing files.
94        #[arg(long)]
95        dry_run: bool,
96    },
97    /// Sentence-level diff between two files.
98    Sdiff {
99        /// Original file.
100        old: PathBuf,
101        /// Modified file.
102        new: PathBuf,
103        /// Input format (auto-detected from extension if omitted).
104        #[arg(short, long)]
105        format: Option<FormatArg>,
106        /// Disable colored output.
107        #[arg(long)]
108        no_color: bool,
109    },
110    /// Sentence-level diff against a git ref.
111    GitDiff {
112        /// Git ref to compare against (default: HEAD).
113        #[arg(default_value = "HEAD")]
114        git_ref: String,
115        /// Files to diff. If omitted, diffs all changed prose files.
116        #[arg()]
117        files: Vec<PathBuf>,
118        /// Input format (auto-detected from extension if omitted).
119        #[arg(short, long)]
120        format: Option<FormatArg>,
121        /// Disable colored output.
122        #[arg(long)]
123        no_color: bool,
124    },
125    /// Start the LSP server (stdin/stdout).
126    Lsp,
127    /// Watch files and reformat on change.
128    Watch {
129        /// Files or glob patterns to watch.
130        #[arg(required = true)]
131        patterns: Vec<String>,
132        /// Input format (auto-detected from extension if omitted).
133        #[arg(short, long)]
134        format: Option<FormatArg>,
135    },
136}
137
138/// Parse a range string "START:END" into (start, end) 1-indexed inclusive.
139pub fn parse_range(s: &str) -> Option<(usize, usize)> {
140    let parts: Vec<&str> = s.split(':').collect();
141    if parts.len() != 2 {
142        return None;
143    }
144    let start = parts[0].parse::<usize>().ok()?;
145    let end = parts[1].parse::<usize>().ok()?;
146    if start == 0 || end == 0 || start > end {
147        return None;
148    }
149    Some((start, end))
150}