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 config::APP_NAME,
12 short_id::ShortIdMap,
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};
19use tokio::try_join;
20
21pub async fn run() -> Result<(), Box<dyn Error>> {
23 env_logger::init();
24 if let Err(e) = Cli::parse().run().await {
25 println!("{} {}", "Error:".red(), e);
26 }
27 Ok(())
28}
29
30#[derive(Debug)]
32pub struct Cli {
33 pub config: Option<PathBuf>,
35
36 pub command: Commands,
38}
39
40impl Cli {
41 pub fn command() -> Command {
43 const STYLES: styling::Styles = styling::Styles::styled()
44 .header(styling::AnsiColor::Green.on_default().bold())
45 .usage(styling::AnsiColor::Green.on_default().bold())
46 .literal(styling::AnsiColor::Blue.on_default().bold())
47 .placeholder(styling::AnsiColor::Cyan.on_default());
48
49 Command::new(APP_NAME)
50 .about("Analyze. Interact. Manage Your Time, with calendar support.")
51 .author("Zexin Yuan <aim@yzx9.xyz>")
52 .version(crate_version!())
53 .styles(STYLES)
54 .subcommand_required(false) .arg_required_else_help(false)
56 .arg(
57 arg!(-c --config [CONFIG] "Path to the configuration file")
58 .long_help(
59 "\
60Path to the configuration file. Defaults to $XDG_CONFIG_HOME/aim/config.toml on Linux and MacOS, \
61%LOCALAPPDATA%/aim/config.toml on Windows.",
62 )
63 .value_parser(value_parser!(PathBuf))
64 .value_hint(ValueHint::FilePath),
65 )
66 .subcommand(CmdDashboard::command())
67 .subcommand(
68 Command::new("event")
69 .alias("e")
70 .about("Manage your event list")
71 .arg_required_else_help(true)
72 .subcommand_required(true)
73 .subcommand(CmdEventList::command()),
74 )
75 .subcommand(
76 Command::new("todo")
77 .alias("t")
78 .about("Manage your todo list")
79 .arg_required_else_help(true)
80 .subcommand_required(true)
81 .subcommand(CmdTodoNew::command())
82 .subcommand(CmdTodoEdit::command())
83 .subcommand(CmdTodoDone::command())
84 .subcommand(CmdTodoUndo::command())
85 .subcommand(CmdTodoList::command()),
86 )
87 .subcommand(CmdTodoDone::command())
88 .subcommand(CmdTodoUndo::command())
89 .subcommand(CmdGenerateCompletion::command())
90 }
91
92 pub fn parse() -> Self {
94 use Commands::*;
95 let matches = Self::command().get_matches();
96 let command = match matches.subcommand() {
97 Some((CmdDashboard::NAME, _)) => Dashboard(CmdDashboard::parse()),
98 Some(("event", matches)) => match matches.subcommand() {
99 Some(("list", matches)) => EventList(CmdEventList::parse(matches)),
100 _ => unreachable!(),
101 },
102 Some(("todo", matches)) => match matches.subcommand() {
103 Some((CmdTodoNew::NAME, matches)) => TodoNew(CmdTodoNew::parse(matches)),
104 Some((CmdTodoEdit::NAME, matches)) => TodoEdit(CmdTodoEdit::parse(matches)),
105 Some((CmdTodoDone::NAME, matches)) => TodoDone(CmdTodoDone::parse(matches)),
106 Some((CmdTodoUndo::NAME, matches)) => TodoUndo(CmdTodoUndo::parse(matches)),
107 Some((CmdTodoList::NAME, matches)) => TodoList(CmdTodoList::parse(matches)),
108 _ => unreachable!(),
109 },
110 Some((CmdTodoDone::NAME, matches)) => TodoDone(CmdTodoDone::parse(matches)),
111 Some((CmdTodoUndo::NAME, matches)) => TodoUndo(CmdTodoUndo::parse(matches)),
112 Some((CmdGenerateCompletion::NAME, matches)) => {
113 GenerateCompletion(CmdGenerateCompletion::parse(matches))
114 }
115 None => Dashboard(CmdDashboard::parse()),
116 _ => unreachable!(),
117 };
118
119 let config = matches.get_one("config").cloned();
120 Cli { config, command }
121 }
122
123 pub async fn run(self) -> Result<(), Box<dyn Error>> {
125 self.command.run(self.config).await
126 }
127}
128
129#[derive(Debug, Clone)]
131pub enum Commands {
132 Dashboard(CmdDashboard),
134
135 EventList(CmdEventList),
137
138 TodoNew(CmdTodoNew),
140
141 TodoEdit(CmdTodoEdit),
143
144 TodoDone(CmdTodoDone),
146
147 TodoUndo(CmdTodoUndo),
149
150 TodoList(CmdTodoList),
152
153 GenerateCompletion(CmdGenerateCompletion),
155}
156
157impl Commands {
158 pub async fn run(self, config: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
160 use Commands::*;
161 match self {
162 Dashboard(a) => Self::run_with(config, |_, y, z| a.run(y, z).boxed()).await,
163 EventList(a) => Self::run_with(config, |_, y, z| a.run(y, z).boxed()).await,
164 TodoNew(a) => Self::run_with(config, |x, y, z| a.run(x, y, z).boxed()).await,
165 TodoEdit(a) => Self::run_with(config, |_, y, z| a.run(y, z).boxed()).await,
166 TodoDone(a) => Self::run_with(config, |_, y, z| a.run(y, z).boxed()).await,
167 TodoUndo(a) => Self::run_with(config, |_, y, z| a.run(y, z).boxed()).await,
168 TodoList(a) => Self::run_with(config, |_, y, z| a.run(y, z).boxed()).await,
169 GenerateCompletion(a) => a.run(),
170 }
171 }
172
173 async fn run_with<F>(config: Option<PathBuf>, f: F) -> Result<(), Box<dyn Error>>
174 where
175 F: for<'a> FnOnce(
176 &'a Config,
177 &'a Aim,
178 &'a ShortIdMap,
179 ) -> BoxFuture<'a, Result<(), Box<dyn Error>>>,
180 {
181 log::debug!("Parsing configuration...");
182 let config = Config::parse(config).await?;
183 let (aim, map) = try_join!(Aim::new(&config.core), ShortIdMap::load_or_new(&config))?;
184
185 f(&config, &aim, &map).await?;
186
187 map.dump(&config).await?;
188 Ok(())
189 }
190}