aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/window_title.rs57
-rw-r--r--src/server/headless.rs38
-rw-r--r--src/server/headless/tests/mod.rs77
-rw-r--r--src/workspace.rs4
4 files changed, 147 insertions, 29 deletions
diff --git a/src/app/window_title.rs b/src/app/window_title.rs
index 09d2f850..41ddce6f 100644
--- a/src/app/window_title.rs
+++ b/src/app/window_title.rs
@@ -46,11 +46,37 @@ impl App {
/// Renders the configured outer window title, or `None` when window titles
/// are disabled or every token resolved empty.
pub(crate) fn window_title(&self) -> Option<String> {
+ let target = self.state.active.and_then(|workspace_index| {
+ self.state
+ .workspaces
+ .get(workspace_index)
+ .map(|workspace| (workspace_index, workspace.active_tab_index()))
+ });
+ self.window_title_for_target(target)
+ }
+
+ pub(crate) fn window_title_for(
+ &self,
+ workspace_index: usize,
+ tab_index: usize,
+ ) -> Option<String> {
+ self.window_title_for_target(Some((workspace_index, tab_index)))
+ }
+
+ fn window_title_for_target(&self, target: Option<(usize, usize)>) -> Option<String> {
let (template, hostname) = self.window_title_template.as_ref()?;
- let workspace = self
- .state
- .active
- .and_then(|ws_idx| self.state.workspaces.get(ws_idx));
+ let workspace =
+ target.and_then(|(workspace_index, _)| self.state.workspaces.get(workspace_index));
+ let tab = target.and_then(|(workspace_index, tab_index)| {
+ self.state
+ .workspaces
+ .get(workspace_index)?
+ .tabs
+ .get(tab_index)
+ });
+ let terminal = tab
+ .and_then(|tab| tab.terminal_id(tab.layout.focused()))
+ .and_then(|terminal_id| self.state.terminals.get(terminal_id));
let mut title = String::new();
for part in template.parts() {
@@ -65,22 +91,25 @@ impl App {
}
}
WindowTitlePart::Token(WindowTitleToken::Tab) => {
- if let Some(name) = workspace.and_then(|ws| ws.active_tab_display_name()) {
+ if let Some(name) = target.and_then(|(workspace_index, tab_index)| {
+ self.state
+ .workspaces
+ .get(workspace_index)?
+ .tab_display_name(tab_index)
+ }) {
title.push_str(&name);
}
}
WindowTitlePart::Token(WindowTitleToken::Pane) => {
- if let Some(label) = self
- .focused_terminal_state()
- .and_then(|terminal| terminal.manual_label.as_deref())
+ if let Some(label) =
+ terminal.and_then(|terminal| terminal.manual_label.as_deref())
{
title.push_str(label);
}
}
WindowTitlePart::Token(WindowTitleToken::TerminalTitle) => {
- if let Some(terminal_title) = self
- .focused_terminal_state()
- .and_then(|terminal| terminal.terminal_title_stripped())
+ if let Some(terminal_title) =
+ terminal.and_then(|terminal| terminal.terminal_title_stripped())
{
title.push_str(&terminal_title);
}
@@ -90,12 +119,6 @@ impl App {
Some(title)
}
-
- fn focused_terminal_state(&self) -> Option<&crate::terminal::TerminalState> {
- let workspace = self.state.workspaces.get(self.state.active?)?;
- let terminal_id = workspace.terminal_id(workspace.focused_pane_id()?)?;
- self.state.terminals.get(terminal_id)
- }
}
#[cfg(test)]
diff --git a/src/server/headless.rs b/src/server/headless.rs
index beda84b5..5648ac0a 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -1483,11 +1483,17 @@ impl HeadlessServer {
sources: &HashSet<crate::layout::PaneId>,
) -> (bool, bool) {
let focused_source = self
- .app
- .state
- .active
- .and_then(|ws_idx| self.app.state.workspaces.get(ws_idx))
- .and_then(|workspace| workspace.focused_pane_id())
+ .foreground_window_title_target()
+ .or_else(|| self.default_shell_target())
+ .and_then(|target| {
+ self.app
+ .state
+ .workspaces
+ .get(target.workspace_index)?
+ .tabs
+ .get(target.tab_index)
+ })
+ .map(|tab| tab.layout.focused())
.is_some_and(|pane_id| sources.contains(&pane_id));
let changes = self.app.sync_terminal_titles(sources);
let outer_title_synced = focused_source && self.app.window_title_uses_terminal_title();
@@ -1500,12 +1506,28 @@ impl HeadlessServer {
)
}
- /// Renders `ui.window_title` against current session state. `None` means
+ fn foreground_window_title_target(&self) -> Option<crate::ui::TabSurfaceTarget> {
+ self.foreground_client_id
+ .filter(|client_id| {
+ self.clients
+ .get(client_id)
+ .is_some_and(|client| client.is_active_shell_client())
+ })
+ .and_then(|client_id| self.shell_target_for_client(client_id))
+ }
+
+ /// Renders `ui.window_title` against the foreground client view. `None` means
/// window titles are disabled or every token resolved empty, which leaves
/// the client on Herdr's default title.
fn configured_window_title(&self) -> Option<String> {
- self.app
- .window_title()
+ self.foreground_window_title_target()
+ .map_or_else(
+ || self.app.window_title(),
+ |target| {
+ self.app
+ .window_title_for(target.workspace_index, target.tab_index)
+ },
+ )
.and_then(|title| crate::config::sanitize_window_title_text(&title))
}
diff --git a/src/server/headless/tests/mod.rs b/src/server/headless/tests/mod.rs
index 0ad5f8a3..4850203f 100644
--- a/src/server/headless/tests/mod.rs
+++ b/src/server/headless/tests/mod.rs
@@ -586,6 +586,83 @@ fn a_newly_promoted_client_gets_the_window_title_again() {
shutdown_test_runtimes(&mut server);
}
+#[tokio::test]
+async fn promoted_client_window_title_uses_its_own_view() {
+ let mut server = test_headless_server();
+ let mut survivor = crate::workspace::Workspace::test_new("survivor");
+ let survivor_tab_index = survivor.test_add_tab(Some("survivor-tab"));
+ let survivor_pane = survivor.tabs[survivor_tab_index].layout.focused();
+ let disconnected = crate::workspace::Workspace::test_new("disconnected");
+ let disconnected_pane = disconnected.tabs[0].layout.focused();
+ server.app.state.workspaces = vec![survivor, disconnected];
+ server.app.state.active = Some(1);
+ server.app.state.selected = 1;
+ server.app.state.ensure_test_terminals();
+ let survivor_terminal = server.app.state.workspaces[0].tabs[survivor_tab_index]
+ .terminal_id(survivor_pane)
+ .expect("survivor terminal")
+ .clone();
+ let terminal = server
+ .app
+ .state
+ .terminals
+ .get_mut(&survivor_terminal)
+ .expect("survivor terminal state");
+ terminal.manual_label = Some("client-pane".into());
+ terminal.set_terminal_title(Some("CLIENT OSC".into()));
+ server
+ .app
+ .configure_window_title("{workspace}/{tab}/{pane}/{terminal_title}");
+
+ let (survivor_control, _) = connect_matching_test_shell(&mut server, 1);
+ let (disconnected_control, _) = connect_matching_test_shell(&mut server, 2);
+ let survivor_tab_id = server
+ .app
+ .public_tab_id(0, survivor_tab_index)
+ .expect("survivor tab id");
+ assert!(server.focus_shell_client_on_tab(1, &survivor_tab_id));
+ server.promote_client_to_foreground(2);
+ drain_window_titles(&survivor_control);
+ drain_window_titles(&disconnected_control);
+
+ assert!(server.handle_server_event(ServerEvent::ClientDisconnected { client_id: 2 }));
+ server.sync_window_title();
+
+ assert_eq!(
+ next_window_title(&survivor_control),
+ Some(Some("survivor/survivor-tab/client-pane/CLIENT OSC".into()))
+ );
+ assert_eq!(server.app.state.active, Some(1));
+ assert_eq!(
+ server.shell_tab_id_for_client(1).as_deref(),
+ Some(survivor_tab_id.as_str())
+ );
+
+ let runtime = crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b"");
+ runtime.test_process_pty_bytes(b"\x1b]0;UPDATED OSC\x07");
+ server
+ .app
+ .terminal_runtimes
+ .insert(survivor_terminal, runtime);
+ assert!(
+ server
+ .sync_terminal_title_sources(&HashSet::from([survivor_pane]))
+ .1
+ );
+ assert_eq!(
+ next_window_title(&survivor_control),
+ Some(Some("survivor/survivor-tab/client-pane/UPDATED OSC".into()))
+ );
+ assert!(
+ !server
+ .sync_terminal_title_sources(&HashSet::from([disconnected_pane]))
+ .1
+ );
+ assert!(no_window_title(&survivor_control));
+
+ shutdown_test_runtimes(&mut server);
+}
+
fn test_client_writer() -> (
ClientWriter,
std::sync::mpsc::Receiver<Vec<u8>>,
diff --git a/src/workspace.rs b/src/workspace.rs
index b4bba45d..8944b884 100644
--- a/src/workspace.rs
+++ b/src/workspace.rs
@@ -439,10 +439,6 @@ impl Workspace {
self.tabs.get_mut(self.active_tab)
}
- pub fn active_tab_display_name(&self) -> Option<String> {
- self.tab_display_name(self.active_tab)
- }
-
pub fn tab_display_name(&self, tab_idx: usize) -> Option<String> {
let tab = self.tabs.get(tab_idx)?;
Some(