diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-26 15:05:28 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-26 15:05:28 +0300 |
| commit | 61f6400675b065bf8f801816ed78c1f3206bcc06 (patch) | |
| tree | 05bfa3914ab9079dd94f2bb0c3e64e6b79bf5aa7 /src | |
| parent | 19cd372f2544f56af9be1f24a67cb229e82bb63d (diff) | |
fix: repaint after transient terminal resizes
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/mod.rs | 26 | ||||
| -rw-r--r-- | src/platform/mod.rs | 52 |
2 files changed, 76 insertions, 2 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs index 787c0c0b..6be41c06 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1502,6 +1502,8 @@ async fn run_client_loop( } ClientLoopEvent::Resize(new_cols, new_rows, cell_width_px, cell_height_px) => { state.reported_size = (new_cols, new_rows); + // Resizing invalidates the host-side blit baseline. + state.request_repaint(); let msg = ClientMessage::Resize { cols: new_cols, rows: new_rows, @@ -2099,7 +2101,16 @@ fn current_terminal_geometry(kitty_graphics_enabled: bool) -> (u16, u16, u32, u3 ) } -/// Polls the terminal size and sends resize events when it changes. +/// Reports polled changes and signalled resizes that return to the same size. +fn resize_report_required( + signalled: bool, + new_size: (u16, u16, u32, u32), + last_size: (u16, u16, u32, u32), +) -> bool { + signalled || new_size != last_size +} + +/// Watches the terminal size and sends resize events when it changes. fn resize_poll_loop( resize_tx: tokio::sync::mpsc::Sender<ClientLoopEvent>, initial_cols: u16, @@ -2107,6 +2118,7 @@ fn resize_poll_loop( kitty_graphics_enabled: bool, should_quit: &Arc<AtomicBool>, ) { + crate::platform::watch_terminal_resize_signal(); let (_, _, initial_cell_width, initial_cell_height) = current_terminal_geometry(kitty_graphics_enabled); let mut last_size = ( @@ -2117,8 +2129,9 @@ fn resize_poll_loop( ); while !should_quit.load(Ordering::Acquire) { std::thread::sleep(Duration::from_millis(100)); + let signalled = crate::platform::take_terminal_resize_signal(); let new_size = current_terminal_geometry(kitty_graphics_enabled); - if new_size != last_size { + if resize_report_required(signalled, new_size, last_size) { last_size = new_size; if resize_tx .blocking_send(ClientLoopEvent::Resize( @@ -2170,6 +2183,15 @@ mod tests { LOCK.get_or_init(|| Mutex::new(())) } + #[test] + fn resize_signal_reports_even_when_polled_size_is_unchanged() { + let size = (120, 40, 8, 16); + assert!(resize_report_required(true, size, size)); + assert!(!resize_report_required(false, size, size)); + assert!(resize_report_required(false, (120, 41, 8, 16), size)); + assert!(resize_report_required(false, (120, 40, 9, 18), size)); + } + fn restore_env_var(key: &str, value: Option<OsString>) { if let Some(value) = value { std::env::set_var(key, value); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 9b763223..ba92ca63 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -78,6 +78,45 @@ pub fn current_process_is_detached_server_daemon() -> bool { unsafe { libc::getsid(0) == libc::getpid() } } +/// Raised by the SIGWINCH handler, consumed by the host resize watcher. +#[cfg(unix)] +static TERMINAL_RESIZE_SIGNALLED: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +#[cfg(unix)] +extern "C" fn record_terminal_resize_signal(_signal: libc::c_int) { + TERMINAL_RESIZE_SIGNALLED.store(true, std::sync::atomic::Ordering::Release); +} + +/// Records SIGWINCH events that size polling can miss. +#[cfg(unix)] +pub(crate) fn watch_terminal_resize_signal() { + let mut action: libc::sigaction = unsafe { std::mem::zeroed() }; + action.sa_sigaction = + record_terminal_resize_signal as extern "C" fn(libc::c_int) as libc::sighandler_t; + // Keep blocking stdin and socket reads from failing with EINTR. + action.sa_flags = libc::SA_RESTART; + unsafe { + libc::sigemptyset(&mut action.sa_mask); + libc::sigaction(libc::SIGWINCH, &action, std::ptr::null_mut()); + } +} + +#[cfg(not(unix))] +pub(crate) fn watch_terminal_resize_signal() {} + +/// Returns whether a terminal size change was signalled since the last call. +#[cfg(unix)] +pub(crate) fn take_terminal_resize_signal() -> bool { + TERMINAL_RESIZE_SIGNALLED.swap(false, std::sync::atomic::Ordering::AcqRel) +} + +/// Windows relies on size polling. +#[cfg(not(unix))] +pub(crate) fn take_terminal_resize_signal() -> bool { + false +} + #[cfg(unix)] #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClipboardCommand { @@ -313,6 +352,19 @@ mod tests { use super::*; #[test] + fn terminal_resize_signal_is_recorded_once_per_delivery() { + watch_terminal_resize_signal(); + assert!(!take_terminal_resize_signal()); + + unsafe { + libc::raise(libc::SIGWINCH); + } + + assert!(take_terminal_resize_signal()); + assert!(!take_terminal_resize_signal()); + } + + #[test] fn pane_shell_process_names_reject_exec_replacement_programs() { for shell in ["bash", "-zsh", "/bin/fish", "pwsh", "powershell.exe"] { assert!(is_pane_shell_process_name(shell), "{shell}"); |
