From 06b5eff6e686fc09e876dac1c75e427f1bcae665 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 18 Jul 2024 20:32:33 +0530 Subject: [PATCH] Add support for in-band window resize notifications Fixes #7642 --- docs/changelog.rst | 2 ++ kitty/fast_data_types.pyi | 1 + kitty/modes.h | 3 +++ kitty/screen.c | 11 +++++++++++ kitty/screen.h | 2 +- kitty/window.py | 8 +++++++- kitty_tests/__init__.py | 4 ++++ kitty_tests/screen.py | 10 +++++++++- 8 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f1ae2dbd5..6cd48cb1e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -92,6 +92,8 @@ Detailed list of changes - Splits layout: Fix the ``move_to_screen_edge`` action breaking when only a single window is present (:iss:`7621`) +- Add support for in-band window resize notifications (:iss:`7642`) + 0.35.2 [2024-06-22] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 6ce12bedd..249b7ea9d 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1131,6 +1131,7 @@ class Screen: historybuf: HistoryBuf linebuf: LineBuf in_bracketed_paste_mode: bool + in_band_resize_notification: bool cursor_visible: bool scrolled_by: int cursor: Cursor diff --git a/kitty/modes.h b/kitty/modes.h index aabbd9d38..615dc02fd 100644 --- a/kitty/modes.h +++ b/kitty/modes.h @@ -85,5 +85,8 @@ // Pending updates mode #define PENDING_UPDATE (2026 << 5) +// In-band resize notification mode +#define INBAND_RESIZE_NOTIFICATION (2048 << 5) + // Handle Ctrl-C/Ctrl-Z mode #define HANDLE_TERMIOS_SIGNALS (19997 << 5) diff --git a/kitty/screen.c b/kitty/screen.c index c59569c19..b282b72d3 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1153,6 +1153,12 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) { log_error("Pending mode change to already current mode (%d) requested. Either pending mode expired or there is an application bug.", val); } break; + case INBAND_RESIZE_NOTIFICATION: + if (val != self->modes.mINBAND_RESIZE_NOTIFICATION) { + self->modes.mINBAND_RESIZE_NOTIFICATION = val; + if (val) CALLBACK("notify_child_of_resize", NULL); + } + break; default: private = mode >= 1 << 5; if (private) mode >>= 5; @@ -1662,6 +1668,7 @@ copy_specific_mode(Screen *self, unsigned int mode, const ScreenModes *src, Scre SIMPLE_MODE(DECARM) SIMPLE_MODE(BRACKETED_PASTE) SIMPLE_MODE(FOCUS_TRACKING) + SIMPLE_MODE(INBAND_RESIZE_NOTIFICATION) SIMPLE_MODE(DECCKM) SIMPLE_MODE(DECTCEM) SIMPLE_MODE(DECAWM) @@ -1702,6 +1709,7 @@ copy_specific_modes(Screen *self, const ScreenModes *src, ScreenModes *dest) { copy_specific_mode(self, DECARM, src, dest); copy_specific_mode(self, BRACKETED_PASTE, src, dest); copy_specific_mode(self, FOCUS_TRACKING, src, dest); + copy_specific_mode(self, INBAND_RESIZE_NOTIFICATION, src, dest); copy_specific_mode(self, DECCKM, src, dest); copy_specific_mode(self, DECTCEM, src, dest); copy_specific_mode(self, DECAWM, src, dest); @@ -2196,6 +2204,7 @@ report_mode_status(Screen *self, unsigned int which, bool private) { KNOWN_MODE(DECCKM); KNOWN_MODE(BRACKETED_PASTE); KNOWN_MODE(FOCUS_TRACKING); + KNOWN_MODE(INBAND_RESIZE_NOTIFICATION); #undef KNOWN_MODE case ALTERNATE_SCREEN: ans = self->linebuf == self->alt_linebuf ? 1 : 2; break; @@ -3830,6 +3839,7 @@ WRAP0(clear_scrollback) MODE_GETSET(in_bracketed_paste_mode, BRACKETED_PASTE) MODE_GETSET(focus_tracking_enabled, FOCUS_TRACKING) +MODE_GETSET(in_band_resize_notification, INBAND_RESIZE_NOTIFICATION) MODE_GETSET(auto_repeat_enabled, DECARM) MODE_GETSET(cursor_visible, DECTCEM) MODE_GETSET(cursor_key_mode, DECCKM) @@ -4852,6 +4862,7 @@ static PyGetSetDef getsetters[] = { GETSET(in_bracketed_paste_mode) GETSET(auto_repeat_enabled) GETSET(focus_tracking_enabled) + GETSET(in_band_resize_notification) GETSET(cursor_visible) GETSET(cursor_key_mode) GETSET(disable_ligatures) diff --git a/kitty/screen.h b/kitty/screen.h index 5b4d9c3e4..e74b8f5f9 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -14,7 +14,7 @@ typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } Scr typedef struct { bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM, - mBRACKETED_PASTE, mFOCUS_TRACKING, mDECSACE, mHANDLE_TERMIOS_SIGNALS; + mBRACKETED_PASTE, mFOCUS_TRACKING, mDECSACE, mHANDLE_TERMIOS_SIGNALS, mINBAND_RESIZE_NOTIFICATION; MouseTrackingMode mouse_tracking_mode; MouseTrackingProtocol mouse_tracking_protocol; } ScreenModes; diff --git a/kitty/window.py b/kitty/window.py index e38d3522f..e04724c33 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -45,6 +45,7 @@ from .fast_data_types import ( CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, + ESC_CSI, ESC_DCS, ESC_OSC, GLFW_MOD_CONTROL, @@ -862,6 +863,8 @@ class Window: boss = get_boss() boss.child_monitor.resize_pty(self.id, *current_pty_size) self.last_resized_at = monotonic() + self.last_reported_pty_size = current_pty_size + self.notify_child_of_resize() if not self.child_is_launched: self.child.mark_terminal_ready() self.child_is_launched = True @@ -871,7 +874,6 @@ class Window: print(f'[{now:.3f}] Child launched', file=sys.stderr) elif boss.args.debug_rendering: print(f'[{monotonic():.3f}] SIGWINCH sent to child in window: {self.id} with size: {current_pty_size}', file=sys.stderr) - self.last_reported_pty_size = current_pty_size else: mark_os_window_dirty(self.os_window_id) @@ -1222,6 +1224,10 @@ class Window: identifier = sanitize_identifier_pat().sub('', identifier) self.screen.send_escape_code_to_child(ESC_OSC, f'99;i={identifier};') + def notify_child_of_resize(self) -> None: + pty_size = self.last_reported_pty_size + if pty_size[0] > -1 and self.screen.in_band_resize_notification: + self.screen.send_escape_code_to_child(ESC_CSI, f'48;{pty_size[0]};{pty_size[1]};{pty_size[3]};{pty_size[2]}t') def set_dynamic_color(self, code: int, value: Union[str, bytes, memoryview] = '') -> None: if isinstance(value, (bytes, memoryview)): diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py index a6f39cde8..64a53de2b 100644 --- a/kitty_tests/__init__.py +++ b/kitty_tests/__init__.py @@ -50,6 +50,9 @@ class Callbacks: def write(self, data) -> None: self.wtcbuf += bytes(data) + def notify_child_of_resize(self): + self.num_of_resize_events += 1 + def title_changed(self, data, is_base64=False) -> None: self.titlebuf.append(process_title_from_child(data, is_base64, '')) @@ -106,6 +109,7 @@ class Callbacks: self.last_cmd_exit_status = sys.maxsize self.last_cmd_cmdline = '' self.last_cmd_at = 0 + self.num_of_resize_events = 0 def on_bell(self) -> None: self.bell_count += 1 diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 1de42fef1..83db04d47 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # License: GPL v3 Copyright: 2016, Kovid Goyal -from kitty.fast_data_types import DECAWM, DECCOLM, DECOM, IRM, VT_PARSER_BUFFER_SIZE, Cursor +from kitty.fast_data_types import DECAWM, DECCOLM, DECOM, IRM, VT_PARSER_BUFFER_SIZE, Cursor, ESC_CSI from kitty.marks import marker_from_function, marker_from_regex from kitty.window import pagerhist @@ -304,6 +304,14 @@ class TestScreen(BaseTest): s.draw('x' * len(str(s.line(1)))) s.resize(s.lines, s.columns + 4) self.ae(str(s.linebuf), 'xxx\nxx\nbb\n\n') + s = self.create_screen() + c = s.callbacks + parse_bytes(s, b'\x1b[?2048$p') # ] + self.ae(c.wtcbuf, b'\x1b[?2048;2$y') # ] + c.clear() + parse_bytes(s, b'\x1b[?2048h\x1b[?2048$p') # ] + self.ae(c.wtcbuf, b'\x1b[?2048;1$y') # ] + self.ae(c.num_of_resize_events, 1) def test_cursor_after_resize(self):