diff --git a/kitty/boss.py b/kitty/boss.py index 2f132f118..328e36283 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -21,8 +21,8 @@ from .child import cached_process_data, default_env, set_default_env from .cli import create_opts, parse_args from .cli_stub import CLIOptions from .clipboard import ( - Clipboard, get_clipboard_string, get_primary_selection, set_clipboard_string, - set_primary_selection, + Clipboard, ClipboardType, get_clipboard_string, get_primary_selection, + set_clipboard_string, set_primary_selection, ) from .conf.utils import BadLine, KeyAction, to_cmdline from .config import common_opts_as_dict, prepare_config_file_for_editing @@ -33,19 +33,18 @@ from .constants import ( ) from .fast_data_types import ( CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT, - GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, GLFW_PRIMARY_SELECTION, - IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED, ChildMonitor, Color, - EllipticCurveKey, KeyEvent, SingleKey, add_timer, apply_options_update, - background_opacity_of, change_background_opacity, change_os_window_state, - cocoa_set_menubar_title, create_os_window, current_application_quit_request, - current_focused_os_window_id, current_os_window, destroy_global_data, - focus_os_window, get_boss, get_options, get_os_window_size, global_font_size, - last_focused_os_window_id, mark_os_window_for_close, os_window_font_size, - patch_global_colors, redirect_mouse_handling, ring_bell, run_with_activation_token, - safe_pipe, send_data_to_peer, set_application_quit_request, set_background_image, - set_boss, set_in_sequence_mode, set_options, set_os_window_size, - set_os_window_title, thread_write, toggle_fullscreen, toggle_maximized, - toggle_secure_input, + GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, IMPERATIVE_CLOSE_REQUESTED, + NO_CLOSE_REQUESTED, ChildMonitor, Color, EllipticCurveKey, KeyEvent, SingleKey, + add_timer, apply_options_update, background_opacity_of, change_background_opacity, + change_os_window_state, cocoa_set_menubar_title, create_os_window, + current_application_quit_request, current_focused_os_window_id, current_os_window, + destroy_global_data, focus_os_window, get_boss, get_options, get_os_window_size, + global_font_size, last_focused_os_window_id, mark_os_window_for_close, + os_window_font_size, patch_global_colors, redirect_mouse_handling, ring_bell, + run_with_activation_token, safe_pipe, send_data_to_peer, + set_application_quit_request, set_background_image, set_boss, set_in_sequence_mode, + set_options, set_os_window_size, set_os_window_title, thread_write, + toggle_fullscreen, toggle_maximized, toggle_secure_input, ) from .key_encoding import get_name_to_functional_number_map from .keys import get_shortcut, shortcut_matches @@ -243,7 +242,7 @@ class Boss: ): set_layout_options(opts) self.clipboard = Clipboard() - self.primary_selection = Clipboard(GLFW_PRIMARY_SELECTION) + self.primary_selection = Clipboard(ClipboardType.primary_selection) self.update_check_started = False self.encryption_key = EllipticCurveKey() self.encryption_public_key = f'{RC_ENCRYPTION_PROTOCOL_VERSION}:{base64.b85encode(self.encryption_key.public).decode("ascii")}' diff --git a/kitty/clipboard.py b/kitty/clipboard.py index 0623f50e6..7ba0dc7a4 100644 --- a/kitty/clipboard.py +++ b/kitty/clipboard.py @@ -2,24 +2,33 @@ # License: GPLv3 Copyright: 2022, Kovid Goyal import io -from typing import IO, Callable, Dict, List, Tuple, Union +from enum import Enum, IntEnum, auto +from gettext import gettext as _ +from typing import ( + IO, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple, Union, +) from .conf.utils import uniq from .constants import supports_primary_selection from .fast_data_types import ( - GLFW_CLIPBOARD, GLFW_PRIMARY_SELECTION, get_boss, get_clipboard_mime, - set_clipboard_data_types + GLFW_CLIPBOARD, GLFW_PRIMARY_SELECTION, OSC, get_boss, get_clipboard_mime, + get_options, set_clipboard_data_types, ) DataType = Union[bytes, 'IO[bytes]'] +class ClipboardType(IntEnum): + clipboard = GLFW_CLIPBOARD + primary_selection = GLFW_PRIMARY_SELECTION + + class Clipboard: - def __init__(self, clipboard_type: int = GLFW_CLIPBOARD) -> None: + def __init__(self, clipboard_type: ClipboardType = ClipboardType.clipboard) -> None: self.data: Dict[str, DataType] = {} self.clipboard_type = clipboard_type - self.enabled = self.clipboard_type == GLFW_CLIPBOARD or supports_primary_selection + self.enabled = self.clipboard_type is ClipboardType.clipboard or supports_primary_selection def set_text(self, x: Union[str, bytes]) -> None: if isinstance(x, str): @@ -108,14 +117,93 @@ def get_primary_selection() -> str: def develop() -> Tuple[Clipboard, Clipboard]: - from .constants import is_macos, detect_if_wayland_ok + from .constants import detect_if_wayland_ok, is_macos from .fast_data_types import set_boss from .main import init_glfw_module glfw_module = 'cocoa' if is_macos else ('wayland' if detect_if_wayland_ok() else 'x11') class Boss: clipboard = Clipboard() - primary_selection = Clipboard(GLFW_PRIMARY_SELECTION) + primary_selection = Clipboard(ClipboardType.primary_selection) init_glfw_module(glfw_module) set_boss(Boss()) # type: ignore return Boss.clipboard, Boss.primary_selection + + +class ProtocolType(Enum): + osc_52 = auto() + + +class ReadRequest(NamedTuple): + clipboard_type: ClipboardType = ClipboardType.clipboard + mime_types: Sequence[str] = ('text/plain',) + id: str = '' + protocol_type: ProtocolType = ProtocolType.osc_52 + + +def encode_osc52(loc: str, response: str) -> str: + from base64 import standard_b64encode + return '52;{};{}'.format( + loc, standard_b64encode(response.encode('utf-8')).decode('ascii')) + + +class ClipboardRequestManager: + + def __init__(self, window_id: int) -> None: + self.window_id = window_id + self.currently_asking_permission_for: Optional[ReadRequest] = None + + def parse_osc_52(self, data: str, is_partial: bool = False) -> None: + where, text = data.partition(';')[::2] + ct = ClipboardType.clipboard if 's' in where or 'c' in where else ClipboardType.primary_selection + if text == '?': + rr = ReadRequest(clipboard_type=ct) + self.handle_read_request(rr) + + def handle_read_request(self, rr: ReadRequest) -> None: + cc = get_options().clipboard_control + if rr.clipboard_type is ClipboardType.primary_selection: + ask_for_permission = 'read-primary-ask' in cc + else: + ask_for_permission = 'read-clipboard-ask' in cc + if ask_for_permission: + self.ask_to_read_clipboard(rr) + else: + self.fulfill_read_request(rr) + + def fulfill_read_request(self, rr: ReadRequest, allowed: bool = True) -> None: + if rr.protocol_type is ProtocolType.osc_52: + self.fulfill_legacy_read_request(rr, allowed) + + def reject_read_request(self, rr: ReadRequest) -> None: + if rr.protocol_type is ProtocolType.osc_52: + self.fulfill_legacy_read_request(rr, False) + + def fulfill_legacy_read_request(self, rr: ReadRequest, allowed: bool = True) -> None: + cp = get_boss().primary_selection if rr.clipboard_type is ClipboardType.primary_selection else get_boss().clipboard + w = get_boss().window_id_map.get(self.window_id) + if w is not None: + text = '' + if cp.enabled and allowed: + text = cp.get_text() + loc = 'c' if rr.clipboard_type is ClipboardType.clipboard else 'p' + w.screen.send_escape_code_to_child(OSC, encode_osc52(loc, text)) + + def ask_to_read_clipboard(self, rr: ReadRequest) -> None: + if self.currently_asking_permission_for is not None: + self.reject_read_request(rr) + return + w = get_boss().window_id_map.get(self.window_id) + if w is not None: + self.currently_asking_permission_for = rr + get_boss().confirm(_( + 'A program running in this window wants to read from the system clipboard.' + ' Allow it do so, once?'), + self.handle_clipboard_confirmation, window=w, + ) + + def handle_clipboard_confirmation(self, confirmed: bool) -> None: + rr = self.currently_asking_permission_for + self.currently_asking_permission_for = None + if rr is not None: + self.fulfill_read_request(rr, confirmed) diff --git a/kitty/screen.c b/kitty/screen.c index 7b9102ff6..0a5974f54 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1989,7 +1989,8 @@ set_dynamic_color(Screen *self, unsigned int code, PyObject *color) { void clipboard_control(Screen *self, int code, PyObject *data) { - CALLBACK("clipboard_control", "OO", data, code == -52 ? Py_True: Py_False); + if (code == 52 || code == -52) { CALLBACK("clipboard_control", "OO", data, code == -52 ? Py_True: Py_False); } + else { CALLBACK("clipboard_control", "OO", data, Py_None);} } void diff --git a/kitty/window.py b/kitty/window.py index 749e8f4a9..349e2e290 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -20,10 +20,7 @@ from typing import ( from .child import ProcessDesc from .cli_stub import CLIOptions -from .clipboard import ( - get_clipboard_string, get_primary_selection, set_clipboard_string, - set_primary_selection, -) +from .clipboard import ClipboardRequestManager, set_clipboard_string from .config import build_ansi_color_table from .constants import ( appname, clear_handled_signals, config_dir, is_macos, wakeup_io_loop, @@ -508,6 +505,7 @@ class Window: self.child_title = self.default_title self.title_stack: Deque[str] = deque(maxlen=10) self.id: int = add_window(tab.os_window_id, tab.id, self.title) + self.clipboard_request_manager = ClipboardRequestManager(self.id) self.margin = EdgeWidths() self.padding = EdgeWidths() self.kitten_result: Optional[Dict[str, Any]] = None @@ -516,7 +514,6 @@ class Window: self.tab_id = tab.id self.os_window_id = tab.os_window_id self.tabref: Callable[[], Optional[TabType]] = weakref.ref(tab) - self.clipboard_pending: Optional[ClipboardPending] = None self.destroyed = False self.geometry: WindowGeometry = WindowGeometry(0, 0, 0, 0, 0, 0) self.needs_layout = True @@ -1172,86 +1169,9 @@ class Window: def file_transmission(self, data: str) -> None: self.file_transmission_control.handle_serialized_command(data) - def clipboard_control(self, data: str, is_partial: bool = False) -> None: - where, text = data.partition(';')[::2] - if is_partial: - if self.clipboard_pending is None: - self.clipboard_pending = ClipboardPending(where, text) - else: - self.clipboard_pending = self.clipboard_pending._replace(data=self.clipboard_pending[1] + text) - limit = get_options().clipboard_max_size - if limit and len(self.clipboard_pending.data) > limit * 1024 * 1024: - log_error('Discarding part of too large OSC 52 paste request') - self.clipboard_pending = self.clipboard_pending._replace(data='', truncated=True) - return - - if not where: - if self.clipboard_pending is not None: - text = self.clipboard_pending.data + text - where = self.clipboard_pending.where - try: - if self.clipboard_pending.truncated: - return - finally: - self.clipboard_pending = None - else: - where = 's0' - cc = get_options().clipboard_control - if text == '?': - response = None - if 's' in where or 'c' in where: - if 'read-clipboard-ask' in cc: - return self.ask_to_read_clipboard(False) - response = get_clipboard_string() if 'read-clipboard' in cc else '' - loc = 'c' - elif 'p' in where: - if 'read-primary-ask' in cc: - return self.ask_to_read_clipboard(True) - response = get_primary_selection() if 'read-primary' in cc else '' - loc = 'p' - response = response or '' - self.send_osc52(loc, response or '') - - else: - from base64 import standard_b64decode - try: - text = standard_b64decode(text).decode('utf-8') - except Exception: - text = '' - - if 's' in where or 'c' in where: - if 'write-clipboard' in cc: - set_clipboard_string(text) - if 'p' in where: - if 'write-primary' in cc: - set_primary_selection(text) - self.clipboard_pending = None - - def send_osc52(self, loc: str, response: str) -> None: - from base64 import standard_b64encode - self.screen.send_escape_code_to_child(OSC, '52;{};{}'.format( - loc, standard_b64encode(response.encode('utf-8')).decode('ascii'))) - - def ask_to_read_clipboard(self, primary: bool = False) -> None: - if self.current_clipboard_read_ask is not None: - self.current_clipboard_read_ask = primary - return - self.current_clipboard_read_ask = primary - get_boss().confirm(_( - 'A program running in this window wants to read from the system clipboard.' - ' Allow it do so, once?'), - self.handle_clipboard_confirmation, window=self, - ) - - def handle_clipboard_confirmation(self, confirmed: bool) -> None: - try: - loc = 'p' if self.current_clipboard_read_ask else 'c' - response = '' - if confirmed: - response = get_primary_selection() if self.current_clipboard_read_ask else get_clipboard_string() - self.send_osc52(loc, response) - finally: - self.current_clipboard_read_ask = None + def clipboard_control(self, data: str, is_partial: Optional[bool] = False) -> None: + if is_partial is not None: + self.clipboard_request_manager.parse_osc_52(data, is_partial) def manipulate_title_stack(self, pop: bool, title: str, icon: Any) -> None: if title: