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