From 42d9ecd3d9cf941053c889d73e50324c554f260e Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Tue, 7 Apr 2026 07:57:28 +0200 Subject: [PATCH 1/3] feat: add --minutes flag to timesheet command Adds a -m/--minutes flag to `streamd timesheet` that displays time in HH:MM format (e.g., 8:30) instead of decimal hours (e.g., 8.5h). Includes unit tests for both formatting functions. --- src/cli/args.rs | 6 ++- src/cli/commands/timesheet.rs | 90 +++++++++++++++++++++++++++-------- src/main.rs | 2 +- 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index be56ee3..83c762c 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -50,7 +50,11 @@ pub enum Commands { }, /// Display extracted timesheets - Timesheet, + Timesheet { + /// Display time as minutes (HH:MM) instead of decimal hours (H.Hh) + #[arg(short, long)] + minutes: bool, + }, /// Generate shell completions Completions { diff --git a/src/cli/commands/timesheet.rs b/src/cli/commands/timesheet.rs index 80346b4..f2571dc 100644 --- a/src/cli/commands/timesheet.rs +++ b/src/cli/commands/timesheet.rs @@ -41,8 +41,14 @@ fn load_all_shards(base_folder: &Path) -> Result, StreamdErr } /// Format hours with sign for display. -fn format_diff(hours: f64) -> String { - if hours >= 0.0 { +fn format_diff(hours: f64, use_minutes: bool) -> String { + if use_minutes { + let total_minutes = (hours * 60.0).round() as i32; + let h = total_minutes.abs() / 60; + let m = total_minutes.abs() % 60; + let sign = if hours >= 0.0 { "+" } else { "-" }; + format!("{}{}:{:02}", sign, h, m) + } else if hours >= 0.0 { format!("+{:.1}h", hours.abs()) } else { format!("{:.1}h", hours) @@ -50,8 +56,15 @@ fn format_diff(hours: f64) -> String { } /// Format hours for display without sign. -fn format_hours(hours: f64) -> String { - format!("{:.1}h", hours.abs()) +fn format_hours(hours: f64, use_minutes: bool) -> String { + if use_minutes { + let total_minutes = (hours * 60.0).round() as i32; + let h = total_minutes.abs() / 60; + let m = total_minutes.abs() % 60; + format!("{}:{:02}", h, m) + } else { + format!("{:.1}h", hours.abs()) + } } /// Get the weekday abbreviation. @@ -77,8 +90,8 @@ fn print_header() { } /// Print a month report. -fn print_month(month: &MonthReport) { - let diff_str = format_diff(month.diff()); +fn print_month(month: &MonthReport, use_minutes: bool) { + let diff_str = format_diff(month.diff(), use_minutes); let month_title = format!("{} {}", month.month_name(), month.year); // Month header with diff @@ -99,9 +112,9 @@ fn print_month(month: &MonthReport) { for day in &month.days { let date_str = day.date.format("%Y-%m-%d").to_string(); let weekday = weekday_abbrev(day.date); - let expected = format_hours(day.expected_hours); - let actual = format_hours(day.actual_hours); - let diff = format_diff(day.diff()); + let expected = format_hours(day.expected_hours, use_minutes); + let actual = format_hours(day.actual_hours, use_minutes); + let diff = format_diff(day.diff(), use_minutes); let type_str = match day.day_type { DayType::Regular => String::new(), @@ -134,26 +147,26 @@ fn print_month(month: &MonthReport) { println!(" {}", light_line); println!( " Monthly: {:>7} {:>7} {:>6}", - format_hours(month.total_expected()), - format_hours(month.total_actual()), - format_diff(month.diff()) + format_hours(month.total_expected(), use_minutes), + format_hours(month.total_actual(), use_minutes), + format_diff(month.diff(), use_minutes) ); println!(); } /// Print the cumulative balance. -fn print_cumulative_balance(balance: f64) { +fn print_cumulative_balance(balance: f64, use_minutes: bool) { let light_line = "\u{2500}".repeat(SEPARATOR_WIDTH); println!("{}", light_line); println!( " CUMULATIVE BALANCE: {}", - format_diff(balance) + format_diff(balance, use_minutes) ); println!("{}", light_line); } /// Print warnings section. -fn print_warnings(report: &TimesheetReport) { +fn print_warnings(report: &TimesheetReport, use_minutes: bool) { if !report.has_warnings() { return; } @@ -216,9 +229,9 @@ fn print_warnings(report: &TimesheetReport) { for w in &outside_period_warnings { if let DayWarning::OutsidePeriod { hours_worked } = &w.warning { println!( - " - {}: {:.1}h worked (no period configured)", + " - {}: {} worked (no period configured)", w.date.format("%Y-%m-%d"), - hours_worked + format_hours(*hours_worked, use_minutes) ); } } @@ -226,7 +239,7 @@ fn print_warnings(report: &TimesheetReport) { } } -pub fn run() -> Result<(), StreamdError> { +pub fn run(use_minutes: bool) -> Result<(), StreamdError> { let settings = Settings::load()?; let base_folder = Path::new(&settings.base_folder); @@ -266,11 +279,46 @@ pub fn run() -> Result<(), StreamdError> { print_header(); for month in &report.months { - print_month(month); + print_month(month, use_minutes); } - print_cumulative_balance(report.cumulative_balance); - print_warnings(&report); + print_cumulative_balance(report.cumulative_balance, use_minutes); + print_warnings(&report, use_minutes); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_hours_decimal() { + assert_eq!(format_hours(8.0, false), "8.0h"); + assert_eq!(format_hours(8.5, false), "8.5h"); + assert_eq!(format_hours(0.0, false), "0.0h"); + } + + #[test] + fn test_format_hours_minutes() { + assert_eq!(format_hours(8.0, true), "8:00"); + assert_eq!(format_hours(8.5, true), "8:30"); + assert_eq!(format_hours(0.0, true), "0:00"); + assert_eq!(format_hours(1.25, true), "1:15"); + } + + #[test] + fn test_format_diff_decimal() { + assert_eq!(format_diff(0.5, false), "+0.5h"); + assert_eq!(format_diff(-1.5, false), "-1.5h"); + assert_eq!(format_diff(0.0, false), "+0.0h"); + } + + #[test] + fn test_format_diff_minutes() { + assert_eq!(format_diff(0.5, true), "+0:30"); + assert_eq!(format_diff(-1.5, true), "-1:30"); + assert_eq!(format_diff(0.0, true), "+0:00"); + assert_eq!(format_diff(1.25, true), "+1:15"); + } +} diff --git a/src/main.rs b/src/main.rs index e6325c8..2a7e1f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ fn main() -> miette::Result<()> { Some(TodoAction::Done { number }) => streamd::cli::commands::todo::run_done(number)?, }, Some(Commands::Edit { number }) => streamd::cli::commands::edit::run(number)?, - Some(Commands::Timesheet) => streamd::cli::commands::timesheet::run()?, + Some(Commands::Timesheet { minutes }) => streamd::cli::commands::timesheet::run(minutes)?, Some(Commands::Completions { shell }) => { streamd::cli::commands::completions::run(shell); } -- 2.53.0 From 9563ff4d936c7c0b64d035155c8e4de2c1a8be8a Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Tue, 7 Apr 2026 07:59:50 +0200 Subject: [PATCH 2/3] feat: bump to 0.2.0 --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f858312..f42563f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,9 +285,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a043dc74da1e37d6afe657061213aa6f425f855399a11d3463c6ecccc4dfda1f" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" @@ -787,7 +787,7 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "streamd" -version = "0.1.1" +version = "0.2.0" dependencies = [ "chrono", "chrono-tz", diff --git a/Cargo.toml b/Cargo.toml index cb39165..c073e7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "streamd" -version = "0.1.1" +version = "0.2.0" edition = "2021" description = "Personal knowledge management and time-tracking CLI using @Tag annotations" license = "AGPL-3.0-only" -- 2.53.0 From a9acd34801bc782450f53c2febe05b844b0405fd Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Tue, 7 Apr 2026 08:09:47 +0200 Subject: [PATCH 3/3] ci: fix release-pipeline running on feature-branches --- .forgejo/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 755c0d5..7c93504 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -2,6 +2,8 @@ name: Release on: push: + branches: + - main workflow_dispatch: jobs: -- 2.53.0