Skip to main content

kasl/commands/
mod.rs

1//! Command-line interface commands for kasl application.
2//!
3//! Contains all CLI command implementations for task management, activity monitoring,
4//! reporting, and system configuration.
5//!
6//! ## Usage
7//!
8//! ```bash
9//! kasl watch                    # Start activity monitoring
10//! kasl task add --name "Review code" # Create a new task
11//! kasl report                   # Generate today's report
12//! kasl export tasks --format csv # Export tasks to CSV
13//! ```
14
15pub mod autostart;
16pub mod export;
17pub mod inbox;
18pub mod init;
19pub mod migrations;
20pub mod pauses;
21pub mod report;
22pub mod server;
23pub mod sum;
24pub mod tag;
25pub mod task;
26pub mod template;
27pub mod toast_action;
28pub mod update;
29pub mod watch;
30
31use crate::{
32    db::workdays::Workdays,
33    libs::{messages::types::Message, output},
34    msg_error_anyhow, msg_info, msg_warning,
35};
36use anyhow::Result;
37use chrono::Local;
38use clap::{Parser, Subcommand};
39
40/// Defines the main subcommands that the application can execute.
41#[derive(Debug, Subcommand)]
42enum Commands {
43    /// Manage autostart configuration for system boot
44    ///
45    /// Controls whether kasl automatically starts monitoring when the system boots.
46    /// Supports both system-level and user-level autostart on Windows.
47    #[command(about = "Manage autostart on system boot")]
48    Autostart(autostart::AutostartArgs),
49
50    /// Set up application configuration interactively
51    ///
52    /// Guides the user through setting up API credentials, monitor settings,
53    /// and other configuration options required for kasl to function properly.
54    ///
55    /// `init` stays as a deprecated alias until 2.0. An alias rather than a
56    /// hidden variant: clap_complete emits hidden subcommands into the
57    /// completion scripts, so Tab would keep teaching the old spelling.
58    #[command(about = "Set up configuration", alias = "init")]
59    Setup(init::SetupArgs),
60
61    /// Comprehensive task management command
62    ///
63    /// Handles all task-related operations including creation, editing, deletion,
64    /// viewing, and integration with external services like GitLab and Jira.
65    #[command(about = "Create task")]
66    Task(task::TaskArgs),
67
68    /// Manually end the current workday
69    ///
70    /// Records the end timestamp for today's work session. Typically used
71    /// when the automatic monitoring needs to be manually finalized.
72    #[command(about = "Write end timestamp to database")]
73    End,
74
75    /// Display monthly working hours summary
76    ///
77    /// Shows a comprehensive overview of work hours, productivity metrics,
78    /// and daily breakdowns for the current month.
79    #[command(about = "Get summary")]
80    Sum(sum::SumArgs),
81
82    /// Update kasl itself to the latest release
83    ///
84    /// Checks GitHub releases for newer versions and automatically downloads
85    /// and installs updates if available.
86    ///
87    /// `update` stays as a deprecated alias until 2.0; it read as "update my
88    /// data", which is what every other command does.
89    #[command(name = "self-update", about = "Update kasl itself to the latest release", alias = "update")]
90    SelfUpdate,
91
92    /// Generate and optionally submit work reports
93    ///
94    /// Creates detailed daily reports with work intervals, tasks, and productivity
95    /// metrics. Can automatically submit reports to configured APIs.
96    #[command(about = "Prepare a report")]
97    Report(report::ReportArgs),
98
99    /// Export application data to external formats
100    ///
101    /// Supports exporting tasks, reports, and summaries to CSV, JSON, and Excel
102    /// formats for external analysis or backup purposes.
103    #[command(about = "Export data to various formats")]
104    Export(export::ExportArgs),
105
106    /// Manage reusable task templates
107    ///
108    /// Create, edit, and use templates for frequently created tasks to
109    /// streamline task creation workflow.
110    #[command(about = "Manage task templates")]
111    Template(template::TemplateArgs),
112
113    /// Organize tasks with custom tags
114    ///
115    /// Create and manage tags to categorize and filter tasks by project,
116    /// priority, or any custom criteria.
117    #[command(about = "Manage task tags")]
118    Tag(tag::TagArgs),
119
120    /// Background activity monitoring daemon
121    ///
122    /// Monitors user input activity to automatically detect work sessions,
123    /// breaks, and workday boundaries. Can run as a background service.
124    #[command(about = "Watch user activity in the background to record pauses")]
125    Watch(watch::WatchArgs),
126
127    /// View recorded pauses and record ones the monitor missed
128    ///
129    /// Lists detected pauses for a date, and lets the user add an absence the
130    /// activity monitor did not catch or remove one recorded by mistake.
131    #[command(about = "View pauses and record ones the monitor missed")]
132    Pauses(pauses::PausesArgs),
133
134    /// Connection to the team's kasl-server
135    ///
136    /// Connects this machine to a kasl-server with an agent token issued by
137    /// an administrator, shows where the connection stands, and forgets it
138    /// again. The token lives in the OS keyring, never in the config file.
139    #[command(about = "Manage the connection to a kasl-server")]
140    Server(server::ServerArgs),
141
142    /// Jira inbox of assigned open issues
143    ///
144    /// Syncs assigned unresolved Jira issues into a local table, lists them
145    /// by priority, and supports pin / dismiss / open / import into tasks,
146    /// and starting, closing and commenting on issues in Jira.
147    // Boxed: a filter set on every subcommand makes these the largest
148    // arguments by far, and every other variant would carry their size.
149    #[command(about = "Manage Jira inbox issues")]
150    Inbox(Box<inbox::InboxArgs>),
151
152    /// Print a shell completion script
153    ///
154    /// Emits the completion script for the chosen shell on stdout; source it
155    /// from the shell profile to get completion for kasl's commands and flags.
156    #[command(about = "Print a shell completion script (source it from your shell profile)")]
157    Completions {
158        /// Target shell
159        shell: clap_complete::Shell,
160    },
161
162    /// Carry one toast button press to the watcher (internal)
163    ///
164    /// Launched by the shortcut behind a toast's Take / Snooze / Dismiss
165    /// button, because a toast button cannot pass an argument of its own.
166    /// It posts the decision to the watcher and exits without printing.
167    /// Hidden from help and completion: the commands a person types for this
168    /// are `kasl inbox take`, `snooze` and `dismiss`, which say what they did.
169    #[command(hide = true, about = "Carry a toast button press to the watcher")]
170    ToastAction(toast_action::ToastActionArgs),
171
172    /// Database migration management utilities (debug builds only)
173    ///
174    /// Provides tools for database schema management, migration history,
175    /// and rollback operations. Available only in debug builds for safety.
176    #[cfg(debug_assertions)]
177    #[command(about = "Database migration management")]
178    Migrations(migrations::MigrationsArgs),
179}
180
181/// The main CLI structure that parses command-line arguments.
182///
183/// Uses `clap` to define the application's interface and delegates
184/// command execution to the appropriate subcommand module. The CLI
185/// requires at least one subcommand to be specified.
186///
187/// # Examples
188///
189/// ```bash
190/// # Display help
191/// kasl --help
192///
193/// # Run a specific command
194/// kasl task add --name "New task"
195///
196/// # The inbox as JSON, and today's report for a log
197/// kasl inbox list --json
198/// kasl report --plain
199/// ```
200#[derive(Debug, Parser)]
201#[command(name = "kasl", author, version, about, long_about = None)]
202#[command(arg_required_else_help(true))]
203pub struct Cli {
204    #[command(subcommand)]
205    command: Commands,
206
207    /// Plain text for logs and pipes: no colour, no emoji, tables as tab-separated lines
208    #[arg(long, global = true)]
209    plain: bool,
210
211    /// Whether the command asked for `--json`, read off the parsed arguments.
212    #[arg(skip)]
213    json: bool,
214}
215
216impl Cli {
217    /// Parses `args` (the program name first), exiting with clap's message on a usage error.
218    ///
219    /// `--json` is flattened into each command that prints data (see
220    /// [`crate::libs::output::Format`]), so whether this run asked for it is
221    /// read off the parsed arguments wherever in the subcommand chain it sits,
222    /// rather than out of a match over every command that has it.
223    pub fn from_args(args: &[String]) -> Cli {
224        use clap::{CommandFactory, FromArgMatches};
225        let matches = Self::command().color(output::clap_colour(args)).get_matches_from(args);
226        let mut cli = Self::from_arg_matches(&matches).unwrap_or_else(|error| error.exit());
227        cli.json = json_requested(&matches);
228        cli
229    }
230
231    /// Whether this run prints a JSON document.
232    pub fn json(&self) -> bool {
233        self.json
234    }
235
236    /// Whether this run asked for plain text.
237    pub fn plain(&self) -> bool {
238        self.plain
239    }
240
241    /// Executes the parsed command.
242    ///
243    /// # Examples
244    ///
245    /// ```rust,no_run
246    /// use kasl::commands::Cli;
247    ///
248    /// #[tokio::main]
249    /// async fn main() -> anyhow::Result<()> {
250    ///     let args: Vec<String> = std::env::args().collect();
251    ///     Cli::from_args(&args).dispatch().await
252    /// }
253    /// ```
254    pub async fn dispatch(self) -> Result<()> {
255        let cli = self;
256        match cli.command {
257            Commands::Autostart(args) => autostart::cmd(args),
258            Commands::Setup(args) => {
259                warn_if_deprecated_alias();
260                init::cmd(args)
261            }
262            Commands::Task(args) => task::cmd(args).await,
263            Commands::End => {
264                // Manually end the current workday. A day that was never
265                // opened is a refusal, not a success: saying "ended" over an
266                // empty database taught users to trust a stamp that is not
267                // there.
268                let today = Local::now().date_naive();
269                if !Workdays::new()?.insert_end(today)? {
270                    return Err(msg_error_anyhow!(Message::WorkdayNeverStarted(today.to_string())));
271                }
272                msg_info!(Message::WorkdayEnded);
273                Ok(())
274            }
275            Commands::Sum(args) => sum::cmd(args).await,
276            Commands::Report(args) => report::cmd(args).await,
277            Commands::Export(args) => export::cmd(args).await,
278            Commands::Template(args) => template::cmd(args),
279            Commands::Tag(args) => tag::cmd(args).await,
280            Commands::SelfUpdate => {
281                warn_if_deprecated_alias();
282                update::cmd().await
283            }
284            Commands::Watch(args) => watch::cmd(args).await,
285            Commands::Pauses(args) => pauses::cmd(args).await,
286            Commands::Completions { shell } => {
287                use clap::CommandFactory;
288                use std::io::Write;
289                // Generated whole, then written: clap_complete panics on a
290                // failed write in its own words, which the closed-pipe hook
291                // does not know, so `kasl completions bash | head` would end
292                // in a panic instead of quietly.
293                let mut script = Vec::new();
294                clap_complete::generate(shell, &mut Self::command(), "kasl", &mut script);
295                match std::io::stdout().write_all(&script) {
296                    Err(error) if output::is_a_closed_pipe_error(&error) => Ok(()),
297                    written => Ok(written?),
298                }
299            }
300            Commands::Server(args) => server::cmd(args).await,
301            Commands::Inbox(args) => inbox::cmd(*args).await,
302            Commands::ToastAction(args) => toast_action::cmd(args).await,
303
304            // Database migrations only available in debug builds
305            #[cfg(debug_assertions)]
306            Commands::Migrations(args) => migrations::cmd(args),
307        }
308    }
309}
310
311/// Whether `--json` was given to the command at the end of the chain or on the way to it.
312fn json_requested(matches: &clap::ArgMatches) -> bool {
313    let here = matches!(matches.try_get_one::<bool>("json"), Ok(Some(true)));
314    here || matches.subcommand().is_some_and(|(_, sub)| json_requested(sub))
315}
316
317/// Old command names still accepted as aliases, with their replacements.
318///
319/// Removed in 2.0; until then the alias works and says so.
320const DEPRECATED_ALIASES: [(&str, &str); 2] = [("init", "setup"), ("update", "self-update")];
321
322/// Prints a rename notice when the command was invoked by its old name.
323///
324/// clap resolves an alias to the canonical variant without recording which
325/// spelling was typed, so the first non-flag argument is what tells them
326/// apart.
327fn warn_if_deprecated_alias() {
328    let Some(typed) = std::env::args().nth(1) else {
329        return;
330    };
331    if let Some((old, new)) = DEPRECATED_ALIASES.iter().find(|(old, _)| *old == typed) {
332        msg_warning!(Message::DeprecatedCommand(old, new));
333    }
334}