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}