1use crate::config::loader::ConfigLoader;
2use crate::logger::log_level::LogLevel;
3use clap::builder::Styles;
4use clap::builder::styling::{AnsiColor, Effects};
5use clap::{Args, Parser, Subcommand, ValueEnum};
6use std::fmt::Display;
7use std::path::PathBuf;
8
9const STYLES: Styles = Styles::styled()
10 .header(AnsiColor::Green.on_default().effects(Effects::BOLD))
11 .usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
12 .literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
13 .placeholder(AnsiColor::Cyan.on_default());
14
15#[derive(Parser, Debug)]
16#[command(
17 author,
18 name = "mdlint",
19 version,
20 about = "An opinionated Markdown formatter and linter",
21 after_help = "For help with a specific command, see: `mdlint help <command>`",
22 styles = STYLES,
23)]
24pub struct Cli {
25 #[command(subcommand)]
26 pub command: Command,
27
28 #[arg(
29 long,
30 global = true,
31 help = "Path to TOML configuration file (`mdlint.toml`)",
32 help_heading = "Configuration",
33 overrides_with = "no_config"
34 )]
35 pub config: Option<PathBuf>,
36
37 #[arg(
38 long,
39 global = true,
40 help = "Ignore all configuration files",
41 help_heading = "Configuration",
42 overrides_with = "config"
43 )]
44 pub no_config: bool,
45
46 #[arg(
47 short,
48 long,
49 global = true,
50 help = "Enable verbose logging",
51 help_heading = "Log levels",
52 conflicts_with_all = ["quiet", "silent"]
53 )]
54 pub verbose: bool,
55
56 #[arg(
57 short,
58 long,
59 global = true,
60 help = "Print diagnostics, nothing else",
61 help_heading = "Log levels",
62 conflicts_with_all = ["verbose", "silent"]
63 )]
64 pub quiet: bool,
65
66 #[arg(
67 short,
68 long,
69 global = true,
70 help = "Disable all logging (exit code still reflects result)",
71 help_heading = "Log levels",
72 conflicts_with_all = ["verbose", "quiet"]
73 )]
74 pub silent: bool,
75
76 #[arg(
77 long,
78 global = true,
79 default_value_t = TerminalColor::Auto,
80 hide_default_value = true,
81 help = "Control colors in output"
82 )]
83 pub color: TerminalColor,
84}
85
86#[derive(Subcommand, Debug)]
87pub enum Command {
88 Check(CheckArgs),
90 Format(FormatArgs),
92}
93
94#[derive(Args, Debug)]
95pub struct CheckArgs {
96 #[arg(
97 value_name = "FILES",
98 help = "Files or directories to check (defaults to current directory)"
99 )]
100 pub files: Vec<PathBuf>,
101
102 #[arg(
103 long,
104 help = "Files and directories to exclude from analysis",
105 help_heading = "File selection"
106 )]
107 pub exclude: Vec<PathBuf>,
108
109 #[arg(
110 long,
111 default_value_t = true,
112 help = "Respect `.gitignore` and similar exclusion files. Use `--no-respect-ignore` to disable",
113 help_heading = "File selection",
114 conflicts_with = "no_respect_ignore"
115 )]
116 pub respect_ignore: bool,
117
118 #[arg(long, hide = true, conflicts_with = "respect_ignore")]
119 pub no_respect_ignore: bool,
120
121 #[arg(long, help = "Apply auto-fixes where possible")]
122 pub fix: bool,
123
124 #[arg(
125 long,
126 value_name = "FORMAT",
127 default_value_t = OutputFormat::Default,
128 help = "Output format"
129 )]
130 pub output_format: OutputFormat,
131
132 #[arg(
133 long,
134 value_delimiter = ',',
135 value_name = "RULE_CODE",
136 help = "Comma-separated list of rules to enable (or `ALL`)",
137 help_heading = "Rule selection"
138 )]
139 pub select: Vec<String>,
140
141 #[arg(
142 long,
143 value_delimiter = ',',
144 value_name = "RULE_CODE",
145 help = "Comma-separated list of rules to disable",
146 help_heading = "Rule selection"
147 )]
148 pub ignore: Vec<String>,
149}
150
151impl CheckArgs {
152 pub fn files(&self) -> Vec<PathBuf> {
153 if self.files.is_empty() {
154 vec![PathBuf::from(".")]
155 } else {
156 self.files.clone()
157 }
158 }
159
160 pub fn should_respect_ignore(&self) -> bool {
161 !self.no_respect_ignore
162 }
163}
164
165#[derive(Args, Debug)]
166pub struct FormatArgs {
167 #[arg(
168 value_name = "FILES",
169 help = "Files or directories to format (defaults to current directory)"
170 )]
171 pub files: Vec<PathBuf>,
172
173 #[arg(
174 long,
175 help = "Files and directories to exclude from formatting",
176 help_heading = "File selection"
177 )]
178 pub exclude: Vec<PathBuf>,
179
180 #[arg(
181 long,
182 default_value_t = true,
183 help = "Respect `.gitignore` and similar exclusion files. Use `--no-respect-ignore` to disable",
184 help_heading = "File selection",
185 conflicts_with = "no_respect_ignore"
186 )]
187 pub respect_ignore: bool,
188
189 #[arg(long, hide = true, conflicts_with = "respect_ignore")]
190 pub no_respect_ignore: bool,
191
192 #[arg(
193 long,
194 help = "Check formatting without modifying files (exits with 1 if any file would change)"
195 )]
196 pub check: bool,
197}
198
199impl FormatArgs {
200 pub fn files(&self) -> Vec<PathBuf> {
201 if self.files.is_empty() {
202 vec![PathBuf::from(".")]
203 } else {
204 self.files.clone()
205 }
206 }
207
208 pub fn should_respect_ignore(&self) -> bool {
209 !self.no_respect_ignore
210 }
211}
212
213impl From<&Cli> for ConfigLoader {
214 fn from(cli: &Cli) -> Self {
215 if cli.no_config {
216 Self::None
217 } else if let Some(config_file) = &cli.config {
218 Self::File(config_file.clone())
219 } else {
220 Self::Detect
221 }
222 }
223}
224
225impl From<&Cli> for LogLevel {
226 fn from(cli: &Cli) -> Self {
227 if cli.silent {
228 Self::Silent
229 } else if cli.quiet {
230 Self::Quiet
231 } else if cli.verbose {
232 Self::Verbose
233 } else {
234 Self::Default
235 }
236 }
237}
238
239#[derive(ValueEnum, Debug, Default, Clone)]
240pub enum OutputFormat {
241 #[default]
242 Default,
243 Json,
244}
245
246impl Display for OutputFormat {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 match self {
249 OutputFormat::Default => write!(f, "default"),
250 OutputFormat::Json => write!(f, "json"),
251 }
252 }
253}
254
255#[derive(ValueEnum, Debug, Default, Clone)]
256pub enum TerminalColor {
257 #[default]
258 Auto,
259 Always,
260 Never,
261}
262
263impl Display for TerminalColor {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 match self {
266 TerminalColor::Auto => write!(f, "auto"),
267 TerminalColor::Always => write!(f, "always"),
268 TerminalColor::Never => write!(f, "never"),
269 }
270 }
271}