From b7d6a6c139a57053ebb38aa5c7e3ef3d176559fd Mon Sep 17 00:00:00 2001 From: "Jens K." <47693+sectore@users.noreply.github.com> Date: Sat, 14 Dec 2024 18:24:51 +0100 Subject: [PATCH] `Args` (#14) --- Cargo.lock | 117 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + README.md | 60 +++++++++++---------- src/app.rs | 20 ++++--- src/args.rs | 95 ++++++++++++++++++++++++++++++++ src/main.rs | 7 ++- src/widgets/pomodoro.rs | 22 ++++---- 7 files changed, 268 insertions(+), 54 deletions(-) create mode 100644 src/args.rs diff --git a/Cargo.lock b/Cargo.lock index c9b0549..7e1b1a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,55 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -98,6 +147,46 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "color-eyre" version = "0.6.3" @@ -125,6 +214,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "compact_str" version = "0.8.0" @@ -433,6 +528,12 @@ dependencies = [ "syn", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.13.0" @@ -941,6 +1042,7 @@ dependencies = [ name = "timr" version = "0.1.0" dependencies = [ + "clap", "color-eyre", "crossterm", "directories", @@ -1114,6 +1216,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.0" @@ -1166,6 +1274,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 0aa3478..a5fa56d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ tokio-util = "0.7.12" tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } directories = "5.0.1" +clap = { version = "4.5.23", features = ["derive"] } diff --git a/README.md b/README.md index 5c34745..361f7d4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # tim:r -**Pronounced `/ˈtʌɪmə/` or `/ˈtaɪmər/`.** Other just say `timer`. +**Pronounced `/ˈtʌɪmə/` or `/ˈtaɪmər/`.** > [!WARNING] -> _Everything is still WIP_ 😎 +> _Still WIP_ # About @@ -21,47 +21,37 @@ It's built with [`ratatui`](https://ratatui.rs/) ([Rust](https://www.rust-lang.o _soon_ -# Installation +# Args -## Build from source +```sh +Usage: timr [OPTIONS] -### Requirements +Options: + -c, --countdown Countdown time to start from. Format: 'ss', 'mm:ss', or 'hh:mm:ss' [default: 10:00] + -w, --work Work time to count down from. Format: 'ss', 'mm:ss', or 'hh:mm:ss' [default: 25:00] + -p, --pause Pause time to count down from. Format: 'ss', 'mm:ss', or 'hh:mm:ss' [default: 5:00] + -h, --help Print help +``` -#### Nix (recommend) +# Build from source + +## Requirements + +### Nix (recommend) `cd` into root directory. -If `direnv` is installed, run `direnv allow` once to install dependencies. Others run `nix develop`. +[`direnv`](https://direnv.net) users run `direnv allow` once to install dependencies. Others run `nix develop`. -#### Non Nix user +### Non Nix user - [`Rust`](https://www.rust-lang.org/learn/get-started) - [`Clippy`](https://github.com/rust-lang/rust-clippy) - [`rustfmt`](https://github.com/rust-lang/rustfmt) - [`just`](https://just.systems) - -#### Run - -```sh -cargo run -``` - - -#### Build - -- Linux -```sh -nix build -``` - -- Windows (cross-compilation) -```sh -nix build .#windows -``` - -#### Commands to `run`, `build` etc. +### Commands to `run`, `lint`, `format` etc. ```sh just --list @@ -80,6 +70,18 @@ Available recipes: t # alias for `test` ``` +### Build + +- Linux +```sh +nix build +``` + +- Windows (cross-compilation) +```sh +nix build .#windows +``` + # Misc. ## Logs diff --git a/src/app.rs b/src/app.rs index 01f9e95..6a1f55e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,5 @@ use crate::{ + args::Args, constants::TICK_VALUE_MS, events::{Event, EventHandler, Events}, terminal::Terminal, @@ -7,7 +8,7 @@ use crate::{ countdown::{Countdown, CountdownWidget}, footer::Footer, header::Header, - pomodoro::{Pomodoro, PomodoroWidget}, + pomodoro::{Pomodoro, PomodoroArgs, PomodoroWidget}, timer::{Timer, TimerWidget}, }, }; @@ -44,29 +45,26 @@ pub struct App { pomodoro: Pomodoro, } -impl Default for App { - fn default() -> Self { +impl App { + pub fn new(args: Args) -> Self { Self { mode: Mode::Running, content: Content::Countdown, show_menu: false, countdown: Countdown::new(Clock::::new( - Duration::from_secs(10 * 60 /* 10min */), + args.countdown, Duration::from_millis(TICK_VALUE_MS), )), timer: Timer::new(Clock::::new( Duration::ZERO, Duration::from_millis(TICK_VALUE_MS), )), - pomodoro: Pomodoro::new(), + pomodoro: Pomodoro::new(PomodoroArgs { + work: args.work, + pause: args.pause, + }), } } -} - -impl App { - pub fn new() -> Self { - Self::default() - } pub async fn run(&mut self, mut terminal: Terminal, mut events: Events) -> Result<()> { while self.is_running() { diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..336dcbf --- /dev/null +++ b/src/args.rs @@ -0,0 +1,95 @@ +use clap::Parser; +use color_eyre::{ + eyre::{ensure, eyre}, + Report, +}; +use std::time::Duration; + +#[derive(Parser)] +pub struct Args { + #[arg(long, short, value_parser = parse_duration, + default_value="10:00" /* 10min */, + help = "Countdown time to start from. Format: 'ss', 'mm:ss', or 'hh:mm:ss'" + )] + pub countdown: Duration, + #[arg(long, short, value_parser = parse_duration, + default_value="25:00" /* 25min */, + help = "Work time to count down from. Format: 'ss', 'mm:ss', or 'hh:mm:ss'" + )] + pub work: Duration, + #[arg(long, short, value_parser = parse_duration, + default_value="5:00" /* 5min */, + help = "Pause time to count down from. Format: 'ss', 'mm:ss', or 'hh:mm:ss'" + )] + pub pause: Duration, +} + +fn parse_duration(arg: &str) -> Result { + let parts: Vec<&str> = arg.split(':').rev().collect(); + + let parse_seconds = |s: &str| -> Result { + let secs = s.parse::().map_err(|_| eyre!("Invalid seconds"))?; + ensure!(secs < 60, "Seconds must be less than 60."); + Ok(secs) + }; + + let parse_minutes = |m: &str| -> Result { + let mins = m.parse::().map_err(|_| eyre!("Invalid minutes"))?; + ensure!(mins < 60, "Minutes must be less than 60."); + Ok(mins) + }; + + let parse_hours = |h: &str| -> Result { + let hours = h.parse::().map_err(|_| eyre!("Invalid hours"))?; + ensure!(hours < 100, "Hours must be less than 100."); + Ok(hours) + }; + + let seconds = match parts.as_slice() { + [ss] => parse_seconds(ss)?, + [ss, mm] => { + let s = parse_seconds(ss)?; + let m = parse_minutes(mm)?; + m * 60 + s + } + [ss, mm, hh] => { + let s = parse_seconds(ss)?; + let m = parse_minutes(mm)?; + let h = parse_hours(hh)?; + h * 60 * 60 + m * 60 + s + } + _ => return Err(eyre!("Invalid time format. Use 'ss', mm:ss, or hh:mm:ss")), + }; + + Ok(Duration::from_secs(seconds)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_duration() { + // ss + assert_eq!(parse_duration("50").unwrap(), Duration::from_secs(50)); + + // mm:ss + assert_eq!( + parse_duration("01:30").unwrap(), + Duration::from_secs(60 + 30) + ); + + // hh:mm:ss + assert_eq!( + parse_duration("01:30:00").unwrap(), + Duration::from_secs(60 * 60 + 30 * 60) + ); + + // errors + assert!(parse_duration("1:60").is_err()); // invalid seconds + assert!(parse_duration("60:00").is_err()); // invalid minutes + assert!(parse_duration("100:00:00").is_err()); // invalid hours + assert!(parse_duration("abc").is_err()); // invalid input + assert!(parse_duration("01:02:03:04").is_err()); // too many parts + } +} diff --git a/src/main.rs b/src/main.rs index e683074..e178fc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,11 +5,14 @@ mod events; #[cfg(debug_assertions)] mod logging; +mod args; mod terminal; mod utils; mod widgets; use app::App; +use args::Args; +use clap::Parser; use color_eyre::Result; #[tokio::main] @@ -20,9 +23,11 @@ async fn main() -> Result<()> { color_eyre::install()?; + let args = Args::parse(); + let terminal = terminal::setup()?; let events = events::Events::new(); - App::new().run(terminal, events).await?; + App::new(args).run(terminal, events).await?; terminal::teardown()?; Ok(()) diff --git a/src/widgets/pomodoro.rs b/src/widgets/pomodoro.rs index 346a52f..d034f5e 100644 --- a/src/widgets/pomodoro.rs +++ b/src/widgets/pomodoro.rs @@ -15,9 +15,6 @@ use std::{cmp::max, time::Duration}; use strum::Display; -static PAUSE_MS: u64 = 5 * 60 * 1000; /* 5min in milliseconds */ -static WORK_MS: u64 = 25 * 60 * 1000; /* 25min in milliseconds */ - #[derive(Debug, Clone, Display, Hash, Eq, PartialEq)] enum Mode { Work, @@ -45,19 +42,18 @@ pub struct Pomodoro { clock_map: ClockMap, } +pub struct PomodoroArgs { + pub work: Duration, + pub pause: Duration, +} + impl Pomodoro { - pub fn new() -> Self { + pub fn new(args: PomodoroArgs) -> Self { Self { mode: Mode::Work, clock_map: ClockMap { - work: Clock::::new( - Duration::from_millis(WORK_MS), - Duration::from_millis(TICK_VALUE_MS), - ), - pause: Clock::::new( - Duration::from_millis(PAUSE_MS), - Duration::from_millis(TICK_VALUE_MS), - ), + work: Clock::::new(args.work, Duration::from_millis(TICK_VALUE_MS)), + pause: Clock::::new(args.pause, Duration::from_millis(TICK_VALUE_MS)), }, } } @@ -91,7 +87,7 @@ impl EventHandler for Pomodoro { KeyCode::Left if edit_mode => { self.get_clock().edit_next(); } - KeyCode::Left if edit_mode => { + KeyCode::Left => { // `next` is acting as same as a `prev` function, we don't have self.next(); }