Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a86e930f9 | ||
|
|
e2c0b08c03 | ||
|
|
51a200d1a3 | ||
|
|
2b55df907b | ||
|
|
9087e69c79 | ||
|
|
b6d1c0ea0c | ||
|
|
7a6aa2489b | ||
|
|
27b9345377 |
6 changed files with 83 additions and 20 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -384,7 +384,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-track"
|
name = "time-track"
|
||||||
version = "2.0.0"
|
version = "2.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "time-track"
|
name = "time-track"
|
||||||
version = "2.0.0"
|
version = "2.1.4"
|
||||||
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
|
||||||
|
|
|
||||||
22
README.md
Normal file
22
README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
@ -5,10 +5,15 @@ 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`)
|
||||||
#[arg(long, default_value_t = 8)]
|
/// Usually defaults to 8 hours, but if discount is set then it defaults to 0
|
||||||
pub hours: i64,
|
#[arg(long)]
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
src/main.rs
31
src/main.rs
|
|
@ -10,18 +10,36 @@ 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.date(), lines.iter())?;
|
let times = time::from_stream(&midnight, 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 => {
|
||||||
|
|
@ -29,18 +47,19 @@ 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();
|
||||||
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();
|
total_minutes += (now - remaining).num_minutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_minutes = args.hours * 60 + args.minutes;
|
println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes, last.unwrap_or_else(|| Local::now())));
|
||||||
println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes));
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
src/time.rs
37
src/time.rs
|
|
@ -1,9 +1,9 @@
|
||||||
use chrono::{Duration, Local, DateTime, Date};
|
use chrono::{Duration, Local, DateTime};
|
||||||
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: &Date<Local>, s: &str) -> Result<DateTime<Local>> {
|
fn parse_datetime(reference_date: &DateTime<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,10 +15,11 @@ fn parse_datetime(reference_date: &Date<Local>, s: &str) -> Result<DateTime<Loca
|
||||||
date = *reference_date + Duration::days(num_days.into());
|
date = *reference_date + Duration::days(num_days.into());
|
||||||
hrs = hrs % 24;
|
hrs = hrs % 24;
|
||||||
}
|
}
|
||||||
Ok(date.and_hms_opt(hrs, mins, 0).ok_or(anyhow!(format!("{hrs}:{mins} not a real time")))?)
|
#[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: &Date<Local>, stream: impl Iterator<Item = &'a String>) -> Result<Vec<DateTime<Local>>> {
|
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![];
|
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)?);
|
||||||
|
|
@ -26,7 +27,7 @@ pub fn from_stream<'a>(reference_date: &Date<Local>, stream: impl Iterator<Item
|
||||||
return Ok(durations);
|
return Ok(durations);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_time(hours: i64, minutes: i64) -> String {
|
pub 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"),
|
||||||
|
|
@ -47,13 +48,17 @@ fn show_time(hours: i64, minutes: i64) -> String {
|
||||||
return format!("{pluralized_hours} and {pluralized_minutes}");
|
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 minutes = total_minutes % 60;
|
||||||
let hours = total_minutes / 60;
|
let hours = total_minutes / 60;
|
||||||
(hours, minutes)
|
(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<Local>,
|
||||||
|
) -> String {
|
||||||
if total_minutes == target_minutes {
|
if total_minutes == target_minutes {
|
||||||
return "Exactly done".to_string();
|
return "Exactly done".to_string();
|
||||||
}
|
}
|
||||||
|
|
@ -65,8 +70,20 @@ pub fn get_charaterized_time_remaining(total_minutes: i64, target_minutes: i64)
|
||||||
} 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 end_at = (Local::now() + Duration::minutes(diff)).time();
|
let now = Local::now();
|
||||||
let end_str = end_at.format("%-I:%M %p");
|
return if ended_at > now {
|
||||||
return format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue