1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum FormatArg {
Org,
Latex,
Markdown,
Plaintext,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum OutputFormat {
Text,
Json,
Sarif,
}
#[derive(Debug, Parser)]
#[command(name = "snapper", version, about = "Semantic line break formatter")]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
/// Input files. Reads stdin if omitted.
#[arg()]
pub files: Vec<PathBuf>,
/// Input format (auto-detected from extension if omitted).
#[arg(short, long)]
pub format: Option<FormatArg>,
/// Assume this filename when reading stdin (for format auto-detection).
#[arg(long)]
pub stdin_filepath: Option<PathBuf>,
/// Output file (stdout if omitted).
#[arg(short, long)]
pub output: Option<PathBuf>,
/// Modify files in place.
#[arg(short, long)]
pub in_place: bool,
/// Maximum line width (0 = unlimited).
#[arg(short = 'w', long, default_value_t = 0)]
pub max_width: usize,
/// Use neural sentence detection (requires neural feature).
#[arg(long)]
pub neural: bool,
/// Exit with code 1 if any file would change.
#[arg(long)]
pub check: bool,
/// Show a unified diff of what would change.
#[arg(long)]
pub diff: bool,
/// Path to config file (default: .snapperrc.toml in current or parent dirs).
#[arg(long)]
pub config: Option<PathBuf>,
/// Only format lines in this range (1-indexed, inclusive). Format: START:END.
#[arg(long)]
pub range: Option<String>,
/// Output format for --check mode.
#[arg(long, default_value = "text")]
pub output_format: OutputFormat,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
/// Initialize snapper for a project (generate config, pre-commit, gitattributes).
Init {
/// Preview what would be generated without writing files.
#[arg(long)]
dry_run: bool,
},
}
/// Parse a range string "START:END" into (start, end) 1-indexed inclusive.
pub fn parse_range(s: &str) -> Option<(usize, usize)> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return None;
}
let start = parts[0].parse::<usize>().ok()?;
let end = parts[1].parse::<usize>().ok()?;
if start == 0 || end == 0 || start > end {
return None;
}
Some((start, end))
}