Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"apps/desktop-tauri/src-tauri",
]
default-members = ["apps/desktop-tauri/src-tauri"]
exclude = ["vendor/tray-icon"]
resolver = "3"

[workspace.lints.clippy]
Expand All @@ -12,3 +13,8 @@ let_underscore_must_use = "deny"
undocumented_unsafe_blocks = "deny"
cast_possible_truncation = "deny"
cast_possible_wrap = "deny"

# tray-icon 0.21.3 leaves NOTIFYICONDATAW.cbSize zeroed, so Explorer reads the
# legacy layout and cuts tray tooltips at 64 UTF-16 units. See vendor/tray-icon/PATCHED.md.
[patch.crates-io]
tray-icon = { path = "vendor/tray-icon" }
34 changes: 15 additions & 19 deletions apps/desktop-tauri/src-tauri/src/tray_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,11 +482,11 @@ fn build_tooltip(
format!("CodexBar\n{}", lines.join("\n"))
}

/// `NOTIFYICONDATAW.szTip` holds 128 UTF-16 units, but tray-icon 0.21 leaves
/// `cbSize` zeroed, so Explorer reads the pre-Windows 2000 layout and shows
/// only the first 64 units, cutting mid-line without a marker. 63 leaves room
/// for the terminating NUL of that legacy `szTip[64]`.
const TRAY_TOOLTIP_MAX_UTF16: usize = 63;
/// `NOTIFYICONDATAW.szTip` holds 128 UTF-16 units; 127 leaves room for the
/// terminating NUL. The full buffer is only honoured because the vendored
/// tray-icon sets `cbSize` (see `vendor/tray-icon/PATCHED.md`); with a zeroed
/// `cbSize` Explorer falls back to the legacy 64-unit tip.
const TRAY_TOOLTIP_MAX_UTF16: usize = 127;

fn utf16_len(text: &str) -> usize {
text.encode_utf16().count()
Expand Down Expand Up @@ -1056,9 +1056,6 @@ mod tests {
assert!(japanese_label.contains("リセットまで"), "{japanese_label}");
}

/// The full `szTip[128]` budget, so layout tests can use realistic line lengths.
const WIDE_BUDGET: usize = 127;

#[test]
fn fit_tooltip_keeps_short_tooltips_unchanged() {
let tooltip = "CodexBar\nClaude: 87% left\nCodex: 92% left";
Expand All @@ -1075,9 +1072,9 @@ mod tests {
}
let tooltip = lines.join("\n");

let fitted = fit_tooltip(&tooltip, WIDE_BUDGET);
let fitted = fit_tooltip(&tooltip, TRAY_TOOLTIP_MAX_UTF16);

assert!(utf16_len(&fitted) <= WIDE_BUDGET, "{fitted}");
assert!(utf16_len(&fitted) <= TRAY_TOOLTIP_MAX_UTF16, "{fitted}");
let shown: Vec<&str> = fitted.split('\n').collect();
assert_eq!(shown[0], "CodexBar", "{fitted}");
assert!(shown.len() > 2, "{fitted}");
Expand All @@ -1095,9 +1092,9 @@ mod tests {
let long = "A".repeat(115);
let tooltip = format!("CodexBar\n{long}\nB line that is dropped entirely here");

let fitted = fit_tooltip(&tooltip, WIDE_BUDGET);
let fitted = fit_tooltip(&tooltip, TRAY_TOOLTIP_MAX_UTF16);

assert!(utf16_len(&fitted) <= WIDE_BUDGET, "{fitted}");
assert!(utf16_len(&fitted) <= TRAY_TOOLTIP_MAX_UTF16, "{fitted}");
assert_eq!(fitted, format!("CodexBar\n{long}…"));
}

Expand All @@ -1111,10 +1108,10 @@ mod tests {
]
.join("\n");

let fitted = fit_tooltip(&tooltip, WIDE_BUDGET);
let fitted = fit_tooltip(&tooltip, TRAY_TOOLTIP_MAX_UTF16);

assert_eq!(fitted, "CodexBar\nOpenAI: Major outage\nClaude: 87% left…");
assert!(utf16_len(&fitted) <= WIDE_BUDGET);
assert!(utf16_len(&fitted) <= TRAY_TOOLTIP_MAX_UTF16);
}

#[test]
Expand All @@ -1132,23 +1129,22 @@ mod tests {
// Each emoji is two UTF-16 units; a char-count cut would overflow or split a pair.
let tooltip = format!("{}\nCodexBar", "🙂".repeat(100));

let fitted = fit_tooltip(&tooltip, WIDE_BUDGET);
let fitted = fit_tooltip(&tooltip, TRAY_TOOLTIP_MAX_UTF16);

assert!(utf16_len(&fitted) <= WIDE_BUDGET, "{fitted}");
assert!(utf16_len(&fitted) <= TRAY_TOOLTIP_MAX_UTF16, "{fitted}");
assert_eq!(fitted.chars().filter(|ch| *ch == '🙂').count(), 63);
assert!(fitted.ends_with('…'), "{fitted}");
assert!(!fitted.contains('\n'), "{fitted}");
}

#[test]
fn fit_tooltip_keeps_whole_provider_lines_within_the_legacy_tip_limit() {
fn fit_tooltip_keeps_two_typical_provider_lines_whole() {
let tooltip =
"CodexBar\nClaude: 58% left • Resets in 2h 59m\nCodex: 82% left • Resets in 4d 3h";

let fitted = fit_tooltip(tooltip, TRAY_TOOLTIP_MAX_UTF16);

assert!(utf16_len(&fitted) <= 63, "{fitted}");
assert_eq!(fitted, "CodexBar\nClaude: 58% left • Resets in 2h 59m…");
assert_eq!(fitted, tooltip);
}

#[test]
Expand Down
Loading