aimcal_cli/
cli.rs

1// SPDX-FileCopyrightText: 2025 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use crate::config::APP_NAME;
6use clap::{Arg, Command, ValueEnum, ValueHint, arg, crate_version, value_parser};
7use clap_complete::generate;
8use std::{io, path::PathBuf, process};
9
10/// Command-line interface
11#[derive(Debug)]
12pub struct Cli {
13    /// Path to the configuration file
14    pub config: Option<PathBuf>,
15
16    /// The command to execute
17    pub command: Commands,
18}
19
20impl Cli {
21    /// Parse the command-line arguments
22    pub fn parse() -> Cli {
23        let matches = build_cli().get_matches();
24
25        fn output_format(matches: &clap::ArgMatches) -> OutputFormat {
26            matches
27                .get_one::<OutputFormat>("output-format")
28                .copied()
29                .unwrap_or(OutputFormat::Table)
30        }
31
32        fn uid_or_short_id(matches: &clap::ArgMatches) -> String {
33            matches
34                .get_one::<String>("id")
35                .expect("id is required")
36                .clone()
37        }
38
39        fn todo_edit_args(matches: &clap::ArgMatches) -> TodoEditArgs {
40            TodoEditArgs {
41                uid_or_short_id: uid_or_short_id(matches),
42                output_format: output_format(matches),
43            }
44        }
45
46        let command = match matches.subcommand() {
47            Some(("dashboard", _)) => Commands::Dashboard,
48            Some(("event", matches)) => Commands::Events(OutputArgs {
49                output_format: output_format(matches),
50            }),
51            Some(("todo", matches)) => Commands::Todos(OutputArgs {
52                output_format: output_format(matches),
53            }),
54            Some(("done", matches)) => Commands::Done(todo_edit_args(matches)),
55            Some(("undo", matches)) => Commands::Undo(todo_edit_args(matches)),
56            Some(("generate-completion", matches)) => match matches.get_one::<Shell>("shell") {
57                Some(shell) => {
58                    shell.generate_completion();
59                    process::exit(1)
60                }
61                _ => unreachable!(),
62            },
63            None => Commands::Dashboard,
64            _ => unreachable!(),
65        };
66
67        let config = matches.get_one::<PathBuf>("config").cloned();
68        Cli { config, command }
69    }
70}
71
72/// The commands available in the CLI
73#[derive(Debug, Clone)]
74pub enum Commands {
75    /// Show the dashboard
76    Dashboard,
77
78    /// List events
79    Events(OutputArgs),
80
81    /// List todos
82    Todos(OutputArgs),
83
84    /// Mark a todo as done
85    Done(TodoEditArgs),
86
87    /// Mark a todo as undone
88    Undo(TodoEditArgs),
89}
90
91/// Arguments for commands that produce output
92#[derive(Debug, Clone, Copy)]
93pub struct OutputArgs {
94    pub output_format: OutputFormat,
95}
96
97#[derive(Debug, Clone)]
98pub struct TodoEditArgs {
99    pub uid_or_short_id: String,
100    pub output_format: OutputFormat,
101}
102
103fn build_cli() -> Command {
104    fn output_format() -> Arg {
105        arg!(--"output-format" <FORMAT> "Output format")
106            .value_parser(value_parser!(OutputFormat))
107            .default_value("table")
108    }
109
110    Command::new(APP_NAME)
111        .about("Analyze. Interact. Manage Your Time.")
112        .author("Zexin Yuan <aim@yzx9.xyz>")
113        .version(crate_version!())
114        .subcommand_required(false) // allow default to dashboard
115        .arg_required_else_help(false)
116        .arg(
117            arg!(-c --config [CONFIG] "Path to the configuration file")
118                .long_help(
119                    "\
120Path to the configuration file. Defaults to $XDG_CONFIG_HOME/aim/config.toml on Linux and MacOS, \
121%LOCALAPPDATA%/aim/config.toml on Windows.",
122                )
123                .value_parser(value_parser!(PathBuf))
124                .value_hint(ValueHint::FilePath),
125        )
126        .subcommand(
127            Command::new("dashboard")
128                .about("Show the dashboard, which includes upcoming events and todos")
129                .arg(output_format()),
130        )
131        .subcommand(
132            Command::new("event")
133                .about("List events")
134                .arg(output_format()),
135        )
136        .subcommand(
137            Command::new("todo")
138                .about("List todos")
139                .arg(output_format()),
140        )
141        .subcommand(
142            Command::new("done")
143                .about("Mark a todo as done")
144                .arg(arg!(<id> "The short id or uid of the todo to mark as done"))
145                .arg(output_format()),
146        )
147        .subcommand(
148            Command::new("undo")
149                .about("Mark a todo as undone")
150                .arg(arg!(<id> "The short id or uid of the todo to mark as undone"))
151                .arg(output_format()),
152        )
153        .subcommand(
154            Command::new("generate-completion")
155                .about("Generate shell completion for the specified shell")
156                .hide(true)
157                .arg(
158                    arg!(shell: <SHELL> "The shell generator to use")
159                        .value_parser(value_parser!(Shell)),
160                ),
161        )
162}
163
164/// The output format for commands
165#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
166pub enum OutputFormat {
167    Json,
168    Table,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
172enum Shell {
173    Bash,
174    Elvish,
175    Fish,
176    Nushell,
177    #[clap(name = "powershell")]
178    #[allow(clippy::enum_variant_names)]
179    PowerShell,
180    Zsh,
181}
182
183impl Shell {
184    fn generate_completion(&self) {
185        use clap_complete::Shell as ClapShell;
186
187        let mut cmd = build_cli();
188        let name = cmd.get_name().to_string();
189        match self {
190            Shell::Bash => generate(ClapShell::Bash, &mut cmd, name, &mut io::stdout()),
191            Shell::Elvish => generate(ClapShell::Elvish, &mut cmd, name, &mut io::stdout()),
192            Shell::Fish => generate(ClapShell::Fish, &mut cmd, name, &mut io::stdout()),
193            Shell::PowerShell => generate(ClapShell::PowerShell, &mut cmd, name, &mut io::stdout()),
194            Shell::Zsh => generate(ClapShell::Zsh, &mut cmd, name, &mut io::stdout()),
195
196            Shell::Nushell => generate(
197                clap_complete_nushell::Nushell {},
198                &mut cmd,
199                name,
200                &mut io::stdout(),
201            ),
202        }
203    }
204}