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 Server(ServerArgs),
94}
95
96#[derive(Args, Debug)]
97pub struct ServerArgs {}
98
99#[derive(Args, Debug)]
100pub struct CheckArgs {
101 #[arg(
102 value_name = "FILES",
103 help = "Files or directories to check (defaults to current directory)"
104 )]
105 pub files: Vec<PathBuf>,
106
107 #[arg(
108 long,
109 help = "Files and directories to exclude from analysis",
110 help_heading = "File selection"
111 )]
112 pub exclude: Vec<PathBuf>,
113
114 #[arg(
115 long,
116 default_value_t = true,
117 help = "Respect `.gitignore` and similar exclusion files. Use `--no-respect-ignore` to disable",
118 help_heading = "File selection",
119 conflicts_with = "no_respect_ignore"
120 )]
121 pub respect_ignore: bool,
122
123 #[arg(long, hide = true, conflicts_with = "respect_ignore")]
124 pub no_respect_ignore: bool,
125
126 #[arg(long, help = "Apply auto-fixes where possible")]
127 pub fix: bool,
128
129 #[arg(
130 long,
131 value_name = "FORMAT",
132 default_value_t = OutputFormat::Default,
133 help = "Output format"
134 )]
135 pub output_format: OutputFormat,
136
137 #[arg(
138 long,
139 help = "Lint files in parallel (experimental)",
140 help_heading = "Experimental",
141 overrides_with = "no_parallel"
142 )]
143 pub parallel: bool,
144
145 #[arg(long, hide = true, overrides_with = "parallel")]
146 pub no_parallel: bool,
147
148 #[arg(
149 long,
150 value_delimiter = ',',
151 value_name = "RULE_CODE",
152 help = "Comma-separated list of rules to enable (or `ALL`)",
153 help_heading = "Rule selection"
154 )]
155 pub select: Vec<String>,
156
157 #[arg(
158 long,
159 value_delimiter = ',',
160 value_name = "RULE_CODE",
161 help = "Comma-separated list of rules to disable",
162 help_heading = "Rule selection"
163 )]
164 pub ignore: Vec<String>,
165}
166
167impl CheckArgs {
168 pub fn files(&self) -> Vec<PathBuf> {
169 if self.files.is_empty() {
170 vec![PathBuf::from(".")]
171 } else {
172 self.files.clone()
173 }
174 }
175
176 pub fn should_respect_ignore(&self) -> bool {
177 !self.no_respect_ignore
178 }
179}
180
181#[derive(Args, Debug)]
182pub struct FormatArgs {
183 #[arg(
184 value_name = "FILES",
185 help = "Files or directories to format (defaults to current directory)"
186 )]
187 pub files: Vec<PathBuf>,
188
189 #[arg(
190 long,
191 help = "Files and directories to exclude from formatting",
192 help_heading = "File selection"
193 )]
194 pub exclude: Vec<PathBuf>,
195
196 #[arg(
197 long,
198 default_value_t = true,
199 help = "Respect `.gitignore` and similar exclusion files. Use `--no-respect-ignore` to disable",
200 help_heading = "File selection",
201 conflicts_with = "no_respect_ignore"
202 )]
203 pub respect_ignore: bool,
204
205 #[arg(long, hide = true, conflicts_with = "respect_ignore")]
206 pub no_respect_ignore: bool,
207
208 #[arg(
209 long,
210 help = "Check formatting without modifying files (exits with 1 if any file would change)"
211 )]
212 pub check: bool,
213}
214
215impl FormatArgs {
216 pub fn files(&self) -> Vec<PathBuf> {
217 if self.files.is_empty() {
218 vec![PathBuf::from(".")]
219 } else {
220 self.files.clone()
221 }
222 }
223
224 pub fn should_respect_ignore(&self) -> bool {
225 !self.no_respect_ignore
226 }
227}
228
229impl From<&Cli> for ConfigLoader {
230 fn from(cli: &Cli) -> Self {
231 if cli.no_config {
232 Self::None
233 } else if let Some(config_file) = &cli.config {
234 Self::File(config_file.clone())
235 } else {
236 Self::Detect
237 }
238 }
239}
240
241impl From<&Cli> for LogLevel {
242 fn from(cli: &Cli) -> Self {
243 if cli.silent {
244 Self::Silent
245 } else if cli.quiet {
246 Self::Quiet
247 } else if cli.verbose {
248 Self::Verbose
249 } else {
250 Self::Default
251 }
252 }
253}
254
255#[derive(ValueEnum, Debug, Default, Clone)]
256pub enum OutputFormat {
257 #[default]
258 Default,
259 Json,
260}
261
262impl Display for OutputFormat {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 OutputFormat::Default => write!(f, "default"),
266 OutputFormat::Json => write!(f, "json"),
267 }
268 }
269}
270
271#[derive(ValueEnum, Debug, Default, Clone)]
272pub enum TerminalColor {
273 #[default]
274 Auto,
275 Always,
276 Never,
277}
278
279impl Display for TerminalColor {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 match self {
282 TerminalColor::Auto => write!(f, "auto"),
283 TerminalColor::Always => write!(f, "always"),
284 TerminalColor::Never => write!(f, "never"),
285 }
286 }
287}