From aadf57a9936fe63f855f25f5a38e2cbd8e939597 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:05:04 +0000 Subject: [PATCH 1/7] fix: consistent width for native tray menu PR labels Extract format_pr_label() helper with a fixed MAX_LABEL_WIDTH (55 chars). Title is dynamically truncated based on available space after prefix (repo #number) and suffix (comments + age), with space-padding so the suffix right-aligns to a consistent column. Eliminates duplicated label-building code in collapsed/non-collapsed branches. --- src-tauri/src/menu.rs | 51 ++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index df4d4d3..73e9186 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -159,16 +159,7 @@ pub fn build_pr_menu( let show_count = section.prs.len().min(5); for pr in §ion.prs[..show_count] { let age = time_ago(&pr.created_at); - let mut label = format!( - " {} #{} — {}", - pr.repository, - pr.number, - truncate(&pr.title, 32), - ); - if pr.comment_count > 0 { - label.push_str(&format!(" 💬{}", pr.comment_count)); - } - label.push_str(&format!(" {}", age)); + let label = format_pr_label(pr, &age); let icon = avatar_cache.get_image(&pr.author_login); let item = IconMenuItem::with_id( @@ -199,16 +190,7 @@ pub fn build_pr_menu( let show_count = section.prs.len().min(5); for pr in §ion.prs[..show_count] { let age = time_ago(&pr.created_at); - let mut label = format!( - " {} #{} — {}", - pr.repository, - pr.number, - truncate(&pr.title, 32), - ); - if pr.comment_count > 0 { - label.push_str(&format!(" 💬{}", pr.comment_count)); - } - label.push_str(&format!(" {}", age)); + let label = format_pr_label(pr, &age); let icon = avatar_cache.get_image(&pr.author_login); let item = IconMenuItem::with_id( @@ -288,6 +270,35 @@ pub fn build_auth_pending_menu( Ok(menu) } +/// Maximum character width for PR menu item labels. +/// Title is dynamically truncated so the suffix (comments + age) right-aligns. +const MAX_LABEL_WIDTH: usize = 55; + +/// Format a PR label with fixed total width and right-aligned suffix. +fn format_pr_label(pr: &PullRequest, age: &str) -> String { + // Build the right-aligned suffix first so we know how much space the title gets. + let suffix = if pr.comment_count > 0 { + format!(" 💬{} {}", pr.comment_count, age) + } else { + format!(" {}", age) + }; + + let prefix = format!(" {} #{} — ", pr.repository, pr.number); + + let prefix_len = prefix.chars().count(); + let suffix_len = suffix.chars().count(); + let title_budget = MAX_LABEL_WIDTH.saturating_sub(prefix_len + suffix_len); + + let title = truncate(&pr.title, title_budget); + let title_len = title.chars().count(); + + // Pad between title and suffix so suffix is right-aligned to MAX_LABEL_WIDTH. + let pad = MAX_LABEL_WIDTH.saturating_sub(prefix_len + title_len + suffix_len); + let padding = " ".repeat(pad); + + format!("{}{}{}{}", prefix, title, padding, suffix) +} + fn time_ago(iso: &str) -> String { let Ok(dt) = chrono::DateTime::parse_from_rfc3339(iso) else { return String::new(); From a854b79efdb2e4d83333d0d9b4becc05ec109c9c Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:08:07 +0000 Subject: [PATCH 2/7] fix: skip title when budget is zero to preserve label width --- src-tauri/src/menu.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 73e9186..9ed041f 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -289,7 +289,11 @@ fn format_pr_label(pr: &PullRequest, age: &str) -> String { let suffix_len = suffix.chars().count(); let title_budget = MAX_LABEL_WIDTH.saturating_sub(prefix_len + suffix_len); - let title = truncate(&pr.title, title_budget); + let title = if title_budget == 0 { + String::new() + } else { + truncate(&pr.title, title_budget) + }; let title_len = title.chars().count(); // Pad between title and suffix so suffix is right-aligned to MAX_LABEL_WIDTH. From 6d6eb5c32533c5e29e28365a18c9663d3cc02a5b Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:12:05 +0000 Subject: [PATCH 3/7] fix: include ellipsis in truncate budget so labels stay within MAX_LABEL_WIDTH --- src-tauri/src/menu.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 9ed041f..8777d6d 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -289,11 +289,7 @@ fn format_pr_label(pr: &PullRequest, age: &str) -> String { let suffix_len = suffix.chars().count(); let title_budget = MAX_LABEL_WIDTH.saturating_sub(prefix_len + suffix_len); - let title = if title_budget == 0 { - String::new() - } else { - truncate(&pr.title, title_budget) - }; + let title = truncate(&pr.title, title_budget); let title_len = title.chars().count(); // Pad between title and suffix so suffix is right-aligned to MAX_LABEL_WIDTH. @@ -321,10 +317,15 @@ fn time_ago(iso: &str) -> String { } fn truncate(s: &str, max: usize) -> String { + if max == 0 { + return String::new(); + } let mut chars = s.chars(); let truncated: String = chars.by_ref().take(max).collect(); if chars.next().is_some() { - format!("{}…", truncated) + // Reserve one char for the ellipsis so the result is exactly `max` chars. + let trimmed: String = truncated.chars().take(max - 1).collect(); + format!("{}…", trimmed) } else { truncated } From b444c1e57a4f402062b96918dd7abebbc2dcd35c Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:15:38 +0000 Subject: [PATCH 4/7] fix: guarantee minimum title width (6 chars) for edge-case long prefixes --- src-tauri/src/menu.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index 8777d6d..f49b3a0 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -274,6 +274,9 @@ pub fn build_auth_pending_menu( /// Title is dynamically truncated so the suffix (comments + age) right-aligns. const MAX_LABEL_WIDTH: usize = 55; +/// Minimum characters reserved for the title so it is never fully hidden. +const MIN_TITLE_WIDTH: usize = 6; + /// Format a PR label with fixed total width and right-aligned suffix. fn format_pr_label(pr: &PullRequest, age: &str) -> String { // Build the right-aligned suffix first so we know how much space the title gets. @@ -287,13 +290,17 @@ fn format_pr_label(pr: &PullRequest, age: &str) -> String { let prefix_len = prefix.chars().count(); let suffix_len = suffix.chars().count(); - let title_budget = MAX_LABEL_WIDTH.saturating_sub(prefix_len + suffix_len); + // Guarantee a minimum title width even when the prefix + suffix are unusually wide. + let title_budget = MAX_LABEL_WIDTH + .saturating_sub(prefix_len + suffix_len) + .max(MIN_TITLE_WIDTH); let title = truncate(&pr.title, title_budget); let title_len = title.chars().count(); // Pad between title and suffix so suffix is right-aligned to MAX_LABEL_WIDTH. - let pad = MAX_LABEL_WIDTH.saturating_sub(prefix_len + title_len + suffix_len); + let used = prefix_len + title_len + suffix_len; + let pad = MAX_LABEL_WIDTH.saturating_sub(used); let padding = " ".repeat(pad); format!("{}{}{}{}", prefix, title, padding, suffix) From 194ae5f9f3d9ac4e82272e44fe36a5ee0dfbc955 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:19:10 +0000 Subject: [PATCH 5/7] fix: remove MIN_TITLE_WIDTH to prevent label overflow on long repo names --- src-tauri/src/menu.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index f49b3a0..d5c1e29 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -274,9 +274,6 @@ pub fn build_auth_pending_menu( /// Title is dynamically truncated so the suffix (comments + age) right-aligns. const MAX_LABEL_WIDTH: usize = 55; -/// Minimum characters reserved for the title so it is never fully hidden. -const MIN_TITLE_WIDTH: usize = 6; - /// Format a PR label with fixed total width and right-aligned suffix. fn format_pr_label(pr: &PullRequest, age: &str) -> String { // Build the right-aligned suffix first so we know how much space the title gets. @@ -290,10 +287,7 @@ fn format_pr_label(pr: &PullRequest, age: &str) -> String { let prefix_len = prefix.chars().count(); let suffix_len = suffix.chars().count(); - // Guarantee a minimum title width even when the prefix + suffix are unusually wide. - let title_budget = MAX_LABEL_WIDTH - .saturating_sub(prefix_len + suffix_len) - .max(MIN_TITLE_WIDTH); + let title_budget = MAX_LABEL_WIDTH.saturating_sub(prefix_len + suffix_len); let title = truncate(&pr.title, title_budget); let title_len = title.chars().count(); From aa003af02f21e02ca721ea9a5108ae7427cc17d7 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:23:17 +0000 Subject: [PATCH 6/7] fix: truncate repo name when needed to always preserve title in menu labels --- src-tauri/src/menu.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index d5c1e29..f0e4a8f 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -282,16 +282,28 @@ fn format_pr_label(pr: &PullRequest, age: &str) -> String { } else { format!(" {}", age) }; + let suffix_len = suffix.chars().count(); - let prefix = format!(" {} #{} — ", pr.repository, pr.number); + // Fixed parts of the prefix (excluding repo name): " " + " #" + number + " — " + let number_len = pr.number.to_string().chars().count(); + let fixed_overhead = 7 + number_len; - let prefix_len = prefix.chars().count(); - let suffix_len = suffix.chars().count(); - let title_budget = MAX_LABEL_WIDTH.saturating_sub(prefix_len + suffix_len); + // Space available for repo name + title together. + let content_budget = MAX_LABEL_WIDTH.saturating_sub(fixed_overhead + suffix_len); + // Give repo its full name, but cap it to leave at least 6 chars for the title. + let repo_name_len = pr.repository.chars().count(); + let repo_budget = repo_name_len.min(content_budget.saturating_sub(6)); + let repo = truncate(&pr.repository, repo_budget); + let repo_len = repo.chars().count(); + + let title_budget = content_budget.saturating_sub(repo_len); let title = truncate(&pr.title, title_budget); let title_len = title.chars().count(); + let prefix = format!(" {} #{} — ", repo, pr.number); + let prefix_len = prefix.chars().count(); + // Pad between title and suffix so suffix is right-aligned to MAX_LABEL_WIDTH. let used = prefix_len + title_len + suffix_len; let pad = MAX_LABEL_WIDTH.saturating_sub(used); From 3c41799b19d20562b5242901f542dd40d4f29416 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:25:59 +0000 Subject: [PATCH 7/7] fix: use tab character for right-aligned suffix in native menu labels macOS native menus treat \\t as a right-aligned tab stop, which handles proportional-font glyph widths correctly. This replaces the space-padding approach that couldn't account for variable-width characters and emoji. --- src-tauri/src/menu.rs | 44 +++++++++++++------------------------------ 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index f0e4a8f..4f102c8 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -270,46 +270,28 @@ pub fn build_auth_pending_menu( Ok(menu) } -/// Maximum character width for PR menu item labels. -/// Title is dynamically truncated so the suffix (comments + age) right-aligns. -const MAX_LABEL_WIDTH: usize = 55; +/// Maximum characters for the left portion of a PR label (prefix + title). +/// The title is dynamically truncated to fit within this budget. +const MAX_LEFT_WIDTH: usize = 42; -/// Format a PR label with fixed total width and right-aligned suffix. +/// Format a PR label with a tab stop so macOS right-aligns the suffix. +/// +/// Native macOS menus treat `\t` as a right-aligned tab stop, so the suffix +/// (comment count + age) lines up regardless of proportional-font glyph widths. fn format_pr_label(pr: &PullRequest, age: &str) -> String { - // Build the right-aligned suffix first so we know how much space the title gets. let suffix = if pr.comment_count > 0 { - format!(" 💬{} {}", pr.comment_count, age) + format!("💬{} {}", pr.comment_count, age) } else { - format!(" {}", age) + age.to_string() }; - let suffix_len = suffix.chars().count(); - // Fixed parts of the prefix (excluding repo name): " " + " #" + number + " — " - let number_len = pr.number.to_string().chars().count(); - let fixed_overhead = 7 + number_len; - - // Space available for repo name + title together. - let content_budget = MAX_LABEL_WIDTH.saturating_sub(fixed_overhead + suffix_len); - - // Give repo its full name, but cap it to leave at least 6 chars for the title. - let repo_name_len = pr.repository.chars().count(); - let repo_budget = repo_name_len.min(content_budget.saturating_sub(6)); - let repo = truncate(&pr.repository, repo_budget); - let repo_len = repo.chars().count(); - - let title_budget = content_budget.saturating_sub(repo_len); - let title = truncate(&pr.title, title_budget); - let title_len = title.chars().count(); - - let prefix = format!(" {} #{} — ", repo, pr.number); + let prefix = format!("{} #{} — ", pr.repository, pr.number); let prefix_len = prefix.chars().count(); + let title_budget = MAX_LEFT_WIDTH.saturating_sub(prefix_len); - // Pad between title and suffix so suffix is right-aligned to MAX_LABEL_WIDTH. - let used = prefix_len + title_len + suffix_len; - let pad = MAX_LABEL_WIDTH.saturating_sub(used); - let padding = " ".repeat(pad); + let title = truncate(&pr.title, title_budget); - format!("{}{}{}{}", prefix, title, padding, suffix) + format!(" {}{}\t{}", prefix, title, suffix) } fn time_ago(iso: &str) -> String {