saturn_cli/
cli_processor.rs

1pub fn format_all_day(entry: &crate::record::Record) -> String {
2    format!(
3        "All Day Event: {}{}",
4        entry.detail(),
5        if entry.completed() { "- Completed" } else { "" }
6    )
7}
8
9pub fn format_at(entry: &crate::record::Record, at: chrono::NaiveTime) -> String {
10    format!(
11        "{} at {}: {}{}",
12        entry.date(),
13        at,
14        entry.detail(),
15        if entry.completed() { "- Completed" } else { "" }
16    )
17}
18
19pub fn format_scheduled(
20    entry: &crate::record::Record,
21    schedule: crate::record::Schedule,
22) -> String {
23    format!(
24        "{} at {} to {}: {}{}",
25        entry.date(),
26        schedule.0,
27        schedule.1,
28        entry.detail(),
29        if entry.completed() { "- Completed" } else { "" }
30    )
31}
32
33#[macro_export]
34macro_rules! launch_editor {
35    ($db: ident, $id: ident, $typ:ty, $fetch:ident, $recur: ident) => {{
36        let record = $db.$fetch($id).await?;
37        let presented: $typ = record.clone().into();
38        let f = tempfile::NamedTempFile::new()?;
39        serde_yaml::to_writer(&f, &presented)?;
40        let (f, path) = f.keep()?;
41        drop(f);
42        let mut cmd = tokio::process::Command::new(
43            std::env::var("EDITOR").unwrap_or("/usr/bin/vim".to_string()),
44        );
45        cmd.args([path.clone()]);
46        let mut child = cmd.spawn()?;
47        if child.wait().await?.success() {
48            let mut io = std::fs::OpenOptions::new();
49            io.read(true);
50            let f = io.open(path)?;
51            let presented: $typ = serde_yaml::from_reader(&f)?;
52            $crate::update_record!($db, presented, record, $recur);
53        }
54    }};
55}
56
57#[macro_export]
58macro_rules! map_record {
59    ($db: ident, $id:ident, true) => {{
60        $db.get_recurring($id).await
61    }};
62    ($db: ident, $id:ident, false) => {{
63        $db.get($id).await
64    }};
65    ($db: ident, $id: ident) => {{
66        map_record!($db, $id, false)
67    }};
68}
69
70#[macro_export]
71macro_rules! update_record {
72    ($db: ident, $presented: ident, $record:ident, true) => {{
73        $db.update_recurring($presented.to_record(
74            $record.clone().record().primary_key(),
75            $record.recurrence_key(),
76            $record.clone().record().internal_key(),
77            $record.internal_key(),
78        ))
79        .await?;
80    }};
81    ($db: ident, $presented: ident, $record:ident, false) => {{
82        $db.update($presented.to_record(
83            $record.primary_key(),
84            $record.recurrence_key(),
85            $record.internal_key(),
86            $record.internal_recurrence_key(),
87        ))
88        .await?;
89    }};
90}
91
92// this is a very filthy macro. be careful when modifying it.
93#[macro_export]
94macro_rules! process_cli {
95    ($cli:ident, $config:ident, $db:ident) => {
96        use $crate::db::google::GoogleClient;
97
98        process_cli!($cli, $config, $db, None::<GoogleClient>);
99    };
100    ($cli:ident, $config:ident, $db:ident, $client:expr) => {{
101        use chrono::{Datelike, Timelike};
102        use $crate::cli_processor::{format_all_day, format_at, format_scheduled};
103        $db.load().await?;
104
105        match $cli.command {
106            Command::Config { command } => match command {
107                ConfigCommand::SetQueryWindow { set } => {
108                    let mut config = Config::load(None)?;
109                    config.set_query_window(FancyDuration::parse(&set)?.duration());
110                    config.save(None)?;
111                }
112                ConfigCommand::Set24hTime { set } => {
113                    let mut config = Config::load(None)?;
114                    config.set_use_24h_time(set);
115                    config.save(None)?;
116                }
117                ConfigCommand::SetClient {
118                    client_id,
119                    client_secret,
120                } => {
121                    let mut config = Config::load(None)?;
122                    config.set_client_info(client_id, client_secret);
123                    config.save(None)?;
124                }
125                ConfigCommand::GetToken {} => $crate::oauth::get_access_token().await?,
126                ConfigCommand::DBType { db_type } => {
127                    let mut config = Config::load(None)?;
128                    let typ = match db_type.as_str() {
129                        "google" => DBType::Google,
130                        "unixfile" => DBType::UnixFile,
131                        _ => {
132                            return Err(anyhow!(
133                                "Invalid db type: valid types are `google` and `unixfile`"
134                            ))
135                        }
136                    };
137
138                    config.set_db_type(typ);
139                    config.save(None)?;
140                }
141                ConfigCommand::ListCalendars => {
142                    if $client.is_none() {
143                        eprintln!("Not supported in unixfile mode");
144                    } else {
145                        list_calendars($client.unwrap()).await?;
146                    }
147                }
148                ConfigCommand::SetCalendarID { id } => {
149                    if $client.is_none() {
150                        eprintln!("Not supported in unixfile mode");
151                    } else {
152                        set_calendar_id(id, $config)?;
153                    }
154                }
155                ConfigCommand::SetDefaultDuration { duration } => {
156                    let duration: fancy_duration::FancyDuration<chrono::Duration> =
157                        fancy_duration::FancyDuration::parse(&duration)?;
158                    let mut config = $crate::config::Config::load(None)?;
159                    config.set_default_duration(Some(duration));
160                    config.save(None)?;
161                }
162            },
163            Command::Complete { id } => $db.complete_task(id).await?,
164            Command::Delete { ids, recur } => {
165                for id in ids {
166                    if recur {
167                        $db.delete_recurrence(id).await?;
168                    } else {
169                        $db.delete(id).await?;
170                    }
171                }
172            }
173            Command::Notify {
174                well,
175                timeout,
176                include_completed,
177                icon,
178            } => {
179                let timeout = timeout.map_or(std::time::Duration::new(60, 0), |t| {
180                    fancy_duration::FancyDuration::<std::time::Duration>::parse(&t)
181                        .expect("Invalid Duration")
182                        .duration()
183                });
184
185                let mut notification = notify_rust::Notification::new();
186                notification.summary("Calendar Event");
187                notification.timeout(timeout);
188                let well = well.map_or_else(
189                    || chrono::TimeDelta::try_minutes(1).unwrap_or_default(),
190                    |x| {
191                        FancyDuration::<chrono::Duration>::parse(&x)
192                            .expect("Invalid duration")
193                            .duration()
194                    },
195                );
196
197                let time = $crate::time::now()
198                    .with_second(0)
199                    .unwrap()
200                    .with_nanosecond(0)
201                    .unwrap();
202
203                for entry in &$db.list_all(include_completed).await? {
204                    if let Some(notifications) = entry.notifications() {
205                        for duration in notifications.iter().map(FancyDuration::duration) {
206                            let notify = match entry.record_type() {
207                                $crate::record::RecordType::AllDay => {
208                                    let top = (entry.datetime() + chrono::TimeDelta::try_days(1).unwrap_or_default())
209                                        .with_hour(0)
210                                        .unwrap()
211                                        .with_minute(0)
212                                        .unwrap()
213                                        .with_second(0)
214                                        .unwrap()
215                                        .with_nanosecond(0)
216                                        .unwrap()
217                                        - duration;
218
219                                    if time - well < top && time + well > top {
220                                        Some(notification.body(&format_all_day(entry)))
221                                    } else {
222                                        None
223                                    }
224                                }
225                                $crate::record::RecordType::At => {
226                                    let at = entry.at().unwrap() - duration;
227                                    let top = (chrono::NaiveDateTime::new(entry.date(), at)
228                                        - duration)
229                                        .and_local_timezone(chrono::Local)
230                                        .unwrap();
231                                    if time - well < top && time + well > top {
232                                        Some(notification.body(&format_at(entry, at)))
233                                    } else {
234                                        None
235                                    }
236                                }
237                                $crate::record::RecordType::Schedule => {
238                                    let schedule = entry.scheduled().unwrap();
239                                    let top =
240                                        (chrono::NaiveDateTime::new(entry.date(), schedule.0)
241                                            - duration)
242                                            .and_local_timezone(chrono::Local)
243                                            .unwrap();
244
245                                    if time - well < top && time + well > top {
246                                        Some(notification.body(&format_scheduled(entry, schedule)))
247                                    } else {
248                                        None
249                                    }
250                                }
251                            };
252
253                            if let Some(mut notify) = notify {
254                                if let Some(icon) = icon.clone() {
255                                    notify = notify.icon(&icon);
256                                }
257
258                                notify.show()?;
259                            }
260                        }
261                    }
262                }
263            }
264            Command::Now {
265                well,
266                include_completed,
267            } => {
268                print_entries($db.events_now(get_well(well)?, include_completed).await?);
269            }
270            Command::List { all, recur } => {
271                if recur {
272                    print_recurring($db.list_recurrence().await?);
273                } else {
274                    let mut list = if all {
275                        $db.list_all(false).await?
276                    } else {
277                        $db.list_today(false).await?
278                    };
279                    list.sort_by($crate::record::sort_records);
280                    print_entries(list);
281                }
282            }
283            Command::Today {} => {
284                print_entries($db.list_today(false).await?);
285            }
286            Command::Entry { args } => {
287                $db.list_all(false).await?;
288                $db.record_entry($crate::parsers::entry::EntryParser::new(
289                    args,
290                    $config.use_24h_time(),
291                ))
292                .await?;
293            }
294            Command::Edit { recur, id } => {
295                if recur {
296                    $crate::launch_editor!(
297                        $db,
298                        id,
299                        $crate::record::PresentedRecurringRecord,
300                        get_recurring,
301                        true
302                    );
303                } else {
304                    $crate::launch_editor!($db, id, $crate::record::PresentedRecord, get, false);
305                }
306            }
307            Command::Show { recur, id } => {
308                if recur {
309                    let presented: $crate::record::PresentedRecurringRecord =
310                        $db.get_recurring(id).await?.into();
311                    println!("{}", serde_yaml::to_string(&presented)?);
312                } else {
313                    let presented: $crate::record::PresentedRecord = $db.get(id).await?.into();
314                    println!("{}", serde_yaml::to_string(&presented)?);
315                }
316            }
317            Command::Search { terms } => {
318                let parser =
319                    $crate::parsers::search::SearchParser::new(terms, $db.list_all(false).await?);
320                print_entries(parser.perform()?);
321            }
322        }
323
324        $db.dump().await?;
325    };};
326}
327
328#[macro_export]
329macro_rules! list_ui {
330    ($db:ident, $list_type:ident) => {{
331        $db.load().await?;
332
333        let all = match $list_type {
334            $crate::ui::types::ListType::All => $db.list_all(true).await?,
335            $crate::ui::types::ListType::Today => $db.list_today(true).await?,
336            $crate::ui::types::ListType::Recurring | $crate::ui::types::ListType::Search => {
337                Vec::new()
338            }
339        };
340
341        $db.dump().await?;
342
343        Ok(all)
344    }};
345}
346
347#[macro_export]
348macro_rules! process_ui_command {
349    ($obj:ident, $db:ident, $config:ident) => {{
350        let mut lock = $obj.lock().await;
351        let commands = lock.commands.clone();
352        lock.commands = Vec::new();
353        lock.block_ui = true;
354        drop(lock);
355        $db.load().await?;
356        for command in commands {
357            match command {
358                $crate::ui::types::CommandType::Search(terms) => {
359                    let parser = $crate::parsers::search::SearchParser::new(
360                        terms,
361                        $db.list_all(false).await?,
362                    );
363                    let mut inner = $obj.lock().await;
364                    inner.list_type = $crate::ui::types::ListType::Search;
365                    inner.records = parser.perform()?;
366                    inner.records.sort_by($crate::record::sort_records);
367                    inner.redraw = true;
368                }
369                $crate::ui::types::CommandType::Delete(items) => {
370                    for item in items {
371                        $db.delete(item).await?
372                    }
373                }
374                $crate::ui::types::CommandType::DeleteRecurring(items) => {
375                    for item in items {
376                        $db.delete_recurrence(item).await?;
377                    }
378                }
379                $crate::ui::types::CommandType::Entry(entry) => {
380                    $db.list_all(false).await?;
381                    let parts = entry
382                        .split(' ')
383                        .filter(|x| !x.is_empty())
384                        .map(|s| s.to_string())
385                        .collect::<Vec<String>>();
386                    $db.record_entry($crate::parsers::entry::EntryParser::new(
387                        parts,
388                        $config.use_24h_time(),
389                    ))
390                    .await?;
391                }
392                $crate::ui::types::CommandType::Edit(recur, id) => {
393                    if recur {
394                        $crate::launch_editor!(
395                            $db,
396                            id,
397                            $crate::record::PresentedRecurringRecord,
398                            get_recurring,
399                            true
400                        );
401                    } else {
402                        $crate::launch_editor!(
403                            $db,
404                            id,
405                            $crate::record::PresentedRecord,
406                            get,
407                            false
408                        );
409                    }
410                }
411                $crate::ui::types::CommandType::Show(recur, id) => {
412                    if recur {
413                        let mut lock = $obj.lock().await;
414                        lock.show_recurring = Some($db.get_recurring(id).await?);
415                        drop(lock);
416                    } else {
417                        let mut lock = $obj.lock().await;
418                        lock.show = Some($db.get(id).await?);
419                        drop(lock);
420                    }
421                }
422            };
423        }
424        $db.dump().await?;
425        let mut lock = $obj.lock().await;
426        lock.block_ui = false;
427    }};
428}