aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-03 02:49:27 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-03 02:49:27 +0300
commitaa0768b734d95a1f54df1719290d977df796068d (patch)
treef173bf0e14b4c951e9ad2dc02dc7e66f102b89b4 /src/client
parente1bbc252e2b40da6e0c6d7b0f8d7babb17cc8814 (diff)
fix: stop sgr mouse sequences leaking into panes
refs #939
Diffstat (limited to 'src/client')
-rw-r--r--src/client/input.rs60
-rw-r--r--src/client/mod.rs23
2 files changed, 77 insertions, 6 deletions
diff --git a/src/client/input.rs b/src/client/input.rs
index 7099311b..bc7faf35 100644
--- a/src/client/input.rs
+++ b/src/client/input.rs
@@ -39,15 +39,21 @@ pub fn stdin_reader_loop(
event_tx: mpsc::Sender<ClientLoopEvent>,
should_quit: &Arc<AtomicBool>,
host_color_query_sent: bool,
+ host_mouse_capture_active: Arc<AtomicBool>,
) {
#[cfg(windows)]
{
- let _ = host_color_query_sent;
+ let _ = (host_color_query_sent, host_mouse_capture_active);
windows_stdin_reader_loop(event_tx, should_quit);
}
#[cfg(unix)]
- unix_stdin_reader_loop(event_tx, should_quit, host_color_query_sent);
+ unix_stdin_reader_loop(
+ event_tx,
+ should_quit,
+ host_color_query_sent,
+ host_mouse_capture_active,
+ );
}
#[cfg(unix)]
@@ -55,6 +61,7 @@ fn unix_stdin_reader_loop(
event_tx: mpsc::Sender<ClientLoopEvent>,
should_quit: &Arc<AtomicBool>,
host_color_query_sent: bool,
+ host_mouse_capture_active: Arc<AtomicBool>,
) {
let stdin = io::stdin();
let mut reader = stdin.lock();
@@ -78,7 +85,11 @@ fn unix_stdin_reader_loop(
}
}
- if stdin_read_ready(&reader, 10) == Some(false) {
+ let timeout_ms = idle_flush_timeout_ms(
+ &framer,
+ host_mouse_capture_active.load(Ordering::Acquire),
+ );
+ if stdin_read_ready(&reader, timeout_ms) == Some(false) {
let had_pending = framer.has_pending_input();
let chunks = framer.flush_timeout();
let held_escape = had_pending && chunks.is_empty();
@@ -90,7 +101,12 @@ fn unix_stdin_reader_loop(
return;
}
}
- if held_escape && stdin_read_ready(&reader, 10) == Some(false) {
+ if held_escape
+ && stdin_read_ready(
+ &reader,
+ crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS,
+ ) == Some(false)
+ {
for data in framer.flush_timeout() {
if event_tx
.blocking_send(ClientLoopEvent::StdinInput(data))
@@ -112,6 +128,18 @@ fn unix_stdin_reader_loop(
}
}
+#[cfg(unix)]
+fn idle_flush_timeout_ms(
+ framer: &crate::raw_input::RawInputByteFramer,
+ host_mouse_capture_active: bool,
+) -> i32 {
+ if host_mouse_capture_active && framer.has_pending_lone_escape() {
+ crate::raw_input::MOUSE_ACTIVE_LONE_ESCAPE_FLUSH_TIMEOUT_MS
+ } else {
+ crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS
+ }
+}
+
#[cfg(windows)]
fn windows_stdin_reader_loop(
event_tx: mpsc::Sender<ClientLoopEvent>,
@@ -366,6 +394,30 @@ mod tests {
_ => panic!("expected StdinInput event"),
}
}
+
+ #[test]
+ fn raw_input_idle_flush_timeout_keeps_escape_responsive() {
+ let timeout_ms = std::hint::black_box(crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS);
+ assert!(timeout_ms <= 20);
+ }
+
+ #[test]
+ fn mouse_active_lone_escape_gets_longer_reassembly_window() {
+ let mut framer = crate::raw_input::RawInputByteFramer::default();
+ assert!(framer.push(b"\x1b").is_empty());
+
+ assert_eq!(
+ idle_flush_timeout_ms(&framer, false),
+ crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS
+ );
+ assert_eq!(
+ idle_flush_timeout_ms(&framer, true),
+ crate::raw_input::MOUSE_ACTIVE_LONE_ESCAPE_FLUSH_TIMEOUT_MS
+ );
+ let mouse_timeout_ms =
+ std::hint::black_box(crate::raw_input::MOUSE_ACTIVE_LONE_ESCAPE_FLUSH_TIMEOUT_MS);
+ assert!(mouse_timeout_ms > 100);
+ }
}
#[cfg(test)]
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 92edb900..ceed9826 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -312,7 +312,14 @@ impl std::error::Error for ClientError {
impl From<protocol::FramingError> for ClientError {
fn from(err: protocol::FramingError) -> Self {
- ClientError::Protocol(err)
+ match err {
+ protocol::FramingError::UnexpectedEof => ClientError::ConnectionLost(io::Error::new(
+ io::ErrorKind::UnexpectedEof,
+ "server closed connection",
+ )),
+ protocol::FramingError::Io(err) => ClientError::ConnectionLost(err),
+ err => ClientError::Protocol(err),
+ }
}
}
@@ -341,6 +348,7 @@ fn setup_terminal_with_capabilities(
mouse_capture: bool,
) -> io::Result<TerminalGuard> {
ratatui::init();
+ crate::terminal_modes::clear_host_mouse_reporting(&mut io::stdout())?;
let host_color_scheme_reports =
should_enable_host_color_scheme_reports(enable_client_protocols);
@@ -536,6 +544,7 @@ fn restore_windows_input_mode_value(mode: u32) {
}
fn set_mouse_capture(enabled: bool) -> io::Result<()> {
+ crate::terminal_modes::clear_host_mouse_reporting(&mut io::stdout())?;
if enabled {
execute!(io::stdout(), EnableMouseCapture)
} else {
@@ -569,6 +578,7 @@ fn restore_terminal_state(
DisableBracketedPaste,
DisableMouseCapture
);
+ let _ = crate::terminal_modes::clear_host_mouse_reporting(&mut io::stdout());
#[cfg(windows)]
if let Some(mode) = restore_windows_input_mode {
restore_windows_input_mode_value(mode);
@@ -1100,6 +1110,7 @@ fn run_client_with_mode(
init_logging();
let loaded_config = crate::config::Config::load();
+ crate::terminal_modes::clear_host_mouse_reporting(&mut io::stdout())?;
let mouse_capture = loaded_config.config.ui.mouse_capture;
let mouse_scroll_lines = loaded_config.config.ui.mouse_scroll_lines();
let redraw_on_focus_gained = loaded_config.config.ui.redraw_on_focus_gained;
@@ -1283,6 +1294,7 @@ async fn run_client_loop(
draw_host_cursor,
};
debug!(?negotiated_encoding, "client render encoding active");
+ let host_mouse_capture_active = Arc::new(AtomicBool::new(state.mouse_capture_active));
// Channel for events from the stdin, resize, and server reader threads.
let (event_tx, mut event_rx) = tokio::sync::mpsc::channel::<ClientLoopEvent>(256);
@@ -1292,8 +1304,14 @@ async fn run_client_loop(
state.attach_escape.is_none() && should_query_host_terminal_theme();
let stdin_quit = should_quit.clone();
let stdin_tx = event_tx.clone();
+ let stdin_mouse_capture_active = host_mouse_capture_active.clone();
std::thread::spawn(move || {
- input::stdin_reader_loop(stdin_tx, &stdin_quit, will_query_host_terminal_theme);
+ input::stdin_reader_loop(
+ stdin_tx,
+ &stdin_quit,
+ will_query_host_terminal_theme,
+ stdin_mouse_capture_active,
+ );
});
if will_query_host_terminal_theme {
@@ -1533,6 +1551,7 @@ async fn run_client_loop(
let _ = enable_windows_virtual_terminal_input();
}
state.mouse_capture_active = desired;
+ host_mouse_capture_active.store(desired, Ordering::Release);
}
}
ServerMessage::Welcome { .. } => {