1use 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
20pub 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#[derive(Debug)]
36pub struct Cli {
37 pub config: Option<PathBuf>,
39
40 pub command: Commands,
42}
43
44impl Cli {
45 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) .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)) .subcommand(CmdGenerateCompletion::command())
96 }
97
98 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 pub async fn run(self) -> Result<(), Box<dyn Error>> {
133 self.command.run(self.config).await
134 }
135}
136
137#[derive(Debug, Clone)]
139pub enum Commands {
140 Dashboard(CmdDashboard),
142
143 New(CmdNew),
145
146 Edit(CmdEdit),
148
149 EventList(CmdEventList),
151
152 TodoNew(CmdTodoNew),
154
155 TodoEdit(CmdTodoEdit),
157
158 TodoDone(CmdTodoDone),
160
161 TodoUndo(CmdTodoUndo),
163
164 TodoList(CmdTodoList),
166
167 Undo(CmdTodoUndo),
169
170 GenerateCompletion(CmdGenerateCompletion),
172}
173
174impl Commands {
175 #[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}