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::{db::workdays::Workdays, libs::messages::types::Message, msg_error_anyhow, msg_info, msg_warning};
32use anyhow::Result;
33use chrono::Local;
34use clap::{Parser, Subcommand};
35
36/// Defines the main subcommands that the application can execute.
37#[derive(Debug, Subcommand)]
38enum Commands {
39 /// Manage autostart configuration for system boot
40 ///
41 /// Controls whether kasl automatically starts monitoring when the system boots.
42 /// Supports both system-level and user-level autostart on Windows.
43 #[command(about = "Manage autostart on system boot")]
44 Autostart(autostart::AutostartArgs),
45
46 /// Set up application configuration interactively
47 ///
48 /// Guides the user through setting up API credentials, monitor settings,
49 /// and other configuration options required for kasl to function properly.
50 ///
51 /// `init` stays as a deprecated alias until 2.0. An alias rather than a
52 /// hidden variant: clap_complete emits hidden subcommands into the
53 /// completion scripts, so Tab would keep teaching the old spelling.
54 #[command(about = "Set up configuration", alias = "init")]
55 Setup(init::SetupArgs),
56
57 /// Comprehensive task management command
58 ///
59 /// Handles all task-related operations including creation, editing, deletion,
60 /// viewing, and integration with external services like GitLab and Jira.
61 #[command(about = "Create task")]
62 Task(task::TaskArgs),
63
64 /// Manually end the current workday
65 ///
66 /// Records the end timestamp for today's work session. Typically used
67 /// when the automatic monitoring needs to be manually finalized.
68 #[command(about = "Write end timestamp to database")]
69 End,
70
71 /// Display monthly working hours summary
72 ///
73 /// Shows a comprehensive overview of work hours, productivity metrics,
74 /// and daily breakdowns for the current month.
75 #[command(about = "Get summary")]
76 Sum(sum::SumArgs),
77
78 /// Update kasl itself to the latest release
79 ///
80 /// Checks GitHub releases for newer versions and automatically downloads
81 /// and installs updates if available.
82 ///
83 /// `update` stays as a deprecated alias until 2.0; it read as "update my
84 /// data", which is what every other command does.
85 #[command(name = "self-update", about = "Update kasl itself to the latest release", alias = "update")]
86 SelfUpdate,
87
88 /// Generate and optionally submit work reports
89 ///
90 /// Creates detailed daily reports with work intervals, tasks, and productivity
91 /// metrics. Can automatically submit reports to configured APIs.
92 #[command(about = "Prepare a report")]
93 Report(report::ReportArgs),
94
95 /// Export application data to external formats
96 ///
97 /// Supports exporting tasks, reports, and summaries to CSV, JSON, and Excel
98 /// formats for external analysis or backup purposes.
99 #[command(about = "Export data to various formats")]
100 Export(export::ExportArgs),
101
102 /// Manage reusable task templates
103 ///
104 /// Create, edit, and use templates for frequently created tasks to
105 /// streamline task creation workflow.
106 #[command(about = "Manage task templates")]
107 Template(template::TemplateArgs),
108
109 /// Organize tasks with custom tags
110 ///
111 /// Create and manage tags to categorize and filter tasks by project,
112 /// priority, or any custom criteria.
113 #[command(about = "Manage task tags")]
114 Tag(tag::TagArgs),
115
116 /// Background activity monitoring daemon
117 ///
118 /// Monitors user input activity to automatically detect work sessions,
119 /// breaks, and workday boundaries. Can run as a background service.
120 #[command(about = "Watch user activity in the background to record pauses")]
121 Watch(watch::WatchArgs),
122
123 /// View recorded pauses and record ones the monitor missed
124 ///
125 /// Lists detected pauses for a date, and lets the user add an absence the
126 /// activity monitor did not catch or remove one recorded by mistake.
127 #[command(about = "View pauses and record ones the monitor missed")]
128 Pauses(pauses::PausesArgs),
129
130 /// Connection to the team's kasl-server
131 ///
132 /// Connects this machine to a kasl-server with an agent token issued by
133 /// an administrator, shows where the connection stands, and forgets it
134 /// again. The token lives in the OS keyring, never in the config file.
135 #[command(about = "Manage the connection to a kasl-server")]
136 Server(server::ServerArgs),
137
138 /// Jira inbox of assigned open issues
139 ///
140 /// Syncs assigned unresolved Jira issues into a local table, lists them
141 /// by priority, and supports pin / dismiss / open / import into tasks.
142 #[command(about = "Manage Jira inbox issues")]
143 Inbox(inbox::InboxArgs),
144
145 /// Print a shell completion script
146 ///
147 /// Emits the completion script for the chosen shell on stdout; source it
148 /// from the shell profile to get completion for kasl's commands and flags.
149 #[command(about = "Print a shell completion script (source it from your shell profile)")]
150 Completions {
151 /// Target shell
152 shell: clap_complete::Shell,
153 },
154
155 /// Carry one toast button press to the watcher (internal)
156 ///
157 /// Launched by the shortcut behind a toast's Take / Snooze / Dismiss
158 /// button, because a toast button cannot pass an argument of its own.
159 /// It posts the decision to the watcher and exits without printing.
160 /// Hidden from help and completion: the commands a person types for this
161 /// are `kasl inbox take`, `snooze` and `dismiss`, which say what they did.
162 #[command(hide = true, about = "Carry a toast button press to the watcher")]
163 ToastAction(toast_action::ToastActionArgs),
164
165 /// Database migration management utilities (debug builds only)
166 ///
167 /// Provides tools for database schema management, migration history,
168 /// and rollback operations. Available only in debug builds for safety.
169 #[cfg(debug_assertions)]
170 #[command(about = "Database migration management")]
171 Migrations(migrations::MigrationsArgs),
172}
173
174/// The main CLI structure that parses command-line arguments.
175///
176/// Uses `clap` to define the application's interface and delegates
177/// command execution to the appropriate subcommand module. The CLI
178/// requires at least one subcommand to be specified.
179///
180/// # Examples
181///
182/// ```bash
183/// # Display help
184/// kasl --help
185///
186/// # Run a specific command
187/// kasl task add --name "New task"
188/// ```
189#[derive(Debug, Parser)]
190#[command(name = "kasl", author, version, about, long_about = None)]
191#[command(arg_required_else_help(true))]
192pub struct Cli {
193 #[command(subcommand)]
194 command: Commands,
195}
196
197impl Cli {
198 /// Parses command-line arguments and executes the corresponding command.
199 ///
200 /// This is the main entry point for the CLI logic. It handles command
201 /// routing and provides centralized error handling for all commands.
202 ///
203 /// # Examples
204 ///
205 /// ```rust,no_run
206 /// use kasl::commands::Cli;
207 ///
208 /// #[tokio::main]
209 /// async fn main() -> anyhow::Result<()> {
210 /// Cli::menu().await
211 /// }
212 /// ```
213 pub async fn menu() -> Result<()> {
214 let cli = Self::parse();
215
216 match cli.command {
217 Commands::Autostart(args) => autostart::cmd(args),
218 Commands::Setup(args) => {
219 warn_if_deprecated_alias();
220 init::cmd(args)
221 }
222 Commands::Task(args) => task::cmd(args).await,
223 Commands::End => {
224 // Manually end the current workday. A day that was never
225 // opened is a refusal, not a success: saying "ended" over an
226 // empty database taught users to trust a stamp that is not
227 // there.
228 let today = Local::now().date_naive();
229 if !Workdays::new()?.insert_end(today)? {
230 return Err(msg_error_anyhow!(Message::WorkdayNeverStarted(today.to_string())));
231 }
232 msg_info!(Message::WorkdayEnded);
233 Ok(())
234 }
235 Commands::Sum(args) => sum::cmd(args).await,
236 Commands::Report(args) => report::cmd(args).await,
237 Commands::Export(args) => export::cmd(args).await,
238 Commands::Template(args) => template::cmd(args),
239 Commands::Tag(args) => tag::cmd(args).await,
240 Commands::SelfUpdate => {
241 warn_if_deprecated_alias();
242 update::cmd().await
243 }
244 Commands::Watch(args) => watch::cmd(args).await,
245 Commands::Pauses(args) => pauses::cmd(args).await,
246 Commands::Completions { shell } => {
247 use clap::CommandFactory;
248 clap_complete::generate(shell, &mut Self::command(), "kasl", &mut std::io::stdout());
249 Ok(())
250 }
251 Commands::Server(args) => server::cmd(args).await,
252 Commands::Inbox(args) => inbox::cmd(args).await,
253 Commands::ToastAction(args) => toast_action::cmd(args),
254
255 // Database migrations only available in debug builds
256 #[cfg(debug_assertions)]
257 Commands::Migrations(args) => migrations::cmd(args),
258 }
259 }
260}
261
262/// Old command names still accepted as aliases, with their replacements.
263///
264/// Removed in 2.0; until then the alias works and says so.
265const DEPRECATED_ALIASES: [(&str, &str); 2] = [("init", "setup"), ("update", "self-update")];
266
267/// Prints a rename notice when the command was invoked by its old name.
268///
269/// clap resolves an alias to the canonical variant without recording which
270/// spelling was typed, so the first non-flag argument is what tells them
271/// apart.
272fn warn_if_deprecated_alias() {
273 let Some(typed) = std::env::args().nth(1) else {
274 return;
275 };
276 if let Some((old, new)) = DEPRECATED_ALIASES.iter().find(|(old, _)| *old == typed) {
277 msg_warning!(Message::DeprecatedCommand(old, new));
278 }
279}