Client: instant SIGWINCH-driven terminal resize (mosh-style)
ci / test (push) Has been cancelled
ci / fuzz-smoke (push) Has been cancelled
ci / remote-bench (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
DuProcess
2026-06-14 14:02:08 -04:00
parent 683c3cd01d
commit a88d912d2b
+37 -10
View File
@@ -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,
}
}
_ = 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() => {
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?;
}
}
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,