feat: forward OSC 7 (working directory) to parent terminal (#4867)

* feat: capture OSC 7 working directory on Grid and expose via Pane trait

Add osc7_payload field to Grid, handle the b"7" OSC sequence in
osc_dispatch, expose it via a Grid accessor, add osc7_payload() to
the Pane trait (default None), and implement it on TerminalPane by
delegating to the Grid. Follows the has_bell/consume_bell pattern.

* feat: forward OSC 7 from focused pane to parent terminal on render

* fix: validate OSC 7 URI and clean up state on client disconnect

Reject URIs containing control characters (< 0x20 or 0x7f) to prevent
injection into the parent terminal when the URI is re-emitted via OSC 7
forwarding. Also remove per-client OSC 7 forwarding state in
remove_client() to avoid stale entries accumulating after disconnect.

* style: fix formatting in OSC 7 tests

* fix: add control char test, clear osc7 on reset, doc comment

- Add test for control character rejection in OSC 7 URIs
- Clear osc7_payload in reset_terminal_state() (RIS/\ec)
- Add doc comment on osc7_payload() getter

* fix: address PR review feedback

- Reject C1 control characters (U+0080..U+009F) in OSC 7 URIs using
  char::is_control() instead of byte-level check
- Reject entire OSC 7 if any param segment has invalid UTF-8 instead
  of silently dropping bad segments
- Avoid String allocation in render hot path by comparing borrowed
  &str, only allocating when the value actually changes
- Add test for C1 control character rejection

* fix: integrate OSC 7 forwarding with upstream CWD tracking

* add some more tests

* docs: update changelog

---------

Co-authored-by: Aram Drevekenin <[email protected]>
This commit is contained in:
Marcio Granzotto Rodrigues
2026-09-14 18:26:14 +02:00
committed by GitHub
co-authored by Aram Drevekenin
parent 232c93721d
commit 474ea0cef6
8 changed files with 672 additions and 1 deletions
+1
View File
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Unreleased]
* fix: forward OSC7 (cwd changes) to host terminal (https://github.com/zellij-org/zellij/pull/4867)
## [0.45.1] - 2026-08-28
* fix: nested-session detection over SSH (https://github.com/zellij-org/zellij/pull/5522)
@@ -0,0 +1,144 @@
#![cfg(unix)]
use zellij_integration_tests::{
claim_first_terminal_and_wait_for_prompt, col, keys, split_down_and_wait_for_prompt,
start_zellij, FakePtyHandle, TestSession,
};
const OSC7_OPENER: &[u8] = b"\x1b]7;";
fn osc7(uri: &str) -> Vec<u8> {
format!("\u{1b}]7;{}\u{1b}\\", uri).into_bytes()
}
fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
occurrences_of(haystack, needle) > 0
}
fn occurrences_of(haystack: &[u8], needle: &[u8]) -> usize {
if needle.is_empty() || haystack.len() < needle.len() {
return 0;
}
haystack
.windows(needle.len())
.filter(|window| *window == needle)
.count()
}
fn claim_quiet_terminal(zellij: &TestSession) -> FakePtyHandle {
let terminal = claim_first_terminal_and_wait_for_prompt(zellij);
terminal.disable_echo();
terminal
}
fn split_down_to_quiet_terminal(zellij: &TestSession) -> FakePtyHandle {
let terminal = split_down_and_wait_for_prompt(zellij);
terminal.disable_echo();
terminal
}
fn report_cwd_and_wait(zellij: &TestSession, terminal: &FakePtyHandle, uri: &str) {
terminal.output(&osc7(uri));
zellij.wait_until_raw_output(
&format!("the host terminal was told about {}", uri),
|raw_bytes| contains_subslice(raw_bytes, &osc7(uri)),
);
}
fn echo_and_wait(zellij: &TestSession, terminal: &FakePtyHandle, sentinel: &str) {
terminal.output(format!("{}\r\n", sentinel).as_bytes());
zellij.wait_until("the pane kept processing its output", |grid_snapshot| {
grid_snapshot.contains(sentinel)
});
}
fn focus_upper_pane(zellij: &TestSession) {
zellij.send_stdin(&keys::ctrl('p'));
zellij.send_stdin(&keys::key('k'));
zellij.send_stdin(&keys::ENTER);
zellij.wait_until("focus moved to the upper pane", |grid_snapshot| {
grid_snapshot.status_bar_appears() && grid_snapshot.cursor_is_at(col(2).row(2))
});
}
#[test]
fn a_working_directory_report_reaches_the_host_terminal() {
let mut zellij = start_zellij();
let terminal = claim_quiet_terminal(&zellij);
report_cwd_and_wait(&zellij, &terminal, "file://host/tmp");
zellij.quit();
}
#[test]
fn an_unchanged_working_directory_is_reported_only_once() {
let mut zellij = start_zellij();
let terminal = claim_quiet_terminal(&zellij);
report_cwd_and_wait(&zellij, &terminal, "file://host/tmp");
terminal.output(&osc7("file://host/tmp"));
echo_and_wait(&zellij, &terminal, "second-report-processed");
assert_eq!(
occurrences_of(&zellij.raw_bytes(), &osc7("file://host/tmp")),
1,
"a pane repeating its working directory does not report it again"
);
zellij.quit();
}
#[test]
fn a_changed_working_directory_is_reported_again() {
let mut zellij = start_zellij();
let terminal = claim_quiet_terminal(&zellij);
report_cwd_and_wait(&zellij, &terminal, "file://host/first");
report_cwd_and_wait(&zellij, &terminal, "file://host/second");
zellij.quit();
}
#[test]
fn moving_focus_reports_the_working_directory_of_the_newly_focused_pane() {
let mut zellij = start_zellij();
let upper_terminal = claim_quiet_terminal(&zellij);
report_cwd_and_wait(&zellij, &upper_terminal, "file://host/upper");
let lower_terminal = split_down_to_quiet_terminal(&zellij);
report_cwd_and_wait(&zellij, &lower_terminal, "file://host/lower");
focus_upper_pane(&zellij);
zellij.wait_until_raw_output(
"the upper pane's working directory was restated on focus",
|raw_bytes| occurrences_of(raw_bytes, &osc7("file://host/upper")) == 2,
);
zellij.quit();
}
#[test]
fn a_pane_that_never_reported_a_working_directory_reports_nothing_on_focus() {
let mut zellij = start_zellij();
let upper_terminal = claim_quiet_terminal(&zellij);
report_cwd_and_wait(&zellij, &upper_terminal, "file://host/upper");
let lower_terminal = split_down_to_quiet_terminal(&zellij);
echo_and_wait(&zellij, &lower_terminal, "lower-pane-is-focused");
assert_eq!(
occurrences_of(&zellij.raw_bytes(), OSC7_OPENER),
1,
"focusing a pane with nothing to report leaves the host terminal as it was"
);
focus_upper_pane(&zellij);
zellij.wait_until_raw_output(
"the upper pane's working directory was restated on focus",
|raw_bytes| occurrences_of(raw_bytes, &osc7("file://host/upper")) == 2,
);
zellij.quit();
}
+20
View File
@@ -889,6 +889,7 @@ pub struct Grid {
osc133_command_selection: bool,
command_output_flash: Option<Selection>,
word_separators: String,
pub osc7_payload: Option<String>,
}
impl Grid {
@@ -1262,6 +1263,7 @@ impl Grid {
osc133_command_selection: true,
command_output_flash: None,
word_separators: DEFAULT_WORD_SEPARATORS.to_owned(),
osc7_payload: None,
}
}
pub fn set_selection_options(&mut self, osc133_command_selection: bool, word_separators: &str) {
@@ -1270,6 +1272,11 @@ impl Grid {
self.word_separators = word_separators.to_owned();
}
}
/// Returns the last OSC 7 working directory URI reported by the child process,
/// or `None` if no OSC 7 has been received (or was rejected for invalid content).
pub fn osc7_payload(&self) -> Option<&str> {
self.osc7_payload.as_deref()
}
pub fn render_full_viewport(&mut self) {
self.output_buffer.update_all_lines();
}
@@ -2812,6 +2819,7 @@ impl Grid {
self.pane_default_fg = None;
self.pane_default_bg = None;
self.osc133_markers_seen = false;
self.osc7_payload = None;
if let Some(images_to_reap) = self.sixel_grid.clear() {
self.sixel_grid.reap_images(images_to_reap);
}
@@ -4591,6 +4599,18 @@ impl Perform for Grid {
self.pending_osc7_cwd = Some(path);
}
}
// Store the raw URI separately for forwarding to the parent terminal.
// Join params[1..] with ";" to preserve semicolons in the URI.
if params.len() >= 2 {
let segments: Option<Vec<&str>> =
params[1..].iter().map(|x| str::from_utf8(x).ok()).collect();
if let Some(segments) = segments {
let uri = segments.join(";");
if !uri.is_empty() && !uri.chars().any(|c| c.is_control()) {
self.osc7_payload = Some(uri);
}
}
}
},
// Set color index.
+3
View File
@@ -1115,6 +1115,9 @@ impl Pane for TerminalPane {
fn consume_bell(&mut self) {
self.grid.ring_bell = false;
}
fn osc7_payload(&self) -> Option<&str> {
self.grid.osc7_payload()
}
fn set_bell_notification(&mut self, val: bool) {
self.has_bell_notification = val;
}
@@ -790,6 +790,165 @@ pub fn has_bell_reflects_grid_ring_bell() {
);
}
#[test]
pub fn osc7_payload_stored_from_pty_bytes() {
let mut terminal_pane = make_terminal_pane_for_bell();
assert!(
terminal_pane.osc7_payload().is_none(),
"Initially osc7_payload should be None"
);
// Send OSC 7 sequence: \x1b]7;file://localhost/tmp\x1b\\
let osc7_bytes = b"\x1b]7;file://localhost/tmp\x1b\\";
terminal_pane.handle_pty_bytes(osc7_bytes.to_vec());
assert_eq!(
terminal_pane.drain_osc7_cwd(),
Some(std::path::PathBuf::from("/tmp")),
"OSC 7 should still update Zellij's internal CWD tracking"
);
assert!(terminal_pane.drain_osc7_cwd().is_none());
assert_eq!(
terminal_pane.osc7_payload(),
Some("file://localhost/tmp"),
"The forwarding URI should persist after the internal CWD update is drained"
);
}
#[test]
pub fn osc7_payload_updated_on_subsequent_osc7() {
let mut terminal_pane = make_terminal_pane_for_bell();
let osc7_first = b"\x1b]7;file://localhost/first\x1b\\";
terminal_pane.handle_pty_bytes(osc7_first.to_vec());
assert_eq!(terminal_pane.osc7_payload(), Some("file://localhost/first"));
let osc7_second = b"\x1b]7;file://localhost/second\x1b\\";
terminal_pane.handle_pty_bytes(osc7_second.to_vec());
assert_eq!(
terminal_pane.osc7_payload(),
Some("file://localhost/second")
);
}
#[test]
pub fn osc7_payload_preserves_percent_encoding() {
let mut terminal_pane = make_terminal_pane_for_bell();
let osc7_bytes = b"\x1b]7;file://localhost/path%20with%20spaces\x1b\\";
terminal_pane.handle_pty_bytes(osc7_bytes.to_vec());
assert_eq!(
terminal_pane.drain_osc7_cwd(),
Some(std::path::PathBuf::from("/path with spaces")),
);
assert_eq!(
terminal_pane.osc7_payload(),
Some("file://localhost/path%20with%20spaces"),
);
}
#[test]
pub fn osc7_payload_works_with_bel_terminator() {
let mut terminal_pane = make_terminal_pane_for_bell();
// BEL (\x07) is an alternative terminator for OSC sequences
let osc7_bytes = b"\x1b]7;file://localhost/tmp\x07";
terminal_pane.handle_pty_bytes(osc7_bytes.to_vec());
assert_eq!(terminal_pane.osc7_payload(), Some("file://localhost/tmp"),);
}
#[test]
pub fn osc7_payload_rejects_c0_control_characters() {
// Test the Grid's osc_dispatch directly, since the VTE parser strips
// control characters before they reach osc_dispatch. This validates
// the defense-in-depth guard in the b"7" handler.
use vte::Perform;
let mut terminal_pane = make_terminal_pane_for_bell();
let params: &[&[u8]] = &[b"7", b"file://host/path\x01bad"];
terminal_pane.grid.osc_dispatch(params, false);
assert!(
terminal_pane.osc7_payload().is_none(),
"URIs with C0 control characters should be rejected"
);
}
#[test]
pub fn osc7_payload_rejects_c1_control_characters() {
// C1 control characters (U+0080..U+009F) include ST (U+009C) which
// could terminate the OSC sequence in the parent terminal.
use vte::Perform;
let mut terminal_pane = make_terminal_pane_for_bell();
// U+009C (ST) in UTF-8 is 0xC2 0x9C
let params: &[&[u8]] = &[b"7", "file://host/path\u{9c}bad".as_bytes()];
terminal_pane.grid.osc_dispatch(params, false);
assert!(
terminal_pane.osc7_payload().is_none(),
"URIs with C1 control characters should be rejected"
);
}
#[test]
pub fn osc7_payload_rejects_an_empty_payload() {
let mut terminal_pane = make_terminal_pane_for_bell();
terminal_pane.handle_pty_bytes(b"\x1b]7;file://localhost/tmp\x1b\\".to_vec());
terminal_pane.handle_pty_bytes(b"\x1b]7;\x1b\\".to_vec());
assert_eq!(
terminal_pane.osc7_payload(),
Some("file://localhost/tmp"),
"An empty OSC 7 is rejected and the previous URI is retained"
);
}
#[test]
pub fn osc7_payload_rejects_invalid_utf8() {
use vte::Perform;
let mut terminal_pane = make_terminal_pane_for_bell();
terminal_pane.handle_pty_bytes(b"\x1b]7;file://localhost/tmp\x1b\\".to_vec());
let params: &[&[u8]] = &[b"7", b"file://localhost/\xff\xfe"];
terminal_pane.grid.osc_dispatch(params, false);
assert_eq!(
terminal_pane.osc7_payload(),
Some("file://localhost/tmp"),
"A non-UTF-8 OSC 7 is rejected and the previous URI is retained"
);
}
#[test]
pub fn osc7_payload_joins_semicolon_separated_segments() {
let mut terminal_pane = make_terminal_pane_for_bell();
terminal_pane.handle_pty_bytes(b"\x1b]7;file://localhost/a;b;c\x1b\\".to_vec());
assert_eq!(
terminal_pane.osc7_payload(),
Some("file://localhost/a;b;c"),
"A URI containing semicolons is reassembled from the split params"
);
}
#[test]
pub fn osc7_payload_is_cleared_by_a_terminal_reset() {
let mut terminal_pane = make_terminal_pane_for_bell();
terminal_pane.handle_pty_bytes(b"\x1b]7;file://localhost/tmp\x1b\\".to_vec());
assert_eq!(terminal_pane.osc7_payload(), Some("file://localhost/tmp"));
terminal_pane.handle_pty_bytes(b"\x1bc".to_vec());
assert!(
terminal_pane.osc7_payload().is_none(),
"RIS clears the reported working directory"
);
}
#[test]
pub fn frameless_pane_position_is_on_frame() {
let mut fake_win_size = PaneGeom {
+33 -1
View File
@@ -1631,6 +1631,7 @@ pub(crate) struct Screen {
client_notification_protocols: HashMap<ClientId, NotificationProtocol>,
host_notification_protocol: HostNotificationProtocol,
client_host_terminal_env: HashMap<ClientId, BTreeMap<String, String>>,
last_forwarded_osc7: HashMap<ClientId, Option<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1838,6 +1839,7 @@ impl Screen {
client_notification_protocols: HashMap::new(),
host_notification_protocol: HostNotificationProtocol::default(),
client_host_terminal_env: HashMap::new(),
last_forwarded_osc7: HashMap::new(),
}
}
@@ -4283,7 +4285,36 @@ impl Screen {
}
}
if non_watcher_output_was_dirty || has_bell {
let mut has_osc7_update = false;
// Forward OSC 7 (working directory) for each client's focused pane
for (&client_id, &tab_index) in &self.active_tab_ids {
if self.watcher_clients.contains_key(&client_id) {
continue;
}
if let Some(tab) = self.tabs.get(&tab_index) {
let current_osc7 = tab
.get_active_pane(client_id)
.and_then(|pane| pane.osc7_payload());
let last = self
.last_forwarded_osc7
.get(&client_id)
.and_then(|s| s.as_deref());
if current_osc7 != last {
if let Some(uri) = current_osc7 {
output.add_post_vte_instruction_to_client(
client_id,
&format!("\x1b]7;{}\x1b\\", uri),
);
has_osc7_update = true;
}
self.last_forwarded_osc7
.insert(client_id, current_osc7.map(|s| s.to_owned()));
}
}
}
if non_watcher_output_was_dirty || has_bell || has_osc7_update {
let serialized_output = output.serialize().context(err_context)?;
if !serialized_output.is_empty() {
let _ = self
@@ -5188,6 +5219,7 @@ impl Screen {
}
self.client_sizes.remove(&client_id);
self.pane_render_subscribers.remove(&client_id);
self.last_forwarded_osc7.remove(&client_id);
self.client_host_focused.remove(&client_id);
self.client_notification_protocols.remove(&client_id);
self.client_host_terminal_env.remove(&client_id);
+3
View File
@@ -700,6 +700,9 @@ pub trait Pane {
false
}
fn consume_bell(&mut self) {}
fn osc7_payload(&self) -> Option<&str> {
None
}
fn set_bell_notification(&mut self, _val: bool) {}
fn get_bell_notification(&self) -> bool {
false
+309
View File
@@ -13695,6 +13695,315 @@ fn a_client_whose_host_focus_was_never_reported_counts_as_focused() {
);
}
fn collect_rendered_output_per_client(
server_receiver: &ServerReceiver,
) -> HashMap<ClientId, String> {
let mut rendered: HashMap<ClientId, String> = HashMap::new();
while let Ok((instruction, _)) = server_receiver.try_recv() {
if let ServerInstruction::Render(Some(client_map)) = instruction {
for (client_id, content) in client_map {
rendered.entry(client_id).or_default().push_str(&content);
}
}
}
rendered
}
fn osc7_sequences_in(rendered: &str) -> Vec<String> {
let opener = "\u{1b}]7;";
let terminator = "\u{1b}\\";
let mut sequences = vec![];
let mut rest = rendered;
while let Some(start) = rest.find(opener) {
let payload_onwards = &rest[start + opener.len()..];
match payload_onwards.find(terminator) {
Some(end) => {
sequences.push(payload_onwards[..end].to_owned());
rest = &payload_onwards[end + terminator.len()..];
},
None => {
sequences.push(payload_onwards.to_owned());
break;
},
}
}
sequences
}
fn render_and_collect_osc7(
screen: &mut Screen,
server_receiver: &ServerReceiver,
) -> HashMap<ClientId, Vec<String>> {
screen.render_to_clients().unwrap();
collect_rendered_output_per_client(server_receiver)
.into_iter()
.map(|(client_id, rendered)| (client_id, osc7_sequences_in(&rendered)))
.collect()
}
fn forwarded_osc7_for(
osc7_per_client: &HashMap<ClientId, Vec<String>>,
client_id: ClientId,
) -> Vec<String> {
osc7_per_client.get(&client_id).cloned().unwrap_or_default()
}
fn emit_osc7_from_pane(screen: &mut Screen, client_id: ClientId, terminal_id: u32, uri: &str) {
emit_bytes_from_pane(
screen,
client_id,
terminal_id,
format!("\u{1b}]7;{}\u{1b}\\", uri).into_bytes(),
);
}
fn emit_bytes_from_pane(
screen: &mut Screen,
client_id: ClientId,
terminal_id: u32,
bytes: Vec<u8>,
) {
screen
.get_active_tab_mut(client_id)
.unwrap()
.handle_pty_bytes(terminal_id, bytes)
.unwrap();
}
fn settle_renders(screen: &mut Screen, server_receiver: &ServerReceiver) {
for _ in 0..3 {
screen.render_to_clients().unwrap();
}
while server_receiver.try_recv().is_ok() {}
}
fn screen_with_one_pane_for_osc7() -> (Screen, ServerReceiver) {
let size = Size {
cols: 121,
rows: 20,
};
let (mut screen, _tty_stdin_bytes, server_receiver) =
create_new_screen_with_capture(size, true, true, true, true);
new_tab(&mut screen, 1, 0);
settle_renders(&mut screen, &server_receiver);
(screen, server_receiver)
}
fn screen_with_two_panes_for_osc7(session_is_mirrored: bool) -> (Screen, ServerReceiver) {
let size = Size {
cols: 121,
rows: 20,
};
let (mut screen, _tty_stdin_bytes, server_receiver) =
create_new_screen_with_capture(size, true, true, true, session_is_mirrored);
new_tab(&mut screen, 1, 0);
{
let active_tab = screen.get_active_tab_mut(1).unwrap();
active_tab
.horizontal_split(PaneId::Terminal(2), None, 1, None, None)
.unwrap();
active_tab.move_focus_up(1).unwrap();
}
settle_renders(&mut screen, &server_receiver);
(screen, server_receiver)
}
#[test]
fn an_osc_7_from_the_focused_pane_is_forwarded_to_the_host_terminal() {
let (mut screen, server_receiver) = screen_with_one_pane_for_osc7();
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/tmp");
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert_eq!(
forwarded_osc7_for(&forwarded, 1),
vec!["file://host/tmp".to_owned()],
"the working directory of the focused pane reaches the host terminal"
);
}
#[test]
fn an_unchanged_osc_7_is_not_forwarded_again() {
let (mut screen, server_receiver) = screen_with_one_pane_for_osc7();
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/tmp");
render_and_collect_osc7(&mut screen, &server_receiver);
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/tmp");
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert!(
forwarded_osc7_for(&forwarded, 1).is_empty(),
"a pane repeating its working directory does not produce a second report"
);
}
#[test]
fn a_changed_osc_7_from_the_focused_pane_is_forwarded_again() {
let (mut screen, server_receiver) = screen_with_one_pane_for_osc7();
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/first");
render_and_collect_osc7(&mut screen, &server_receiver);
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/second");
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert_eq!(
forwarded_osc7_for(&forwarded, 1),
vec!["file://host/second".to_owned()],
"a pane changing directory produces a new report"
);
}
#[test]
fn a_bel_terminated_osc_7_is_forwarded_with_a_string_terminator() {
let (mut screen, server_receiver) = screen_with_one_pane_for_osc7();
emit_bytes_from_pane(&mut screen, 1, 1, b"\x1b]7;file://host/tmp\x07".to_vec());
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert_eq!(
forwarded_osc7_for(&forwarded, 1),
vec!["file://host/tmp".to_owned()],
"the report is re-emitted in its string-terminated form regardless of how it arrived"
);
}
#[test]
fn an_osc_7_from_an_unfocused_pane_is_not_forwarded() {
let (mut screen, server_receiver) = screen_with_two_panes_for_osc7(true);
emit_osc7_from_pane(&mut screen, 1, 2, "file://host/unfocused");
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert!(
forwarded_osc7_for(&forwarded, 1).is_empty(),
"only the focused pane reports its working directory"
);
}
#[test]
fn changing_focus_forwards_the_osc_7_of_the_newly_focused_pane() {
let (mut screen, server_receiver) = screen_with_two_panes_for_osc7(true);
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/first");
emit_osc7_from_pane(&mut screen, 1, 2, "file://host/second");
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert_eq!(
forwarded_osc7_for(&forwarded, 1),
vec!["file://host/first".to_owned()],
);
screen
.get_active_tab_mut(1)
.unwrap()
.move_focus_down(1)
.unwrap();
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert_eq!(
forwarded_osc7_for(&forwarded, 1),
vec!["file://host/second".to_owned()],
"focusing the lower pane reports its working directory"
);
screen
.get_active_tab_mut(1)
.unwrap()
.move_focus_up(1)
.unwrap();
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert_eq!(
forwarded_osc7_for(&forwarded, 1),
vec!["file://host/first".to_owned()],
"focusing back reports the first pane's working directory again"
);
}
#[test]
fn focusing_a_pane_that_never_reported_an_osc_7_forwards_nothing() {
let (mut screen, server_receiver) = screen_with_two_panes_for_osc7(true);
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/first");
render_and_collect_osc7(&mut screen, &server_receiver);
screen
.get_active_tab_mut(1)
.unwrap()
.move_focus_down(1)
.unwrap();
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert!(
forwarded_osc7_for(&forwarded, 1).is_empty(),
"a pane with nothing to report leaves the host terminal as it was"
);
screen
.get_active_tab_mut(1)
.unwrap()
.move_focus_up(1)
.unwrap();
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert_eq!(
forwarded_osc7_for(&forwarded, 1),
vec!["file://host/first".to_owned()],
"returning to the reporting pane restates its working directory"
);
}
#[test]
fn clients_focused_on_different_panes_are_forwarded_their_own_osc_7() {
let size = Size {
cols: 121,
rows: 20,
};
let (mut screen, server_receiver) = screen_with_two_panes_for_osc7(false);
screen.set_client_size(2, size);
screen.add_client(2, false).unwrap();
screen
.get_active_tab_mut(2)
.unwrap()
.move_focus_down(2)
.unwrap();
settle_renders(&mut screen, &server_receiver);
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/upper");
emit_osc7_from_pane(&mut screen, 1, 2, "file://host/lower");
let forwarded = render_and_collect_osc7(&mut screen, &server_receiver);
assert_eq!(
forwarded_osc7_for(&forwarded, 1),
vec!["file://host/upper".to_owned()],
"the first client is told about the pane it is focused on"
);
assert_eq!(
forwarded_osc7_for(&forwarded, 2),
vec!["file://host/lower".to_owned()],
"the second client is told about the pane it is focused on"
);
}
#[test]
fn a_departing_client_leaves_no_osc_7_state_behind() {
let size = Size {
cols: 121,
rows: 20,
};
let (mut screen, server_receiver) = screen_with_one_pane_for_osc7();
screen.set_client_size(2, size);
screen.add_client(2, false).unwrap();
emit_osc7_from_pane(&mut screen, 1, 1, "file://host/tmp");
render_and_collect_osc7(&mut screen, &server_receiver);
assert!(screen.last_forwarded_osc7.contains_key(&2));
screen.remove_client(2).unwrap();
assert!(
!screen.last_forwarded_osc7.contains_key(&2),
"the forwarded working directory is forgotten"
);
}
fn resize_pty_pixel_dimensions(
instruction: &PtyWriteInstruction,
) -> Option<(Option<u16>, Option<u16>)> {