From a88d912d2b8559a12f551e19db9ed5a5b9dd94be Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:02:08 -0400 Subject: [PATCH] Client: instant SIGWINCH-driven terminal resize (mosh-style) The client only polled crossterm size() every 250ms, so window resizes (esp. in VSCode/terminals that signal rather than expecting polling) lagged or felt unresponsive. Add a SIGWINCH select branch that resizes the instant the signal fires, matching mosh (which reacts to SIGWINCH + TIOCGWINSZ). Keep the 250ms poll as a fallback. Client-only; wire VERSION unchanged. Co-Authored-By: Claude Opus 4.8 --- src/bin/dosh-client.rs | 47 +++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 103dab4..6582038 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -40,6 +40,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream, UdpSocket, UnixStream}; +use tokio::signal::unix::{SignalKind, signal}; use tokio::sync::mpsc; const STREAM_INITIAL_WINDOW: usize = 1024 * 1024; @@ -2137,6 +2138,11 @@ async fn run_terminal( let mut status_tick = tokio::time::interval(Duration::from_secs(1)); let mut resize_tick = tokio::time::interval(Duration::from_millis(250)); let mut last_size = terminal_size(); + // React to terminal resize the instant it happens via SIGWINCH (mosh-style), + // instead of waiting up to one `resize_tick`. The 250ms poll below stays as a + // fallback for environments where the signal doesn't fire. `signal()` can fail + // (rare), in which case we rely on the poll. + let mut winch = signal(SignalKind::window_change()).ok(); let mut frame_buffer = FrameBuffer::default(); // Resolve the prediction display policy (off / experimental / always). An // env var wins for ad-hoc tuning; otherwise the client config's @@ -2231,17 +2237,16 @@ async fn run_terminal( None => break, } } - _ = resize_tick.tick() => { - if let Ok((cols, rows)) = size() - && cols > 0 - && rows > 0 - && (cols, rows) != last_size - { - last_size = (cols, rows); - if cred.mode != "view-only" && cred.mode != "forward-only" { - send_resize(&socket, addr, &cred, &mut send_seq, cols, rows).await?; - } + _ = async { + match winch.as_mut() { + Some(w) => { w.recv().await; } + None => std::future::pending::<()>().await, } + } => { + maybe_send_resize(&socket, addr, &cred, &mut send_seq, &mut last_size).await?; + } + _ = resize_tick.tick() => { + maybe_send_resize(&socket, addr, &cred, &mut send_seq, &mut last_size).await?; } recv = socket.recv_from(&mut recv_buf) => { let (n, _) = recv?; @@ -3713,6 +3718,28 @@ impl FrameBuffer { } } +/// Read the current terminal size and, if it changed since `last_size`, send a +/// resize to the server. Shared by the SIGWINCH branch and the poll fallback. +async fn maybe_send_resize( + socket: &UdpSocket, + addr: SocketAddr, + cred: &CachedCredential, + send_seq: &mut u64, + last_size: &mut (u16, u16), +) -> Result<()> { + if let Ok((cols, rows)) = size() + && cols > 0 + && rows > 0 + && (cols, rows) != *last_size + { + *last_size = (cols, rows); + if cred.mode != "view-only" && cred.mode != "forward-only" { + send_resize(socket, addr, cred, send_seq, cols, rows).await?; + } + } + Ok(()) +} + async fn send_resize( socket: &UdpSocket, addr: SocketAddr,