Compare commits

..

No commits in common. "main" and "v0.2.1" have entirely different histories.
main ... v0.2.1

6 changed files with 115 additions and 245 deletions

71
Cargo.lock generated
View file

@ -2,15 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@ -76,10 +67,15 @@ dependencies = [
] ]
[[package]] [[package]]
name = "anyhow" name = "atty"
version = "1.0.72" version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@ -206,6 +202,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 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]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.2" version = "0.3.2"
@ -241,7 +246,7 @@ version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi 0.3.2",
"rustix", "rustix",
"windows-sys", "windows-sys",
] ]
@ -273,12 +278,6 @@ version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.15" version = "0.2.15"
@ -312,35 +311,6 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.3" version = "0.38.3"
@ -384,12 +354,11 @@ dependencies = [
[[package]] [[package]]
name = "time-track" name = "time-track"
version = "2.1.4" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow", "atty",
"chrono", "chrono",
"clap", "clap",
"regex",
] ]
[[package]] [[package]]

View file

@ -1,12 +1,11 @@
[package] [package]
name = "time-track" name = "time-track"
version = "2.1.4" version = "0.2.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
[dependencies] [dependencies]
anyhow = "1.0.72" atty = "0.2.14"
chrono = "0.4.26" chrono = "0.4.26"
clap = { version = "4.3.11", features = ["derive"] } clap = { version = "4.3.11", features = ["derive"] }
regex = "1.11.1"

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

@ -1,19 +0,0 @@
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`)
/// Usually defaults to 8 hours, but if discount is set then it defaults to 0
#[arg(long)]
pub hours: Option<i64>,
/// 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,
}

View file

@ -1,65 +1,97 @@
use std::io::{self, BufRead}; use std::io::{stdin, BufRead};
use chrono::{DateTime, Local};
use chrono::{NaiveTime, Duration, Local};
use clap::Parser; use clap::Parser;
use anyhow::{anyhow, Result}; use atty::Stream;
mod args;
mod time;
use args::Args; /// A simple program to track time spans
#[derive(Parser, Debug)]
fn main() -> Result<()> { #[command(author, version, about, long_about = None)]
let args = Args::parse(); struct Args {
let stdin = io::stdin(); /// When passed the script will collect time spans live. Close a span by pressing ENTER and
let hours = args.hours.unwrap_or( /// finish the calulation by passing in an EOF character
if args.discount { #[arg(short, long, default_value_t = false)]
0 live: bool,
} else { }
8
} fn parse_time(time_str: &str) -> NaiveTime {
); NaiveTime::parse_from_str(time_str, "%H:%M")
.expect(&format!("Failed to parse time from {time_str}"))
let target_minutes = if args.discount { }
(8 * 60) - ((hours * 60) + args.minutes)
} else { fn all_at_once(is_terminal: bool) -> Vec<Duration> {
hours * 60 + args.minutes if is_terminal {
}; println!("Enter simple times one per line. Send an EOF character to sum all time spans");
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 durations = vec![];
let mut lines: Vec<String> = vec![]; let mut seen: Option<String> = None;
for line in stdin.lock().lines() { for line in stdin().lock().lines() {
lines.push(line.expect("Issues when reading from stdin")); let cleaned = line.expect("failed to read a line").trim().to_string();
} if let Some(previous) = &seen {
let first = parse_time(previous);
// This is immeidately going to be turned back into a DateTime, which the doc says is fine let mut second = parse_time(&cleaned);
#[allow(deprecated)] if second < first {
let midnight = Local::now().date().and_hms_opt(0, 0, 0).ok_or(anyhow!("Expected midnight to exist"))?; second = second + Duration::hours(12);
let times = time::from_stream(&midnight, lines.iter())?; }
durations.push(second - first);
let mut total_minutes: i64 = 0; seen = None;
let mut first: Option<DateTime<Local>> = None; } else {
let mut last: Option<DateTime<Local>> = None; seen = Some(cleaned.to_string());
for time in times { }
match first { }
None => { if let Some(unpaired) = seen {
first = Some(time); println!("Ended with an open span from {unpaired}... Ignoring");
}, }
Some(prev) => { return durations;
total_minutes += (time - prev).num_minutes(); }
first = None;
last = Some(time); fn live_spans() -> Vec<Duration> {
} println!("Tracking Spans Live. Press ENTER to start a span\n");
} let mut durations = vec![];
} let mut seen: Option<NaiveTime> = None;
for _line in stdin().lock().lines() {
if let Some(remaining) = first { let mut now = Local::now().naive_local().time();
let now = Local::now(); if let Some(previous) = &seen {
let now_str = now.format("%-I:%M %p"); if now < *previous {
println!("Ended with unclosed span... assuming ending now: {}", now_str); now = now + Duration::hours(12);
total_minutes += (now - remaining).num_minutes(); }
} let prev_time = previous.format("%H:%M");
let now_time = now.format("%H:%M");
println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes, last.unwrap_or_else(|| Local::now()))); println!("Closing span from {prev_time} - {now_time}");
Ok(()) 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!("Ended with an open span from {unpaired}... Ignoring");
}
return durations;
}
fn main() {
let args = Args::parse();
let is_terminal = atty::is(Stream::Stdin);
if args.live && !is_terminal {
panic!("Cannot run live mode on piped input");
}
let durations = if args.live {
live_spans()
} else {
all_at_once(is_terminal)
};
let total_minutes: i64 = durations.iter().map(|d| { d.num_minutes() }).sum();
let minutes = total_minutes % 60;
let hours = total_minutes / 60;
println!("You have been working for {hours} hour(s) and {minutes} minute(s)");
} }

View file

@ -1,89 +0,0 @@
use chrono::{Duration, Local, DateTime};
use anyhow::{Result, anyhow};
use std::iter::Iterator;
use regex::Regex;
fn parse_datetime(reference_date: &DateTime<Local>, s: &str) -> Result<DateTime<Local>> {
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;
}
#[allow(deprecated)]
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>>> {
let mut durations: Vec<DateTime<Local>> = vec![];
for val in stream {
durations.push(parse_datetime(reference_date, val)?);
}
return Ok(durations);
}
pub 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}");
}
pub 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,
ended_at: DateTime<Local>,
) -> 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 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 {
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)
}
}
}