kasl/lib.rs
1//! # Kasl - Key Activity Synchronization and Logging
2//!
3//! A command-line utility for tracking work activities, managing tasks,
4//! and generating productivity reports.
5//!
6//! ## Usage
7//!
8//! ```rust,no_run
9//! fn main() -> std::process::ExitCode {
10//! kasl::run()
11//! }
12//! ```
13
14pub mod api;
15pub mod commands;
16pub mod db;
17pub mod libs;
18
19/// Runs the CLI: the shared entry point behind both the `kasl` and `ka` binaries.
20///
21/// Parses the command line before anything else happens, because how the run
22/// prints - for a person, `--plain`, `--json` - decides what every later line
23/// looks like and where it goes, the debug log included. Then handles the
24/// internal `--daemon-run` mode used when the watcher spawns itself, and
25/// otherwise dispatches the parsed command.
26///
27/// The error a command fails with is printed here, marked for the run's style
28/// (`❌` for a person, `error:` for `--plain` and `--json`), and turns into
29/// exit code 1.
30pub fn run() -> std::process::ExitCode {
31 libs::output::quiet_on_a_closed_pipe();
32
33 // Clear the binary a previous update left behind; it is only deletable
34 // once it is no longer the running image, which is now.
35 libs::update::Updater::sweep_backup();
36
37 // Intercepted before clap: this flag is how the background monitoring
38 // process is launched, not part of the public command surface.
39 let args: Vec<String> = std::env::args().collect();
40 let daemon = args.get(1).is_some_and(|arg| arg == "--daemon-run");
41 let toast_action = args.get(1).is_some_and(|arg| arg == "toast-action");
42 let cli = (!daemon).then(|| commands::Cli::from_args(&args));
43 let style = libs::output::init(cli.as_ref().is_some_and(|cli| cli.json()), cli.as_ref().is_some_and(|cli| cli.plain()));
44
45 // Tracing only in debug mode; otherwise log output would clutter normal
46 // CLI usage. On stderr, so a debug run of `--json` still prints a
47 // document stdout can parse.
48 if libs::messages::macros::is_debug_mode() {
49 tracing_subscriber::fmt()
50 .with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "kasl=debug".into()))
51 .with_writer(std::io::stderr)
52 .with_ansi(style.colour_stderr)
53 .init();
54 }
55
56 let outcome = tokio::runtime::Runtime::new().map_err(anyhow::Error::from).and_then(|runtime| {
57 runtime.block_on(async {
58 let Some(cli) = cli else {
59 return commands::watch::run_as_daemon().await;
60 };
61 // The update notice is for a person at a terminal. A script would
62 // wait on GitHub before its answer, a pipe would carry the notice
63 // as data, and a cron job would spend the day's one check where
64 // nobody reads it. The courier behind a toast button skips it
65 // too: it has no terminal, and must not delay the press.
66 if !toast_action && !style.json && std::io::IsTerminal::is_terminal(&std::io::stdout()) {
67 libs::update::Updater::show_update_notification().await;
68 }
69 cli.dispatch().await
70 })
71 });
72
73 match outcome {
74 Ok(()) => std::process::ExitCode::SUCCESS,
75 Err(error) => {
76 libs::output::report_error(&error);
77 std::process::ExitCode::FAILURE
78 }
79 }
80}