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 #[arg()]
29 pub files: Vec<PathBuf>,
30
31 #[arg(short, long)]
33 pub format: Option<FormatArg>,
34
35 #[arg(long)]
37 pub stdin_filepath: Option<PathBuf>,
38
39 #[arg(short, long)]
41 pub output: Option<PathBuf>,
42
43 #[arg(short, long)]
45 pub in_place: bool,
46
47 #[arg(short = 'w', long, default_value_t = 0)]
49 pub max_width: usize,
50
51 #[arg(long)]
53 pub neural: bool,
54
55 #[arg(long)]
58 pub lang: Option<String>,
59
60 #[arg(long)]
62 pub model_path: Option<PathBuf>,
63
64 #[arg(long)]
66 pub use_pandoc: bool,
67
68 #[arg(long, default_value = "auto", value_name = "BACKEND")]
73 pub pandoc_backend: String,
74
75 #[arg(long)]
77 pub check: bool,
78
79 #[arg(long)]
81 pub diff: bool,
82
83 #[arg(long)]
85 pub config: Option<PathBuf>,
86
87 #[arg(long)]
89 pub range: Option<String>,
90
91 #[arg(long, default_value = "text")]
93 pub output_format: OutputFormat,
94
95 #[arg(long, default_value_t = false)]
101 pub format_code: bool,
102}
103
104#[derive(Debug, Subcommand)]
105pub enum Commands {
106 Init {
108 #[arg(long)]
110 dry_run: bool,
111 },
112 Sdiff {
114 old: PathBuf,
116 new: PathBuf,
118 #[arg(short, long)]
120 format: Option<FormatArg>,
121 #[arg(long)]
123 no_color: bool,
124 },
125 GitDiff {
127 #[arg(default_value = "HEAD")]
129 git_ref: String,
130 #[arg()]
132 files: Vec<PathBuf>,
133 #[arg(short, long)]
135 format: Option<FormatArg>,
136 #[arg(long)]
138 no_color: bool,
139 },
140 Lsp,
142 Mcp,
144 Watch {
146 #[arg(required = true)]
148 patterns: Vec<String>,
149 #[arg(short, long)]
151 format: Option<FormatArg>,
152 },
153}
154
155pub fn parse_range(s: &str) -> Option<(usize, usize)> {
157 let parts: Vec<&str> = s.split(':').collect();
158 if parts.len() != 2 {
159 return None;
160 }
161 let start = parts[0].parse::<usize>().ok()?;
162 let end = parts[1].parse::<usize>().ok()?;
163 if start == 0 || end == 0 || start > end {
164 return None;
165 }
166 Some((start, end))
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn parse_range_valid() {
175 assert_eq!(parse_range("1:10"), Some((1, 10)));
176 assert_eq!(parse_range("5:5"), Some((5, 5)));
177 assert_eq!(parse_range("1:1"), Some((1, 1)));
178 }
179
180 #[test]
181 fn parse_range_zero_rejected() {
182 assert_eq!(parse_range("0:5"), None);
183 assert_eq!(parse_range("5:0"), None);
184 assert_eq!(parse_range("0:0"), None);
185 }
186
187 #[test]
188 fn parse_range_reversed_rejected() {
189 assert_eq!(parse_range("10:5"), None);
190 }
191
192 #[test]
193 fn parse_range_bad_format() {
194 assert_eq!(parse_range("abc"), None);
195 assert_eq!(parse_range("1:2:3"), None);
196 assert_eq!(parse_range(""), None);
197 assert_eq!(parse_range("a:b"), None);
198 assert_eq!(parse_range(":5"), None);
199 assert_eq!(parse_range("5:"), None);
200 }
201}