From 71c705a1bd805e9d189a7160788b8b35f668002a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 17 Feb 2026 10:09:10 +0530 Subject: [PATCH] When dropping text into the terminal apply the paste actions This means control codes are now automatically sanitized or queried as per paste actions settings. --- kitty/options/definition.py | 2 +- kitty/window.py | 50 +++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/kitty/options/definition.py b/kitty/options/definition.py index f8b4c159a..d9c7e1ba7 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -768,7 +768,7 @@ selections even if :opt:`copy_on_select` is enabled. opt('paste_actions', 'quote-urls-at-prompt,confirm', option_type='paste_actions', long_text=''' -A comma separated list of actions to take when pasting text into the terminal. +A comma separated list of actions to take when pasting or dropping text into the terminal. The supported paste actions are: :code:`quote-urls-at-prompt`: diff --git a/kitty/window.py b/kitty/window.py index 17527a312..7c0663faf 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1845,7 +1845,7 @@ class Window: self.clipboard_request_manager.send_paste_event(is_primary_selection) return True - def paste_with_actions(self, text: str) -> None: + def paste_with_actions(self, text: str, from_drop: bool = False, is_uri_list: bool = False) -> None: if self.destroyed or not text: return opts = get_options() @@ -1854,18 +1854,24 @@ class Window: if not text: return if 'quote-urls-at-prompt' in opts.paste_actions and self.at_prompt: - prefixes = '|'.join(opts.url_prefixes) - m = re.match(f'({prefixes}):(.+)', text) - if m is not None: - scheme, rest = m.group(1), m.group(2) - if rest.startswith('//') or scheme in ('mailto', 'irc'): - import shlex - text = shlex.quote(text) + if is_uri_list: + import shlex + urls = text.splitlines(keepends=False) + text = ' '.join(map(shlex.quote, urls)) + else: + prefixes = '|'.join(opts.url_prefixes) + m = re.match(f'({prefixes}):(.+)', text) + if m is not None: + scheme, rest = m.group(1), m.group(2) + if rest.startswith('//') or scheme in ('mailto', 'irc'): + import shlex + text = shlex.quote(text) if 'replace-dangerous-control-codes' in opts.paste_actions: text = replace_c0_codes_except_nl_space_tab(text) if 'replace-newline' in opts.paste_actions and 'confirm' not in opts.paste_actions: text = text.replace('\n', '\x1bE') btext = text.encode('utf-8') + which = 'drop' if from_drop else 'paste' if 'confirm' in opts.paste_actions: sanitized = replace_c0_codes_except_nl_space_tab(btext) replaced_c0_control_codes = sanitized != btext @@ -1882,20 +1888,24 @@ class Window: replaced_newlines = t != sanitized sanitized = t if replaced_c0_control_codes or replaced_newlines: - msg = _('The text to be pasted contains terminal control codes.\n\nIf the terminal program you are pasting into does not properly' - ' sanitize pasted text, this can lead to \x1b[31mcode execution vulnerabilities\x1b[39m.\n\nHow would you like to proceed?') + msg = _( + 'The text to be {0} contains terminal control codes.\n\nIf the terminal program you are {1}' + ' into does not properly sanitize text, this can lead to' + ' \x1b[31mcode execution vulnerabilities\x1b[39m.\n\nHow would you like to proceed?' + ).format('dropped' if from_drop else 'pasted', 'dropping' if from_drop else 'pasting') get_boss().choose( msg, partial(self.handle_dangerous_paste_confirmation, btext, sanitized), - 's;green:Sanitize and paste', 'p;red:Paste anyway', 'c;yellow:Cancel', - window=self, default='s', title=_('Allow paste?'), + 's;green:Sanitize and ' + which, f'a;red:{which.capitalize()} anyway', 'c;yellow:Cancel', + window=self, default='s', title=_('Allow {}?').format(which), ) return if 'confirm-if-large' in opts.paste_actions: msg = '' if len(btext) > 16 * 1024: - msg = _('Pasting very large amounts of text ({} bytes) can be slow.').format(len(btext)) - get_boss().confirm(msg + _(' Are you sure?'), partial(self.handle_large_paste_confirmation, btext), window=self, title=_( - 'Allow large paste?')) + msg = _('{1} very large amounts of text ({0} bytes) can be slow.').format( + len(btext), 'Dropping' if from_drop else 'Pasting') + get_boss().confirm(msg + _(' Are you sure?'), partial(self.handle_large_paste_confirmation, btext), + window=self, title=_('Allow large {}?').format('drop' if from_drop else 'paste')) return self.paste_text(btext) @@ -1941,19 +1951,17 @@ class Window: def on_drop(self, drop: dict[str, bytes]) -> None: text = '' + is_uri_list = False if uri_list := drop.pop('text/uri-list', b''): urls = parse_uri_list(uri_list.decode('utf-8', 'replace')) - if self.at_prompt: - import shlex - text = ' '.join(map(shlex.quote, urls)) - else: - text = '\n'.join(urls) + text = '\n'.join(urls) + is_uri_list = True elif tp := drop.pop('text/plain', b''): text = tp.decode('utf-8', 'replace') elif tp := drop.pop('text/plain;charset=utf-8', b''): text = tp.decode('utf-8', 'replace') if text: - self.paste_text(text) + self.paste_with_actions(text, from_drop=True, is_uri_list=is_uri_list) # Serialization {{{