aimcal_cli/
cli.rs

1// SPDX-FileCopyrightText: 2025 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use crate::{
6    Config,
7    cmd_dashboard::CmdDashboard,
8    cmd_event::CmdEventList,
9    cmd_generate_completion::CmdGenerateCompletion,
10    cmd_todo::{CmdTodoDone, CmdTodoEdit, CmdTodoList, CmdTodoNew, CmdTodoUndo},
11    cmd_tui::{CmdEdit, CmdNew},
12    config::APP_NAME,
13};
14use aimcal_core::Aim;
15use clap::{Command, ValueHint, arg, builder::styling, crate_version, value_parser};
16use colored::Colorize;
17use futures::{FutureExt, future::BoxFuture};
18use std::{error::Error, path::PathBuf};
19
20/// Run the AIM command-line interface.
21pub async fn run() -> Result<(), Box<dyn Error>> {
22    env_logger::init();
23    match Cli::parse() {
24        Ok(cli) => {
25            if let Err(e) = cli.run().await {
26                println!("{} {}", "Error:".red(), e);
27            }
28        }
29        Err(e) => println!("{} {}", "Error:".red(), e),
30    };
31    Ok(())
32}
33
34/// Command-line interface
35#[derive(Debug)]
36pub struct Cli {
37    /// Path to the configuration file
38    pub config: Option<PathBuf>,
39
40    /// The command to execute
41    pub command: Commands,
42}
43
44impl Cli {
45    /// Create the command-line interface
46    pub fn command() -> Command {
47        const STYLES: styling::Styles = styling::Styles::styled()
48            .header(styling::AnsiColor::Green.on_default().bold())
49            .usage(styling::AnsiColor::Green.on_default().bold())
50            .literal(styling::AnsiColor::Blue.on_default().bold())
51            .placeholder(styling::AnsiColor::Cyan.on_default());
52
53        Command::new(APP_NAME)
54            .about("Analyze. Interact. Manage Your Time, with calendar support.")
55            .author("Zexin Yuan <aim@yzx9.xyz>")
56            .version(crate_version!())
57            .styles(STYLES)
58            .subcommand_required(false) // allow default to dashboard
59            .arg_required_else_help(false)
60            .arg(
61                arg!(-c --config [CONFIG] "Path to the configuration file")
62                    .long_help(
63                        "\
64Path to the configuration file. Defaults to $XDG_CONFIG_HOME/aim/config.toml on Linux and MacOS, \
65%LOCALAPPDATA%/aim/config.toml on Windows.",
66                    )
67                    .value_parser(value_parser!(PathBuf))
68                    .value_hint(ValueHint::FilePath),
69            )
70            .subcommand(CmdDashboard::command())
71            .subcommand(CmdNew::command())
72            .subcommand(CmdEdit::command())
73            .subcommand(
74                Command::new("event")
75                    .alias("e")
76                    .about("Manage your event list")
77                    .arg_required_else_help(true)
78                    .subcommand_required(true)
79                    .subcommand(CmdEventList::command()),
80            )
81            .subcommand(
82                Command::new("todo")
83                    .alias("t")
84                    .about("Manage your todo list")
85                    .arg_required_else_help(true)
86                    .subcommand_required(true)
87                    .subcommand(CmdTodoNew::command())
88                    .subcommand(CmdTodoEdit::command())
89                    .subcommand(CmdTodoDone::command())
90                    .subcommand(CmdTodoUndo::command())
91                    .subcommand(CmdTodoList::command()),
92            )
93            .subcommand(CmdTodoDone::command())
94            .subcommand(CmdTodoUndo::command().hide(true)) // TODO: remove in v0.4.0
95            .subcommand(CmdGenerateCompletion::command())
96    }
97
98    /// Parse the command-line arguments
99    pub fn parse() -> Result<Self, Box<dyn Error>> {
100        use Commands::*;
101        let matches = Self::command().get_matches();
102        let command = match matches.subcommand() {
103            Some((CmdDashboard::NAME, matches)) => Dashboard(CmdDashboard::from(matches)),
104            Some((CmdNew::NAME, matches)) => New(CmdNew::from(matches)),
105            Some((CmdEdit::NAME, matches)) => Edit(CmdEdit::from(matches)),
106            Some(("event", matches)) => match matches.subcommand() {
107                Some(("list", matches)) => EventList(CmdEventList::from(matches)),
108                _ => unreachable!(),
109            },
110            Some(("todo", matches)) => match matches.subcommand() {
111                Some((CmdTodoNew::NAME, matches)) => TodoNew(CmdTodoNew::from(matches)?),
112                Some((CmdTodoEdit::NAME, matches)) => TodoEdit(CmdTodoEdit::from(matches)),
113                Some((CmdTodoDone::NAME, matches)) => TodoDone(CmdTodoDone::from(matches)),
114                Some((CmdTodoUndo::NAME, matches)) => TodoUndo(CmdTodoUndo::from(matches)),
115                Some((CmdTodoList::NAME, matches)) => TodoList(CmdTodoList::from(matches)),
116                _ => unreachable!(),
117            },
118            Some((CmdTodoDone::NAME, matches)) => TodoDone(CmdTodoDone::from(matches)),
119            Some((CmdTodoUndo::NAME, matches)) => Undo(CmdTodoUndo::from(matches)),
120            Some((CmdGenerateCompletion::NAME, matches)) => {
121                GenerateCompletion(CmdGenerateCompletion::from(matches))
122            }
123            None => Dashboard(CmdDashboard),
124            _ => unreachable!(),
125        };
126
127        let config = matches.get_one("config").cloned();
128        Ok(Cli { config, command })
129    }
130
131    /// Run the command
132    pub async fn run(self) -> Result<(), Box<dyn Error>> {
133        self.command.run(self.config).await
134    }
135}
136
137/// The commands available in the CLI
138#[derive(Debug, Clone)]
139pub enum Commands {
140    /// Show the dashboard
141    Dashboard(CmdDashboard),
142
143    /// New a event or todo
144    New(CmdNew),
145
146    /// Edit a event or todo
147    Edit(CmdEdit),
148
149    /// List events
150    EventList(CmdEventList),
151
152    /// Add a new todo
153    TodoNew(CmdTodoNew),
154
155    /// Edit a todo
156    TodoEdit(CmdTodoEdit),
157
158    /// Mark a todo as done
159    TodoDone(CmdTodoDone),
160
161    /// Mark a todo as undone
162    TodoUndo(CmdTodoUndo),
163
164    /// List todos
165    TodoList(CmdTodoList),
166
167    /// Mark a todo as undone
168    Undo(CmdTodoUndo),
169
170    /// Generate shell completion
171    GenerateCompletion(CmdGenerateCompletion),
172}
173
174impl Commands {
175    /// Run the command with the given configuration
176    #[rustfmt::skip]
177    pub async fn run(self, config: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
178        use Commands::*;
179        match self {
180            Dashboard(a) => Self::run_with(config, |x| a.run(x).boxed()).await,
181            New(a)       => Self::run_with(config, |x| a.run(x).boxed()).await,
182            Edit(a)      => Self::run_with(config, |x| a.run(x).boxed()).await,
183            EventList(a) => Self::run_with(config, |x| a.run(x).boxed()).await,
184            TodoNew(a)   => Self::run_with(config, |x| a.run(x).boxed()).await,
185            TodoEdit(a)  => Self::run_with(config, |x| a.run(x).boxed()).await,
186            TodoDone(a)  => Self::run_with(config, |x| a.run(x).boxed()).await,
187            TodoUndo(a)  => Self::run_with(config, |x| a.run(x).boxed()).await,
188            TodoList(a)  => Self::run_with(config, |x| a.run(x).boxed()).await,
189            Undo(a) => {
190                println!(
191                    "{} `aim undo` is now `aim todo undo`, the shortcut will be removed in v0.4.0",
192                    "Deprecated:".yellow(),
193                );
194                Self::run_with(config, |x| a.run(x).boxed()).await
195            },
196            GenerateCompletion(a) => a.run(),
197        }
198    }
199
200    async fn run_with<F>(config: Option<PathBuf>, f: F) -> Result<(), Box<dyn Error>>
201    where
202        F: for<'a> FnOnce(&'a mut Aim) -> BoxFuture<'a, Result<(), Box<dyn Error>>>,
203    {
204        log::debug!("Parsing configuration...");
205        let config = Config::parse(config).await?;
206        let mut aim = Aim::new(config.core).await?;
207
208        f(&mut aim).await?;
209
210        aim.close().await?;
211        Ok(())
212    }
213}