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)]
70 pub check: bool,
71
72 #[arg(long)]
74 pub diff: bool,
75
76 #[arg(long)]
78 pub config: Option<PathBuf>,
79
80 #[arg(long)]
82 pub range: Option<String>,
83
84 #[arg(long, default_value = "text")]
86 pub output_format: OutputFormat,
87
88 #[arg(long, default_value_t = false)]
94 pub format_code: bool,
95}
96
97#[derive(Debug, Subcommand)]
98pub enum Commands {
99 Init {
101 #[arg(long)]
103 dry_run: bool,
104 },
105 Sdiff {
107 old: PathBuf,
109 new: PathBuf,
111 #[arg(short, long)]
113 format: Option<FormatArg>,
114 #[arg(long)]
116 no_color: bool,
117 },
118 GitDiff {
120 #[arg(default_value = "HEAD")]
122 git_ref: String,
123 #[arg()]
125 files: Vec<PathBuf>,
126 #[arg(short, long)]
128 format: Option<FormatArg>,
129 #[arg(long)]
131 no_color: bool,
132 },
133 Lsp,
135 Mcp,
137 Watch {
139 #[arg(required = true)]
141 patterns: Vec<String>,
142 #[arg(short, long)]
144 format: Option<FormatArg>,
145 },
146}
147
148pub fn parse_range(s: &str) -> Option<(usize, usize)> {
150 let parts: Vec<&str> = s.split(':').collect();
151 if parts.len() != 2 {
152 return None;
153 }
154 let start = parts[0].parse::<usize>().ok()?;
155 let end = parts[1].parse::<usize>().ok()?;
156 if start == 0 || end == 0 || start > end {
157 return None;
158 }
159 Some((start, end))
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn parse_range_valid() {
168 assert_eq!(parse_range("1:10"), Some((1, 10)));
169 assert_eq!(parse_range("5:5"), Some((5, 5)));
170 assert_eq!(parse_range("1:1"), Some((1, 1)));
171 }
172
173 #[test]
174 fn parse_range_zero_rejected() {
175 assert_eq!(parse_range("0:5"), None);
176 assert_eq!(parse_range("5:0"), None);
177 assert_eq!(parse_range("0:0"), None);
178 }
179
180 #[test]
181 fn parse_range_reversed_rejected() {
182 assert_eq!(parse_range("10:5"), None);
183 }
184
185 #[test]
186 fn parse_range_bad_format() {
187 assert_eq!(parse_range("abc"), None);
188 assert_eq!(parse_range("1:2:3"), None);
189 assert_eq!(parse_range(""), None);
190 assert_eq!(parse_range("a:b"), None);
191 assert_eq!(parse_range(":5"), None);
192 assert_eq!(parse_range("5:"), None);
193 }
194}