aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-26 15:05:28 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-26 15:05:28 +0300
commit61f6400675b065bf8f801816ed78c1f3206bcc06 (patch)
tree05bfa3914ab9079dd94f2bb0c3e64e6b79bf5aa7 /src
parent19cd372f2544f56af9be1f24a67cb229e82bb63d (diff)
fix: repaint after transient terminal resizes
Diffstat (limited to 'src')
-rw-r--r--src/client/mod.rs26
-rw-r--r--src/platform/mod.rs52
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}");