pace_rs/
commands.rs

1//! Pace Subcommands
2//!
3//! This is where you specify the subcommands of your application.
4//!
5//! The default application comes with two subcommands:
6//!
7//! - `start`: launches the application
8//! - `--version`: print application version
9//!
10//! See the `impl Configurable` below for how to specify the path to the
11//! application's configuration file.
12
13pub mod adjust;
14pub mod begin;
15pub mod docs;
16pub mod end;
17pub mod hold;
18pub mod now;
19pub mod reflect;
20pub mod resume;
21pub mod settings;
22pub mod setup;
23
24use abscissa_core::{
25    config::Override, status_warn, tracing::debug, Command, Configurable, FrameworkError, Runnable,
26};
27use clap::builder::{styling::AnsiColor, Styles};
28use human_panic::setup_panic;
29use std::path::PathBuf;
30
31use pace_core::{
32    constants::PACE_CONFIG_FILENAME,
33    prelude::{get_config_paths, ActivityLogFormatKind, PaceConfig},
34};
35
36/// Pace Subcommands
37/// Subcommands need to be listed in an enum.
38#[derive(clap::Parser, Command, Debug, Runnable)]
39pub enum PaceCmd {
40    /// πŸ“ Adjust the details of the current activity, such as its category, description, or tags.
41    #[clap(visible_alias = "a")]
42    Adjust(adjust::AdjustCmd),
43
44    /// ⌚ Starts tracking time for an activity.
45    #[clap(visible_alias = "b")]
46    Begin(begin::BeginCmd),
47
48    /// ⏹️  Stops time tracking for the most recent or all activities.
49    #[clap(visible_alias = "e")]
50    End(end::EndCmd),
51
52    /// ⏸️  Pauses the time tracking for the most recent active activity.
53    #[clap(visible_alias = "h")]
54    Hold(hold::HoldCmd),
55
56    /// ⏲️  Shows you at a glance what you're currently tracking.
57    #[clap(visible_alias = "n")]
58    Now(now::NowCmd),
59
60    /// ⏯️  Resumes a previously paused activity, allowing you to continue where you left off.
61    #[clap(visible_alias = "r")]
62    Resume(resume::ResumeCmd),
63
64    /// πŸ“ˆ Get sophisticated insights on your activities.
65    #[clap(visible_alias = "ref")]
66    Reflect(reflect::ReflectCmd),
67
68    /// πŸ› οΈ  Set up a pace configuration, a new project, or generate shell completions.
69    Setup(setup::SetupCmd),
70
71    /// βš™οΈ  Changes various application settings, including Pomodoro lengths, time zone, and reflection format.
72    #[clap(visible_alias = "s")]
73    Settings(settings::SettingsCmd),
74
75    /// πŸ“š Open the online documentation for pace.
76    #[clap(visible_alias = "d")]
77    Docs(docs::DocsCmd),
78    // /// Exports your tracked data and reflections in JSON or CSV format, suitable for analysis or record-keeping.
79    // Export(export::ExportCmd),
80
81    // /// Starts a Pomodoro session for the specified task, integrating the Pomodoro technique directly with your tasks.
82    // Pomo(pomo::PomoCmd),
83    //
84    // /// Lists all tasks with optional filters. Use this to view active, completed, or today's tasks.
85    // Tasks(tasks::TasksCmd),
86}
87
88/// Define CLI colour styles for the application
89const fn cli_colour_styles() -> Styles {
90    Styles::styled()
91        .header(AnsiColor::BrightBlue.on_default())
92        .usage(AnsiColor::BrightYellow.on_default())
93        .literal(AnsiColor::BrightGreen.on_default())
94        .placeholder(AnsiColor::Magenta.on_default())
95}
96
97/// Entry point for the application.
98// It needs to be a struct to allow using subcommands!
99#[derive(clap::Parser, Command, Debug)]
100#[command(name="pace", author, about, styles=cli_colour_styles(), version, arg_required_else_help = true, propagate_version = true, )]
101pub struct EntryPoint {
102    #[command(subcommand)]
103    cmd: PaceCmd,
104
105    /// Enable verbose logging
106    #[arg(short, long)]
107    pub verbose: bool,
108
109    /// Use the specified config file
110    #[arg(long, env = "PACE_CONFIG_FILE", value_hint = clap::ValueHint::FilePath)]
111    pub config: Option<PathBuf>,
112
113    /// Use the specified activity log file
114    #[arg(long, env = "PACE_ACTIVITY_LOG_FILE", value_hint = clap::ValueHint::FilePath)]
115    pub activity_log_file: Option<PathBuf>,
116
117    /// Pace Home Directory
118    #[arg(long, env = "PACE_HOME", value_hint = clap::ValueHint::DirPath)]
119    pub home: Option<PathBuf>,
120}
121
122impl Runnable for EntryPoint {
123    fn run(&self) {
124        setup_panic!();
125        self.cmd.run();
126    }
127}
128
129impl Override<PaceConfig> for EntryPoint {
130    fn override_config(&self, mut config: PaceConfig) -> Result<PaceConfig, FrameworkError> {
131        // Override the activity log file if it's set
132        if let Some(activity_log_file) = &self.activity_log_file {
133            debug!("Overriding activity log file with: {:?}", activity_log_file);
134
135            // Handle not existing activity log file and parent directory
136            match (activity_log_file.parent(), activity_log_file.exists()) {
137                (Some(dir), false) if dir.exists() => {
138                    std::fs::File::create(activity_log_file)?;
139                }
140                (Some(dir), false) if !dir.exists() => {
141                    std::fs::create_dir_all(dir)?;
142                    std::fs::File::create(activity_log_file)?;
143                }
144                _ => {}
145            };
146
147            *config.general_mut().activity_log_options_mut().path_mut() =
148                activity_log_file.to_path_buf();
149
150            // Set the activity log format to TOML
151            // TODO: This should be configurable
152            *config
153                .general_mut()
154                .activity_log_options_mut()
155                .format_kind_mut() = Some(ActivityLogFormatKind::Toml);
156        };
157
158        debug!("Overridden config: {:?}", config);
159
160        Ok(config)
161    }
162}
163
164/// This trait allows you to define how application configuration is loaded.
165impl Configurable<PaceConfig> for EntryPoint {
166    /// Location of the configuration file
167    fn config_path(&self) -> Option<PathBuf> {
168        let automatically_determined = get_config_paths(PACE_CONFIG_FILENAME)
169            .into_iter()
170            .filter(|f| f.exists())
171            .collect::<Vec<_>>();
172
173        debug!(
174            "Automatically determined config paths: {:?}",
175            automatically_determined
176        );
177
178        if automatically_determined.len() > 1 {
179            status_warn!("Multiple config files found in standard locations, we will use the first one found: {:?}", automatically_determined);
180        }
181
182        // Get the first path that exists
183        // TODO!: Let the user specify the config file location in case there are multiple existing ones
184        let first_automatically_determined = automatically_determined.first();
185
186        debug!(
187            "First automatically determined config path: {:?}",
188            first_automatically_determined
189        );
190
191        let user_specified = self.config.as_ref().and_then(|f| {
192            if f.exists() {
193                Some(f)
194            } else {
195                // If the parent directory doesn't exist, create it
196                if let Some(parent) = f.parent() {
197                    std::fs::create_dir_all(parent).ok()?;
198                }
199
200                // If the file doesn't exist, create it
201                std::fs::File::create(f).ok()?;
202                Some(f)
203            }
204        });
205
206        // If the user has specified a config file, use that
207        // otherwise, use the first config file found in specified
208        // standard locations
209        let config_path = match (user_specified, first_automatically_determined) {
210            (Some(filename), _) => Some(filename.clone()),
211            (None, Some(first_path)) => Some(first_path.clone()),
212            _ => None,
213        };
214
215        debug!("Using config path: {:?}", config_path);
216
217        config_path
218    }
219
220    /// Apply changes to the config after it's been loaded, e.g. overriding
221    /// values in a config file using command-line options.
222    ///
223    /// This can be safely deleted if you don't want to override config
224    /// settings from command-line options.
225    fn process_config(&self, config: PaceConfig) -> Result<PaceConfig, FrameworkError> {
226        // Override the config file with options from CLI arguments globally
227        let config = self.override_config(config)?;
228
229        // You can also override settings based on the subcommand
230        // match &self.cmd {
231        // PaceCmd::Start(cmd) => cmd.override_config(config),
232        //
233        // If you don't need special overrides for some
234        // subcommands, you can just use a catch all
235        // _ => Ok(config),
236        // }
237
238        Ok(config)
239    }
240}