aimcal_cli/
command.rs

1// SPDX-FileCopyrightText: 2025 Zexin Yuan <aim@yzx9.xyz>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use crate::{
6    cli::{OutputArgs, OutputFormat, TodoEditArgs},
7    config::Config,
8    event_formatter::EventFormatter,
9    short_id::{EventWithShortId, ShortIdMap, TodoWithShortId},
10    todo_formatter::TodoFormatter,
11};
12use aimcal_core::{
13    Aim, EventConditions, Pager, SortOrder, TodoConditions, TodoPatch, TodoSort, TodoStatus,
14};
15use chrono::{Duration, Local, Utc};
16use colored::Colorize;
17use std::{error::Error, path::PathBuf};
18
19/// Show the dashboard with events and todos.
20pub async fn command_dashboard(config: Option<PathBuf>) -> Result<(), Box<dyn Error>> {
21    log::debug!("Parsing configuration...");
22    let config = Config::parse(config).await?;
23    let aim = Aim::new(&config.core).await?;
24    let map = ShortIdMap::load_or_new(&config)?;
25
26    log::debug!("Generating dashboard...");
27    let now = Local::now().naive_local();
28
29    println!("🗓️ {}", "Events".bold());
30    let conds = EventConditions { now };
31    let args = OutputArgs {
32        output_format: OutputFormat::Table,
33    };
34    list_events(&aim, &map, &conds, &args).await?;
35    println!();
36
37    println!("✅ {}", "Todos".bold());
38    let conds = TodoConditions {
39        now,
40        status: Some(TodoStatus::NeedsAction),
41        due: Some(Duration::days(2)),
42    };
43    let args = OutputArgs {
44        output_format: OutputFormat::Table,
45    };
46    list_todos(&aim, &map, &conds, &args).await?;
47
48    map.dump(&config)?;
49    Ok(())
50}
51
52/// List all events.
53pub async fn command_events(
54    config: Option<PathBuf>,
55    args: &OutputArgs,
56) -> Result<(), Box<dyn Error>> {
57    log::debug!("Parsing configuration...");
58    let config = Config::parse(config).await?;
59    let aim = Aim::new(&config.core).await?;
60    let map = ShortIdMap::load_or_new(&config)?;
61
62    log::debug!("Listing events...");
63    let now = Local::now().naive_local();
64    let conds = EventConditions { now };
65    list_events(&aim, &map, &conds, args).await?;
66
67    map.dump(&config)?;
68    Ok(())
69}
70
71/// List all todos.
72pub async fn command_todos(
73    config: Option<PathBuf>,
74    args: &OutputArgs,
75) -> Result<(), Box<dyn Error>> {
76    log::debug!("Parsing configuration...");
77    let config = Config::parse(config).await?;
78    let aim = Aim::new(&config.core).await?;
79    let map = ShortIdMap::load_or_new(&config)?;
80
81    log::debug!("Listing todos...");
82    let now = Local::now().naive_local();
83    let conds = TodoConditions {
84        now,
85        status: Some(TodoStatus::NeedsAction),
86        due: Some(Duration::days(2)),
87    };
88    list_todos(&aim, &map, &conds, args).await?;
89
90    map.dump(&config)?;
91    Ok(())
92}
93
94/// Mark a todo as done.
95pub async fn command_done(
96    config: Option<PathBuf>,
97    args: &TodoEditArgs,
98) -> Result<(), Box<dyn Error>> {
99    log::debug!("Marking todo as done...");
100    let patch = TodoPatch {
101        completed: Some(Some(Utc::now().into())),
102        status: Some(TodoStatus::Completed),
103        ..Default::default()
104    };
105    edit_todo(config, args, patch).await
106}
107
108/// Mark a todo as undone.
109pub async fn command_undo(
110    config: Option<PathBuf>,
111    args: &TodoEditArgs,
112) -> Result<(), Box<dyn Error>> {
113    log::debug!("Marking todo as undone...");
114    let patch = TodoPatch {
115        completed: Some(None),
116        status: Some(TodoStatus::NeedsAction),
117        ..Default::default()
118    };
119    edit_todo(config, args, patch).await
120}
121
122/// List events with the given conditions and output format.
123async fn list_events(
124    aim: &Aim,
125    map: &ShortIdMap,
126    conds: &EventConditions,
127    args: &OutputArgs,
128) -> Result<(), Box<dyn Error>> {
129    const MAX: i64 = 16;
130    let pager: Pager = (MAX, 0).into();
131    let events = aim.list_events(conds, &pager).await?;
132    if events.len() >= (MAX as usize) {
133        let total = aim.count_events(conds).await?;
134        if total > MAX {
135            println!("Displaying the {total}/{MAX} events");
136        }
137    }
138
139    let events: Vec<_> = events
140        .into_iter()
141        .map(|event| EventWithShortId::with(map, event))
142        .collect();
143
144    let formatter = EventFormatter::new(conds.now).with_output_format(args.output_format);
145    println!("{}", formatter.format(&events));
146    Ok(())
147}
148
149/// List todos with the given conditions and output format.
150async fn list_todos(
151    aim: &Aim,
152    map: &ShortIdMap,
153    conds: &TodoConditions,
154    args: &OutputArgs,
155) -> Result<(), Box<dyn Error>> {
156    const MAX: i64 = 16;
157    let pager = (MAX, 0).into();
158    let sort = vec![
159        TodoSort::Priority {
160            order: SortOrder::Desc,
161            none_first: false, // TODO: add config entry
162        },
163        TodoSort::Due(SortOrder::Desc),
164    ];
165    let todos = aim.list_todos(conds, &sort, &pager).await?;
166    if todos.len() >= (MAX as usize) {
167        let total = aim.count_todos(conds).await?;
168        if total > MAX {
169            println!("Displaying the {total}/{MAX} todos");
170        }
171    }
172
173    let todos: Vec<_> = todos
174        .into_iter()
175        .map(|todo| TodoWithShortId::with(map, todo))
176        .collect();
177
178    let formatter = TodoFormatter::new(conds.now).with_output_format(args.output_format);
179    println!("{}", formatter.format(&todos));
180    Ok(())
181}
182
183async fn edit_todo(
184    config: Option<PathBuf>,
185    args: &TodoEditArgs,
186    mut patch: TodoPatch,
187) -> Result<(), Box<dyn Error>> {
188    let now = Local::now().naive_local();
189
190    log::debug!("Parsing configuration...");
191    let config = Config::parse(config).await?;
192    let aim = Aim::new(&config.core).await?;
193    let map = ShortIdMap::load_or_new(&config)?;
194
195    log::debug!("Edit todo ...");
196    patch.uid = args
197        .uid_or_short_id
198        .parse()
199        .ok()
200        .and_then(|a| map.find(a))
201        .unwrap_or_else(|| args.uid_or_short_id.to_string()); // treat it as a UID if is not a short ID
202    let todo = aim.upsert_todo(patch.clone()).await?;
203    let todo = TodoWithShortId::with(&map, todo);
204
205    let formatter = TodoFormatter::new(now).with_output_format(args.output_format);
206    println!("{}", formatter.format(&[todo]));
207    Ok(())
208}