Nicer status command

This commit is contained in:
Thomas Avé 2025-02-14 00:26:01 +01:00
parent 88138dc783
commit 227e29e9c4
3 changed files with 620 additions and 183 deletions

666
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ reqwest = {version="0.12.9", features = ["json"]}
serde = {version="1.0.214", features = ["derive"]}
serde_json = "1.0.132"
tokio = {version="1.41.0", features = ["full"]}
colored = "2.1.0"
[[bin]]
name = "timer"

View File

@ -10,6 +10,7 @@ use std::{
io::Read,
process::Command,
};
use colored::*;
pub struct Settings {
pub url: String,
@ -205,14 +206,141 @@ pub struct Status {
pub active: String,
}
fn format_bar(seconds: i64, max_value: i64, width: usize) -> String {
const BLOCKS: &[&str] = &["", "", "", "", "", "", "", ""];
let ratio = (seconds as f64) / (max_value as f64);
let full_blocks = (ratio * (width as f64)) as usize;
let remainder = ((ratio * (width as f64) - full_blocks as f64) * 8.0) as usize;
let mut bar = String::new();
for _ in 0..full_blocks {
bar.push_str(BLOCKS[7]);
}
if remainder > 0 && full_blocks < width {
bar.push_str(BLOCKS[remainder - 1]);
}
while bar.len() < width {
bar.push(' ');
}
// Color based on hours worked
let hours = seconds as f64 / 3600.0;
let colored_bar = match hours {
h if h >= 8.0 => bar.green(),
h if h >= 4.0 => bar.yellow(),
_ => bar.red(),
};
format!("{}", colored_bar)
}
pub async fn status(settings: Settings) -> Result<(), reqwest::Error> {
// Get additional metrics from status endpoint
let client = Client::new();
let response = client.get(settings.url.to_string() + "/")
let status_response = client.get(settings.url.to_string() + "/")
.send()
.await?;
let status: Status = serde_json::from_str(&status_response.text().await?).unwrap();
let response_text = response.text().await.unwrap();
let status: Status = serde_json::from_str(&response_text).unwrap();
println!("{}", serde_json::to_string_pretty(&status).unwrap());
// Compute current week boundaries (Monday to Sunday)
let now = chrono::Local::now();
let today = now.date_naive();
let weekday = today.weekday().num_days_from_monday() as i64;
let week_start_date = today - chrono::Duration::days(weekday);
let week_start = week_start_date.and_hms(0, 0, 0);
let week_end = week_start_date.and_hms(23, 59, 59) + chrono::Duration::days(6);
// Build query URL for history API
let query_params = [
("since", week_start.format("%Y-%m-%dT%H:%M:%S").to_string()),
("until", week_end.format("%Y-%m-%dT%H:%M:%S").to_string()),
("count", "100".to_string()),
("project", settings.project.clone()),
];
let url = reqwest::Url::parse_with_params(&(settings.url + "/api/history"), &query_params).unwrap();
let client = reqwest::Client::new();
let response = client.get(url).send().await?;
let body = response.text().await?;
let periods = parse_periods(body).unwrap_or_default();
// Check for active session first
if let Some(active_period) = periods.iter().find(|p| p.end_time.is_none()) {
let duration = now.naive_local().signed_duration_since(active_period.start_time);
let duration_str = format_duration(duration.num_seconds());
println!("\n{}", "Current Session".bold());
println!("Active for: {} (started at {})",
duration_str.yellow(),
active_period.start_time.format("%H:%M").to_string().yellow()
);
}
// Group durations by day (assume each period is within a single day)
use std::collections::HashMap;
let mut daily: HashMap<chrono::NaiveDate, i64> = HashMap::new();
for period in &periods {
let start = period.start_time;
let end = period.end_time.unwrap_or(now.naive_local());
let duration = (end - start).num_seconds();
let day = start.date();
*daily.entry(day).or_insert(0) += duration;
}
// Find maximum hours for scaling
let max_seconds = daily.values().cloned().max().unwrap_or(8 * 3600);
let max_seconds = std::cmp::max(max_seconds, 8 * 3600); // At least 8 hours for scale
const BOX_WIDTH: usize = 50;
const BAR_WIDTH: usize = 20;
// Print active session if exists
if status.active != "0" && status.active != "" {
println!("\n{}", "Current Session".bold());
println!("Active for: {}", status.active.yellow());
}
// Print weekly overview with visual bars
println!("\n{}", "Weekly Work Overview".bold());
println!("{}", "".repeat(BOX_WIDTH));
let mut total = 0;
for i in 0..7 {
let date = week_start_date + chrono::Duration::days(i);
let seconds = daily.get(&date).cloned().unwrap_or(0);
total += seconds;
let day = date.format("%a").to_string().bold();
let duration = format_duration(seconds);
// Ensure the day and duration part is exactly 15 characters wide
let day_str = format!("{:<3} {:<10}", day, duration);
println!("{:<15} {:<width$}",
day_str,
format_bar(seconds, max_seconds, BAR_WIDTH),
width = BOX_WIDTH - 15
);
}
println!("{}", "".repeat(BOX_WIDTH));
println!("\nTotal Worked: {}", format_duration(total).bold());
let weekly_target = 39 * 3600;
if total < weekly_target {
println!("Remaining: {}", format_duration(weekly_target - total).red());
} else {
println!("Overtime: {}", format_duration(total - weekly_target).green());
}
// Print monthly and yearly totals
println!("\n{}", "Extended Statistics".bold());
println!("This Month: {}", status.month.cyan());
println!("This Year: {}", status.year.cyan());
Ok(())
}
fn format_duration(seconds: i64) -> String {
let hours = seconds / 3600;
let minutes = (seconds % 3600) / 60;
format!("{}h {}m", hours, minutes)
}