From 943efdf6cef21327024bc85c3e9569fc9ee7daef Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:52:58 +0800 Subject: [PATCH] feat(ui): spell tab titles out in full in the rail and title bar The rail's rows and the centred title have room to spare, so they take the whole label from a new full_tab_label rather than tab_label's three-segment cut; only the width they have decides what gets elided. --- src/ui/tab_sidebar.rs | 60 ++++++++++++++++--------------------------- src/ui/tab_strip.rs | 41 ++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/ui/tab_sidebar.rs b/src/ui/tab_sidebar.rs index 175497b1..dcf6137a 100644 --- a/src/ui/tab_sidebar.rs +++ b/src/ui/tab_sidebar.rs @@ -22,7 +22,7 @@ use crate::ui::reorder::{self, Reorder, Surface}; use crate::ui::right_panel::RESIZE_HANDLE_WIDTH; use crate::ui::tab_strip::{ DragTab, REORDER_SLIDE_MS, abbreviate_home, elide_label, elide_path_keep_tail, - elide_tail_clusters, measure_text, strip_host_prefix, + elide_tail_clusters, measure_text, }; pub(crate) const MIN_SIDEBAR_WIDTH: f32 = 180.; @@ -537,44 +537,28 @@ impl Tty7App { // `full_title` is the unelided string the card can expand // back to; `None` means the row is showing a placeholder that // no card can improve on. - let (title_text, full_title) = - if let Some(name) = tab.name.as_ref().filter(|n| !n.trim().is_empty()) { - // A renamed tab is elided like anything else — and so - // the card has to be able to spell the name back out. - let full = SharedString::from(name.trim().to_string()); + // + // The ladder the strip and the switcher climb, read here for + // the name and not for the shortening: this column measures in + // pixels and lets a card expand the row back to the whole + // string, so it wants what `label_of` would have cut down + // rather than the cut. A renamed tab is elided like anything + // else — and so the card has to be able to spell it back out. + let (title_text, full_title) = match self.full_tab_label(tab, Some(window), cx) { + Some(raw) => { + let full = SharedString::from(raw); (full.clone(), Some(full)) - } else { - // The ladder the strip and the switcher climb, read - // here for the name and not for the shortening: this - // column measures in pixels and lets a card expand the - // row back to the whole string, so it wants what - // `label_of` would have cut down rather than the cut. - use crate::ui::machine_mirror::TabLabel; - let (view, home) = tab.label_view(Some(window), cx); - let raw = match view.label() { - TabLabel::Osc(title) | TabLabel::Cwd(title) => { - abbreviate_home(strip_host_prefix(title.trim()), home.as_deref()) - .into_owned() - } - TabLabel::Agent(agent) => agent.display_name().to_string(), - // A tab holding a name got one above. - TabLabel::Named(name) => name.to_string(), - TabLabel::Process(title) => title.to_string(), - TabLabel::Unknown => String::new(), - }; - if raw.trim().is_empty() { - // Nothing to expand: the row is naming an unnamed - // shell, not hiding a title behind an ellipsis. - let placeholder = SharedString::from(t_fmt( - L10nKey::TabUnnamedShell, - &[("n", &((i + 1).to_string()))], - )); - (placeholder, None) - } else { - let full = SharedString::from(raw); - (full.clone(), Some(full)) - } - }; + } + // Nothing to expand: the row is naming an unnamed shell, + // not hiding a title behind an ellipsis. + None => { + let placeholder = SharedString::from(t_fmt( + L10nKey::TabUnnamedShell, + &[("n", &((i + 1).to_string()))], + )); + (placeholder, None) + } + }; let mut branch_shown: Option<(SharedString, SharedString, u32, u32)> = None; let mut cwd_shown: Option<(SharedString, SharedString)> = None; let git_line = match shared_git.is_some() { diff --git a/src/ui/tab_strip.rs b/src/ui/tab_strip.rs index 95c9a6f0..ae479432 100644 --- a/src/ui/tab_strip.rs +++ b/src/ui/tab_strip.rs @@ -1656,6 +1656,37 @@ impl Tty7App { label_of(&view, index, home.as_deref()) } + /// The same ladder as [`tab_label`](Self::tab_label), left whole: a path + /// is only abbreviated under the home and stripped of its `user@host:`, + /// never cut down to its last segments. For the surfaces with room to + /// spare — the rail's rows and the centred title — where only the width + /// they have may decide what gets elided, not a segment count picked for + /// the chips. `None` when there is nothing to say and the caller should + /// fall back to its own placeholder. + pub(crate) fn full_tab_label( + &self, + tab: &Tab, + window: Option<&Window>, + cx: &App, + ) -> Option { + use crate::ui::machine_mirror::TabLabel; + + if let Some(name) = tab.name.as_ref().filter(|n| !n.trim().is_empty()) { + return Some(name.trim().to_string()); + } + let (view, home) = tab.label_view(window, cx); + let raw = match view.label() { + TabLabel::Osc(title) | TabLabel::Cwd(title) => { + abbreviate_home(strip_host_prefix(title.trim()), home.as_deref()).into_owned() + } + TabLabel::Agent(agent) => agent.display_name().to_string(), + TabLabel::Named(name) => name.to_string(), + TabLabel::Process(title) => title.to_string(), + TabLabel::Unknown => String::new(), + }; + (!raw.trim().is_empty()).then_some(raw) + } + /// The New Tab control: one `+` that drops the list of everything it could /// open — the installed shells, and the saved SSH hosts. /// @@ -2384,10 +2415,12 @@ impl Tty7App { .then(|| self.tabs.get(active)) .flatten() .map(|tab| { - let title = match tab.name.as_ref().filter(|n| !n.trim().is_empty()) { - Some(name) => name.trim().to_string(), - None => self.tab_label(tab, active, Some(window), cx), - }; + // Whole, not `tab_label`'s three segments: the bar has half the + // window to spend, and the rail beside it spells the same tab + // out in full. + let title = self + .full_tab_label(tab, Some(window), cx) + .unwrap_or_else(|| self.tab_label(tab, active, Some(window), cx)); div() .absolute() .inset_0()