pace_rs/commands/
resume.rs

1//! `resume` subcommand
2
3use abscissa_core::{status_err, tracing::debug, Application, Command, Runnable, Shutdown};
4
5use clap::Parser;
6use eyre::Result;
7
8use pace_cli::{confirmation_or_break, prompt_resume_activity};
9use pace_core::prelude::{
10    get_storage_from_config, ActivityQuerying, ActivityReadOps, ActivityStateManagement,
11    ActivityStore, ResumeCommandOptions, ResumeOptions, SyncStorage, UserMessage,
12};
13use pace_time::{date_time::PaceDateTime, time_zone::PaceTimeZoneKind, Validate};
14
15use crate::prelude::PACE_APP;
16
17/// `resume` subcommand
18#[derive(Command, Debug, Parser)]
19pub struct ResumeCmd {
20    #[clap(flatten)]
21    resume_opts: ResumeCommandOptions,
22}
23
24impl Runnable for ResumeCmd {
25    fn run(&self) {
26        match self.inner_run() {
27            Ok(user_message) => user_message.display(),
28            Err(err) => {
29                status_err!("{}", err);
30                PACE_APP.shutdown(Shutdown::Crash);
31            }
32        };
33    }
34}
35
36// TODO!: Move the inner_run implementation to the pace-core crate
37// TODO: Factor out cli related stuff to pace-cli
38impl ResumeCmd {
39    /// Inner run implementation for the resume command
40    pub fn inner_run(&self) -> Result<UserMessage> {
41        let config = &PACE_APP.config();
42
43        // Validate the time and time zone as early as possible
44        let date_time = PaceDateTime::try_from((
45            self.resume_opts.at().as_ref(),
46            PaceTimeZoneKind::try_from((
47                self.resume_opts.time_zone().as_ref(),
48                self.resume_opts.time_zone_offset().as_ref(),
49            ))?,
50            PaceTimeZoneKind::from(config.general().default_time_zone().as_ref()),
51        ))?
52        .validate()?;
53
54        debug!("Parsed time: {date_time:?}");
55
56        let activity_store =
57            ActivityStore::with_storage(get_storage_from_config(&PACE_APP.config())?)?;
58
59        let (msg, resumed) = if let Some(resumed_activity) = activity_store
60            .resume_most_recent_activity(ResumeOptions::builder().resume_time(date_time).build())?
61        {
62            (format!("Resumed {}", resumed_activity.activity()), true)
63        } else {
64            ("".to_string(), false)
65        };
66
67        // If there is no activity to resume or the user wants to list activities to resume
68        let user_message = if *self.resume_opts.list() || !resumed {
69            // List activities to resume with fuzzy search and select
70            let Some(activity_ids) = activity_store.list_most_recent_activities(usize::from(
71                PACE_APP
72                    .config()
73                    .general()
74                    .most_recent_count()
75                    // Default to '9' if the most recent count is not set
76                    .unwrap_or_else(|| 9u8),
77            ))?
78            else {
79                return Ok(UserMessage::new("No recent activities to continue."));
80            };
81
82            let activity_items = activity_ids
83                .iter()
84                // TODO: With pomodoro, we might want to filter for activities that are not intermissions
85                .flat_map(|activity_id| activity_store.read_activity(*activity_id))
86                .filter_map(|activity| activity.activity().is_resumable().then_some(activity))
87                .collect::<Vec<_>>();
88
89            if activity_items.is_empty() {
90                return Ok(UserMessage::new("No activities to continue."));
91            }
92
93            let string_repr = activity_items
94                .iter()
95                .map(|activity| activity.activity().to_string())
96                .collect::<Vec<_>>();
97
98            let selection = prompt_resume_activity(&string_repr)?;
99
100            let Some(activity_item) = activity_items.get(selection) else {
101                return Ok(UserMessage::new("No activity selected to resume."));
102            };
103
104            let result =
105                activity_store.resume_activity(*activity_item.guid(), ResumeOptions::default());
106
107            let user_message = match result {
108                Ok(_) => format!("Resumed {}", activity_item.activity()),
109                // Handle the case where we can't resume the activity and ask the user if they want to create a new activity
110                // with the same contents
111                Err(recoverable_err) if recoverable_err.possible_new_activity_from_resume() => {
112                    debug!("Activity to resume: {:?}", activity_item.activity());
113
114                    confirmation_or_break(
115                            "We can't resume this already ended activity. Do you want to begin one with the same contents?",
116                        )?;
117
118                    debug!("Creating new activity from the same contents");
119
120                    let new_activity = activity_item.activity().new_from_self();
121
122                    debug!("New Activity: {:?}", new_activity);
123
124                    let new_stored_activity = activity_store.begin_activity(new_activity)?;
125
126                    debug!("Started Activity: {:?}", new_stored_activity);
127
128                    format!("Resumed {}", new_stored_activity.activity())
129                }
130                Err(err) => return Err(err.into()),
131            };
132
133            user_message
134        } else {
135            // If we have resumed an activity, we don't need to do anything else
136            // and can just return the message from the resume
137            msg
138        };
139
140        activity_store.sync()?;
141
142        Ok(UserMessage::new(user_message))
143    }
144}