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    /// Exit with code 1 if any file would change.
65    #[arg(long)]
66    pub check: bool,
67
68    /// Show a unified diff of what would change.
69    #[arg(long)]
70    pub diff: bool,
71
72    /// Path to config file (default: .snapperrc.toml in current or parent dirs).
73    #[arg(long)]
74    pub config: Option<PathBuf>,
75
76    /// Only format lines in this range (1-indexed, inclusive). Format: START:END.
77    #[arg(long)]
78    pub range: Option<String>,
79
80    /// Output format for --check mode.
81    #[arg(long, default_value = "text")]
82    pub output_format: OutputFormat,
83}
84
85#[derive(Debug, Subcommand)]
86pub enum Commands {
87    /// Initialize snapper for a project (generate config, pre-commit, gitattributes).
88    Init {
89        /// Preview what would be generated without writing files.
90        #[arg(long)]
91        dry_run: bool,
92    },
93    /// Sentence-level diff between two files.
94    Sdiff {
95        /// Original file.
96        old: PathBuf,
97        /// Modified file.
98        new: PathBuf,
99        /// Input format (auto-detected from extension if omitted).
100        #[arg(short, long)]
101        format: Option<FormatArg>,
102        /// Disable colored output.
103        #[arg(long)]
104        no_color: bool,
105    },
106    /// Sentence-level diff against a git ref.
107    GitDiff {
108        /// Git ref to compare against (default: HEAD).
109        #[arg(default_value = "HEAD")]
110        git_ref: String,
111        /// Files to diff. If omitted, diffs all changed prose files.
112        #[arg()]
113        files: Vec<PathBuf>,
114        /// Input format (auto-detected from extension if omitted).
115        #[arg(short, long)]
116        format: Option<FormatArg>,
117        /// Disable colored output.
118        #[arg(long)]
119        no_color: bool,
120    },
121    /// Start the LSP server (stdin/stdout).
122    Lsp,
123    /// Watch files and reformat on change.
124    Watch {
125        /// Files or glob patterns to watch.
126        #[arg(required = true)]
127        patterns: Vec<String>,
128        /// Input format (auto-detected from extension if omitted).
129        #[arg(short, long)]
130        format: Option<FormatArg>,
131    },
132}
133
134/// Parse a range string "START:END" into (start, end) 1-indexed inclusive.
135pub fn parse_range(s: &str) -> Option<(usize, usize)> {
136    let parts: Vec<&str> = s.split(':').collect();
137    if parts.len() != 2 {
138        return None;
139    }
140    let start = parts[0].parse::<usize>().ok()?;
141    let end = parts[1].parse::<usize>().ok()?;
142    if start == 0 || end == 0 || start > end {
143        return None;
144    }
145    Some((start, end))
146}