Compare commits

..

No commits in common. "main" and "v2.0.0" have entirely different histories.
main ... v2.0.0

6 changed files with 20 additions and 83 deletions

2
Cargo.lock generated
View file

@ -384,7 +384,7 @@ dependencies = [
[[package]] [[package]]
name = "time-track" name = "time-track"
version = "2.1.4" version = "2.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "time-track" name = "time-track"
version = "2.1.4" version = "2.0.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -1,22 +0,0 @@
# 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).
```
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
```

View file

@ -5,15 +5,10 @@ use clap::Parser;
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub struct Args { pub struct Args {
/// How many hours you intend to work (sums with `minutes`) /// How many hours you intend to work (sums with `minutes`)
/// Usually defaults to 8 hours, but if discount is set then it defaults to 0 #[arg(long, default_value_t = 8)]
#[arg(long)] pub hours: i64,
pub hours: Option<i64>,
/// How many minutes you intend to work (sums with `hours`) /// How many minutes you intend to work (sums with `hours`)
#[arg(long, default_value_t = 0)] #[arg(long, default_value_t = 0)]
pub minutes: i64, 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,
} }

View file

@ -10,36 +10,18 @@ use args::Args;
fn main() -> Result<()> { fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
println!("{:?}", args);
let stdin = io::stdin(); 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<String> = vec![]; let mut lines: Vec<String> = vec![];
for line in stdin.lock().lines() { for line in stdin.lock().lines() {
lines.push(line.expect("Issues when reading from stdin")); 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 midnight = Local::now().date().and_hms_opt(0, 0, 0).ok_or(anyhow!("Expected midnight to exist"))?;
let times = time::from_stream(&midnight, lines.iter())?; let times = time::from_stream(&midnight.date(), lines.iter())?;
let mut total_minutes: i64 = 0; let mut total_minutes: i64 = 0;
let mut first: Option<DateTime<Local>> = None; let mut first: Option<DateTime<Local>> = None;
let mut last: Option<DateTime<Local>> = None;
for time in times { for time in times {
match first { match first {
None => { None => {
@ -47,19 +29,18 @@ fn main() -> Result<()> {
}, },
Some(prev) => { Some(prev) => {
total_minutes += (time - prev).num_minutes(); total_minutes += (time - prev).num_minutes();
first = None; first = None
last = Some(time);
} }
} }
} }
if let Some(remaining) = first { if let Some(remaining) = first {
let now = Local::now(); let now = Local::now();
let now_str = now.format("%-I:%M %p"); println!("Ended with unclosed span... assuming ending now: {}", now.time());
println!("Ended with unclosed span... assuming ending now: {}", now_str);
total_minutes += (now - remaining).num_minutes(); total_minutes += (now - remaining).num_minutes();
} }
println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes, last.unwrap_or_else(|| Local::now()))); let target_minutes = args.hours * 60 + args.minutes;
println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes));
Ok(()) Ok(())
} }

View file

@ -1,9 +1,9 @@
use chrono::{Duration, Local, DateTime}; use chrono::{Duration, Local, DateTime, Date};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use std::iter::Iterator; use std::iter::Iterator;
use regex::Regex; use regex::Regex;
fn parse_datetime(reference_date: &DateTime<Local>, s: &str) -> Result<DateTime<Local>> { fn parse_datetime(reference_date: &Date<Local>, s: &str) -> Result<DateTime<Local>> {
let re = Regex::new(r"^\s*([12]?\d):([012345]\d)\s*$")?; 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 (_, [hrs, mins]) = re.captures(s).ok_or(anyhow!(format!("Failed to parse \"{s}\" as a time")))?.extract();
let mins: u32 = mins.parse()?; let mins: u32 = mins.parse()?;
@ -15,11 +15,10 @@ fn parse_datetime(reference_date: &DateTime<Local>, s: &str) -> Result<DateTime<
date = *reference_date + Duration::days(num_days.into()); date = *reference_date + Duration::days(num_days.into());
hrs = hrs % 24; hrs = hrs % 24;
} }
#[allow(deprecated)] Ok(date.and_hms_opt(hrs, mins, 0).ok_or(anyhow!(format!("{hrs}:{mins} not a real time")))?)
Ok(date.date().and_hms_opt(hrs, mins, 0).ok_or(anyhow!(format!("{}:{:0>2} not a real time", hrs, mins)))?)
} }
pub fn from_stream<'a>(reference_date: &DateTime<Local>, stream: impl Iterator<Item = &'a String>) -> Result<Vec<DateTime<Local>>> { pub fn from_stream<'a>(reference_date: &Date<Local>, stream: impl Iterator<Item = &'a String>) -> Result<Vec<DateTime<Local>>> {
let mut durations: Vec<DateTime<Local>> = vec![]; let mut durations: Vec<DateTime<Local>> = vec![];
for val in stream { for val in stream {
durations.push(parse_datetime(reference_date, val)?); durations.push(parse_datetime(reference_date, val)?);
@ -27,7 +26,7 @@ pub fn from_stream<'a>(reference_date: &DateTime<Local>, stream: impl Iterator<I
return Ok(durations); return Ok(durations);
} }
pub fn show_time(hours: i64, minutes: i64) -> String { fn show_time(hours: i64, minutes: i64) -> String {
let pluralized_hours = match hours { let pluralized_hours = match hours {
1 => "1 hour".to_string(), 1 => "1 hour".to_string(),
_ => format!("{hours} hours"), _ => format!("{hours} hours"),
@ -48,17 +47,13 @@ pub fn show_time(hours: i64, minutes: i64) -> String {
return format!("{pluralized_hours} and {pluralized_minutes}"); return format!("{pluralized_hours} and {pluralized_minutes}");
} }
pub fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) { fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) {
let minutes = total_minutes % 60; let minutes = total_minutes % 60;
let hours = total_minutes / 60; let hours = total_minutes / 60;
(hours, minutes) (hours, minutes)
} }
pub fn get_charaterized_time_remaining( pub fn get_charaterized_time_remaining(total_minutes: i64, target_minutes: i64) -> String {
total_minutes: i64,
target_minutes: i64,
ended_at: DateTime<Local>,
) -> String {
if total_minutes == target_minutes { if total_minutes == target_minutes {
return "Exactly done".to_string(); return "Exactly done".to_string();
} }
@ -70,20 +65,8 @@ pub fn get_charaterized_time_remaining(
} else { } else {
let diff = target_minutes - total_minutes; let diff = target_minutes - total_minutes;
let (hours, minutes) = to_hrs_minutes(diff); let (hours, minutes) = to_hrs_minutes(diff);
let now = Local::now(); let end_at = (Local::now() + Duration::minutes(diff)).time();
return if ended_at > now { let end_str = end_at.format("%-I:%M %p");
let end_at = (ended_at + Duration::minutes(diff)).time(); return format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str)
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 {
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)
}
} }
} }