Start work on a new clipboard protocol to allow terminal programs to access more than just text/plain content from the clipboard
This commit is contained in:
parent
4c72f92939
commit
a8725d6307
@ -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")}'
|
||||
|
||||
@ -2,24 +2,33 @@
|
||||
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user