tsdl/
args.rs

1use std::{collections::BTreeMap, fmt, path::PathBuf};
2
3use clap::{
4    builder::styling::{AnsiColor, Color, Style},
5    crate_authors,
6};
7use clap_verbosity_flag::{InfoLevel, Verbosity};
8use diff::Diff;
9use serde::{Deserialize, Serialize};
10
11use crate::consts::{
12    TREE_SITTER_PLATFORM, TREE_SITTER_REPO, TREE_SITTER_VERSION, TSDL_BUILD_DIR, TSDL_CONFIG_FILE,
13    TSDL_FRESH, TSDL_OUT_DIR, TSDL_PREFIX, TSDL_SHOW_CONFIG,
14};
15
16const TSDL_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/tsdl.version"));
17
18/// Command-line arguments.
19#[derive(Clone, Debug, Deserialize, clap::Parser, Serialize)]
20#[command(author = crate_authors!("\n"), version = TSDL_VERSION, about, styles=get_styles(), allow_external_subcommands = true)]
21#[command(help_template(
22    "{before-help}{name} {version}
23{author-with-newline}{about-with-newline}
24{usage-heading} {usage}
25
26{all-args}{after-help}"
27))]
28pub struct Args {
29    #[command(subcommand)]
30    pub command: Command,
31
32    /// Path to the config file (TOML).
33    #[arg(short, long, env = "TSDL_CONFIG", default_value = TSDL_CONFIG_FILE, global = true)]
34    pub config: PathBuf,
35
36    /// Path to the logging file. If unspecified, it will go to `build-dir/log`.
37    #[arg(short, long, env = "TSDL_LOG", global = true)]
38    pub log: Option<PathBuf>,
39
40    /// Whether to emit colored logs.
41    #[arg(long, value_enum, default_value_t = LogColor::Auto, global = true)]
42    pub log_color: LogColor,
43
44    /// Progress style.
45    #[arg(long, value_enum, default_value_t = ProgressStyle::Auto, global = true)]
46    pub progress: ProgressStyle,
47
48    /// Verbosity level: -v, -vv, or -q, -qq.
49    // clap_verbosity_flag, as of now, refuses to add a serialization feature, so this will not be part of the config file.
50    // It's global by default, so we don't need to specify it.
51    #[serde(skip_serializing, skip_deserializing)]
52    #[command(flatten)]
53    pub verbose: Verbosity<InfoLevel>,
54}
55
56#[derive(clap::ValueEnum, Clone, Debug, Deserialize, Serialize)]
57pub enum LogColor {
58    Auto,
59    No,
60    Yes,
61}
62
63#[derive(clap::ValueEnum, Clone, Debug, Deserialize, Serialize)]
64pub enum ProgressStyle {
65    Auto,
66    Fancy,
67    Plain,
68}
69
70#[derive(clap::Subcommand, Clone, Debug, Deserialize, Serialize)]
71pub enum Command {
72    /// Build one or many parsers.
73    #[command(visible_alias = "b")]
74    Build(BuildCommand),
75
76    /// Configuration helpers.
77    #[serde(skip_serializing, skip_deserializing)]
78    #[command(visible_alias = "c")]
79    Config {
80        #[command(subcommand)]
81        command: ConfigCommand,
82    },
83
84    /// Update tsdl to its latest version.
85    #[serde(skip_serializing, skip_deserializing)]
86    #[command(visible_alias = "u")]
87    Selfupdate,
88}
89
90impl Command {
91    #[must_use]
92    pub fn as_build(&self) -> Option<&BuildCommand> {
93        if let Command::Build(build) = self {
94            Some(build)
95        } else {
96            None
97        }
98    }
99
100    #[must_use]
101    pub fn as_config(&self) -> Option<&ConfigCommand> {
102        if let Command::Config { command } = self {
103            Some(command)
104        } else {
105            None
106        }
107    }
108}
109
110#[derive(clap::ValueEnum, Clone, Copy, Debug, Deserialize, Diff, PartialEq, Eq, Serialize)]
111#[diff(attr(
112    #[derive(Debug, PartialEq)]
113))]
114#[serde(rename_all = "kebab-case")]
115pub enum Target {
116    Native,
117    Wasm,
118    All,
119}
120
121impl Default for Target {
122    fn default() -> Self {
123        Self::Native
124    }
125}
126
127impl Target {
128    #[must_use]
129    pub fn native(&self) -> bool {
130        matches!(self, Self::All | Self::Native)
131    }
132
133    #[must_use]
134    pub fn wasm(&self) -> bool {
135        matches!(self, Self::All | Self::Wasm)
136    }
137}
138
139#[allow(clippy::struct_excessive_bools)]
140#[derive(clap::Args, Clone, Debug, Deserialize, Diff, PartialEq, Eq, Serialize)]
141#[diff(attr(
142    #[derive(Debug, PartialEq)]
143))]
144#[serde(rename_all = "kebab-case")]
145pub struct BuildCommand {
146    /// Parsers to compile.
147    #[serde(skip_serializing, skip_deserializing)]
148    #[arg(verbatim_doc_comment)]
149    pub languages: Option<Vec<String>>,
150
151    /// Configured Parsers.
152    #[clap(skip)]
153    pub parsers: Option<BTreeMap<String, ParserConfig>>,
154
155    /// Build Directory.
156    #[serde(default)]
157    #[arg(short, long, env = "TSDL_BUILD_DIR", default_value = TSDL_BUILD_DIR)]
158    pub build_dir: PathBuf,
159
160    /// Number of threads; defaults to the number of available CPUs.
161    #[arg(short, long, env = "TSDL_NCPUS", default_value_t = num_cpus::get())]
162    #[serde(default)]
163    pub ncpus: usize,
164
165    /// Clears the `build-dir` and starts a fresh build.
166    #[arg(short, long, default_value_t = TSDL_FRESH)]
167    #[serde(default)]
168    pub fresh: bool,
169
170    /// Output Directory.
171    #[arg(short, long, env = "TSDL_OUT_DIR", default_value = TSDL_OUT_DIR)]
172    #[serde(default)]
173    pub out_dir: PathBuf,
174
175    /// Prefix parser names.
176    #[arg(short, long, env = "TSDL_PREFIX", default_value = TSDL_PREFIX)]
177    #[serde(default)]
178    pub prefix: String,
179
180    /// Show Config.
181    #[arg(long, default_value_t = TSDL_SHOW_CONFIG)]
182    #[serde(default)]
183    pub show_config: bool,
184
185    /// Build target.
186    #[arg(short, long, value_enum, default_value_t = Target::default())]
187    pub target: Target,
188
189    #[command(flatten)]
190    #[serde(default)]
191    pub tree_sitter: TreeSitter,
192}
193
194impl Default for BuildCommand {
195    fn default() -> Self {
196        Self {
197            build_dir: PathBuf::from(TSDL_BUILD_DIR),
198            fresh: TSDL_FRESH,
199            languages: None,
200            ncpus: num_cpus::get(),
201            out_dir: PathBuf::from(TSDL_OUT_DIR),
202            parsers: None,
203            prefix: String::from(TSDL_PREFIX),
204            show_config: TSDL_SHOW_CONFIG,
205            target: Target::default(),
206            tree_sitter: TreeSitter::default(),
207        }
208    }
209}
210
211#[derive(Clone, Debug, Deserialize, Diff, Serialize, PartialEq, Eq)]
212#[diff(attr(
213    #[derive(Debug, PartialEq)]
214))]
215#[serde(untagged)]
216#[serde(rename_all = "kebab-case")]
217pub enum ParserConfig {
218    Full {
219        #[serde(alias = "cmd", alias = "script")]
220        build_script: Option<String>,
221        #[serde(rename = "ref")]
222        #[diff(attr(
223            #[derive(Debug, PartialEq)]
224        ))]
225        git_ref: String,
226        #[diff(attr(
227            #[derive(Debug, PartialEq)]
228        ))]
229        from: Option<String>,
230    },
231    Ref(String),
232}
233
234#[derive(clap::Args, Clone, Debug, Diff, Deserialize, PartialEq, Eq, Serialize)]
235#[diff(attr(
236    #[derive(Debug, PartialEq)]
237))]
238pub struct TreeSitter {
239    /// Tree-sitter version.
240    #[arg(short = 'V', long = "tree-sitter-version", default_value = TREE_SITTER_VERSION)]
241    pub version: String,
242
243    /// Tree-sitter repo.
244    #[arg(short = 'R', long = "tree-sitter-repo", default_value = TREE_SITTER_REPO)]
245    pub repo: String,
246
247    /// Tree-sitter platform to build. Change at your own risk.
248    #[clap(long = "tree-sitter-platform", default_value = TREE_SITTER_PLATFORM)]
249    pub platform: String,
250}
251
252impl Default for TreeSitter {
253    fn default() -> Self {
254        Self {
255            version: TREE_SITTER_VERSION.to_string(),
256            repo: TREE_SITTER_REPO.to_string(),
257            platform: TREE_SITTER_PLATFORM.to_string(),
258        }
259    }
260}
261
262#[derive(clap::Subcommand, Clone, Debug, Default)]
263pub enum ConfigCommand {
264    #[default]
265    Current,
266    Default,
267}
268
269impl fmt::Display for ConfigCommand {
270    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
271        write!(f, "{:?}", format!("{self:?}").to_lowercase())
272    }
273}
274
275#[must_use]
276const fn get_styles() -> clap::builder::Styles {
277    clap::builder::Styles::styled()
278        .usage(
279            Style::new()
280                .bold()
281                .fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
282        )
283        .header(
284            Style::new()
285                .bold()
286                .fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
287        )
288        .literal(Style::new().fg_color(Some(Color::Ansi(AnsiColor::Blue))))
289        .invalid(
290            Style::new()
291                .bold()
292                .fg_color(Some(Color::Ansi(AnsiColor::Red))),
293        )
294        .error(
295            Style::new()
296                .bold()
297                .fg_color(Some(Color::Ansi(AnsiColor::Red))),
298        )
299        .valid(
300            Style::new()
301                .bold()
302                .fg_color(Some(Color::Ansi(AnsiColor::Blue))),
303        )
304        .placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White))))
305}