Send an OTP for paste events

This commit is contained in:
Kovid Goyal
2025-11-08 08:48:34 +05:30
parent 8b2d92d58d
commit 2eddb6ab19
2 changed files with 29 additions and 5 deletions

View File

@@ -180,6 +180,12 @@ free to request whatever MIME data it wants from the list of types.
The mode can be enabled using the standard DECSET or DECRST control sequences. The mode can be enabled using the standard DECSET or DECRST control sequences.
``CSI ? 5522 h`` to enable the mode. ``CSI ? 5522 l`` to disable the mode. ``CSI ? 5522 h`` to enable the mode. ``CSI ? 5522 l`` to disable the mode.
The terminal *should* send a one time password with the list of mime
types, as the ``pw`` key (base64 encoded). The application can then use this
password to request data from the clipboard without needing a permission
prompt. The human name *should* be set to ``Paste event`` (base64 encoded) when
the application uses this one time password.
Detecting support for this protocol Detecting support for this protocol
----------------------------------------- -----------------------------------------

View File

@@ -214,8 +214,12 @@ class ReadRequest(NamedTuple):
protocol_type: ProtocolType = ProtocolType.osc_52 protocol_type: ProtocolType = ProtocolType.osc_52
human_name: str = '' human_name: str = ''
password: str = '' password: str = ''
otp_for_response: str = ''
def encode_response(self, status: str = 'DATA', mime: str = '', payload: bytes | memoryview = b'') -> bytes: def encode_response(self, status: str = 'DATA', mime: str = '', payload: bytes | memoryview = b'') -> bytes:
from base64 import standard_b64encode
def encode_b64(s: str) -> str:
return standard_b64encode(s.encode()).decode()
ans = f'{self.protocol_type.value};type=read:status={status}' ans = f'{self.protocol_type.value};type=read:status={status}'
if status == 'OK' and self.is_primary_selection: if status == 'OK' and self.is_primary_selection:
ans += ':loc=primary' ans += ':loc=primary'
@@ -223,10 +227,11 @@ class ReadRequest(NamedTuple):
ans += f':id={self.id}' ans += f':id={self.id}'
if mime: if mime:
ans += f':mime={encode_mime(mime)}' ans += f':mime={encode_mime(mime)}'
if self.otp_for_response:
ans += f':pw={encode_b64(self.otp_for_response)}'
a = ans.encode('ascii') a = ans.encode('ascii')
if payload: if payload:
import base64 a += b';' + standard_b64encode(payload)
a += b';' + base64.standard_b64encode(payload)
return a return a
@@ -329,9 +334,13 @@ class WriteRequest:
class GrantedPermission: class GrantedPermission:
def __init__(self, read: bool = False, write: bool = False): one_time: bool = False
write_ban: bool = False
read_ban: bool = False
def __init__(self, read: bool = False, write: bool = False, one_time: bool = False):
self.read, self.write = read, write self.read, self.write = read, write
self.write_ban = self.read_ban = False self.one_time = one_time
class ClipboardRequestManager: class ClipboardRequestManager:
@@ -521,7 +530,12 @@ class ClipboardRequestManager:
default='d', window=window, title=_('A program wants to access the clipboard')) default='d', window=window, title=_('A program wants to access the clipboard'))
def password_is_allowed_already(self, password: str, for_write: bool = False) -> bool: def password_is_allowed_already(self, password: str, for_write: bool = False) -> bool:
return (q := self.granted_passwords.get(password)) is not None and (q.write if for_write else q.read) q = self.granted_passwords.get(password)
if q is not None:
if q.one_time:
self.granted_passwords.pop(password, None)
return q.write if for_write else q.read
return False
def fulfill_legacy_write_request(self, wr: WriteRequest, allowed: bool = True) -> None: def fulfill_legacy_write_request(self, wr: WriteRequest, allowed: bool = True) -> None:
cp = get_boss().primary_selection if wr.is_primary_selection else get_boss().clipboard cp = get_boss().primary_selection if wr.is_primary_selection else get_boss().clipboard
@@ -543,7 +557,11 @@ class ClipboardRequestManager:
self.fulfill_read_request(rr, allowed=allowed) self.fulfill_read_request(rr, allowed=allowed)
def send_paste_event(self, is_primary_selection: bool) -> None: def send_paste_event(self, is_primary_selection: bool) -> None:
from kitty.short_uuid import uuid4
pw = uuid4()
self.granted_passwords[pw] = GrantedPermission(read=True, one_time=True)
rr = ReadRequest(is_primary_selection=is_primary_selection, mime_types=(TARGETS_MIME,), protocol_type=ProtocolType.osc_5522) rr = ReadRequest(is_primary_selection=is_primary_selection, mime_types=(TARGETS_MIME,), protocol_type=ProtocolType.osc_5522)
rr = rr._replace(otp_for_response=pw)
self.fulfill_read_request(rr) self.fulfill_read_request(rr)
def fulfill_read_request(self, rr: ReadRequest, allowed: bool = True) -> None: def fulfill_read_request(self, rr: ReadRequest, allowed: bool = True) -> None: