Skip to main content

whiskers/
cli.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4};
5
6use clap::Parser;
7use clap_stdin::FileOrStdin;
8
9type ValueMap = HashMap<String, serde_json::Value>;
10
11#[derive(Parser, Debug)]
12#[command(version, about)]
13#[allow(clippy::struct_excessive_bools)] // not a problem for cli flags
14pub struct Args {
15    /// Path to the template file, or - for stdin
16    #[arg(required_unless_present_any = ["list_functions", "list_flavors", "list_accents"])]
17    pub template: Option<FileOrStdin>,
18
19    /// Render a single flavor instead of all four
20    #[arg(long, short)]
21    pub flavor: Option<Flavor>,
22
23    /// Set color overrides
24    #[arg(long, value_parser = json_map::<ColorOverrides>)]
25    pub color_overrides: Option<ColorOverrides>,
26
27    /// Set frontmatter overrides
28    #[arg(long, value_parser = json_map::<ValueMap>)]
29    pub overrides: Option<ValueMap>,
30
31    /// Instead of creating an output, check it against an example
32    ///
33    /// In single-output mode, a path to the example file must be provided.
34    /// In multi-output mode, no path is required and, if one is provided, it
35    /// will be ignored.
36    #[arg(long, value_name = "EXAMPLE_PATH")]
37    pub check: Option<Option<PathBuf>>,
38
39    /// Dry run, don't write anything to disk
40    #[arg(long)]
41    pub dry_run: bool,
42
43    /// List all Tera filters and functions
44    #[arg(long)]
45    pub list_functions: bool,
46
47    /// List the Catppuccin flavors
48    #[arg(long)]
49    pub list_flavors: bool,
50
51    /// List the Catppuccin accent colors
52    #[arg(long)]
53    pub list_accents: bool,
54
55    /// Output format of --list-functions
56    #[arg(short, long, default_value = "json")]
57    pub output_format: OutputFormat,
58}
59
60#[derive(Debug, thiserror::Error)]
61enum Error {
62    #[error("Invalid JSON literal argument: {message}")]
63    InvalidJsonLiteralArg { message: String },
64
65    #[error("Invalid JSON file argument: {message}")]
66    InvalidJsonFileArg { message: String },
67
68    #[error("Failed to read file: {path}")]
69    ReadFile {
70        path: String,
71        #[source]
72        source: std::io::Error,
73    },
74}
75
76#[derive(Copy, Clone, Debug, clap::ValueEnum)]
77pub enum Flavor {
78    Latte,
79    Frappe,
80    Macchiato,
81    Mocha,
82}
83
84impl From<Flavor> for catppuccin::FlavorName {
85    fn from(val: Flavor) -> Self {
86        match val {
87            Flavor::Latte => Self::Latte,
88            Flavor::Frappe => Self::Frappe,
89            Flavor::Macchiato => Self::Macchiato,
90            Flavor::Mocha => Self::Mocha,
91        }
92    }
93}
94
95#[derive(Clone, Debug, serde::Deserialize)]
96pub struct ColorOverrides {
97    #[serde(default)]
98    pub all: HashMap<String, String>,
99    #[serde(default)]
100    pub latte: HashMap<String, String>,
101    #[serde(default)]
102    pub frappe: HashMap<String, String>,
103    #[serde(default)]
104    pub macchiato: HashMap<String, String>,
105    #[serde(default)]
106    pub mocha: HashMap<String, String>,
107}
108
109#[derive(Clone, Copy, Debug, clap::ValueEnum)]
110pub enum OutputFormat {
111    Json,
112    Yaml,
113    Markdown,
114    Plain,
115
116    /// Deprecated, now equivalent to `Markdown`
117    #[clap(hide = true)]
118    MarkdownTable,
119}
120
121fn json_map<T>(s: &str) -> Result<T, Error>
122where
123    T: serde::de::DeserializeOwned,
124{
125    if Path::new(s).is_file() {
126        let s = std::fs::read_to_string(s).map_err(|e| Error::ReadFile {
127            path: s.to_string(),
128            source: e,
129        })?;
130        serde_json::from_str(&s).map_err(|e| Error::InvalidJsonFileArg {
131            message: e.to_string(),
132        })
133    } else {
134        serde_json::from_str(s).map_err(|e| Error::InvalidJsonLiteralArg {
135            message: e.to_string(),
136        })
137    }
138}