From b6df8af5417f465fc152035ea3e50b8528604800 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Mon, 21 Oct 2024 15:11:38 +0900 Subject: [PATCH 01/10] Add an option for how many hours you intend to work --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27cf410..0250120 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,7 +360,7 @@ dependencies = [ [[package]] name = "time-track" -version = "1.0.0" +version = "1.1.0" dependencies = [ "anyhow", "atty", diff --git a/Cargo.toml b/Cargo.toml index f633b2d..bb70b58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "time-track" -version = "1.0.0" +version = "1.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/main.rs b/src/main.rs index 5ee5ba2..543711a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,9 @@ struct Args { /// Specify how the program should behave #[clap(value_enum, default_value_t = Modes::default())] mode: Modes, + + #[arg(long, default_value_t = 8)] + hours: i64, } fn epoch() -> NaiveTime { @@ -176,7 +179,7 @@ fn main() { println!("-----------------"); println!("You have been working for {}", show_time(hours, minutes)); - println!("{}", get_charaterized_time_remaining(total_minutes, 8 * 60)) + println!("{}", get_charaterized_time_remaining(total_minutes, args.hours * 60)) }, Err(err) => eprintln!("{}\nExiting...", err), } From a11d5c0f5ded70d8f274c0a0638eb6ebb7ec3efb Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Thu, 7 Nov 2024 15:48:37 +0900 Subject: [PATCH 02/10] Massively rework the time tracker, mainly to support times across midnight --- Cargo.lock | 70 +++++++++++------ Cargo.toml | 4 +- src/args.rs | 14 ++++ src/main.rs | 210 +++++++++------------------------------------------ src/modes.rs | 36 --------- src/time.rs | 72 ++++++++++++++++++ 6 files changed, 170 insertions(+), 236 deletions(-) create mode 100644 src/args.rs delete mode 100644 src/modes.rs create mode 100644 src/time.rs diff --git a/Cargo.lock b/Cargo.lock index 0250120..35bd882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -72,17 +81,6 @@ version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -208,15 +206,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.2" @@ -252,7 +241,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi", "rustix", "windows-sys", ] @@ -284,6 +273,12 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "num-traits" version = "0.2.15" @@ -317,6 +312,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustix" version = "0.38.3" @@ -360,12 +384,12 @@ dependencies = [ [[package]] name = "time-track" -version = "1.1.0" +version = "2.0.0" dependencies = [ "anyhow", - "atty", "chrono", "clap", + "regex", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bb70b58..5348706 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "time-track" -version = "1.1.0" +version = "2.0.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.72" -atty = "0.2.14" chrono = "0.4.26" clap = { version = "4.3.11", features = ["derive"] } +regex = "1.11.1" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..abee82c --- /dev/null +++ b/src/args.rs @@ -0,0 +1,14 @@ +use clap::Parser; + +/// A simple program to track time spans and calculate remaining hours to work +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + /// How many hours you intend to work (sums with `minutes`) + #[arg(long, default_value_t = 8)] + pub hours: i64, + + /// How many minutes you intend to work (sums with `hours`) + #[arg(long, default_value_t = 0)] + pub minutes: i64, +} diff --git a/src/main.rs b/src/main.rs index 543711a..7541cd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,186 +1,46 @@ -use std::io::{stdin, BufRead}; +use std::io::{self, BufRead}; +use chrono::{DateTime, Local}; -use chrono::{NaiveTime, Duration, Local, NaiveDateTime}; use clap::Parser; -use atty::Stream; use anyhow::{anyhow, Result}; +mod args; +mod time; -mod modes; -use modes::Modes; +use args::Args; -/// A simple program to track time spans -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// Specify how the program should behave - #[clap(value_enum, default_value_t = Modes::default())] - mode: Modes, - - #[arg(long, default_value_t = 8)] - hours: i64, -} - -fn epoch() -> NaiveTime { - NaiveTime::from_hms_opt(0, 0, 0).expect("0, 0, 0 to be valid arguments to from_hms_opt") -} - -/// Given an arbitrary string, attempt to parse it as a naive time in the format HH:mm -fn parse_time(time_str: &str) -> Result { - let time = NaiveTime::parse_from_str(time_str, "%H:%M")?; - let naive_date = NaiveDateTime::from_timestamp_millis( - time.signed_duration_since(epoch()).num_milliseconds(), - ).ok_or(anyhow!("Should be able to make a datetime"))?; - Ok(naive_date) -} - -fn adjust_last(first: &NaiveTime, second: NaiveTime) -> NaiveTime { - // The following logic attempts to handle wrapping from AM -> PM and also from PM -> - // AM but cannot handle multiple days in a single span - if (second < *first) && first.signed_duration_since(epoch()).num_hours() < 12 { - // Assuming wrapped to PM - // first = 8:00 - // second: 1:00 - second + Duration::hours(12) - } else if second < *first { - // Wrapped to a new day because first was after noon. - // first = 14:40 - // second = 1:30 - second + Duration::hours(24) - } else { - second - } -} - -/// The default way of calculating time. Time values are given one per line and subsequent pairs of -/// lines are considered time spans. If an odd number of spans is given, then the final time value -/// is ignored. -fn all_at_once(is_terminal: bool) -> Result> { - if is_terminal { - println!("Enter simple times one per line. Send an EOF character to sum all time spans"); - } - - let mut durations = vec![]; - let mut seen: Option = None; - for line in stdin().lock().lines() { - let cleaned = line?.trim().to_string(); - if let Some(previous) = &seen { - let first = parse_time(previous)?; - let second = adjust_last(&first.time(), parse_time(&cleaned)?.time()); - - durations.push(second - first.time()); - seen = None; - } else { - seen = Some(cleaned.to_string()); - } - } - if let Some(unpaired) = seen { - let now = Local::now().naive_local().time(); - let last = adjust_last(&now, parse_time(&unpaired)?.time()); - println!("Ended with an open span from {unpaired}... assuming now: {}", now.format("%H:%M")); - durations.push(now - last); - } - return Ok(durations); -} - -/// A "live" time tracker. Each time the user presses enter a new span is started or an existing -/// span is closed. The user may end the tracking at any time by sending an EOF command. -fn live_spans() -> Result> { - println!("Tracking Spans Live. Press ENTER to start a span\n"); - let mut durations = vec![]; - let mut seen: Option = None; - for _line in stdin().lock().lines() { - let mut now = Local::now().naive_local().time(); - if let Some(previous) = &seen { - now = adjust_last(previous, now); - let prev_time = previous.format("%H:%M"); - let now_time = now.format("%H:%M"); - println!("Closing span from {prev_time} - {now_time}"); - println!("Status: Away"); - durations.push(now - *previous); - seen = None; - } else { - let now_time = now.format("%H:%M"); - println!("Starting span at {now_time}"); - println!("Status: Working"); - seen = Some(now); - } - } - if let Some(unpaired) = seen { - println!("Closing unpaired span now"); - let mut now = Local::now().naive_local().time(); - now = adjust_last(&unpaired, now); - durations.push(now - unpaired); - } - return Ok(durations); -} - -fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) { - let minutes = total_minutes % 60; - let hours = total_minutes / 60; - (hours, minutes) -} - -fn show_time(hours: i64, minutes: i64) -> String { - let pluralized_hours = match hours { - 1 => "1 hour".to_string(), - _ => format!("{hours} hours"), - }; - let pluralized_minutes = match minutes { - 1 => "1 minute".to_string(), - _ => format!("{minutes} minutes"), - }; - - if hours == 0 { - return pluralized_minutes.to_string(); - } - - if minutes == 0 { - return pluralized_hours.to_string(); - } - - return format!("{pluralized_hours} and {pluralized_minutes}"); -} - -fn get_charaterized_time_remaining(total_minutes: i64, target_minutes: i64) -> String { - if total_minutes == target_minutes { - return "Exactly done".to_string(); - } - - if total_minutes > target_minutes { - let diff = total_minutes - target_minutes; - let (hours, minutes) = to_hrs_minutes(diff); - return format!("You have overworked {}", show_time(hours, minutes)) - } else { - let diff = target_minutes - total_minutes; - let (hours, minutes) = to_hrs_minutes(diff); - let end_at = (Local::now() + Duration::minutes(diff)).time(); - let end_str = end_at.format("%-I:%M %p"); - return format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str) - } -} - -fn main() { +fn main() -> Result<()> { let args = Args::parse(); - let is_terminal = atty::is(Stream::Stdin); - - if !is_terminal && !args.mode.supports_piped_input() { - panic!("Cannot run {0} mode on piped input", args.mode); + println!("{:?}", args); + let stdin = io::stdin(); + let mut lines: Vec = vec![]; + for line in stdin.lock().lines() { + lines.push(line.expect("Issues when reading from stdin")); } - let maybe_durations = match args.mode { - Modes::TimeTable => all_at_once(is_terminal), - Modes::Live => live_spans(), - }; + let midnight = Local::now().date().and_hms_opt(0, 0, 0).ok_or(anyhow!("Expected midnight to exist"))?; + let times = time::from_stream(&midnight.date(), lines.iter())?; - match maybe_durations { - Ok(durations) => { - let total_minutes: i64 = durations.iter().map(|d| { d.num_minutes() }).sum(); - let (hours, minutes) = to_hrs_minutes(total_minutes); - - println!("-----------------"); - println!("You have been working for {}", show_time(hours, minutes)); - println!("{}", get_charaterized_time_remaining(total_minutes, args.hours * 60)) - }, - Err(err) => eprintln!("{}\nExiting...", err), + let mut total_minutes: i64 = 0; + let mut first: Option> = None; + for time in times { + match first { + None => { + first = Some(time); + }, + Some(prev) => { + total_minutes += (time - prev).num_minutes(); + first = None + } + } } + + if let Some(remaining) = first { + let now = Local::now(); + println!("Ended with unclosed span... assuming ending now: {}", now.time()); + total_minutes += (now - remaining).num_minutes(); + } + + let target_minutes = args.hours * 60 + args.minutes; + println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes)); + Ok(()) } diff --git a/src/modes.rs b/src/modes.rs deleted file mode 100644 index 7aeeac2..0000000 --- a/src/modes.rs +++ /dev/null @@ -1,36 +0,0 @@ -use clap::ValueEnum; - -#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)] -pub enum Modes { - /// Perform calculations by treating stdin as a stream of time pairs separated by newlines - TimeTable, - /// Perform live tracking of the user's time until an EOF character is received. - Live, -} - -impl Modes { - /// Determine whether or not the variant supports piped input vs being used as a CLI tool - /// directly by a human - pub fn supports_piped_input(&self) -> bool { - match self { - Modes::TimeTable => true, - Modes::Live => false, - } - } -} - - -impl Default for Modes { - fn default() -> Self { - Self::TimeTable - } -} - -impl std::fmt::Display for Modes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.to_possible_value() - .expect("no values are skipped") - .get_name() - .fmt(f) - } -} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..dd4cbd8 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,72 @@ +use chrono::{Duration, Local, DateTime, Date}; +use anyhow::{Result, anyhow}; +use std::iter::Iterator; +use regex::Regex; + +fn parse_datetime(reference_date: &Date, s: &str) -> Result> { + let re = Regex::new(r"^\s*([12]?\d):([012345]\d)\s*$")?; + let (_, [hrs, mins]) = re.captures(s).ok_or(anyhow!(format!("Failed to parse \"{s}\" as a time")))?.extract(); + let mins: u32 = mins.parse()?; + let mut hrs: u32 = hrs.parse()?; + let mut date = *reference_date; + + if hrs > 23 { + let num_days = hrs / 24; + date = *reference_date + Duration::days(num_days.into()); + hrs = hrs % 24; + } + Ok(date.and_hms_opt(hrs, mins, 0).ok_or(anyhow!(format!("{hrs}:{mins} not a real time")))?) +} + +pub fn from_stream<'a>(reference_date: &Date, stream: impl Iterator) -> Result>> { + let mut durations: Vec> = vec![]; + for val in stream { + durations.push(parse_datetime(reference_date, val)?); + } + return Ok(durations); +} + +fn show_time(hours: i64, minutes: i64) -> String { + let pluralized_hours = match hours { + 1 => "1 hour".to_string(), + _ => format!("{hours} hours"), + }; + let pluralized_minutes = match minutes { + 1 => "1 minute".to_string(), + _ => format!("{minutes} minutes"), + }; + + if hours == 0 { + return pluralized_minutes.to_string(); + } + + if minutes == 0 { + return pluralized_hours.to_string(); + } + + return format!("{pluralized_hours} and {pluralized_minutes}"); +} + +fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) { + let minutes = total_minutes % 60; + let hours = total_minutes / 60; + (hours, minutes) +} + +pub fn get_charaterized_time_remaining(total_minutes: i64, target_minutes: i64) -> String { + if total_minutes == target_minutes { + return "Exactly done".to_string(); + } + + if total_minutes > target_minutes { + let diff = total_minutes - target_minutes; + let (hours, minutes) = to_hrs_minutes(diff); + return format!("You have overworked {}", show_time(hours, minutes)) + } else { + let diff = target_minutes - total_minutes; + let (hours, minutes) = to_hrs_minutes(diff); + let end_at = (Local::now() + Duration::minutes(diff)).time(); + let end_str = end_at.format("%-I:%M %p"); + return format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str) + } +} From 27b9345377dcaedb4e9808fa717562223627b5f4 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Thu, 7 Nov 2024 21:54:04 +0900 Subject: [PATCH 03/10] Fix a few small issues with v2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 6 ++++-- src/time.rs | 9 +++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35bd882..5aee76c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "time-track" -version = "2.0.0" +version = "2.0.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 5348706..f3dc1a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "time-track" -version = "2.0.0" +version = "2.0.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/main.rs b/src/main.rs index 7541cd5..945df04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,15 +10,17 @@ use args::Args; fn main() -> Result<()> { let args = Args::parse(); - println!("{:?}", args); let stdin = io::stdin(); + println!("Input times one per line. Send an EOF character to finish inputting..."); let mut lines: Vec = vec![]; for line in stdin.lock().lines() { lines.push(line.expect("Issues when reading from stdin")); } + // This is immeidately going to be turned back into a DateTime, which the doc says is fine + #[allow(deprecated)] let midnight = Local::now().date().and_hms_opt(0, 0, 0).ok_or(anyhow!("Expected midnight to exist"))?; - let times = time::from_stream(&midnight.date(), lines.iter())?; + let times = time::from_stream(&midnight, lines.iter())?; let mut total_minutes: i64 = 0; let mut first: Option> = None; diff --git a/src/time.rs b/src/time.rs index dd4cbd8..787041d 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,9 +1,9 @@ -use chrono::{Duration, Local, DateTime, Date}; +use chrono::{Duration, Local, DateTime}; use anyhow::{Result, anyhow}; use std::iter::Iterator; use regex::Regex; -fn parse_datetime(reference_date: &Date, s: &str) -> Result> { +fn parse_datetime(reference_date: &DateTime, s: &str) -> Result> { let re = Regex::new(r"^\s*([12]?\d):([012345]\d)\s*$")?; let (_, [hrs, mins]) = re.captures(s).ok_or(anyhow!(format!("Failed to parse \"{s}\" as a time")))?.extract(); let mins: u32 = mins.parse()?; @@ -15,10 +15,11 @@ fn parse_datetime(reference_date: &Date, s: &str) -> Result2} not a real time", hrs, mins)))?) } -pub fn from_stream<'a>(reference_date: &Date, stream: impl Iterator) -> Result>> { +pub fn from_stream<'a>(reference_date: &DateTime, stream: impl Iterator) -> Result>> { let mut durations: Vec> = vec![]; for val in stream { durations.push(parse_datetime(reference_date, val)?); From 7a6aa2489bc4ab8c0c01812421c01f3b2d646163 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Fri, 13 Dec 2024 18:11:48 +0900 Subject: [PATCH 04/10] Add the option to treat minutes and hours flags as discounting from an 8 hr day --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/args.rs | 4 ++++ src/main.rs | 6 +++++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5aee76c..52be8ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "time-track" -version = "2.0.1" +version = "2.1.0" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index f3dc1a8..ed1f106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "time-track" -version = "2.0.1" +version = "2.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/args.rs b/src/args.rs index abee82c..e4a0c2d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -11,4 +11,8 @@ pub struct Args { /// How many minutes you intend to work (sums with `hours`) #[arg(long, default_value_t = 0)] pub minutes: i64, + + /// If true, the the hours and minutes fields are treated as subtracting from 8 hours + #[arg(long, default_value_t = false)] + pub discount: bool, } diff --git a/src/main.rs b/src/main.rs index 945df04..6539549 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,11 @@ fn main() -> Result<()> { total_minutes += (now - remaining).num_minutes(); } - let target_minutes = args.hours * 60 + args.minutes; + let target_minutes = if args.discount { + (8 * 60) - (args.hours * 60 + args.minutes) + } else { + args.hours * 60 + args.minutes + }; println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes)); Ok(()) } From b6d1c0ea0c801072cc8557586cf7d3d9cdf2eef8 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Fri, 13 Dec 2024 18:21:55 +0900 Subject: [PATCH 05/10] Change the default behavior for hours to 0 if discount is set --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/args.rs | 5 +++-- src/main.rs | 20 +++++++++++++++----- src/time.rs | 4 ++-- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52be8ab..f186d3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "time-track" -version = "2.1.0" +version = "2.1.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index ed1f106..944e031 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "time-track" -version = "2.1.0" +version = "2.1.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/args.rs b/src/args.rs index e4a0c2d..e24e8c3 100644 --- a/src/args.rs +++ b/src/args.rs @@ -5,8 +5,9 @@ use clap::Parser; #[command(author, version, about, long_about = None)] pub struct Args { /// How many hours you intend to work (sums with `minutes`) - #[arg(long, default_value_t = 8)] - pub hours: i64, + /// Usually defaults to 8 hours, but if discount is set then it defaults to 0 + #[arg(long)] + pub hours: Option, /// How many minutes you intend to work (sums with `hours`) #[arg(long, default_value_t = 0)] diff --git a/src/main.rs b/src/main.rs index 6539549..2963b9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,21 @@ use args::Args; fn main() -> Result<()> { let args = Args::parse(); let stdin = io::stdin(); + let hours = args.hours.unwrap_or( + if args.discount { + 0 + } else { + 8 + } + ); + + let target_minutes = if args.discount { + (8 * 60) - ((hours * 60) + args.minutes) + } else { + hours * 60 + args.minutes + }; + let (hrs, mins) = time::to_hrs_minutes(target_minutes); + println!("Working for {}", time::show_time(hrs, mins)); println!("Input times one per line. Send an EOF character to finish inputting..."); let mut lines: Vec = vec![]; for line in stdin.lock().lines() { @@ -42,11 +57,6 @@ fn main() -> Result<()> { total_minutes += (now - remaining).num_minutes(); } - let target_minutes = if args.discount { - (8 * 60) - (args.hours * 60 + args.minutes) - } else { - args.hours * 60 + args.minutes - }; println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes)); Ok(()) } diff --git a/src/time.rs b/src/time.rs index 787041d..6d49767 100644 --- a/src/time.rs +++ b/src/time.rs @@ -27,7 +27,7 @@ pub fn from_stream<'a>(reference_date: &DateTime, stream: impl Iterator String { +pub fn show_time(hours: i64, minutes: i64) -> String { let pluralized_hours = match hours { 1 => "1 hour".to_string(), _ => format!("{hours} hours"), @@ -48,7 +48,7 @@ fn show_time(hours: i64, minutes: i64) -> String { return format!("{pluralized_hours} and {pluralized_minutes}"); } -fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) { +pub fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) { let minutes = total_minutes % 60; let hours = total_minutes / 60; (hours, minutes) From 9087e69c7902733c1939a5908ea3e3a799dd96de Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Mon, 23 Dec 2024 16:59:09 +0900 Subject: [PATCH 06/10] Simplify the now string --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f186d3f..db20599 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "time-track" -version = "2.1.1" +version = "2.1.2" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 944e031..76bb2be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "time-track" -version = "2.1.1" +version = "2.1.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/main.rs b/src/main.rs index 2963b9e..4ea78a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,7 +53,8 @@ fn main() -> Result<()> { if let Some(remaining) = first { let now = Local::now(); - println!("Ended with unclosed span... assuming ending now: {}", now.time()); + let now_str = now.format("%-I:%M %p"); + println!("Ended with unclosed span... assuming ending now: {}", now_str); total_minutes += (now - remaining).num_minutes(); } From 2b55df907bc556774d1bd7538fdd24270262d567 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Thu, 26 Dec 2024 18:47:35 +0900 Subject: [PATCH 07/10] Count end time from the last time if the last time is in the future --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/main.rs | 6 ++++-- src/time.rs | 19 ++++++++++++++++--- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db20599..925ae63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "time-track" -version = "2.1.2" +version = "2.1.3" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 76bb2be..e318688 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "time-track" -version = "2.1.2" +version = "2.1.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/main.rs b/src/main.rs index 4ea78a7..19dc159 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,7 @@ fn main() -> Result<()> { let mut total_minutes: i64 = 0; let mut first: Option> = None; + let mut last: Option> = None; for time in times { match first { None => { @@ -46,7 +47,8 @@ fn main() -> Result<()> { }, Some(prev) => { total_minutes += (time - prev).num_minutes(); - first = None + first = None; + last = Some(time); } } } @@ -58,6 +60,6 @@ fn main() -> Result<()> { total_minutes += (now - remaining).num_minutes(); } - println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes)); + println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes, last.unwrap_or_else(|| Local::now()))); Ok(()) } diff --git a/src/time.rs b/src/time.rs index 6d49767..eb0e0cf 100644 --- a/src/time.rs +++ b/src/time.rs @@ -54,7 +54,11 @@ pub fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) { (hours, minutes) } -pub fn get_charaterized_time_remaining(total_minutes: i64, target_minutes: i64) -> String { +pub fn get_charaterized_time_remaining( + total_minutes: i64, + target_minutes: i64, + ended_at: DateTime, +) -> String { if total_minutes == target_minutes { return "Exactly done".to_string(); } @@ -66,8 +70,17 @@ pub fn get_charaterized_time_remaining(total_minutes: i64, target_minutes: i64) } else { let diff = target_minutes - total_minutes; let (hours, minutes) = to_hrs_minutes(diff); - let end_at = (Local::now() + Duration::minutes(diff)).time(); + let end_at = (ended_at + Duration::minutes(diff)).time(); let end_str = end_at.format("%-I:%M %p"); - return format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str) + return if ended_at > Local::now() { + format!( + "You have {} remaining (end at {} starting from {})", + show_time(hours, minutes), + end_str, + ended_at.format("%-I:%M %p"), + ) + } else { + format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str) + } } } From 51a200d1a3097264d0985f03905058efc69f9a0e Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Fri, 27 Dec 2024 09:36:49 +0900 Subject: [PATCH 08/10] Bug Fix: Count time from now if the end time is before now --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/time.rs | 23 +++++++++++++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 925ae63..e66d302 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,7 +384,7 @@ dependencies = [ [[package]] name = "time-track" -version = "2.1.3" +version = "2.1.4" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index e318688..74de412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "time-track" -version = "2.1.3" +version = "2.1.4" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/time.rs b/src/time.rs index eb0e0cf..2b31d50 100644 --- a/src/time.rs +++ b/src/time.rs @@ -70,17 +70,20 @@ pub fn get_charaterized_time_remaining( } else { let diff = target_minutes - total_minutes; let (hours, minutes) = to_hrs_minutes(diff); - let end_at = (ended_at + Duration::minutes(diff)).time(); - let end_str = end_at.format("%-I:%M %p"); - return if ended_at > Local::now() { - format!( - "You have {} remaining (end at {} starting from {})", - show_time(hours, minutes), - end_str, - ended_at.format("%-I:%M %p"), - ) + let now = Local::now(); + return if ended_at > now { + let end_at = (ended_at + Duration::minutes(diff)).time(); + let end_str = end_at.format("%-I:%M %p"); + format!( + "You have {} remaining (end at {} starting from {})", + show_time(hours, minutes), + end_str, + ended_at.format("%-I:%M %p"), + ) } else { - format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str) + let end_at = (now + Duration::minutes(diff)).time(); + let end_str = end_at.format("%-I:%M %p"); + format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str) } } } From e2c0b08c032ecec8f27236f4f7132519472a165f Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Sun, 20 Apr 2025 22:11:19 +0900 Subject: [PATCH 09/10] Create README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..21c3754 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# time-track + +A simple CLI tool for tracking how much time you have left to work. I find myself stressing about whether I'm hitting 8 real hours, so this little tool helps me avoid wasting +time calculating when my work day will end. + +Simply enter times, one per line and send an EOF character when you're done. The first lines opens a span of work and the next line closes it so that you can build up working +time be clocking in and out. Finally, you can send additional arguments to the program to configure how long you intend to work (the default is 8 hours). From 4a86e930f9120149a5cfcb9bace10d021cc888f6 Mon Sep 17 00:00:00 2001 From: Campbell Alden Date: Sun, 20 Apr 2025 22:18:34 +0900 Subject: [PATCH 10/10] Update README.md --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 21c3754..bbcf4bc 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,18 @@ time calculating when my work day will end. Simply enter times, one per line and send an EOF character when you're done. The first lines opens a span of work and the next line closes it so that you can build up working time be clocking in and out. Finally, you can send additional arguments to the program to configure how long you intend to work (the default is 8 hours). + +``` +❯ time-track +Working for 8 hours +Input times one per line. Send an EOF character to finish inputting... +8:30 +9:30 +11:15 +12:30 +13:16 +18:20 +19:30 +20:11 # Send an EOF +Exactly done +```