use std::io;
#[cfg(any(unix, windows))]
use std::time::Duration;
use rmux_core::{
key_code_lookup_bits, key_code_to_bytes, key_string_lookup_key, key_string_lookup_string,
};
#[cfg(windows)]
use rmux_proto::ProcessCommand;
use rmux_proto::{
ErrorResponse, OptionName, PaneTarget, Response, RmuxError, SendKeysResponse, SessionName,
};
use rmux_pty::PtyMaster;
#[cfg(windows)]
use rmux_pty::{ProcessId, WindowsConsoleKeyEvent};
use crate::input_keys::{encode_key_with_backspace, encode_mouse_event, ExtendedKeyFormat};
use crate::keys::parse_key_code;
#[cfg(windows)]
use crate::pane_terminals::DeferredInitialPaneConsoleInputAction;
use crate::pane_terminals::{session_not_found, HandlerState, PasteDelimiters};
#[cfg(unix)]
const IMMEDIATE_PANE_INPUT_MAX_BYTES: usize = 256;
#[cfg(any(unix, windows))]
const PANE_INPUT_WRITE_TIMEOUT: Duration = Duration::from_secs(2);
#[cfg(windows)]
const PANE_INPUT_WRITE_RECOVERY_GRACE: Duration = Duration::from_millis(500);
const DEAD_PANE_INPUT_ERROR: &str = "target pane has exited";
pub(in crate::handler) struct PaneInputWrite {
session_name: SessionName,
window_index: u32,
pane_index: u32,
sink: PaneInputSink,
}
impl PaneInputWrite {
pub(super) fn session_name(&self) -> &SessionName {
&self.session_name
}
}
enum PaneInputSink {
Pty(PtyMaster),
#[cfg(windows)]
ConsoleUtf8(ProcessId),
Disabled,
#[cfg(windows)]
QueuedStarting,
#[cfg(test)]
CapturedForTest,
}
#[cfg(windows)]
pub(in crate::handler) const LEGACY_CONPTY_NON_UTF8_BRACKETED_PASTE_ERROR: &str =
"cannot preserve bracketed paste containing non-UTF-8 bytes on this Windows host";
#[cfg(windows)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in crate::handler) enum WindowsPasteSink {
Pty,
ConsoleUtf8,
RejectNonUtf8,
}
#[cfg(windows)]
pub(super) struct PaneConsoleInputWrite {
session_name: SessionName,
window_index: u32,
pane_index: u32,
sink: PaneConsoleInputSink,
}
#[cfg(windows)]
impl PaneConsoleInputWrite {
pub(super) fn session_name(&self) -> &SessionName {
&self.session_name
}
}
#[cfg(windows)]
enum PaneConsoleInputSink {
ConsolePid(ProcessId),
Disabled,
QueuedStarting,
#[cfg(test)]
CapturedForTest,
}
#[cfg(windows)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum WindowsConsoleInputAction {
Key(WindowsConsoleKeyEvent),
KeyThenInterrupt(WindowsConsoleKeyEvent),
Interrupt,
}
#[cfg(windows)]
impl WindowsConsoleInputAction {
const fn deferred(self) -> DeferredInitialPaneConsoleInputAction {
match self {
Self::Key(key) => DeferredInitialPaneConsoleInputAction::Key(key),
Self::KeyThenInterrupt(key) => {
DeferredInitialPaneConsoleInputAction::KeyThenInterrupt(key)
}
Self::Interrupt => DeferredInitialPaneConsoleInputAction::Interrupt,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(in crate::handler) enum PaneInputLiveness {
TolerateDead,
RejectDead,
}
pub(in crate::handler) fn prepare_pane_input_write(
state: &mut HandlerState,
target: &PaneTarget,
bytes: &[u8],
liveness: PaneInputLiveness,
) -> Result<PaneInputWrite, RmuxError> {
prepare_pane_input_write_with_encoding(state, target, bytes, liveness, None)
}
pub(in crate::handler) fn prepare_pane_bracketed_paste_write(
state: &mut HandlerState,
target: &PaneTarget,
bytes: &[u8],
liveness: PaneInputLiveness,
delimiters: PasteDelimiters,
) -> Result<PaneInputWrite, RmuxError> {
prepare_pane_input_write_with_encoding(state, target, bytes, liveness, Some(delimiters))
}
fn prepare_pane_input_write_with_encoding(
state: &mut HandlerState,
target: &PaneTarget,
bytes: &[u8],
liveness: PaneInputLiveness,
paste: Option<PasteDelimiters>,
) -> Result<PaneInputWrite, RmuxError> {
let session_name = target.session_name().clone();
let window_index = target.window_index();
let pane_index = target.pane_index();
let pane_id = pane_id_for_input_target(state, target)?;
if state.pane_input_is_disabled(pane_id) {
#[cfg(not(test))]
let _ = bytes;
return Ok(PaneInputWrite {
session_name,
window_index,
pane_index,
sink: PaneInputSink::Disabled,
});
}
#[cfg(test)]
if state.append_pane_input_capture_for_test(target, bytes) {
return Ok(PaneInputWrite {
session_name,
window_index,
pane_index,
sink: PaneInputSink::CapturedForTest,
});
}
#[cfg(windows)]
if if let Some(delimiters) = paste {
state.queue_starting_pane_paste_input(
&session_name,
window_index,
pane_index,
bytes,
delimiters,
)?
} else {
state.queue_starting_pane_input(&session_name, window_index, pane_index, bytes)?
} {
return Ok(PaneInputWrite {
session_name,
window_index,
pane_index,
sink: PaneInputSink::QueuedStarting,
});
}
let master = match liveness {
PaneInputLiveness::RejectDead => {
state.clone_pane_master_if_alive(&session_name, window_index, pane_index)?
}
PaneInputLiveness::TolerateDead => {
state.clone_pane_master(&session_name, window_index, pane_index)?
}
};
#[cfg(windows)]
if let Some(delimiters) = paste {
match windows_paste_sink(master.preserves_verbatim_input(), bytes, delimiters) {
WindowsPasteSink::Pty => {}
WindowsPasteSink::ConsoleUtf8 => {
let raw_pid = state.pane_pid_in_window(&session_name, window_index, pane_index)?;
let pid = ProcessId::new(raw_pid)
.map_err(|error| RmuxError::Server(error.to_string()))?;
return Ok(PaneInputWrite {
session_name,
window_index,
pane_index,
sink: PaneInputSink::ConsoleUtf8(pid),
});
}
WindowsPasteSink::RejectNonUtf8 => {
return Err(RmuxError::Message(
LEGACY_CONPTY_NON_UTF8_BRACKETED_PASTE_ERROR.to_owned(),
));
}
}
}
#[cfg(not(windows))]
let _ = paste;
#[cfg(not(any(test, windows)))]
let _ = bytes;
Ok(PaneInputWrite {
session_name,
window_index,
pane_index,
sink: PaneInputSink::Pty(master),
})
}
#[cfg(windows)]
pub(in crate::handler) fn windows_paste_sink(
preserves_verbatim_input: bool,
bytes: &[u8],
delimiters: PasteDelimiters,
) -> WindowsPasteSink {
if preserves_verbatim_input {
return WindowsPasteSink::Pty;
}
if std::str::from_utf8(bytes).is_ok() {
return WindowsPasteSink::ConsoleUtf8;
}
match delimiters {
PasteDelimiters::Wrapped => WindowsPasteSink::RejectNonUtf8,
PasteDelimiters::Bare => WindowsPasteSink::Pty,
}
}
pub(super) fn prepare_attached_pane_input_writes(
state: &mut HandlerState,
target: &PaneTarget,
bytes: &[u8],
) -> Result<Vec<PaneInputWrite>, RmuxError> {
prepare_synchronized_pane_input_writes(state, target, bytes)
}
#[cfg(windows)]
pub(super) fn prepare_attached_pane_console_input_writes(
state: &mut HandlerState,
target: &PaneTarget,
bytes: &[u8],
action: WindowsConsoleInputAction,
) -> Result<Vec<PaneConsoleInputWrite>, RmuxError> {
synchronized_input_targets(state, target)?
.into_iter()
.map(|target| prepare_pane_console_input_write(state, &target, bytes, action))
.collect()
}
#[cfg(windows)]
pub(super) fn prepare_pane_console_input_write(
state: &mut HandlerState,
target: &PaneTarget,
bytes: &[u8],
action: WindowsConsoleInputAction,
) -> Result<PaneConsoleInputWrite, RmuxError> {
let session_name = target.session_name().clone();
let window_index = target.window_index();
let pane_index = target.pane_index();
let pane_id = pane_id_for_input_target(state, target)?;
if state.pane_input_is_disabled(pane_id) {
let _ = bytes;
return Ok(PaneConsoleInputWrite {
session_name,
window_index,
pane_index,
sink: PaneConsoleInputSink::Disabled,
});
}
#[cfg(test)]
if state.append_pane_input_capture_for_test(target, bytes) {
return Ok(PaneConsoleInputWrite {
session_name,
window_index,
pane_index,
sink: PaneConsoleInputSink::CapturedForTest,
});
}
if state.queue_starting_pane_console_input(
&session_name,
window_index,
pane_index,
action.deferred(),
bytes.len(),
)? {
return Ok(PaneConsoleInputWrite {
session_name,
window_index,
pane_index,
sink: PaneConsoleInputSink::QueuedStarting,
});
}
let raw_pid = state.pane_pid_in_window(&session_name, window_index, pane_index)?;
let pid = ProcessId::new(raw_pid).map_err(|error| RmuxError::Server(error.to_string()))?;
Ok(PaneConsoleInputWrite {
session_name,
window_index,
pane_index,
sink: PaneConsoleInputSink::ConsolePid(pid),
})
}
#[cfg(windows)]
pub(super) fn windows_console_input_for_attached_key(
state: &HandlerState,
target: &PaneTarget,
decoded_key: rmux_core::KeyCode,
console_key: WindowsConsoleKeyEvent,
) -> WindowsConsoleInputAction {
let console_key = if key_matches_name(decoded_key, "C-d")
&& !target_uses_windows_cmd_console_ctrl_d(state, target)
{
windows_ctrl_d_console_key(false).with_repeat_count(console_key.repeat_count())
} else {
console_key
};
windows_console_input_for_attached_key_event(console_key)
}
#[cfg(windows)]
fn windows_console_input_for_attached_key_event(
console_key: WindowsConsoleKeyEvent,
) -> WindowsConsoleInputAction {
if console_key.virtual_key_code() == b'C' as u16 && console_key.unicode_char() == 0x03 {
WindowsConsoleInputAction::KeyThenInterrupt(console_key)
} else {
WindowsConsoleInputAction::Key(console_key)
}
}
#[cfg(windows)]
pub(super) fn windows_console_input_for_tokens(
tokens: &[String],
repeat_count: usize,
) -> Option<(WindowsConsoleInputAction, Vec<u8>)> {
windows_console_input_for_tokens_with_ctrl_d(
tokens,
repeat_count,
WindowsConsoleKeyEvent::ctrl_d(),
)
}
#[cfg(windows)]
pub(super) fn windows_console_input_for_target_tokens(
state: &HandlerState,
target: &PaneTarget,
tokens: &[String],
repeat_count: usize,
) -> Option<(WindowsConsoleInputAction, Vec<u8>)> {
let target_uses_cmd =
tokens_are_windows_ctrl_d(tokens) && target_uses_windows_cmd_console_ctrl_d(state, target);
let ctrl_d = windows_ctrl_d_console_key(target_uses_cmd);
windows_console_input_for_tokens_with_ctrl_d(tokens, repeat_count, ctrl_d)
}
#[cfg(windows)]
const fn windows_ctrl_d_console_key(target_uses_cmd: bool) -> WindowsConsoleKeyEvent {
if target_uses_cmd {
WindowsConsoleKeyEvent::ctrl_d()
} else {
WindowsConsoleKeyEvent::ctrl_d_eot()
}
}
#[cfg(windows)]
fn windows_console_input_for_tokens_with_ctrl_d(
tokens: &[String],
repeat_count: usize,
ctrl_d: WindowsConsoleKeyEvent,
) -> Option<(WindowsConsoleInputAction, Vec<u8>)> {
let [token] = tokens else {
return None;
};
windows_console_input_for_token_with_ctrl_d(token, repeat_count, ctrl_d)
}
#[cfg(windows)]
pub(super) fn windows_console_input_for_token(
token: &str,
repeat_count: usize,
) -> Option<(WindowsConsoleInputAction, Vec<u8>)> {
windows_console_input_for_token_with_ctrl_d(
token,
repeat_count,
WindowsConsoleKeyEvent::ctrl_d(),
)
}
#[cfg(windows)]
fn windows_console_input_for_token_with_ctrl_d(
token: &str,
repeat_count: usize,
ctrl_d: WindowsConsoleKeyEvent,
) -> Option<(WindowsConsoleInputAction, Vec<u8>)> {
let key = key_code_lookup_bits(parse_key_code(token)?);
let repeat_count = repeat_count.min(usize::from(u16::MAX)).max(1);
let repeat_count_u16 = repeat_count as u16;
let (key_event, byte) = windows_console_ctrl_letter_for_key(key, ctrl_d)?;
let key_event = key_event.with_repeat_count(repeat_count_u16);
let action = if key_matches_name(key, "C-c") {
WindowsConsoleInputAction::KeyThenInterrupt(key_event)
} else {
WindowsConsoleInputAction::Key(key_event)
};
Some((action, vec![byte; repeat_count]))
}
#[cfg(windows)]
fn tokens_are_windows_ctrl_d(tokens: &[String]) -> bool {
let [token] = tokens else {
return false;
};
parse_key_code(token).is_some_and(|key| key_matches_name(key_code_lookup_bits(key), "C-d"))
}
#[cfg(windows)]
pub(super) fn tokens_contain_windows_console_interrupt(tokens: &[String]) -> bool {
tokens.iter().any(|token| {
windows_console_input_for_token(token, 1).is_some_and(|(action, _)| {
matches!(action, WindowsConsoleInputAction::KeyThenInterrupt(_))
})
})
}
#[cfg(windows)]
fn windows_console_ctrl_letter_for_key(
key: rmux_core::KeyCode,
ctrl_d: WindowsConsoleKeyEvent,
) -> Option<(WindowsConsoleKeyEvent, u8)> {
for letter in b'A'..=b'Z' {
let name = format!("C-{}", char::from(letter.to_ascii_lowercase()));
if Some(key) == key_string_lookup_string(&name).map(key_code_lookup_bits) {
let event = match letter {
b'C' => WindowsConsoleKeyEvent::ctrl_c(),
b'D' => ctrl_d,
b'Z' => WindowsConsoleKeyEvent::ctrl_z(),
_ => WindowsConsoleKeyEvent::ctrl_letter(letter)?,
};
return Some((event, letter - b'A' + 1));
}
}
None
}
#[cfg(windows)]
pub(super) fn should_emulate_windows_cmd_select_all(
state: &HandlerState,
target: &PaneTarget,
key: rmux_core::KeyCode,
) -> bool {
key_matches_name(key, "C-a") && target_uses_windows_cmd_shell(state, target)
}
#[cfg(windows)]
pub(super) fn tokens_emulate_windows_cmd_select_all(
state: &HandlerState,
target: &PaneTarget,
tokens: &[String],
) -> bool {
let [token] = tokens else {
return false;
};
parse_key_code(token)
.is_some_and(|key| should_emulate_windows_cmd_select_all(state, target, key))
}
#[cfg(windows)]
pub(super) fn should_route_windows_control_as_pty_bytes(
state: &HandlerState,
target: &PaneTarget,
key: rmux_core::KeyCode,
) -> bool {
if key_matches_name(key, "C-d") {
return target_routes_windows_ctrl_d_as_posix_eot(state, target);
}
key_matches_name(key, "C-c") && target_uses_wsl_host_process(state, target)
}
#[cfg(windows)]
pub(super) fn tokens_route_windows_control_as_pty_bytes(
state: &HandlerState,
target: &PaneTarget,
tokens: &[String],
) -> bool {
let [token] = tokens else {
return false;
};
parse_key_code(token)
.is_some_and(|key| should_route_windows_control_as_pty_bytes(state, target, key))
}
#[cfg(windows)]
fn target_uses_windows_cmd_shell(state: &HandlerState, target: &PaneTarget) -> bool {
let profile_shell_is_cmd = state
.pane_profile_in_window(
target.session_name(),
target.window_index(),
target.pane_index(),
)
.ok()
.and_then(|profile| profile.shell().file_name())
.and_then(|name| name.to_str())
.is_some_and(is_windows_cmd_name);
if profile_shell_is_cmd {
return true;
}
state
.pane_pid_in_window(
target.session_name(),
target.window_index(),
target.pane_index(),
)
.ok()
.and_then(rmux_os::process::command_name)
.as_deref()
.is_some_and(is_windows_cmd_name)
}
#[cfg(windows)]
fn target_uses_windows_cmd_console_ctrl_d(state: &HandlerState, target: &PaneTarget) -> bool {
if let Some(uses_cmd) = pane_id_for_input_target(state, target)
.ok()
.and_then(|pane_id| state.pane_start_process_command_for_id(pane_id))
.and_then(process_command_windows_cmd_hint)
{
return uses_cmd;
}
if let Some(profile_shell_is_cmd) = state
.pane_profile_in_window(
target.session_name(),
target.window_index(),
target.pane_index(),
)
.ok()
.and_then(|profile| {
profile
.shell()
.file_name()
.and_then(|name| name.to_str())
.map(is_windows_cmd_name)
})
{
return profile_shell_is_cmd;
}
state
.pane_pid_in_window(
target.session_name(),
target.window_index(),
target.pane_index(),
)
.ok()
.and_then(rmux_os::process::command_name)
.as_deref()
.is_some_and(is_windows_cmd_name)
}
#[cfg(windows)]
fn process_command_windows_cmd_hint(command: &ProcessCommand) -> Option<bool> {
match command {
ProcessCommand::Shell(command) => shell_text_windows_cmd_hint(command),
ProcessCommand::Argv(argv) => Some(argv_invokes_windows_cmd(argv)),
_ => None,
}
}
#[cfg(windows)]
fn shell_text_windows_cmd_hint(command: &str) -> Option<bool> {
let head = windows_shell_command_head(command)?;
let name = windows_command_name(head);
if is_windows_cmd_name(name) {
Some(true)
} else if matches!(
name.to_ascii_lowercase().as_str(),
"pwsh" | "pwsh.exe" | "powershell" | "powershell.exe" | "wsl" | "wsl.exe"
) {
Some(false)
} else {
None
}
}
#[cfg(windows)]
fn windows_shell_command_head(command: &str) -> Option<&str> {
let command = command.trim_start();
let quote = command.as_bytes().first().copied()?;
if matches!(quote, b'"' | b'\'') {
let quoted = &command[1..];
let end = quoted.as_bytes().iter().position(|byte| *byte == quote)?;
return Some("ed[..end]);
}
let end = command.find(char::is_whitespace).unwrap_or(command.len());
Some(&command[..end])
}
#[cfg(windows)]
fn argv_invokes_windows_cmd(argv: &[String]) -> bool {
let Some(head) = argv.first() else {
return false;
};
let name = windows_command_name(head);
is_windows_cmd_name(name)
|| std::path::Path::new(name)
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
extension.eq_ignore_ascii_case("bat") || extension.eq_ignore_ascii_case("cmd")
})
}
#[cfg(windows)]
fn windows_command_name(command: &str) -> &str {
let trimmed = command.trim_matches(['"', '\'']);
std::path::Path::new(trimmed)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(trimmed)
}
#[cfg(windows)]
fn target_uses_wsl_host_process(state: &HandlerState, target: &PaneTarget) -> bool {
let lifecycle_command = pane_id_for_input_target(state, target)
.ok()
.and_then(|pane_id| state.pane_start_command_for_id(pane_id));
let lifecycle_matches = lifecycle_command.is_some_and(command_invokes_wsl);
let process_name = state
.pane_pid_in_window(
target.session_name(),
target.window_index(),
target.pane_index(),
)
.ok()
.and_then(rmux_os::process::command_name);
let process_matches = process_name.as_deref().is_some_and(is_wsl_host_name);
trace_windows_wsl_detection(process_name.as_deref(), lifecycle_matches, process_matches);
if lifecycle_matches {
return true;
}
process_matches
}
#[cfg(windows)]
fn target_routes_windows_ctrl_d_as_posix_eot(state: &HandlerState, target: &PaneTarget) -> bool {
target_uses_wsl_host_process(state, target) || target_has_wsl_descendant_process(state, target)
}
#[cfg(windows)]
fn target_has_wsl_descendant_process(state: &HandlerState, target: &PaneTarget) -> bool {
let Some(pane_pid) = state
.pane_pid_in_window(
target.session_name(),
target.window_index(),
target.pane_index(),
)
.ok()
else {
return false;
};
let descendant_matches = rmux_os::process::descendant_command_names(pane_pid)
.iter()
.any(|name| is_wsl_host_name(name));
trace_windows_wsl_descendant_detection(pane_pid, descendant_matches);
descendant_matches
}
#[cfg(windows)]
fn trace_windows_wsl_descendant_detection(pane_pid: u32, descendant_matches: bool) {
if std::env::var_os("RMUX_TRACE_WINDOWS_KEYS").is_none() {
return;
}
tracing::debug!(
target: "rmux::windows_keys",
pane_pid,
descendant_matches,
"detect Windows WSL descendant control-key routing"
);
}
#[cfg(windows)]
fn trace_windows_wsl_detection(
process_name: Option<&str>,
lifecycle_matches: bool,
process_matches: bool,
) {
if std::env::var_os("RMUX_TRACE_WINDOWS_KEYS").is_none() {
return;
}
tracing::debug!(
target: "rmux::windows_keys",
process_name,
lifecycle_matches,
process_matches,
"detect Windows WSL control-key routing"
);
}
#[cfg(windows)]
fn command_invokes_wsl(command: &[String]) -> bool {
command.iter().any(|part| {
part.split_whitespace()
.next()
.is_some_and(is_wsl_command_head)
|| part.to_ascii_lowercase().contains("wsl.exe")
})
}
#[cfg(windows)]
fn is_wsl_command_head(head: &str) -> bool {
let trimmed = head.trim_matches(['"', '\'']);
std::path::Path::new(trimmed)
.file_name()
.and_then(|name| name.to_str())
.is_some_and(is_wsl_host_name)
|| is_wsl_host_name(trimmed)
}
#[cfg(windows)]
fn is_wsl_host_name(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
lower == "wsl.exe"
|| lower == "wsl"
|| lower.ends_with("\\wsl.exe")
|| lower.ends_with("/wsl.exe")
}
#[cfg(windows)]
fn is_windows_cmd_name(name: &str) -> bool {
name.eq_ignore_ascii_case("cmd.exe") || name.eq_ignore_ascii_case("cmd")
}
#[cfg(windows)]
fn windows_cmd_select_all_sequence(
state: &HandlerState,
target: &PaneTarget,
) -> Result<Option<Vec<u8>>, RmuxError> {
let mut bytes = Vec::new();
for key_name in ["C-Home", "S-End"] {
let Some(key) = key_string_lookup_string(key_name) else {
return Ok(None);
};
let Some(encoded) = encode_key_for_target(state, target, key)? else {
return Ok(None);
};
bytes.extend_from_slice(&encoded);
}
Ok(Some(bytes))
}
#[cfg(windows)]
fn key_matches_name(key: rmux_core::KeyCode, name: &str) -> bool {
key_string_lookup_string(name)
.is_some_and(|candidate| key_code_lookup_bits(candidate) == key_code_lookup_bits(key))
}
pub(super) fn prepare_synchronized_pane_input_writes(
state: &mut HandlerState,
target: &PaneTarget,
bytes: &[u8],
) -> Result<Vec<PaneInputWrite>, RmuxError> {
synchronized_input_targets(state, target)?
.into_iter()
.map(|target| {
prepare_pane_input_write(state, &target, bytes, PaneInputLiveness::TolerateDead)
})
.collect()
}
pub(super) fn synchronized_input_targets(
state: &HandlerState,
target: &PaneTarget,
) -> Result<Vec<PaneTarget>, RmuxError> {
let session_name = target.session_name();
let window_index = target.window_index();
let pane_index = target.pane_index();
let synchronized =
state
.options
.resolve_for_window(session_name, window_index, OptionName::SynchronizePanes)
== Some("on");
let panes = {
let session = state
.sessions
.session(session_name)
.ok_or_else(|| session_not_found(session_name))?;
let window = session.window_at(window_index).ok_or_else(|| {
RmuxError::invalid_target(
format!("{session_name}:{window_index}"),
"window index does not exist in session",
)
})?;
let Some(target_pane) = window.pane(pane_index) else {
return Err(RmuxError::invalid_target(
target.to_string(),
"pane index does not exist in window",
));
};
if synchronized {
window
.panes()
.iter()
.map(|pane| (pane.index(), pane.id()))
.collect::<Vec<_>>()
} else {
vec![(pane_index, target_pane.id())]
}
};
Ok(panes
.into_iter()
.filter(|(_, pane_id)| {
!state.pane_is_dead(session_name, *pane_id) && !state.pane_input_is_disabled(*pane_id)
})
.map(|(pane_index, _)| {
PaneTarget::with_window(session_name.clone(), window_index, pane_index)
})
.collect())
}
pub(super) async fn write_bytes_to_target(
write: PaneInputWrite,
bytes: Vec<u8>,
key_count: usize,
) -> Response {
match write_bytes_to_target_io(write, bytes).await {
Ok(()) => Response::SendKeys(SendKeysResponse { key_count }),
Err(error) => Response::Error(ErrorResponse { error }),
}
}
pub(super) async fn write_bytes_to_targets(
writes: Vec<PaneInputWrite>,
bytes: Vec<u8>,
key_count: usize,
) -> Response {
for write in writes {
if let Err(error) = write_bytes_to_target_io(write, bytes.clone()).await {
return Response::Error(ErrorResponse { error });
}
}
Response::SendKeys(SendKeysResponse { key_count })
}
pub(in crate::handler) async fn write_bytes_to_target_io(
write: PaneInputWrite,
bytes: Vec<u8>,
) -> Result<(), RmuxError> {
write_bytes_to_target_io_classified(write, bytes)
.await
.map_err(PaneInputWriteFailure::into_error)
}
async fn write_bytes_to_target_io_classified(
write: PaneInputWrite,
bytes: Vec<u8>,
) -> Result<(), PaneInputWriteFailure> {
if bytes.is_empty() {
return Ok(());
}
let PaneInputWrite {
session_name,
window_index,
pane_index,
sink,
} = write;
match sink {
PaneInputSink::Disabled => Ok(()),
#[cfg(windows)]
PaneInputSink::QueuedStarting => Ok(()),
#[cfg(windows)]
PaneInputSink::ConsoleUtf8(pid) => {
tokio::task::spawn_blocking(move || rmux_pty::write_windows_console_utf8(pid, &bytes))
.await
.map_err(|error| {
PaneInputWriteFailure::other(RmuxError::Server(format!(
"pane console text task failed: {error}"
)))
})?
.map_err(|error| {
PaneInputWriteFailure::from_console(
&error,
&session_name,
window_index,
pane_index,
)
})
}
PaneInputSink::Pty(master) => match write_pane_bytes(master, bytes).await {
Ok(()) => Ok(()),
Err(error) => Err(PaneInputWriteFailure::from_pty(
error,
&session_name,
window_index,
pane_index,
)),
},
#[cfg(test)]
PaneInputSink::CapturedForTest => Ok(()),
}
}
pub(in crate::handler) async fn write_attached_bytes_to_target_io(
write: PaneInputWrite,
bytes: Vec<u8>,
) -> Result<(), RmuxError> {
match write_bytes_to_target_io_classified(write, bytes).await {
Ok(()) => Ok(()),
Err(failure) => failure.into_attached_result(),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PaneInputFailureKind {
PaneGone,
#[cfg(any(windows, test))]
Congested,
Other,
}
#[derive(Debug)]
struct PaneInputWriteFailure {
kind: PaneInputFailureKind,
error: RmuxError,
}
impl PaneInputWriteFailure {
fn from_pty(
error: io::Error,
session_name: &SessionName,
window_index: u32,
pane_index: u32,
) -> Self {
if is_dead_pane_write_error(&error) {
return Self::pane_gone();
}
#[cfg(windows)]
if error.kind() == io::ErrorKind::TimedOut {
return Self::congested(RmuxError::Server(format!(
"failed to write to pane {session_name}:{window_index}.{pane_index}: {error}"
)));
}
Self::other(RmuxError::Server(format!(
"failed to write to pane {session_name}:{window_index}.{pane_index}: {error}"
)))
}
#[cfg(any(windows, test))]
fn from_console(
error: &io::Error,
session_name: &SessionName,
window_index: u32,
pane_index: u32,
) -> Self {
if is_dead_pane_console_input_error(error) {
return Self::pane_gone();
}
let congested = error.kind() == io::ErrorKind::TimedOut;
let error = RmuxError::Server(format!(
"failed to write console input to pane \
{session_name}:{window_index}.{pane_index}: {error}"
));
if congested {
return Self::congested(error);
}
Self::other(error)
}
fn pane_gone() -> Self {
Self {
kind: PaneInputFailureKind::PaneGone,
error: RmuxError::Server(DEAD_PANE_INPUT_ERROR.to_owned()),
}
}
#[cfg(any(windows, test))]
fn congested(error: RmuxError) -> Self {
Self {
kind: PaneInputFailureKind::Congested,
error,
}
}
fn other(error: RmuxError) -> Self {
Self {
kind: PaneInputFailureKind::Other,
error,
}
}
fn into_error(self) -> RmuxError {
self.error
}
fn into_attached_result(self) -> Result<(), RmuxError> {
match self.kind {
PaneInputFailureKind::PaneGone => Ok(()),
#[cfg(any(windows, test))]
PaneInputFailureKind::Congested => Ok(()),
PaneInputFailureKind::Other => Err(self.error),
}
}
}
pub(in crate::handler) fn is_dead_pane_write_error(error: &io::Error) -> bool {
matches!(
error.kind(),
io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset
) || is_unix_pty_eio(error)
}
#[cfg(unix)]
fn is_unix_pty_eio(error: &io::Error) -> bool {
error.raw_os_error() == Some(rustix::io::Errno::IO.raw_os_error())
}
#[cfg(not(unix))]
fn is_unix_pty_eio(_error: &io::Error) -> bool {
false
}
#[cfg(any(windows, test))]
fn is_dead_pane_console_input_error(error: &io::Error) -> bool {
const ERROR_FILE_NOT_FOUND: i32 = 2;
const ERROR_INVALID_HANDLE: i32 = 6;
const ERROR_INVALID_PARAMETER: i32 = 87;
is_dead_pane_write_error(error)
|| matches!(
error.raw_os_error(),
Some(ERROR_FILE_NOT_FOUND | ERROR_INVALID_HANDLE | ERROR_INVALID_PARAMETER)
)
}
#[cfg(any(windows, test))]
fn pane_console_input_failure(
error: &io::Error,
session_name: &SessionName,
window_index: u32,
pane_index: u32,
) -> PaneInputWriteFailure {
PaneInputWriteFailure::from_console(error, session_name, window_index, pane_index)
}
#[cfg(windows)]
pub(super) async fn write_windows_console_input_action_to_target_io(
write: PaneConsoleInputWrite,
action: WindowsConsoleInputAction,
) -> Result<(), RmuxError> {
write_windows_console_input_action_to_target_io_classified(write, action)
.await
.map_err(PaneInputWriteFailure::into_error)
}
#[cfg(windows)]
async fn write_windows_console_input_action_to_target_io_classified(
write: PaneConsoleInputWrite,
action: WindowsConsoleInputAction,
) -> Result<(), PaneInputWriteFailure> {
let PaneConsoleInputWrite {
session_name,
window_index,
pane_index,
sink,
} = write;
match sink {
PaneConsoleInputSink::Disabled => Ok(()),
PaneConsoleInputSink::QueuedStarting => Ok(()),
PaneConsoleInputSink::ConsolePid(pid) => {
trace_windows_console_input(
&session_name,
window_index,
pane_index,
pid,
action,
"dispatch",
);
tokio::task::spawn_blocking(move || match action {
WindowsConsoleInputAction::Key(key) => {
crate::windows_console_input::write_with_transient_retry(|| {
rmux_pty::write_windows_console_key(pid, key)
})
}
WindowsConsoleInputAction::KeyThenInterrupt(key) => {
crate::windows_console_input::write_console_key_then_processed_interrupt(
|| rmux_pty::write_windows_console_key_reporting_processed_input(pid, key),
|| rmux_pty::send_windows_console_interrupt(pid),
)
}
WindowsConsoleInputAction::Interrupt => {
crate::windows_console_input::write_with_transient_retry(|| {
rmux_pty::send_windows_console_interrupt(pid)
})
}
})
.await
.map_err(|error| {
PaneInputWriteFailure::other(RmuxError::Server(format!(
"pane console input task failed: {error}"
)))
})?
.map_err(|error| {
PaneInputWriteFailure::from_console(&error, &session_name, window_index, pane_index)
})
}
#[cfg(test)]
PaneConsoleInputSink::CapturedForTest => Ok(()),
}
}
#[cfg(windows)]
pub(super) async fn write_attached_windows_console_input_action_to_target_io(
write: PaneConsoleInputWrite,
action: WindowsConsoleInputAction,
) -> Result<(), RmuxError> {
match write_windows_console_input_action_to_target_io_classified(write, action).await {
Ok(()) => Ok(()),
Err(failure) => failure.into_attached_result(),
}
}
#[cfg(windows)]
fn trace_windows_console_input(
session_name: &SessionName,
window_index: u32,
pane_index: u32,
pid: ProcessId,
action: WindowsConsoleInputAction,
stage: &'static str,
) {
if std::env::var_os("RMUX_TRACE_WINDOWS_KEYS").is_none() {
return;
}
tracing::debug!(
target: "rmux::windows_keys",
%session_name,
window_index,
pane_index,
pid = pid.as_u32(),
?action,
stage,
"Windows console input action"
);
}
pub(super) fn pane_id_for_input_target(
state: &HandlerState,
target: &PaneTarget,
) -> Result<rmux_core::PaneId, RmuxError> {
super::super::require_expected_pane_identity(state, target)?;
let session_name = target.session_name();
let window_index = target.window_index();
let pane_index = target.pane_index();
let session = state
.sessions
.session(session_name)
.ok_or_else(|| session_not_found(session_name))?;
let window = session.window_at(window_index).ok_or_else(|| {
RmuxError::invalid_target(
format!("{session_name}:{window_index}"),
"window index does not exist in session",
)
})?;
window
.pane(pane_index)
.map(rmux_core::Pane::id)
.ok_or_else(|| {
RmuxError::invalid_target(target.to_string(), "pane index does not exist in window")
})
}
#[cfg(any(unix, windows))]
async fn write_pane_bytes(master: PtyMaster, bytes: Vec<u8>) -> std::io::Result<()> {
#[cfg(unix)]
if should_try_immediate_pane_input(bytes.len()) {
let written = master.try_write_immediate(&bytes)?;
if written == bytes.len() {
return Ok(());
}
return write_pane_bytes_blocking(master, bytes[written..].to_vec()).await;
}
write_pane_bytes_blocking(master, bytes).await
}
#[cfg(any(unix, windows))]
async fn write_pane_bytes_blocking(master: PtyMaster, bytes: Vec<u8>) -> std::io::Result<()> {
tokio::task::spawn_blocking(move || {
#[cfg(unix)]
{
master.write_all_with_timeout(&bytes, PANE_INPUT_WRITE_TIMEOUT)
}
#[cfg(windows)]
{
master.write_all_with_stall_recovery(
&bytes,
PANE_INPUT_WRITE_TIMEOUT,
PANE_INPUT_WRITE_RECOVERY_GRACE,
)
}
})
.await
.map_err(|error| std::io::Error::other(format!("pane write task failed: {error}")))?
}
#[cfg(unix)]
fn should_try_immediate_pane_input(byte_len: usize) -> bool {
(1..=IMMEDIATE_PANE_INPUT_MAX_BYTES).contains(&byte_len)
}
#[cfg(not(any(unix, windows)))]
async fn write_pane_bytes(master: PtyMaster, bytes: Vec<u8>) -> std::io::Result<()> {
master.write_all(&bytes)
}
pub(super) fn encode_tokens_for_target(
state: &HandlerState,
target: &PaneTarget,
tokens: &[String],
) -> Result<Vec<u8>, RmuxError> {
let mut bytes = Vec::new();
for token in tokens {
if let Some(key) = parse_key_code(token) {
let Some(encoded) = encode_key_for_target(state, target, key)? else {
return Err(RmuxError::Server(format!(
"key {} cannot be sent to a pane",
key_string_lookup_key(key_code_lookup_bits(key), false)
)));
};
bytes.extend_from_slice(&encoded);
} else {
bytes.extend_from_slice(token.as_bytes());
}
}
Ok(bytes)
}
pub(super) fn encode_key_for_target(
state: &HandlerState,
target: &PaneTarget,
key: rmux_core::KeyCode,
) -> Result<Option<Vec<u8>>, RmuxError> {
#[cfg(windows)]
if should_emulate_windows_cmd_select_all(state, target, key) {
return windows_cmd_select_all_sequence(state, target);
}
let pane_mode = pane_input_mode(state, target)?;
let format =
ExtendedKeyFormat::parse(state.options.resolve(None, OptionName::ExtendedKeysFormat));
let backspace = state
.options
.resolve(None, OptionName::Backspace)
.and_then(key_string_lookup_string)
.and_then(key_code_to_bytes)
.and_then(|bytes| (bytes.len() == 1).then_some(bytes[0]))
.unwrap_or(0x7f);
Ok(encode_key_with_backspace(pane_mode, format, key, backspace))
}
pub(super) fn pane_input_mode(state: &HandlerState, target: &PaneTarget) -> Result<u32, RmuxError> {
let pane_id = state
.sessions
.session(target.session_name())
.and_then(|session| session.window_at(target.window_index()))
.and_then(|window| window.pane(target.pane_index()))
.map(|pane| pane.id())
.ok_or_else(|| {
RmuxError::invalid_target(target.to_string(), "pane index does not exist in session")
})?;
let pane_mode = state
.pane_screen_state(target.session_name(), pane_id)
.map(|screen_state| screen_state.mode)
.unwrap_or_default();
Ok(pane_mode)
}
pub(super) fn encode_mouse_for_target(
state: &HandlerState,
target: &PaneTarget,
event: &crate::mouse::AttachedMouseEvent,
) -> Result<Vec<u8>, RmuxError> {
let session = state
.sessions
.session(target.session_name())
.ok_or_else(|| session_not_found(target.session_name()))?;
let window = session.window_at(target.window_index()).ok_or_else(|| {
RmuxError::invalid_target(target.to_string(), "window index does not exist in session")
})?;
let pane = window.pane(target.pane_index()).ok_or_else(|| {
RmuxError::invalid_target(target.to_string(), "pane index does not exist in session")
})?;
if event.ignore || event.pane_id != Some(pane.id()) {
return Ok(Vec::new());
}
let pane_mode = state
.pane_screen_state(target.session_name(), pane.id())
.map(|screen_state| screen_state.mode)
.unwrap_or_default();
let adjusted_y = match event.status_at {
Some(0) if event.raw.y >= event.status_lines => event.raw.y - event.status_lines,
_ => event.raw.y,
};
let Some(geometry) = crate::mouse::pane_content_geometry_for_target(state, target) else {
return Ok(Vec::new());
};
let Some((x, y)) = relative_mouse_position(event.raw.x, adjusted_y, geometry) else {
return Ok(Vec::new());
};
Ok(encode_mouse_event(pane_mode, &event.raw, x, y).unwrap_or_default())
}
fn relative_mouse_position(
x: u16,
y: u16,
geometry: rmux_core::PaneGeometry,
) -> Option<(u16, u16)> {
if x < geometry.x()
|| x >= geometry.x().saturating_add(geometry.cols())
|| y < geometry.y()
|| y >= geometry.y().saturating_add(geometry.rows())
{
return None;
}
Some((x - geometry.x(), y - geometry.y()))
}
pub(super) fn expand_send_key_tokens(
_state: &HandlerState,
_target: &PaneTarget,
tokens: &[String],
_expand_formats: bool,
) -> Result<Vec<String>, RmuxError> {
Ok(tokens.to_vec())
}
#[cfg(test)]
mod dead_pane_input_tests {
use super::*;
const GONE_PANE_CONSOLE_ERRORS: [i32; 3] = [2, 6, 87];
const EXHAUSTED_TRANSIENT_CONSOLE_ERROR: i32 = 31;
fn console_failure(raw_os_error: i32) -> PaneInputWriteFailure {
pane_console_input_failure(
&io::Error::from_raw_os_error(raw_os_error),
&SessionName::new("console-input").expect("valid session"),
0,
0,
)
}
#[test]
fn attached_input_survives_a_console_write_to_a_pane_that_exited() {
for raw_os_error in GONE_PANE_CONSOLE_ERRORS {
let failure = console_failure(raw_os_error);
assert!(
failure.into_attached_result().is_ok(),
"a console key written to a pane that exited must not end the \
attach connection (os error {raw_os_error})"
);
}
}
#[test]
fn console_writes_report_the_same_dead_pane_error_as_pane_bytes() {
for raw_os_error in GONE_PANE_CONSOLE_ERRORS {
let failure = console_failure(raw_os_error);
assert_eq!(
failure.kind,
PaneInputFailureKind::PaneGone,
"os error {raw_os_error} must retain a typed dead-pane classification"
);
assert_eq!(
failure.into_error(),
RmuxError::Server(DEAD_PANE_INPUT_ERROR.to_owned()),
"os error {raw_os_error} must reach send-keys as the canonical dead-pane error"
);
}
}
#[test]
fn other_console_write_failures_stay_reportable() {
let failure = console_failure(EXHAUSTED_TRANSIENT_CONSOLE_ERROR);
assert!(
failure
.error
.to_string()
.contains("failed to write console input to pane console-input:0.0"),
"unexpected error: {}",
failure.error
);
assert_eq!(failure.kind, PaneInputFailureKind::Other);
assert!(failure.into_attached_result().is_err());
}
#[test]
fn attached_input_drops_conpty_congestion_without_hiding_command_failure() {
let session = SessionName::new("console-input").expect("valid session");
let attached_failure = PaneInputWriteFailure::from_console(
&io::Error::new(io::ErrorKind::TimedOut, "transient ConPTY congestion"),
&session,
0,
0,
);
assert_eq!(
attached_failure.kind,
PaneInputFailureKind::Congested,
"timeout must remain distinct from a dead pane"
);
assert!(
attached_failure.into_attached_result().is_ok(),
"a congested pane drops only the current attached input"
);
let command_failure = PaneInputWriteFailure::from_console(
&io::Error::new(io::ErrorKind::TimedOut, "transient ConPTY congestion"),
&session,
0,
0,
);
assert!(
command_failure
.into_error()
.to_string()
.contains("transient ConPTY congestion"),
"command paths must still report the timeout"
);
}
#[cfg(windows)]
#[test]
fn attached_pty_timeout_uses_the_same_typed_congestion_policy() {
let session = SessionName::new("conpty-input").expect("valid session");
let failure = PaneInputWriteFailure::from_pty(
io::Error::new(io::ErrorKind::TimedOut, "ConPTY input stalled"),
&session,
0,
0,
);
assert_eq!(failure.kind, PaneInputFailureKind::Congested);
assert!(failure.into_attached_result().is_ok());
}
}
#[cfg(test)]
mod mouse_geometry_tests {
use rmux_core::PaneGeometry;
use rmux_proto::{OptionName, ScopeSelector, SetOptionMode, TerminalSize, WindowTarget};
use super::*;
#[test]
fn left_scrollbar_application_mouse_coordinates_start_at_content_zero() {
let mut state = HandlerState::default();
let session_name = SessionName::new("mouse-left-scrollbar").expect("valid session");
state
.sessions
.create_session(session_name.clone(), TerminalSize { cols: 20, rows: 8 })
.expect("session creation");
state
.sessions
.session_mut(&session_name)
.expect("created session")
.resize_active_window_geometry(
TerminalSize { cols: 20, rows: 8 },
TerminalSize { cols: 20, rows: 7 },
);
let window = WindowTarget::with_window(session_name.clone(), 0);
for (option, value) in [
(OptionName::PaneScrollbars, "on"),
(OptionName::PaneScrollbarsPosition, "left"),
(OptionName::PaneScrollbarsStyle, "width=2,pad=1"),
] {
state
.options
.set(
ScopeSelector::Window(window.clone()),
option,
value.to_owned(),
SetOptionMode::Replace,
)
.expect("scrollbar option");
}
let target = PaneTarget::with_window(session_name, 0, 0);
let geometry = crate::mouse::pane_content_geometry_for_target(&state, &target)
.expect("pane content geometry");
assert_eq!(geometry, PaneGeometry::new(3, 0, 17, 7));
assert_eq!(relative_mouse_position(3, 0, geometry), Some((0, 0)));
assert_eq!(relative_mouse_position(19, 0, geometry), Some((16, 0)));
assert_eq!(relative_mouse_position(2, 0, geometry), None);
}
}
#[cfg(all(test, unix))]
mod tests {
#[test]
fn immediate_pane_input_is_reserved_for_short_interactive_writes() {
assert!(!super::should_try_immediate_pane_input(0));
assert!(super::should_try_immediate_pane_input(1));
assert!(super::should_try_immediate_pane_input(
super::IMMEDIATE_PANE_INPUT_MAX_BYTES
));
assert!(!super::should_try_immediate_pane_input(
super::IMMEDIATE_PANE_INPUT_MAX_BYTES + 1
));
}
}
#[cfg(all(test, windows))]
mod windows_tests {
use crate::pane_terminals::{DeferredInitialPaneConsoleInputAction, PasteDelimiters};
use rmux_proto::ProcessCommand;
use rmux_pty::WindowsConsoleKeyEvent;
#[test]
fn windows_console_key_mapping_covers_control_signals() {
assert_eq!(
super::windows_console_input_for_tokens(&["C-a".to_owned()], 1),
Some((
super::WindowsConsoleInputAction::Key(
WindowsConsoleKeyEvent::ctrl_letter(b'A').unwrap()
),
vec![0x01]
))
);
assert_eq!(
super::windows_console_input_for_tokens(&["C-c".to_owned()], 1),
Some((
super::WindowsConsoleInputAction::KeyThenInterrupt(WindowsConsoleKeyEvent::ctrl_c()),
vec![0x03]
))
);
assert_eq!(
super::windows_console_input_for_attached_key_event(WindowsConsoleKeyEvent::ctrl_c()),
super::WindowsConsoleInputAction::KeyThenInterrupt(WindowsConsoleKeyEvent::ctrl_c())
);
assert_eq!(
super::windows_console_input_for_tokens(&["C-d".to_owned()], 1),
Some((
super::WindowsConsoleInputAction::Key(WindowsConsoleKeyEvent::ctrl_d()),
vec![0x04]
))
);
assert_eq!(
super::windows_console_input_for_tokens(&["C-z".to_owned()], 2),
Some((
super::WindowsConsoleInputAction::Key(
WindowsConsoleKeyEvent::ctrl_z().with_repeat_count(2)
),
vec![0x1a, 0x1a]
))
);
assert!(super::tokens_contain_windows_console_interrupt(&[
"Enter".to_owned(),
"C-c".to_owned()
]));
assert!(super::tokens_contain_windows_console_interrupt(&[
"C-c".to_owned(),
"Enter".to_owned()
]));
assert!(!super::tokens_contain_windows_console_interrupt(&[
"C-d".to_owned(),
"Enter".to_owned()
]));
}
#[test]
fn deferred_windows_console_actions_preserve_control_semantics() {
assert_eq!(
super::WindowsConsoleInputAction::Key(WindowsConsoleKeyEvent::ctrl_d()).deferred(),
DeferredInitialPaneConsoleInputAction::Key(WindowsConsoleKeyEvent::ctrl_d())
);
assert_eq!(
super::WindowsConsoleInputAction::KeyThenInterrupt(WindowsConsoleKeyEvent::ctrl_c())
.deferred(),
DeferredInitialPaneConsoleInputAction::KeyThenInterrupt(
WindowsConsoleKeyEvent::ctrl_c()
)
);
assert_eq!(
super::WindowsConsoleInputAction::Interrupt.deferred(),
DeferredInitialPaneConsoleInputAction::Interrupt
);
}
#[test]
fn windows_ctrl_d_uses_a_physical_key_only_for_cmd() {
assert_eq!(
super::windows_ctrl_d_console_key(false),
WindowsConsoleKeyEvent::ctrl_d_eot()
);
assert_eq!(
super::windows_ctrl_d_console_key(true),
WindowsConsoleKeyEvent::ctrl_d()
);
}
#[test]
fn legacy_conpty_routes_valid_utf8_and_rejects_unrepresentable_paste() {
let bracketed = b"\x1b[200~alpha\r\nbeta\x1b[201~";
assert_eq!(
super::windows_paste_sink(false, bracketed, PasteDelimiters::Wrapped),
super::WindowsPasteSink::ConsoleUtf8
);
assert_eq!(
super::windows_paste_sink(true, bracketed, PasteDelimiters::Wrapped),
super::WindowsPasteSink::Pty,
"passthrough ConPTY must retain the byte-oriented path"
);
assert_eq!(
super::windows_paste_sink(
false,
b"\x1b[200~invalid \xff\x1b[201~",
PasteDelimiters::Wrapped
),
super::WindowsPasteSink::RejectNonUtf8,
"legacy ConPTY must not silently strip the paste delimiters"
);
}
#[test]
fn a_bare_paste_body_takes_the_same_console_sink_as_a_wrapped_one() {
let body = "alpha\r\n\u{1b}[9;2u \u{1b}[<64;2;2M omega".as_bytes();
assert_eq!(
super::windows_paste_sink(false, body, PasteDelimiters::Bare),
super::WindowsPasteSink::ConsoleUtf8,
"a bare body must not be left on the sequence-consuming raw pipe"
);
assert_eq!(
super::windows_paste_sink(true, body, PasteDelimiters::Bare),
super::WindowsPasteSink::Pty,
"a passthrough ConPTY already carries the body verbatim"
);
}
#[test]
fn a_bare_paste_body_that_is_not_utf8_keeps_the_byte_oriented_path() {
assert_eq!(
super::windows_paste_sink(false, b"invalid \xff bytes", PasteDelimiters::Bare),
super::WindowsPasteSink::Pty
);
}
#[test]
fn windows_ctrl_d_launch_plan_detection_preserves_shell_wrapper() {
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Shell(
"timeout.exe /T 10000".to_owned()
)),
None,
"shell text must defer to the configured shell profile"
);
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Shell(
"pwsh.exe -NoLogo -NoProfile".to_owned()
)),
Some(false),
"an explicit PowerShell command must override a cmd profile"
);
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Shell(
r#""C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile"#.to_owned()
)),
Some(false),
"a quoted PowerShell path must remain one command token"
);
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Shell(
"cmd.exe /D /Q /K".to_owned()
)),
Some(true)
);
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Shell(
r#""C:\Windows\System32\cmd.exe" /D /Q /K"#.to_owned()
)),
Some(true),
"a quoted cmd path must retain physical Ctrl-D semantics"
);
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Argv(vec![
"cmd.exe".to_owned(),
"/K".to_owned(),
])),
Some(true)
);
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Argv(vec![
r"C:\tools\build.cmd".to_owned(),
])),
Some(true)
);
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Argv(vec![
"pwsh.exe".to_owned(),
"-NoProfile".to_owned(),
])),
Some(false)
);
assert_eq!(
super::process_command_windows_cmd_hint(&ProcessCommand::Argv(vec![
"python.exe".to_owned(),
])),
Some(false)
);
}
}