1use std::path::PathBuf;
2
3use clap::{Parser, Subcommand, ValueEnum};
4
5use crate::diff::ColorMode;
6
7#[derive(Debug, Clone, Copy, ValueEnum)]
8pub enum FormatArg {
9 Org,
10 Latex,
11 Markdown,
12 Rst,
13 Plaintext,
14}
15
16#[derive(Debug, Clone, Copy, ValueEnum)]
17pub enum OutputFormat {
18 Text,
19 Json,
20 Sarif,
21}
22
23#[derive(Debug, Clone, Copy, ValueEnum, Default, PartialEq, Eq)]
25pub enum ColorWhen {
26 #[default]
28 Auto,
29 Always,
31 Never,
33}
34
35impl From<ColorWhen> for ColorMode {
36 fn from(value: ColorWhen) -> Self {
37 match value {
38 ColorWhen::Auto => ColorMode::Auto,
39 ColorWhen::Always => ColorMode::Always,
40 ColorWhen::Never => ColorMode::Never,
41 }
42 }
43}
44
45#[derive(Debug, Parser)]
46#[command(name = "snapper", version, about = "Semantic line break formatter")]
47pub struct Cli {
48 #[command(subcommand)]
49 pub command: Option<Commands>,
50
51 #[arg()]
53 pub files: Vec<PathBuf>,
54
55 #[arg(short, long)]
57 pub format: Option<FormatArg>,
58
59 #[arg(long)]
61 pub stdin_filepath: Option<PathBuf>,
62
63 #[arg(short, long)]
65 pub output: Option<PathBuf>,
66
67 #[arg(short, long)]
69 pub in_place: bool,
70
71 #[arg(short = 'w', long, default_value_t = 0)]
73 pub max_width: usize,
74
75 #[arg(long, default_value_t = false)]
78 pub clause_breaks: bool,
79
80 #[arg(long)]
82 pub neural: bool,
83
84 #[arg(long)]
87 pub lang: Option<String>,
88
89 #[arg(long)]
91 pub model_path: Option<PathBuf>,
92
93 #[arg(long)]
95 pub use_pandoc: bool,
96
97 #[arg(long, default_value = "auto", value_name = "BACKEND")]
102 pub pandoc_backend: String,
103
104 #[arg(long)]
106 pub check: bool,
107
108 #[arg(long)]
110 pub diff: bool,
111
112 #[arg(long, value_enum, default_value_t = ColorWhen::Auto, global = true, value_name = "WHEN")]
120 pub color: ColorWhen,
121
122 #[arg(long)]
124 pub config: Option<PathBuf>,
125
126 #[arg(long)]
128 pub range: Option<String>,
129
130 #[arg(long, default_value = "text")]
132 pub output_format: OutputFormat,
133
134 #[arg(long, default_value_t = false)]
140 pub format_code: bool,
141}
142
143#[derive(Debug, Subcommand)]
144pub enum Commands {
145 Init {
147 #[arg(long)]
149 dry_run: bool,
150 },
151 Sdiff {
153 old: PathBuf,
155 new: PathBuf,
157 #[arg(short, long)]
159 format: Option<FormatArg>,
160 #[arg(long)]
162 no_color: bool,
163 },
164 GitDiff {
166 #[arg(default_value = "HEAD")]
168 git_ref: String,
169 #[arg()]
171 files: Vec<PathBuf>,
172 #[arg(short, long)]
174 format: Option<FormatArg>,
175 #[arg(long)]
177 no_color: bool,
178 },
179 Lsp,
181 Mcp,
183 Watch {
185 #[arg(required = true)]
187 patterns: Vec<String>,
188 #[arg(short, long)]
190 format: Option<FormatArg>,
191 },
192}
193
194pub fn parse_range(s: &str) -> Option<(usize, usize)> {
196 let parts: Vec<&str> = s.split(':').collect();
197 if parts.len() != 2 {
198 return None;
199 }
200 let start = parts[0].parse::<usize>().ok()?;
201 let end = parts[1].parse::<usize>().ok()?;
202 if start == 0 || end == 0 || start > end {
203 return None;
204 }
205 Some((start, end))
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn parse_range_valid() {
214 assert_eq!(parse_range("1:10"), Some((1, 10)));
215 assert_eq!(parse_range("5:5"), Some((5, 5)));
216 assert_eq!(parse_range("1:1"), Some((1, 1)));
217 }
218
219 #[test]
220 fn parse_range_zero_rejected() {
221 assert_eq!(parse_range("0:5"), None);
222 assert_eq!(parse_range("5:0"), None);
223 assert_eq!(parse_range("0:0"), None);
224 }
225
226 #[test]
227 fn parse_range_reversed_rejected() {
228 assert_eq!(parse_range("10:5"), None);
229 }
230
231 #[test]
232 fn parse_range_bad_format() {
233 assert_eq!(parse_range("abc"), None);
234 assert_eq!(parse_range("1:2:3"), None);
235 assert_eq!(parse_range(""), None);
236 assert_eq!(parse_range("a:b"), None);
237 assert_eq!(parse_range(":5"), None);
238 assert_eq!(parse_range("5:"), None);
239 }
240}