Skip to content

Commit 7f20987

Browse files
authored
Merge pull request #40 from umpire274/v0.8.4
feat(list): improve National Holiday rendering and meta handling
2 parents f0915a9 + 5611ee1 commit 7f20987

File tree

5 files changed

+214
-22
lines changed

5 files changed

+214
-22
lines changed

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
# Changelog
22

3-
## [0.8.3] – Unreleased
3+
## v0.8.4 — 2026-01-09
4+
5+
### Changed
6+
7+
- Improved `list` command output for `National Holiday` days:
8+
- time placeholders (`--:--`) are no longer shown
9+
- the `meta` field is displayed instead, providing a meaningful holiday description
10+
- Made `National Holiday` row layout adaptive to table width:
11+
- meta column now expands dynamically based on the current table layout
12+
- works consistently across weekday display modes and `--compact`
13+
- Unified and hardened `meta` rendering:
14+
- meta values are filtered, concatenated, and truncated in a single helper
15+
- truncation is Unicode-safe (char-based, UTF-8 safe)
16+
- optional ellipsis (``) is applied when truncation occurs
17+
- Added focused unit tests for `get_meta_string` to validate:
18+
- filtering of empty metadata
19+
- correct concatenation
20+
- Unicode-safe truncation behavior
21+
22+
### Internal
23+
24+
- Refactored list rendering logic to reduce hardcoded column widths
25+
- Improved robustness of table layout against future column size changes
26+
27+
---
28+
29+
## [0.8.3] – 2026-01-08
430

531
### ✨ New features
632

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rtimelogger"
3-
version = "0.8.3"
3+
version = "0.8.4"
44
edition = "2024"
55
authors = ["Umpire274 <umpire274@gmail.com>"]
66
description = "A simple cross-platform CLI tool to track working hours, lunch breaks, and calculate surplus time"
@@ -25,7 +25,7 @@ winresource = "0.1.28"
2525
OriginalFilename = "rtimelogger.exe"
2626
FileDescription = "rTimelogger - Time Tracking Utility"
2727
ProductName = "rTimelogger"
28-
ProductVersion = "0.7.5"
28+
ProductVersion = "0.8.4"
2929
LegalCopyright = "© 2025 Alessandro Maestri"
3030
Icon = "res/rtimelogger.ico"
3131

src/cli/commands/list.rs

Lines changed: 156 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ const CDWORK_W: usize = 7;
9999
fn compact_table_width(mode: WeekdayMode) -> usize {
100100
let dw = date_col_width(mode);
101101
// date + 3 + pos + 3 + triple + 3 + tgt + 3 + dwork
102-
dw + 3 + CPOS_W + 3 + TRIPLE_W + 3 + CTGT_W + 3 + CDWORK_W + 3
102+
dw + 3 + CPOS_W + 3 + TRIPLE_W + 3 + CTGT_W + 3 + CDWORK_W + 7
103103
}
104104

105105
fn format_date_with_weekday(date: &NaiveDate, mode: WeekdayMode) -> String {
@@ -112,6 +112,36 @@ fn format_date_with_weekday(date: &NaiveDate, mode: WeekdayMode) -> String {
112112
}
113113
}
114114

115+
fn get_meta_string(events: &[Event], max_chars: usize) -> String {
116+
if max_chars == 0 {
117+
return String::new();
118+
}
119+
120+
let joined = events
121+
.iter()
122+
.filter_map(|e| e.meta.as_deref())
123+
.filter(|s| !s.trim().is_empty())
124+
.collect::<Vec<_>>()
125+
.join(", ");
126+
127+
let count = joined.chars().count();
128+
if count <= max_chars {
129+
return joined;
130+
}
131+
132+
if max_chars == 1 {
133+
return "…".to_string();
134+
}
135+
136+
let mut out: String = joined.chars().take(max_chars - 1).collect();
137+
out.push('…');
138+
out
139+
}
140+
141+
fn remaining_width(total_width: usize, plain_prefix: &str) -> usize {
142+
total_width.saturating_sub(plain_prefix.len())
143+
}
144+
115145
//
116146
// ───────────────────────────────────────────────────────────────────────────────
117147
// Public entry
@@ -529,19 +559,42 @@ fn print_daily_row(
529559
}
530560
}
531561

532-
println!(
533-
" {:<dw$} | {}{}\x1b[0m | {:^5} | {:^5} | {:^5} | {:^5} | {}{:>7}\x1b[0m",
534-
date_str,
535-
pos_color,
536-
pos_fmt,
537-
first_in_str,
538-
lunch_c,
539-
end_c,
540-
expected_exit_str,
541-
surplus_color,
542-
surplus_display,
543-
dw = dw
544-
);
562+
if day_position == Location::NationalHoliday {
563+
let twidth = daily_table_width(wd_mode);
564+
565+
// prefisso “plain” (senza colori) uguale a ciò che stampi prima del meta
566+
let plain_prefix = format!(" {:<dw$} | {:<16} | ", date_str, pos_label, dw = dw);
567+
let meta_w = remaining_width(twidth, &plain_prefix);
568+
569+
let meta = get_meta_string(events, meta_w);
570+
571+
println!(
572+
" {:<dw$} | {}{:<16}{}\x1b[0m | {}{:<meta_w$}{}",
573+
date_str,
574+
pos_color,
575+
pos_label,
576+
colors::RESET,
577+
pos_color,
578+
meta,
579+
colors::RESET,
580+
dw = dw,
581+
meta_w = meta_w,
582+
);
583+
} else {
584+
println!(
585+
" {:<dw$} | {}{}\x1b[0m | {:^5} | {:^5} | {:^5} | {:^5} | {}{:>7}\x1b[0m",
586+
date_str,
587+
pos_color,
588+
pos_fmt,
589+
first_in_str,
590+
lunch_c,
591+
end_c,
592+
expected_exit_str,
593+
surplus_color,
594+
surplus_display,
595+
dw = dw
596+
);
597+
}
545598

546599
surplus_opt
547600
}
@@ -616,7 +669,7 @@ fn print_compact_header(wd_mode: WeekdayMode) {
616669
let twidth = compact_table_width(wd_mode);
617670

618671
println!(
619-
"{:^dw$} | {:^12} | {:^21} | {:^5} | {:^7}",
672+
"{:^dw$} | {:^16} | {:^21} | {:^5} | {:^7}",
620673
"DATE",
621674
"POSITION",
622675
"IN / LNCH / OUT",
@@ -652,9 +705,9 @@ fn print_daily_row_compact(
652705
let pos_label = day_position.label();
653706
let pos_color = day_position.color();
654707

655-
if day_position == Location::Holiday || day_position == Location::NationalHoliday {
708+
if day_position == Location::Holiday {
656709
println!(
657-
"{:<dw$} | {}{:<12}{}\x1b[0m | {:<21} | {:^5} | {}Δ -{}\x1b[0m",
710+
"{:<dw$} | {}{:<16}{}\x1b[0m | {:<21} | {:^5} | {}Δ -{}\x1b[0m",
658711
date_str,
659712
pos_color,
660713
pos_label,
@@ -666,6 +719,27 @@ fn print_daily_row_compact(
666719
dw = dw
667720
);
668721
return Some(0);
722+
} else if day_position == Location::NationalHoliday {
723+
let twidth = compact_table_width(wd_mode);
724+
725+
let plain_prefix = format!("{:<dw$} | {:<16} | ", date_str, pos_label, dw = dw);
726+
let meta_w = remaining_width(twidth, &plain_prefix);
727+
728+
let meta = get_meta_string(events, meta_w);
729+
730+
println!(
731+
"{:<dw$} | {}{:<16}{}\x1b[0m | {}{:<meta_w$}{}",
732+
date_str,
733+
pos_color,
734+
pos_label,
735+
colors::RESET,
736+
pos_color,
737+
meta,
738+
colors::RESET,
739+
dw = dw,
740+
meta_w = meta_w
741+
);
742+
return Some(0);
669743
}
670744

671745
let first_in = timeline.pairs[0].in_event.timestamp();
@@ -718,7 +792,7 @@ fn print_daily_row_compact(
718792
let times_string = format!("{} / {} / {}", first_in_str, lunch_str, end_str);
719793
let delta_value = format!("Δ {}", delta_str);
720794
println!(
721-
"{:<dw$} | {}{:<12}{}\x1b[0m | {:<21} | {:^5} | {}{}{}\x1b[0m",
795+
"{:<dw$} | {}{:<16}{}\x1b[0m | {:<21} | {:^5} | {}{}{}\x1b[0m",
722796
date_str,
723797
pos_color,
724798
pos_label,
@@ -733,3 +807,67 @@ fn print_daily_row_compact(
733807

734808
surplus_opt
735809
}
810+
811+
#[cfg(test)]
812+
mod tests {
813+
use super::*;
814+
815+
// Helper per creare Event con meta valorizzato.
816+
// Variante A: se Event implementa Default.
817+
fn ev(meta: Option<&str>) -> Event {
818+
Event::test_with_meta(meta)
819+
}
820+
821+
// Se Event NON implementa Default, commenta la funzione sopra e crea qui un costruttore
822+
// coerente col tuo modello (es. Event::new(...) o struct literal con tutti i campi richiesti).
823+
824+
#[test]
825+
fn meta_string_returns_empty_when_max_is_zero() {
826+
let events = vec![ev(Some("Epiphany"))];
827+
assert_eq!(get_meta_string(&events, 0), "");
828+
}
829+
830+
#[test]
831+
fn meta_string_filters_empty_and_whitespace() {
832+
let events = vec![ev(Some("")), ev(Some(" ")), ev(Some("Epiphany"))];
833+
assert_eq!(get_meta_string(&events, 100), "Epiphany");
834+
}
835+
836+
#[test]
837+
fn meta_string_joins_multiple_meta_with_comma_space() {
838+
let events = vec![ev(Some("Epiphany")), ev(Some("Republic Day"))];
839+
assert_eq!(get_meta_string(&events, 100), "Epiphany, Republic Day");
840+
}
841+
842+
#[test]
843+
fn meta_string_truncates_unicode_safely_by_chars() {
844+
let events = vec![ev(Some("caffè 漢字")), ev(Some("fine"))];
845+
846+
let full = get_meta_string(&events, 1_000);
847+
assert_eq!(full, "caffè 漢字, fine");
848+
849+
let n = 7;
850+
851+
// Atteso secondo policy ellissi: max_chars include il carattere '…'
852+
let expected = if n == 0 {
853+
String::new()
854+
} else if full.chars().count() <= n {
855+
full.clone()
856+
} else if n == 1 {
857+
"…".to_string()
858+
} else {
859+
let mut s: String = full.chars().take(n - 1).collect();
860+
s.push('…');
861+
s
862+
};
863+
864+
let got = get_meta_string(&events, n);
865+
assert_eq!(got, expected);
866+
}
867+
868+
#[test]
869+
fn meta_string_does_not_truncate_when_within_limit() {
870+
let events = vec![ev(Some("Epiphany"))];
871+
assert_eq!(get_meta_string(&events, 10), "Epiphany");
872+
}
873+
}

src/models/event.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,32 @@ impl Event {
112112

113113
Ok(exists)
114114
}
115+
116+
#[cfg(test)]
117+
pub fn test_with_meta(meta: Option<&str>) -> Self {
118+
Self {
119+
id: 0,
120+
date: Default::default(),
121+
time: Default::default(),
122+
kind: EventType::In,
123+
location: Location::Office,
124+
lunch: None,
125+
work_gap: false,
126+
pair: 0,
127+
source: "".to_string(),
128+
meta: meta.map(|s| s.to_string()),
129+
// Inizializza qui TUTTI gli altri campi con valori “dummy” validi.
130+
// Esempi tipici:
131+
// id: 0,
132+
// date: chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap(),
133+
// kind: EventKind::In,
134+
// location: Location::Office,
135+
// lunch: None,
136+
// source: "test".into(),
137+
// pair: 0,
138+
// work_gap: false,
139+
// ...
140+
created_at: "".to_string(),
141+
}
142+
}
115143
}

0 commit comments

Comments
 (0)