mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(tabs): name a titleless tab after its working directory (#792)
Refs #740.
This commit is contained in:
@@ -718,7 +718,7 @@ impl RemoteTerminal {
|
||||
/// here wins: it describes where the shell actually landed, which is not
|
||||
/// always where we asked (a missing directory sends the daemon home, an
|
||||
/// rc file may `cd` on its own).
|
||||
fn seed_cwd(&self, cwd: Option<PathBuf>) {
|
||||
pub(crate) fn seed_cwd(&self, cwd: Option<PathBuf>) {
|
||||
let Some(cwd) = cwd else { return };
|
||||
if let Ok(mut guard) = self.cwd.lock() {
|
||||
guard.get_or_insert(cwd);
|
||||
|
||||
@@ -163,6 +163,24 @@ pub struct NativeSshParts {
|
||||
/// What a pane is called when nothing running in it has said otherwise.
|
||||
pub(crate) const DEFAULT_TITLE: &str = "tty7";
|
||||
|
||||
/// What a pane is *saying* about itself, if anything — the reading behind
|
||||
/// [`TerminalView::stated_title`], split out so it can be pinned without a
|
||||
/// live pane.
|
||||
///
|
||||
/// Anything but the placeholder counts. That is wider than "arrived over OSC
|
||||
/// 0/2" on purpose: an SSH pane answers to the host it dialled and a workspace
|
||||
/// pane to its workspace's name, and those are names tty7 gave the pane
|
||||
/// deliberately (#438) rather than the absence of one. The literal string
|
||||
/// `tty7` is the only title that says nothing, because it is the app's own
|
||||
/// name standing in for a pane that has never introduced itself.
|
||||
pub(crate) fn stated_title(title: &str) -> Option<&str> {
|
||||
match title.trim() {
|
||||
"" => None,
|
||||
t if t == DEFAULT_TITLE => None,
|
||||
t => Some(t),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShellParts {
|
||||
terminal: RemoteTerminal,
|
||||
pub(crate) pane_id: u64,
|
||||
@@ -1563,6 +1581,16 @@ impl TerminalView {
|
||||
self.terminal.foreground_cwd()
|
||||
}
|
||||
|
||||
/// The title this pane is showing, or `None` while it is still answering
|
||||
/// to the app's own name — see [`stated_title`]. The label ladder reads
|
||||
/// this where the machine tree reads
|
||||
/// [`PaneRecord::osc_title`](tty7_core::core::machine::PaneRecord::osc_title),
|
||||
/// which is what lets the tab strip and the switcher name a tab the same
|
||||
/// way.
|
||||
pub(crate) fn stated_title(&self) -> Option<&str> {
|
||||
stated_title(&self.title)
|
||||
}
|
||||
|
||||
/// Sets how opaque the pane wants this terminal painted; the pane leaf
|
||||
/// calls this every frame while rendering, and the terminal element
|
||||
/// blends its colours toward the window background during paint (see
|
||||
@@ -7343,6 +7371,31 @@ fn drag_scroll_step(overshoot: f32) -> i32 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
/// What the label ladder asks a pane: are you showing a name of your own,
|
||||
/// or still standing under the app's? (#740)
|
||||
#[test]
|
||||
fn a_pane_states_a_title_whenever_it_is_not_the_placeholder() {
|
||||
use super::stated_title;
|
||||
|
||||
// Nothing has spoken — this is the pane a directory stands in for.
|
||||
assert_eq!(stated_title("tty7"), None);
|
||||
assert_eq!(stated_title(" tty7 "), None);
|
||||
assert_eq!(stated_title(" "), None);
|
||||
|
||||
// A title from the program running in it.
|
||||
assert_eq!(stated_title("vim — main.rs"), Some("vim — main.rs"));
|
||||
assert_eq!(stated_title(" user@host:~/repo "), Some("user@host:~/repo"));
|
||||
// A default tty7 chose for the pane itself is a name, not the absence
|
||||
// of one: an SSH pane answers to its host (#438) and a workspace pane
|
||||
// to its workspace, and neither gives way to a directory.
|
||||
assert_eq!(stated_title("prod-web"), Some("prod-web"));
|
||||
// So does the state a finished pane is left showing.
|
||||
assert_eq!(
|
||||
stated_title("tty7 — process exited"),
|
||||
Some("tty7 — process exited")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unfocused_input_caret_is_always_a_steady_outline() {
|
||||
use super::{InputCaretPaint, input_caret_paint};
|
||||
|
||||
@@ -536,6 +536,67 @@ impl Tab {
|
||||
(leaf.title.clone(), leaf.display_home(cx))
|
||||
}
|
||||
|
||||
/// This tab as the shared label ladder reads it, together with what a `~`
|
||||
/// in whatever it ends up named would mean.
|
||||
///
|
||||
/// [`TabView`](tty7_core::core::tab_view::TabView) is how a tab looks to
|
||||
/// someone who is *not* the window showing it — the switcher listing
|
||||
/// another window's workspace, `tty7 tab ls` on the far side of a socket.
|
||||
/// Building one here from the live pane is what stops this window having a
|
||||
/// second opinion: both sides then rank a given name, a title, an agent and
|
||||
/// a directory through
|
||||
/// [`TabView::label`](tty7_core::core::tab_view::TabView::label), so the
|
||||
/// strip's answer to "which repo is this?" is the switcher's answer too.
|
||||
///
|
||||
/// Everything comes off the one leaf the tab names itself after, so the
|
||||
/// title and the directory standing in for it can never describe different
|
||||
/// panes (#580).
|
||||
pub(crate) fn label_view(
|
||||
&self,
|
||||
window: Option<&Window>,
|
||||
cx: &App,
|
||||
) -> (
|
||||
tty7_core::core::tab_view::TabView,
|
||||
Option<std::path::PathBuf>,
|
||||
) {
|
||||
let name = self.name.clone();
|
||||
let Some(leaf) = self.title_leaf(window, cx) else {
|
||||
return (
|
||||
tty7_core::core::tab_view::TabView {
|
||||
id: self.tree_id.get(),
|
||||
name,
|
||||
title: String::new(),
|
||||
osc_title: None,
|
||||
cwd: None,
|
||||
agent: None,
|
||||
status: None,
|
||||
live: false,
|
||||
panes: 0,
|
||||
},
|
||||
None,
|
||||
);
|
||||
};
|
||||
let leaf = leaf.read(cx);
|
||||
let view = tty7_core::core::tab_view::TabView {
|
||||
id: self.tree_id.get(),
|
||||
name,
|
||||
// The tree's `title` is the foreground process name — what it falls
|
||||
// back on once a pane has said nothing about itself. A live pane's
|
||||
// equivalent is the placeholder it answers to unprompted: any
|
||||
// *other* default it was given (an SSH host, a workspace name) is a
|
||||
// name tty7 chose for it deliberately, and `stated_title` hands
|
||||
// those up as the title the pane is showing.
|
||||
title: crate::terminal::view::DEFAULT_TITLE.to_string(),
|
||||
osc_title: leaf.stated_title().map(str::to_string),
|
||||
cwd: leaf.cwd().map(|p| p.display().to_string()),
|
||||
agent: leaf.agent(),
|
||||
status: leaf.agent_session().map(|s| s.status),
|
||||
live: !leaf.terminal.exited,
|
||||
panes: self.pane.terminals().len(),
|
||||
};
|
||||
(view, leaf.display_home(cx))
|
||||
}
|
||||
|
||||
pub(crate) fn git_status(
|
||||
&self,
|
||||
window: Option<&Window>,
|
||||
|
||||
+8
-35
@@ -2965,47 +2965,20 @@ impl TabRow {
|
||||
}
|
||||
}
|
||||
|
||||
/// Names a tab of a workspace this window does not own, matching what
|
||||
/// `Tty7App::tab_label` shows for local ones.
|
||||
/// Names a tab of a workspace this window does not own.
|
||||
///
|
||||
/// The two read different sources and have to be talked into agreeing. A local
|
||||
/// tab is named by its live terminal's OSC title, which shells set to the
|
||||
/// working directory and agents overwrite with what they are doing. The tree
|
||||
/// carries a copy of that title (`PaneRecord::osc_title`), which is what makes
|
||||
/// the two columns agree; `PaneRecord::title` is the *foreground process name*
|
||||
/// ("zsh") and only stands in when there is no title at all.
|
||||
/// The two surfaces used to read different sources and had to be talked into
|
||||
/// agreeing: a local tab was named by its live terminal's title, this one by
|
||||
/// the tree's copy of it (`PaneRecord::osc_title`). They now go through the one
|
||||
/// renderer, [`crate::ui::tab_strip::label_of`] — a local tab is turned into
|
||||
/// the same [`TabView`](crate::ui::machine_mirror::TabView) this one already
|
||||
/// is, so neither column can rank the evidence its own way.
|
||||
fn tab_view_label(
|
||||
view: &crate::ui::machine_mirror::TabView,
|
||||
index: usize,
|
||||
home: Option<&std::path::Path>,
|
||||
) -> String {
|
||||
let unnamed = || {
|
||||
t_fmt(
|
||||
L10nKey::TabUnnamedShell,
|
||||
&[("n", &((index + 1).to_string()))],
|
||||
)
|
||||
};
|
||||
// A path can shorten away to nothing (a bare "user@host:"), and the process
|
||||
// name is still worth more than a number.
|
||||
let shortened = |raw: &str| match crate::ui::tab_strip::short_title(raw, home) {
|
||||
shortened if !shortened.trim().is_empty() => shortened,
|
||||
_ => match view.title.trim() {
|
||||
"" => unnamed(),
|
||||
title => title.to_string(),
|
||||
},
|
||||
};
|
||||
match view.label() {
|
||||
crate::ui::machine_mirror::TabLabel::Named(name) => name.to_string(),
|
||||
// Through `short_title` because the local strip puts its own titles
|
||||
// through it too: the shell integration writes `user@host:~/dir`, and a
|
||||
// tab that spelled that out in full where the strip says "…/dir" would
|
||||
// be the same disagreement in a new place.
|
||||
crate::ui::machine_mirror::TabLabel::Osc(title) => shortened(title),
|
||||
crate::ui::machine_mirror::TabLabel::Agent(agent) => agent.display_name().to_string(),
|
||||
crate::ui::machine_mirror::TabLabel::Cwd(cwd) => shortened(cwd),
|
||||
crate::ui::machine_mirror::TabLabel::Process(title) => title.to_string(),
|
||||
crate::ui::machine_mirror::TabLabel::Unknown => unnamed(),
|
||||
}
|
||||
crate::ui::tab_strip::label_of(view, index, home)
|
||||
}
|
||||
|
||||
impl Group {
|
||||
|
||||
+19
-4
@@ -326,9 +326,24 @@ impl Tty7App {
|
||||
);
|
||||
(shown, Some(full))
|
||||
} else {
|
||||
let (raw_title, home) = tab.leaf_title_and_home(Some(window), cx);
|
||||
let title = strip_host_prefix(raw_title.trim());
|
||||
let raw = abbreviate_home(title, home.as_deref());
|
||||
// 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.
|
||||
@@ -338,7 +353,7 @@ impl Tty7App {
|
||||
));
|
||||
(placeholder, None)
|
||||
} else {
|
||||
let full = SharedString::from(raw.as_ref());
|
||||
let full = SharedString::from(raw);
|
||||
let shown = elide_label(
|
||||
&window.text_system(),
|
||||
title_font,
|
||||
|
||||
+360
-27
@@ -140,6 +140,94 @@ pub(crate) fn short_title(raw: &str, home: Option<&std::path::Path>) -> String {
|
||||
label
|
||||
}
|
||||
|
||||
/// The one place a tab gets its displayed name, whichever surface is asking.
|
||||
///
|
||||
/// `label()` ranks the evidence — a given name, then the title the pane is
|
||||
/// showing, then an agent, then the working directory, then the process it is
|
||||
/// running — and this renders whatever came back. Both callers arrive with a
|
||||
/// [`TabView`](crate::ui::machine_mirror::TabView): the switcher reads one out
|
||||
/// of the machine tree for a window it does not own, and the strip builds one
|
||||
/// from its own live panes in
|
||||
/// [`Tab::label_view`](crate::ui::app::Tab::label_view).
|
||||
///
|
||||
/// They used to rank their own evidence, and disagreed where it mattered most:
|
||||
/// a pane with a working directory and no title — every non-PowerShell shell
|
||||
/// tty7 ships integration for reports OSC 7 and no OSC 0 — was listed by the
|
||||
/// switcher as `~/repo/tty7` and by the strip that owned it as "tty7", the
|
||||
/// app's own name (#740).
|
||||
pub(crate) fn label_of(
|
||||
view: &crate::ui::machine_mirror::TabView,
|
||||
index: usize,
|
||||
home: Option<&std::path::Path>,
|
||||
) -> String {
|
||||
use crate::ui::machine_mirror::TabLabel;
|
||||
|
||||
let unnamed = || {
|
||||
t_fmt(
|
||||
L10nKey::TabUnnamedShell,
|
||||
&[("n", &((index + 1).to_string()))],
|
||||
)
|
||||
};
|
||||
// A path can shorten away to nothing (a bare "user@host:"), and the process
|
||||
// name the tree carries ("zsh") is still worth more than a number.
|
||||
//
|
||||
// Through `stated_title` because a tab of *this* window has no process name
|
||||
// to offer: `Tab::label_view` fills that slot with the placeholder a pane
|
||||
// answers to before anything has spoken, and printing the app's own name
|
||||
// here is the one thing #740 exists to stop. Nothing to say falls to the
|
||||
// number, which is what the strip showed before it shared this renderer.
|
||||
let shortened = |raw: &str| match short_title(raw, home) {
|
||||
shortened if !shortened.trim().is_empty() => shortened,
|
||||
_ => match crate::terminal::view::stated_title(&view.title) {
|
||||
Some(title) => title.to_string(),
|
||||
None => unnamed(),
|
||||
},
|
||||
};
|
||||
match view.label() {
|
||||
TabLabel::Named(name) => name.to_string(),
|
||||
// Through `short_title` because a title is so often a path: the shell
|
||||
// integration writes `user@host:~/dir`, and a tab spelling that out in
|
||||
// full where the one beside it says "…/dir" would be the same
|
||||
// disagreement in a new place.
|
||||
TabLabel::Osc(title) => shortened(title),
|
||||
TabLabel::Agent(agent) => agent.display_name().to_string(),
|
||||
TabLabel::Cwd(cwd) => shortened(cwd),
|
||||
TabLabel::Process(title) => title.to_string(),
|
||||
TabLabel::Unknown => unnamed(),
|
||||
}
|
||||
}
|
||||
|
||||
/// What a row can add on hover: the name behind the one [`label_of`] cut down,
|
||||
/// or `None` when it cut nothing and the tooltip would only repeat the row.
|
||||
///
|
||||
/// The comparison has to happen on the *same* spelling, which is the whole
|
||||
/// trick here. `label_of` abbreviates a path under the home before it elides
|
||||
/// it, and this returns the abbreviated form too, so a raw `/Users/x/repo`
|
||||
/// measured against a label of `~/repo` looks like a difference that isn't
|
||||
/// one — and every tab named after a directory inside the home would hang a
|
||||
/// tooltip saying exactly what it already says. Abbreviate first, compare
|
||||
/// after.
|
||||
fn tooltip_of(
|
||||
view: &crate::ui::machine_mirror::TabView,
|
||||
index: usize,
|
||||
home: Option<&std::path::Path>,
|
||||
) -> Option<SharedString> {
|
||||
use crate::ui::machine_mirror::TabLabel;
|
||||
|
||||
// The other rungs are never shortened: a given name and a process name are
|
||||
// printed whole, and an agent's is a word.
|
||||
let raw = match view.label() {
|
||||
TabLabel::Osc(title) => title,
|
||||
TabLabel::Cwd(cwd) => cwd,
|
||||
_ => return None,
|
||||
};
|
||||
let full = abbreviate_home(raw.trim(), home);
|
||||
if full.trim().is_empty() || full.as_ref() == label_of(view, index, home).as_str() {
|
||||
return None;
|
||||
}
|
||||
Some(SharedString::from(full.into_owned()))
|
||||
}
|
||||
|
||||
/// Width of `text` shaped in `font` at `size`, in pixels.
|
||||
///
|
||||
/// The window's text system caches shaped runs, so measuring the same labels
|
||||
@@ -1229,6 +1317,11 @@ impl Tty7App {
|
||||
/// read `…/a/b/c` with no way to find out which `a` that was. `None` when
|
||||
/// nothing was dropped, so tabs that already show their whole name stay
|
||||
/// quiet under the pointer.
|
||||
///
|
||||
/// It has to unshorten whatever the label was *made of*, which is why it
|
||||
/// reads the same [`TabView`](crate::ui::machine_mirror::TabView) the label
|
||||
/// did: a tab named after its directory wants that directory spelled out,
|
||||
/// not the title it never had. See [`tooltip_of`].
|
||||
pub(crate) fn tab_title_tooltip(
|
||||
&self,
|
||||
tab: &Tab,
|
||||
@@ -1236,19 +1329,13 @@ impl Tty7App {
|
||||
window: Option<&Window>,
|
||||
cx: &App,
|
||||
) -> Option<SharedString> {
|
||||
if tab.name.as_ref().is_some_and(|n| !n.trim().is_empty()) {
|
||||
return None;
|
||||
}
|
||||
let (raw, home) = tab.leaf_title_and_home(window, cx);
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() || raw == self.tab_label(tab, index, window, cx) {
|
||||
return None;
|
||||
}
|
||||
Some(SharedString::from(
|
||||
abbreviate_home(raw, home.as_deref()).into_owned(),
|
||||
))
|
||||
let (view, home) = tab.label_view(window, cx);
|
||||
tooltip_of(&view, index, home.as_deref())
|
||||
}
|
||||
|
||||
/// What this window puts on a tab of its own — the same ladder, through the
|
||||
/// same renderer, as the switcher uses for a tab of somebody else's window.
|
||||
/// See [`label_of`].
|
||||
pub(crate) fn tab_label(
|
||||
&self,
|
||||
tab: &Tab,
|
||||
@@ -1256,22 +1343,8 @@ impl Tty7App {
|
||||
window: Option<&Window>,
|
||||
cx: &App,
|
||||
) -> String {
|
||||
if let Some(name) = tab.name.as_ref() {
|
||||
let trimmed = name.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
}
|
||||
let (raw, home) = tab.leaf_title_and_home(window, cx);
|
||||
let label = short_title(&raw, home.as_deref());
|
||||
if label.trim().is_empty() {
|
||||
t_fmt(
|
||||
L10nKey::TabUnnamedShell,
|
||||
&[("n", &((index + 1).to_string()))],
|
||||
)
|
||||
} else {
|
||||
label
|
||||
}
|
||||
let (view, home) = tab.label_view(window, cx);
|
||||
label_of(&view, index, home.as_deref())
|
||||
}
|
||||
|
||||
/// The New Tab control: one `+` that drops the list of everything it could
|
||||
@@ -2688,4 +2761,264 @@ mod tests {
|
||||
assert_eq!(spec.args, ["--login"]);
|
||||
assert!(!spec.args_are_tty7_defaults);
|
||||
}
|
||||
|
||||
/// A tab of this window as the strip reads it: `tab_label` is nothing but
|
||||
/// [`label_of`] over the [`TabView`](crate::ui::machine_mirror::TabView)
|
||||
/// that [`Tab::label_view`](crate::ui::app::Tab::label_view) builds from
|
||||
/// the live leaf, so naming one here climbs the same ladder a real tab
|
||||
/// climbs. `title` is the placeholder `label_view` fills that slot with —
|
||||
/// the machine tree puts a process name there, a live pane has only the
|
||||
/// name it answers to before anything has spoken.
|
||||
fn strip_tab() -> crate::ui::machine_mirror::TabView {
|
||||
crate::ui::machine_mirror::TabView {
|
||||
id: tty7_core::core::machine::TabId::new(),
|
||||
name: None,
|
||||
title: crate::terminal::view::DEFAULT_TITLE.to_string(),
|
||||
osc_title: None,
|
||||
cwd: None,
|
||||
agent: None,
|
||||
status: None,
|
||||
live: true,
|
||||
panes: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// The home the paths below are measured against — named rather than read
|
||||
/// off this machine, so the assertions do not depend on who is running
|
||||
/// them (#580).
|
||||
fn home() -> &'static Path {
|
||||
Path::new("/Users/x")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_renamed_tab_keeps_its_name_over_every_other_answer() {
|
||||
let mut tab = strip_tab();
|
||||
tab.name = Some(" build ".into());
|
||||
tab.osc_title = Some("vim — main.rs".into());
|
||||
tab.cwd = Some("/Users/x/repo/tty7".into());
|
||||
|
||||
assert_eq!(label_of(&tab, 0, Some(home())), "build");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pane_showing_a_title_is_named_by_it_and_not_by_its_directory() {
|
||||
let mut tab = strip_tab();
|
||||
tab.osc_title = Some("vim — main.rs".into());
|
||||
tab.cwd = Some("/Users/x/repo/tty7".into());
|
||||
|
||||
assert_eq!(label_of(&tab, 0, Some(home())), "vim — main.rs");
|
||||
|
||||
// Including the title an SSH pane answers to before the far shell has
|
||||
// said anything (#438): `label_view` hands that up here, so a window
|
||||
// full of them still reads as hosts rather than as directories.
|
||||
tab.osc_title = Some("prod-web".into());
|
||||
assert_eq!(label_of(&tab, 0, Some(home())), "prod-web");
|
||||
}
|
||||
|
||||
/// #740: every shell tty7 ships integration for except PowerShell reports
|
||||
/// its directory over OSC 7 and never sets a title, which left the tab
|
||||
/// reading "tty7" — the app's own name — while the switcher listing the
|
||||
/// very same tab showed the directory.
|
||||
#[test]
|
||||
fn a_pane_that_has_only_said_where_it_is_is_named_after_that() {
|
||||
let mut tab = strip_tab();
|
||||
tab.cwd = Some("/Users/x/repo/tty7".into());
|
||||
|
||||
assert_eq!(label_of(&tab, 0, Some(home())), "~/repo/tty7");
|
||||
// Through the same shortener as a title, so a deep directory is cut
|
||||
// where a deep path in a title would be.
|
||||
tab.cwd = Some("/Users/x/repo/tty7/crates/tty7-core/src".into());
|
||||
assert_eq!(
|
||||
label_of(&tab, 0, Some(home())),
|
||||
super::short_title("/Users/x/repo/tty7/crates/tty7-core/src", Some(home())),
|
||||
);
|
||||
}
|
||||
|
||||
/// A tooltip exists to say what the row had to leave out. One that repeats
|
||||
/// the row is worse than none, and the label and the raw string it came
|
||||
/// from are not comparable until both have been abbreviated: `~/repo` and
|
||||
/// `/Users/x/repo` are the same name spelled two ways, and reading them as
|
||||
/// a difference hung a tooltip on every tab named after a directory under
|
||||
/// the home — which, after this change, is most of them.
|
||||
#[test]
|
||||
fn a_tab_named_after_a_directory_says_nothing_more_on_hover_unless_it_was_cut() {
|
||||
let mut tab = strip_tab();
|
||||
tab.cwd = Some("/Users/x/repo".into());
|
||||
|
||||
assert_eq!(label_of(&tab, 0, Some(home())), "~/repo");
|
||||
assert_eq!(
|
||||
tooltip_of(&tab, 0, Some(home())),
|
||||
None,
|
||||
"the row is already showing the whole directory"
|
||||
);
|
||||
|
||||
// Cut down to its last three segments, so the head is worth having.
|
||||
tab.cwd = Some("/Users/x/repo/crates/tty7-core/src".into());
|
||||
assert_eq!(label_of(&tab, 0, Some(home())), "…/crates/tty7-core/src");
|
||||
assert_eq!(
|
||||
tooltip_of(&tab, 0, Some(home())).as_deref(),
|
||||
Some("~/repo/crates/tty7-core/src")
|
||||
);
|
||||
|
||||
// The same holds for a title that happens to be a path — the rung this
|
||||
// guard was already getting wrong before a directory could reach it.
|
||||
let mut titled = strip_tab();
|
||||
titled.osc_title = Some("/Users/x/repo".into());
|
||||
assert_eq!(tooltip_of(&titled, 0, Some(home())), None);
|
||||
|
||||
// A shell integration's `user@host:` head is not in the label, so it
|
||||
// is still worth spelling out.
|
||||
titled.osc_title = Some("me@box:/Users/x/repo".into());
|
||||
assert_eq!(
|
||||
tooltip_of(&titled, 0, Some(home())).as_deref(),
|
||||
Some("me@box:/Users/x/repo")
|
||||
);
|
||||
}
|
||||
|
||||
/// The one test that fails if any of the wiring is put back: a real tab,
|
||||
/// built the way the window builds one, named through `tab_label` — and
|
||||
/// checked against what the switcher renders from the machine tree's view
|
||||
/// of that very same pane. Before this change the strip said "tty7" and
|
||||
/// the switcher said the directory (#740).
|
||||
#[gpui::test]
|
||||
fn the_strip_names_a_titleless_pane_exactly_as_the_switcher_does(cx: &mut TestAppContext) {
|
||||
use crate::ui::pane::{Pane, PaneSlot};
|
||||
|
||||
let (app, mut vcx) = crate::ui::app::test_window::harness(cx);
|
||||
let _stream = app.update_in(&mut vcx, |app, window, cx| {
|
||||
let (view, stream) = crate::terminal::view::quiet_test_pane(1, window, cx);
|
||||
// A pane that has reported where it is over OSC 7 and has never
|
||||
// titled itself — every shell tty7 ships integration for except
|
||||
// PowerShell.
|
||||
view.read(cx)
|
||||
.terminal
|
||||
.seed_cwd(Some(std::path::PathBuf::from("/work/repo")));
|
||||
app.tabs
|
||||
.push(crate::ui::app::Tab::new(Pane::leaf(PaneSlot::Ready(view))));
|
||||
app.active = app.tabs.len() - 1;
|
||||
stream
|
||||
});
|
||||
vcx.background_executor.run_until_parked();
|
||||
|
||||
app.update_in(&mut vcx, |app, window, cx| {
|
||||
let index = app.active;
|
||||
let tab = &app.tabs[index];
|
||||
let (view, home) = tab.label_view(Some(window), cx);
|
||||
assert_eq!(view.osc_title, None, "the pane never titled itself");
|
||||
assert_eq!(view.cwd.as_deref(), Some("/work/repo"));
|
||||
|
||||
let strip = app.tab_label(tab, index, Some(window), cx);
|
||||
assert_eq!(strip, "/work/repo");
|
||||
assert_ne!(
|
||||
strip,
|
||||
crate::terminal::view::DEFAULT_TITLE,
|
||||
"and is not named after the app any more"
|
||||
);
|
||||
|
||||
// The machine tree's reading of the same pane, which is all the
|
||||
// switcher ever has: no title was seen, the cwd is the one above,
|
||||
// and `title` is the foreground process name.
|
||||
let from_tree = crate::ui::machine_mirror::TabView {
|
||||
id: tab.tree_id.get(),
|
||||
name: None,
|
||||
title: "zsh".into(),
|
||||
osc_title: None,
|
||||
cwd: Some("/work/repo".into()),
|
||||
agent: None,
|
||||
status: None,
|
||||
live: true,
|
||||
panes: 1,
|
||||
};
|
||||
assert_eq!(
|
||||
strip,
|
||||
label_of(&from_tree, index, home.as_deref()),
|
||||
"the two columns name the same tab the same way"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
app.tab_title_tooltip(tab, index, Some(window), cx),
|
||||
None,
|
||||
"and the row is showing the whole path, so it stays quiet"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pane_with_nothing_to_say_falls_back_the_way_it_always_did() {
|
||||
// No title and no directory: the placeholder, exactly as before.
|
||||
let tab = strip_tab();
|
||||
assert_eq!(label_of(&tab, 0, Some(home())), "tty7");
|
||||
|
||||
// And a tab holding no live pane at all is still numbered.
|
||||
let mut empty = strip_tab();
|
||||
empty.title = String::new();
|
||||
assert!(label_of(&empty, 2, Some(home())).contains('3'));
|
||||
}
|
||||
|
||||
/// The rung under the shortener, which the two surfaces reach holding
|
||||
/// different things. A shell that has said who and where it is but not
|
||||
/// *where* — `user@host:` with nothing after the colon — leaves nothing to
|
||||
/// show, and whatever stands in has to be something the tab does not
|
||||
/// already say: the switcher has the foreground process name, and a tab of
|
||||
/// this window has only the placeholder, which is the answer #740 removed.
|
||||
#[test]
|
||||
fn a_title_that_shortens_away_never_puts_the_app_name_back_on_the_tab() {
|
||||
let mut strip = strip_tab();
|
||||
strip.osc_title = Some("user@host:".into());
|
||||
assert_ne!(
|
||||
label_of(&strip, 0, Some(home())),
|
||||
crate::terminal::view::DEFAULT_TITLE
|
||||
);
|
||||
assert!(
|
||||
label_of(&strip, 0, Some(home())).contains('1'),
|
||||
"the numbered placeholder, which is what the strip showed here \
|
||||
before it shared this renderer"
|
||||
);
|
||||
|
||||
// The switcher arrives with a real process name in that slot, and it
|
||||
// is still worth more than a number.
|
||||
let from_tree = crate::ui::machine_mirror::TabView {
|
||||
title: "zsh".into(),
|
||||
osc_title: Some("user@host:".into()),
|
||||
..strip_tab()
|
||||
};
|
||||
assert_eq!(label_of(&from_tree, 0, Some(home())), "zsh");
|
||||
}
|
||||
|
||||
/// A path is spelled the way the machine it is on spells it, and which
|
||||
/// machine that is has nothing to do with which one tty7 is running on: a
|
||||
/// remote pane reports POSIX to a Windows client, and a Windows pane
|
||||
/// reports backslashes to a client that has never seen one (#580).
|
||||
#[test]
|
||||
fn a_cwd_is_cut_in_its_own_spelling_whichever_client_is_reading_it() {
|
||||
let windows_home = Path::new(r"C:\Users\x");
|
||||
|
||||
// A Windows pane: shortened under its own home, and a path too deep to
|
||||
// fit is rejoined with its own separator rather than with `/`.
|
||||
let mut win = strip_tab();
|
||||
win.cwd = Some(r"C:\Users\x\repo".into());
|
||||
assert_eq!(label_of(&win, 0, Some(windows_home)), "~/repo");
|
||||
win.cwd = Some(r"D:\work\a\b\proj".into());
|
||||
assert_eq!(label_of(&win, 0, Some(windows_home)), r"…\a\b\proj");
|
||||
|
||||
// A remote pane's cwd is POSIX even when the client reading it is the
|
||||
// Windows one: no drive to hang it off, no `~` borrowed from this
|
||||
// machine's home, and no backslash anywhere in the answer.
|
||||
let mut remote = strip_tab();
|
||||
remote.cwd = Some("/srv/app".into());
|
||||
assert_eq!(label_of(&remote, 0, Some(windows_home)), "/srv/app");
|
||||
remote.cwd = Some("/home/deploy/app".into());
|
||||
assert_eq!(
|
||||
label_of(&remote, 0, Some(Path::new("/home/deploy"))),
|
||||
"~/app",
|
||||
"measured against the home of the host it is on, not of this one"
|
||||
);
|
||||
|
||||
// The root of a filesystem is a directory like any other: a tab
|
||||
// sitting in it says so, and says nothing more on hover.
|
||||
let mut root = strip_tab();
|
||||
root.cwd = Some("/".into());
|
||||
assert_eq!(label_of(&root, 0, Some(home())), "/");
|
||||
assert_eq!(tooltip_of(&root, 0, Some(home())), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user