use super::set_titles_support::{
active_pane_id, append_global, delivered_paths, delivered_titles, set_global,
title_capable_context,
};
use super::*;
async fn enable_osc7(handler: &RequestHandler) {
append_global(handler, OptionName::TerminalFeatures, "xterm*:osc7").await;
}
async fn feed_active_pane(
handler: &RequestHandler,
session: &rmux_proto::SessionName,
bytes: &[u8],
) {
let pane_id = active_pane_id(handler, session).await;
let (transcript, output) = {
let state = handler.state.lock().await;
let target = PaneTarget::with_window(session.clone(), 0, 0);
(
state
.transcript_handle(&target)
.expect("session transcript must exist"),
state
.pane_output_for_target(session, 0, 0)
.expect("pane output must exist"),
)
};
let events = crate::pane_io::publish_pane_bytes_capturing_alerts(
session,
pane_id,
&transcript,
&output,
bytes.to_vec(),
);
assert!(
!events.is_empty(),
"the production reader must report an alert for {bytes:?}"
);
for event in events {
handler.handle_pane_alert_event(event).await;
}
}
async fn pane_path(handler: &RequestHandler, session: &rmux_proto::SessionName) -> String {
let state = handler.state.lock().await;
let pane_id = state
.sessions
.session(session)
.and_then(|session| session.window_at(0))
.and_then(|window| window.pane(0).map(rmux_core::Pane::id))
.expect("window pane exists");
state
.pane_screen_state(session, pane_id)
.map(|pane_state| pane_state.path)
.unwrap_or_default()
}
#[tokio::test]
async fn a_live_pane_path_change_reaches_the_outer_terminal() {
let handler = RequestHandler::new();
let alpha = session_name("alpha");
create_quiet_session(&handler, &alpha).await;
let attach_pid = std::process::id();
let (control_tx, mut control_rx) = mpsc::unbounded_channel();
let _attach_id = handler
.register_attach_with_terminal_context(
attach_pid,
alpha.clone(),
control_tx,
title_capable_context(),
)
.await;
enable_osc7(&handler).await;
set_global(&handler, OptionName::SetTitlesString, "PATHTEST").await;
set_global(&handler, OptionName::SetTitles, "on").await;
feed_active_pane(&handler, &alpha, b"\x1b]7;file:///review-before\x07").await;
assert_eq!(pane_path(&handler, &alpha).await, "file:///review-before");
assert_eq!(
delivered_paths(&mut control_rx),
vec![String::new(), "file:///review-before".to_owned()],
"the first path must reach the outer terminal"
);
feed_active_pane(&handler, &alpha, b"\x1b]7;file:///review-after\x07").await;
assert_eq!(pane_path(&handler, &alpha).await, "file:///review-after");
let delivered = delivered_paths(&mut control_rx);
assert_eq!(
delivered,
vec!["file:///review-after".to_owned()],
"a path-only change must refresh the client exactly once"
);
}
#[tokio::test]
async fn a_pane_clearing_its_path_clears_the_outer_terminal() {
let handler = RequestHandler::new();
let alpha = session_name("alpha");
create_quiet_session(&handler, &alpha).await;
let attach_pid = std::process::id();
let (control_tx, mut control_rx) = mpsc::unbounded_channel();
let _attach_id = handler
.register_attach_with_terminal_context(
attach_pid,
alpha.clone(),
control_tx,
title_capable_context(),
)
.await;
enable_osc7(&handler).await;
set_global(&handler, OptionName::SetTitlesString, "PATHTEST").await;
set_global(&handler, OptionName::SetTitles, "on").await;
feed_active_pane(&handler, &alpha, b"\x1b]7;file:///review-set\x07").await;
assert_eq!(
delivered_paths(&mut control_rx),
vec![String::new(), "file:///review-set".to_owned()],
"the attach reports the empty pane path, then the value it is given"
);
feed_active_pane(&handler, &alpha, b"\x1b]7;\x07").await;
assert_eq!(
pane_path(&handler, &alpha).await,
"",
"the pane itself must record the cleared path"
);
assert_eq!(
delivered_paths(&mut control_rx),
vec![String::new()],
"clearing the pane path must clear the outer terminal's"
);
feed_active_pane(&handler, &alpha, b"\x1b]7;\x07x").await;
assert!(
delivered_paths(&mut control_rx).is_empty(),
"an unchanged empty path must not be rewritten"
);
}
#[tokio::test]
async fn a_path_change_writes_nothing_while_set_titles_is_off() {
let handler = RequestHandler::new();
let alpha = session_name("alpha");
create_quiet_session(&handler, &alpha).await;
let attach_pid = std::process::id();
let (control_tx, mut control_rx) = mpsc::unbounded_channel();
let _attach_id = handler
.register_attach_with_terminal_context(
attach_pid,
alpha.clone(),
control_tx,
title_capable_context(),
)
.await;
enable_osc7(&handler).await;
let _ = delivered_paths(&mut control_rx);
feed_active_pane(&handler, &alpha, b"\x1b]7;file:///never-written\x07").await;
assert_eq!(
pane_path(&handler, &alpha).await,
"file:///never-written",
"the pane still records the path"
);
assert!(
delivered_paths(&mut control_rx).is_empty(),
"set-titles off must write no OSC 7"
);
}
#[tokio::test]
async fn a_client_without_the_osc7_capability_receives_no_path() {
let handler = RequestHandler::new();
let alpha = session_name("alpha");
create_quiet_session(&handler, &alpha).await;
let attach_pid = std::process::id();
let (control_tx, mut control_rx) = mpsc::unbounded_channel();
let _attach_id = handler
.register_attach_with_terminal_context(
attach_pid,
alpha.clone(),
control_tx,
title_capable_context(),
)
.await;
set_global(&handler, OptionName::SetTitlesString, "NOPATH").await;
set_global(&handler, OptionName::SetTitles, "on").await;
assert_eq!(delivered_titles(&mut control_rx), vec!["NOPATH".to_owned()]);
feed_active_pane(&handler, &alpha, b"\x1b]7;file:///no-capability\x07").await;
assert!(
delivered_paths(&mut control_rx).is_empty(),
"a client without osc7 must receive no path"
);
}
#[tokio::test]
async fn a_linked_session_with_set_titles_off_is_not_redrawn_by_a_title_change() {
let handler = RequestHandler::new();
let owner = session_name("titled-owner");
let peer = session_name("untitled-peer");
create_quiet_session(&handler, &owner).await;
create_quiet_session(&handler, &peer).await;
let owner_pid = u32::MAX - 182;
let peer_pid = u32::MAX - 183;
let (owner_tx, mut owner_rx) = mpsc::unbounded_channel();
let (peer_tx, mut peer_rx) = mpsc::unbounded_channel();
let _owner_id = handler
.register_attach_with_terminal_context(
owner_pid,
owner.clone(),
owner_tx,
title_capable_context(),
)
.await;
let _peer_id = handler
.register_attach_with_terminal_context(
peer_pid,
peer.clone(),
peer_tx,
title_capable_context(),
)
.await;
let linked = handler
.handle(Request::LinkWindow(LinkWindowRequest {
source: WindowTarget::with_window(owner.clone(), 0),
target: WindowTarget::with_window(peer.clone(), 0),
after: false,
before: false,
kill_destination: true,
detached: false,
}))
.await;
assert!(matches!(linked, Response::LinkWindow(_)), "{linked:?}");
set_session_option(
&handler,
&owner,
OptionName::SetTitlesString,
"T:#{pane_title}",
)
.await;
set_session_option(&handler, &owner, OptionName::SetTitles, "on").await;
set_session_option(&handler, &peer, OptionName::SetTitles, "off").await;
let _ = delivered_titles(&mut owner_rx);
let _ = delivered_titles(&mut peer_rx);
feed_active_pane(&handler, &owner, b"\x1b]2;LINKED-APP\x07").await;
assert_eq!(
delivered_titles(&mut owner_rx),
vec!["T:LINKED-APP".to_owned()],
"the session with set-titles on must be told the new title"
);
assert!(
matches!(peer_rx.try_recv(), Err(TryRecvError::Empty)),
"a linked session with set-titles off must pay no redraw for a title change"
);
}
async fn set_session_option(
handler: &RequestHandler,
session: &rmux_proto::SessionName,
option: OptionName,
value: &str,
) {
let set = handler
.handle(Request::SetOption(SetOptionRequest {
scope: ScopeSelector::Session(session.clone()),
option,
value: value.to_owned(),
mode: SetOptionMode::Replace,
}))
.await;
assert!(matches!(set, Response::SetOption(_)), "set {option:?}");
}