diff --git a/docs/changelog.rst b/docs/changelog.rst index f2a1a61a3..75a1a6e9b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -134,6 +134,11 @@ consumption to do the same tasks. Detailed list of changes ------------------------------------- +0.44.1 [future] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Add support for the `paste events protocol `__ (:iss:`9183`) + 0.44.0 [2025-11-03] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/boss.py b/kitty/boss.py index 5ae5f92fc..3611fe9eb 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -2400,10 +2400,12 @@ class Boss: @ac('cp', 'Paste from the clipboard to the active window') def paste_from_clipboard(self) -> None: - text = get_clipboard_string() - if text: - w = self.window_for_dispatch or self.active_window - if w is not None: + w = self.window_for_dispatch or self.active_window + if w is not None: + if w.send_paste_event(): + return + text = get_clipboard_string() + if text: w.paste_with_actions(text) def current_primary_selection(self) -> str: @@ -2414,10 +2416,12 @@ class Boss: @ac('cp', 'Paste from the primary selection, if present, otherwise the clipboard to the active window') def paste_from_selection(self) -> None: - text = self.current_primary_selection_or_clipboard() - if text: - w = self.window_for_dispatch or self.active_window - if w is not None: + w = self.window_for_dispatch or self.active_window + if w is not None: + if w.send_paste_event(is_primary_selection=True): + return + text = self.current_primary_selection_or_clipboard() + if text: w.paste_with_actions(text) def set_primary_selection(self) -> None: diff --git a/kitty/clipboard.py b/kitty/clipboard.py index d371539eb..b145f8590 100644 --- a/kitty/clipboard.py +++ b/kitty/clipboard.py @@ -542,6 +542,10 @@ class ClipboardRequestManager: else: self.fulfill_read_request(rr, allowed=allowed) + def send_paste_event(self, is_primary_selection: bool) -> None: + rr = ReadRequest(is_primary_selection=is_primary_selection, mime_types=(TARGETS_MIME,), protocol_type=ProtocolType.osc_5522) + self.fulfill_read_request(rr) + def fulfill_read_request(self, rr: ReadRequest, allowed: bool = True) -> None: if rr.protocol_type is ProtocolType.osc_52: return self.fulfill_legacy_read_request(rr, allowed) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index d3d6006a3..f2f5fa7c4 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1218,6 +1218,7 @@ class Screen: linebuf: LineBuf in_bracketed_paste_mode: bool in_band_resize_notification: bool + paste_events: bool color_preference_notification: bool cursor_visible: bool scrolled_by: int diff --git a/kitty/modes.h b/kitty/modes.h index c1bacf537..f86a2c984 100644 --- a/kitty/modes.h +++ b/kitty/modes.h @@ -91,5 +91,8 @@ // In-band resize notification mode #define INBAND_RESIZE_NOTIFICATION (2048 << 5) +// Paste events mode https://rockorager.dev/misc/bracketed-paste-mime/ +#define PASTE_EVENTS (5522 << 5) + // Handle Ctrl-C/Ctrl-Z mode #define HANDLE_TERMIOS_SIGNALS (19997 << 5) diff --git a/kitty/screen.c b/kitty/screen.c index b593234c4..c91017662 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1576,6 +1576,7 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) { bool private; switch(mode) { SIMPLE_MODE(LNM) + SIMPLE_MODE(PASTE_EVENTS) SIMPLE_MODE(IRM) SIMPLE_MODE(DECARM) SIMPLE_MODE(BRACKETED_PASTE) @@ -2182,6 +2183,7 @@ copy_specific_mode(Screen *self, unsigned int mode, const ScreenModes *src, Scre SIMPLE_MODE(BRACKETED_PASTE) SIMPLE_MODE(FOCUS_TRACKING) SIMPLE_MODE(COLOR_PREFERENCE_NOTIFICATION) + SIMPLE_MODE(PASTE_EVENTS) SIMPLE_MODE(INBAND_RESIZE_NOTIFICATION) SIMPLE_MODE(DECCKM) SIMPLE_MODE(DECTCEM) @@ -2225,6 +2227,7 @@ copy_specific_modes(Screen *self, const ScreenModes *src, ScreenModes *dest) { copy_specific_mode(self, FOCUS_TRACKING, src, dest); copy_specific_mode(self, COLOR_PREFERENCE_NOTIFICATION, src, dest); copy_specific_mode(self, INBAND_RESIZE_NOTIFICATION, src, dest); + copy_specific_mode(self, PASTE_EVENTS, src, dest); copy_specific_mode(self, DECCKM, src, dest); copy_specific_mode(self, DECTCEM, src, dest); copy_specific_mode(self, DECAWM, src, dest); @@ -2795,6 +2798,7 @@ report_mode_status(Screen *self, unsigned int which, bool private) { KNOWN_MODE(FOCUS_TRACKING); KNOWN_MODE(COLOR_PREFERENCE_NOTIFICATION); KNOWN_MODE(INBAND_RESIZE_NOTIFICATION); + KNOWN_MODE(PASTE_EVENTS); #undef KNOWN_MODE case ALTERNATE_SCREEN: ans = self->linebuf == self->alt_linebuf ? 1 : 2; break; @@ -4686,6 +4690,7 @@ MODE_GETSET(in_bracketed_paste_mode, BRACKETED_PASTE) MODE_GETSET(focus_tracking_enabled, FOCUS_TRACKING) MODE_GETSET(color_preference_notification, COLOR_PREFERENCE_NOTIFICATION) MODE_GETSET(in_band_resize_notification, INBAND_RESIZE_NOTIFICATION) +MODE_GETSET(paste_events, PASTE_EVENTS) MODE_GETSET(auto_repeat_enabled, DECARM) MODE_GETSET(cursor_visible, DECTCEM) MODE_GETSET(cursor_key_mode, DECCKM) diff --git a/kitty/screen.h b/kitty/screen.h index 745a8c7c5..07dd46ec5 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -16,7 +16,8 @@ typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } Scr typedef struct { bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM, mCOLOR_PREFERENCE_NOTIFICATION, - mBRACKETED_PASTE, mFOCUS_TRACKING, mDECSACE, mHANDLE_TERMIOS_SIGNALS, mINBAND_RESIZE_NOTIFICATION; + mBRACKETED_PASTE, mFOCUS_TRACKING, mDECSACE, mHANDLE_TERMIOS_SIGNALS, mINBAND_RESIZE_NOTIFICATION, + mPASTE_EVENTS; MouseTrackingMode mouse_tracking_mode; MouseTrackingProtocol mouse_tracking_protocol; } ScreenModes; diff --git a/kitty/window.py b/kitty/window.py index eaf108717..91e1ed1db 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1834,6 +1834,12 @@ class Window: path = resolve_custom_file(path) if path else '' set_window_logo(self.os_window_id, self.tab_id, self.id, path, position or '', alpha, png_data) + def send_paste_event(self, is_primary_selection: bool = False) -> bool: + if not self.screen.paste_events: + return False + self.clipboard_request_manager.send_paste_event(is_primary_selection) + return True + def paste_with_actions(self, text: str) -> None: if self.destroyed or not text: return