Compare commits

...

3 commits

Author SHA1 Message Date
a9acd34801
ci: fix release-pipeline running on feature-branches
All checks were successful
Continuous Integration / Build Package (push) Successful in 26s
Continuous Integration / Lint, Check & Test (push) Successful in 56s
Release / Build and Release (push) Successful in 2m37s
2026-04-07 08:09:47 +02:00
9563ff4d93
feat: bump to 0.2.0
All checks were successful
Release / Build and Release (push) Successful in 5s
Continuous Integration / Lint, Check & Test (push) Successful in 2m9s
Continuous Integration / Build Package (push) Successful in 2m43s
2026-04-07 08:06:55 +02:00
42d9ecd3d9
feat: add --minutes flag to timesheet command
All checks were successful
Release / Build and Release (push) Successful in 5s
Continuous Integration / Lint, Check & Test (push) Successful in 1m43s
Continuous Integration / Build Package (push) Successful in 1m56s
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.
2026-04-07 07:57:28 +02:00
6 changed files with 81 additions and 27 deletions

View file

@ -2,6 +2,8 @@ name: Release
on:
push:
branches:
- main
workflow_dispatch:
jobs:

6
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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 {

View file

@ -41,8 +41,14 @@ fn load_all_shards(base_folder: &Path) -> Result<Vec<LocalizedShard>, 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");
}
}

View file

@ -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);
}