diff options
| author | JJ Liebig <jonathan.liebig@gmail.com> | 2026-09-14 18:35:18 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-14 18:35:18 +0200 |
| commit | ad415672878a98bfac92d0e7abed0fc8db26e16a (patch) | |
| tree | cdbc98801405977b9282477fb080da6ad68635df /src/server | |
| parent | b5154541cc75f9f12b7c14964d4d277d505a5f45 (diff) | |
fix: scope window titles to each client view (#4130)
refs #4091
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/headless.rs | 38 | ||||
| -rw-r--r-- | src/server/headless/tests/mod.rs | 77 |
2 files changed, 107 insertions, 8 deletions
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>>, |
