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