Skip to main content

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}