From c9bc517931171e2536e0f869e6f0269ba90457b1 Mon Sep 17 00:00:00 2001 From: fix2it Date: Sun, 18 Oct 2026 22:34:45 +0300 Subject: [PATCH 1/5] fix(theme): round short reset durations instead of truncating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit duration_short truncated to whole units, so 4d23h read "4d" and 59m40s could not be told from 59m. It now rounds: half steps below 10 h and 10 d (1.5ч, 2.5д), whole units above, minutes clamped to 59 so the next step is "1h", and 23h45m rolls over to "1d". Co-Authored-By: Claude Opus 5.5 --- src/theme_engine/tests.rs | 57 +++++++++++++++++++++++++- src/theme_engine/theme_expression.rs | 60 +++++++++++++++++++++------- 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/src/theme_engine/tests.rs b/src/theme_engine/tests.rs index f97ba5a1..14df2b7c 100644 --- a/src/theme_engine/tests.rs +++ b/src/theme_engine/tests.rs @@ -485,9 +485,64 @@ fn templates_apply_numeric_character_formats() { format_template("Used {claude.session.percentage:0.0}%", &context), "Used 73.5%" ); - assert_eq!(format_template("{reset:duration_short}", &context), "4d"); + assert_eq!(format_template("{reset:duration_short}", &context), "5d"); } +#[test] +fn duration_short_precision_scale_and_half_steps() { + let mut context = DataContext::default(); + context.insert_string("i18n.day_suffix", "д"); + context.insert_string("i18n.hour_suffix", "ч"); + context.insert_string("i18n.minute_suffix", "м"); + context.insert_string("i18n.second_suffix", "с"); + context.insert_string("i18n.now", "сейчас"); + + let check = |secs: f64, expected: &str, ctx: &DataContext| { + let mut c = ctx.clone(); + c.insert("val", secs); + assert_eq!(format_template("{val:duration_short}", &c), expected); + }; + + // Minutes under 1 hour (< 3600s) + check(3580.0, "59м", &context); // 59m 40s must clamp to 59m, never 60m + check(3300.0, "55м", &context); + check(2700.0, "45м", &context); + check(900.0, "15м", &context); + check(60.0, "1м", &context); + + // Hours under 10 hours (1h .. 10h): 0.5 step rounding with dot + check(3600.0, "1ч", &context); + check(3900.0, "1ч", &context); // 1h 05m -> 1ч + check(5400.0, "1.5ч", &context); // 1h 30m -> 1.5ч + check(7800.0, "2ч", &context); // 2h 10m -> 2ч + check(9300.0, "2.5ч", &context); // 2h 35m -> 2.5ч + check(10500.0, "3ч", &context); // 2h 55m -> 3ч + check(34200.0, "9.5ч", &context); // 9h 30m -> 9.5ч + + // Hours >= 10 hours: integer math rounding + check(36000.0, "10ч", &context); + check(43200.0, "12ч", &context); + check(66300.0, "18ч", &context); // 18h 25m -> 18ч + check(85500.0, "1д", &context); // 23h 45m -> rolls over to 1д + + // Days < 10 days: 0.5 step rounding with dot + check(86400.0, "1д", &context); + check(129600.0, "1.5д", &context); // 1d 12h -> 1.5д + check(216000.0, "2.5д", &context); // 2d 12h -> 2.5д + check(532800.0, "6д", &context); // 6d 4h -> strictly 6д + check(565200.0, "6.5д", &context); // 6d 13h -> 6.5д + check(604800.0, "7д", &context); // 7d -> 7д + + // Days >= 10 days: integer math rounding + check(892800.0, "10д", &context); // 10d 8h -> 10д + check(1036800.0, "12д", &context); // 12d -> 12д + + // Seconds and now + check(45.0, "45с", &context); + check(0.0, "сейчас", &context); +} + + #[test] fn text_templates_allow_if_to_return_quoted_strings() { let mut context = DataContext::default(); diff --git a/src/theme_engine/theme_expression.rs b/src/theme_engine/theme_expression.rs index 62bf59ca..1d0ff10d 100644 --- a/src/theme_engine/theme_expression.rs +++ b/src/theme_engine/theme_expression.rs @@ -582,26 +582,58 @@ pub(super) fn localized<'a>(context: &'a DataContext, name: &str, fallback: &'a context.get_string(name).unwrap_or(fallback) } +fn format_half_step(val: f64, suffix: &str) -> String { + let rounded = (val * 2.0).round() / 2.0; + if rounded.fract().abs() < 1e-4 { + format!("{:.0}{suffix}", rounded) + } else { + format!("{:.1}{suffix}", rounded) + } +} + +pub(super) fn format_duration_short(seconds: u64, context: &DataContext) -> String { + let day_suffix = localized(context, "i18n.day_suffix", "d"); + let hour_suffix = localized(context, "i18n.hour_suffix", "h"); + let minute_suffix = localized(context, "i18n.minute_suffix", "m"); + let second_suffix = localized(context, "i18n.second_suffix", "s"); + + if seconds >= 86_400 { + if seconds >= 864_000 { + let days = (seconds + 43_200) / 86_400; + format!("{days}{day_suffix}") + } else { + let days = seconds as f64 / 86_400.0; + format_half_step(days, day_suffix) + } + } else if seconds >= 3_600 { + if seconds >= 36_000 { + let hours = (seconds + 1800) / 3600; + if hours >= 24 { + format!("1{day_suffix}") + } else { + format!("{hours}{hour_suffix}") + } + } else { + let hours = seconds as f64 / 3600.0; + format_half_step(hours, hour_suffix) + } + } else if seconds >= 60 { + let minutes = ((seconds + 30) / 60).min(59); + format!("{minutes}{minute_suffix}") + } else if seconds > 0 { + format!("{seconds}{second_suffix}") + } else { + localized(context, "i18n.now", "now").to_string() + } +} + pub(super) fn format_value(value: f64, format: &str, context: &DataContext) -> String { if let Some(value) = format_timestamp(value, format, context) { return value; } if format.eq_ignore_ascii_case("duration_short") { let seconds = value.max(0.0).round() as u64; - let days = seconds / 86_400; - let hours = seconds / 3_600; - let minutes = seconds / 60; - return if days > 0 { - format!("{days}{}", localized(context, "i18n.day_suffix", "d")) - } else if hours > 0 { - format!("{hours}{}", localized(context, "i18n.hour_suffix", "h")) - } else if minutes > 0 { - format!("{minutes}{}", localized(context, "i18n.minute_suffix", "m")) - } else if seconds > 0 { - format!("{seconds}{}", localized(context, "i18n.second_suffix", "s")) - } else { - localized(context, "i18n.now", "now").to_string() - }; + return format_duration_short(seconds, context); } if format.eq_ignore_ascii_case("duration") { let seconds = value.max(0.0).round() as u64; From 144576b09a245ab9fedd8622c0fa5af0c2ec528e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:29:58 -0500 Subject: [PATCH 2/5] chore(deps): bump serde_json from 1.0.151 to 1.0.154 (#6) Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.151 to 1.0.154. - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.151...v1.0.154) --- updated-dependencies: - dependency-name: serde_json dependency-version: 1.0.154 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c203c717..55882753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2196,9 +2196,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ "itoa", "memchr", From 4207f1819fb1089f99b2bc5820e2d719b6f75eb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:30:10 -0500 Subject: [PATCH 3/5] chore(deps): bump lucide-icons from 1.52.0 to 1.55.0 (#4) Bumps [lucide-icons](https://github.com/WhySoBad/lucide-icons-rs) from 1.52.0 to 1.55.0. - [Commits](https://github.com/WhySoBad/lucide-icons-rs/commits) --- updated-dependencies: - dependency-name: lucide-icons dependency-version: 1.55.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55882753..8f31f7c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1306,9 +1306,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lucide-icons" -version = "1.52.0" +version = "1.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600e9f65483f450fef5d8e5ea075f55ab891ccfcd0671d5e4dd60dd18c0d5a2b" +checksum = "15477dff3b89bd25ddbbd9b6855fb1c7055558a296f1d2282a6294b2b37ab458" [[package]] name = "memchr" From 10a384823fa06c4ef2e5017634afba41e3ce9ad2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:30:26 -0500 Subject: [PATCH 4/5] chore(deps): bump winresource from 0.1.31 to 0.1.34 (#7) Bumps [winresource](https://github.com/BenjaminRi/winresource) from 0.1.31 to 0.1.34. - [Commits](https://github.com/BenjaminRi/winresource/commits) --- updated-dependencies: - dependency-name: winresource dependency-version: 0.1.34 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f31f7c5..b22360f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3216,9 +3216,9 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.31" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b" dependencies = [ "toml", "version_check", From f30db1d9b8e85a258948a0a2efeacf76164466b6 Mon Sep 17 00:00:00 2001 From: fix2it Date: Mon, 19 Oct 2026 02:07:59 +0300 Subject: [PATCH 5/5] test(studio): pin English in the studio test app Two UI text assertions followed the system language and failed on non-English Windows. Co-Authored-By: Claude Opus 5.5 --- src/studio_app/tests.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/studio_app/tests.rs b/src/studio_app/tests.rs index 0919d912..6a27132c 100644 --- a/src/studio_app/tests.rs +++ b/src/studio_app/tests.rs @@ -436,7 +436,12 @@ fn app_with_surfaces(surfaces: Vec) -> StudioApp { update_status: crate::dashboard::UpdateStatus::Idle, diagnostics: studio_diagnostics::DiagnosticsView::new(), page: Page::Studio, - settings: SettingsFile::default(), + // Pin English so UI text assertions do not follow the system language. + settings: { + let mut settings = SettingsFile::default(); + settings.language = Some("en-US".into()); + settings + }, synced_poll_interval_ms: SettingsFile::default().poll_interval_ms, synced_taskbar_lock: false, poll_interval_editor_generation: 0,