From 816360c27587456d02771b1fc7bf44d553927c16 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 30 Oct 2021 13:56:48 +0530 Subject: [PATCH] A new remote control command to visually select a window Fixes #4165 --- docs/changelog.rst | 3 ++ kitty/boss.py | 18 ++++++---- kitty/child-monitor.c | 19 ++++++++--- kitty/fast_data_types.pyi | 4 +++ kitty/rc/select_window.py | 71 +++++++++++++++++++++++++++++++++++++++ kitty/remote_control.py | 11 +++++- kitty/tabs.py | 10 +++--- 7 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 kitty/rc/select_window.py diff --git a/docs/changelog.rst b/docs/changelog.rst index d24d625f5..f554ff50e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,6 +31,9 @@ To update |kitty|, :doc:`follow the instructions `. - A new remote control command to :program:`change the tab color ` (:iss:`1287`) +- A new remote control command to :program:`visually select a window ` (:iss:`4165`) + - Add support for reporting mouse events with pixel co-ordinates using the ``SGR_PIXEL_PROTOCOL`` introduced in xterm 359 diff --git a/kitty/boss.py b/kitty/boss.py index f2870cdd7..dd4e0c0ac 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -156,7 +156,7 @@ class Boss: self.update_check_process: Optional['PopenType[bytes]'] = None self.window_id_map: WeakValueDictionary[int, Window] = WeakValueDictionary() self.startup_colors = {k: opts[k] for k in opts if isinstance(opts[k], Color)} - self.visual_window_select_callback: Optional[Callable[[Tab, Window], None]] = None + self.visual_window_select_callback: Optional[Callable[[Optional[Tab], Optional[Window]], None]] = None self.startup_cursor_text_color = opts.cursor_text_color self.pending_sequences: Optional[SubSequenceMap] = None self.default_pending_action: Optional[KeyAction] = None @@ -459,7 +459,8 @@ class Boss: response = self._handle_remote_command(cmd, peer_id=peer_id) if response is None: return None - return cmd_prefix + json.dumps(response).encode('utf-8') + terminator + from kitty.remote_control import encode_response_for_peer + return encode_response_for_peer(response) data = json.loads(msg_bytes.decode('utf-8')) if isinstance(data, dict) and data.get('cmd') == 'new_instance': @@ -814,7 +815,7 @@ class Boss: if matched_action is not None: self.dispatch_action(matched_action) - def visual_window_select_action(self, tab: Tab, callback: Callable[[Tab, Window], None], choose_msg: str) -> None: + def visual_window_select_action(self, tab: Tab, callback: Callable[[Optional[Tab], Optional[Window]], None], choose_msg: str) -> None: self.visual_window_select_callback = callback if tab.current_layout.only_active_window_visible: self.select_window_in_tab_using_overlay(tab, choose_msg) @@ -846,15 +847,20 @@ class Boss: def visual_window_select_action_trigger(self, tab_id: int, window_id: int = 0) -> None: redirect_mouse_handling(False) self.clear_pending_sequences() + cb = self.visual_window_select_callback + self.visual_window_select_callback = None + called = False for tab in self.all_tabs: if tab.id == tab_id: for window in tab: window.screen.set_window_number() if window.id == window_id: - if self.visual_window_select_callback: - self.visual_window_select_callback(tab, window) + if cb is not None: + cb(tab, window) + called = True break - self.visual_window_select_callback = None + if not called and cb is not None: + cb(None, None) def focus_visible_window_mouse_handler(self, ev: WindowSystemMouseEvent) -> None: tab = self.active_tab diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 36bfb38d1..3c50cea06 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -181,7 +181,7 @@ wakeup_io_loop(ChildMonitor *self, bool in_signal_handler) { static void* io_loop(void *data); static void* talk_loop(void *data); -static void send_response(id_type peer_id, const char *msg, size_t msg_sz); +static void send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz); static void wakeup_talk_loop(bool); static bool talk_thread_started = false; @@ -423,9 +423,9 @@ parse_input(ChildMonitor *self) { if (!resp) PyErr_Print(); } if (resp) { - if (PyBytes_Check(resp)) send_response(msg->peer_id, PyBytes_AS_STRING(resp), PyBytes_GET_SIZE(resp)); + if (PyBytes_Check(resp)) send_response_to_peer(msg->peer_id, PyBytes_AS_STRING(resp), PyBytes_GET_SIZE(resp)); Py_CLEAR(resp); - } else send_response(msg->peer_id, NULL, 0); + } else send_response_to_peer(msg->peer_id, NULL, 0); } free(msgs); msgs = NULL; } @@ -1624,7 +1624,7 @@ end: } static void -send_response(id_type peer_id, const char *msg, size_t msg_sz) { +send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz) { bool wakeup = false; talk_mutex(lock); for (size_t i = 0; i < talk_data.num_peers; i++) { @@ -1699,11 +1699,22 @@ cocoa_set_menubar_title(PyObject *self UNUSED, PyObject *args UNUSED) { Py_RETURN_NONE; } +static PyObject* + +send_data_to_peer(PyObject *self UNUSED, PyObject *args) { + char * msg; Py_ssize_t sz; + unsigned long long peer_id; + if (!PyArg_ParseTuple(args, "Ks#", &peer_id, &msg, &sz)) return NULL; + send_response_to_peer(peer_id, msg, sz); + Py_RETURN_NONE; +} + static PyMethodDef module_methods[] = { METHODB(safe_pipe, METH_VARARGS), {"add_timer", (PyCFunction)add_python_timer, METH_VARARGS, ""}, {"remove_timer", (PyCFunction)remove_python_timer, METH_VARARGS, ""}, METHODB(monitor_pid, METH_VARARGS), + METHODB(send_data_to_peer, METH_VARARGS), METHODB(cocoa_set_menubar_title, METH_VARARGS), {NULL} /* Sentinel */ }; diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index e58f1549d..177455124 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1317,3 +1317,7 @@ def redirect_mouse_handling(yes: bool) -> None: def get_click_interval() -> float: pass + + +def send_data_to_peer(peer_id: int, data: Union[str, bytes]) -> None: + pass diff --git a/kitty/rc/select_window.py b/kitty/rc/select_window.py new file mode 100644 index 000000000..55bc4af73 --- /dev/null +++ b/kitty/rc/select_window.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2020, Kovid Goyal + + +from typing import TYPE_CHECKING, Dict, Optional, Union + +from kitty.fast_data_types import send_data_to_peer + +from .base import ( + MATCH_TAB_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, + RemoteCommand, ResponseType, Window, no_response +) + +if TYPE_CHECKING: + from kitty.cli_stub import LaunchRCOptions as CLIOptions + from kitty.tabs import Tab + + +class SelectWindow(RemoteCommand): + + ''' + match: The tab to open the new window in + self: Boolean, if True use tab the command was run in + ''' + + short_desc = 'Visually select a window in the specified tab' + desc = ( + ' Prints out the id of the selected window. Other commands ' + ' can then be chained to make use of it.' + ) + options_spec = MATCH_TAB_OPTION + '\n\n' + '''\ +--response-timeout +type=float +default=60 +The time in seconds to wait for the user to select a window. + + +--self +type=bool-set +If specified the tab containing the window this command is run in is used +instead of the active tab. +''' + + def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: + ans = {'self': opts.self, 'match': opts.match} + return ans + + def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: + peer_id: int = payload_get('peer_id', missing=0) + window_id: int = getattr(window, 'id', 0) + + def callback(tab: Optional['Tab'], window: Optional[Window]) -> None: + if window: + response: Dict[str, Union[bool, int, str]] = {'ok': True, 'data': window.id} + else: + response = {'ok': False, 'error': 'No window selected'} + if peer_id > 0: + from kitty.remote_control import encode_response_for_peer + send_data_to_peer(peer_id, encode_response_for_peer(response)) + elif window_id > 0: + w = boss.window_id_map[window_id] + if w is not None: + w.send_cmd_response(response) + for tab in self.tabs_for_match_payload(boss, window, payload_get): + if tab: + boss.visual_window_select_action(tab, callback, 'Choose window') + break + return no_response + + +select_window = SelectWindow() diff --git a/kitty/remote_control.py b/kitty/remote_control.py index cf36ab3e9..a2a6e769b 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -24,6 +24,11 @@ from .typing import BossType, WindowType from .utils import TTYIO, parse_address_spec +def encode_response_for_peer(response: Any) -> bytes: + import json + return b'\x1bP@kitty-cmd' + json.dumps(response).encode('utf-8') + b'\x1b\\' + + def handle_cmd(boss: BossType, window: Optional[WindowType], serialized_cmd: str, peer_id: int) -> Optional[Dict[str, Any]]: cmd = json.loads(serialized_cmd) v = cmd['version'] @@ -188,7 +193,11 @@ def main(args: List[str]) -> None: send = create_basic_command(cmd, payload=payload, no_response=no_response) if not global_opts.to and 'KITTY_LISTEN_ON' in os.environ: global_opts.to = os.environ['KITTY_LISTEN_ON'] - response = do_io(global_opts.to, send, no_response, response_timeout) + import socket + try: + response = do_io(global_opts.to, send, no_response, response_timeout) + except (TimeoutError, socket.timeout): + raise SystemExit(f'Timed out after {response_timeout} seconds waiting for response form kitty') if no_response: return if not response.get('ok'): diff --git a/kitty/tabs.py b/kitty/tabs.py index 0d9449f6c..0808a0070 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -601,15 +601,17 @@ class Tab: # {{{ over the windows for easy selection in this mode. ''') def focus_visible_window(self) -> None: - def callback(tab: Tab, window: Window) -> None: - tab.set_active_window(window) + def callback(tab: Optional[Tab], window: Optional[Window]) -> None: + if tab and window: + tab.set_active_window(window) get_boss().visual_window_select_action(self, callback, 'Choose window to switch to') @ac('win', 'Swap the current window with another window in the current tab, selected visually') def swap_with_window(self) -> None: - def callback(tab: Tab, window: Window) -> None: - tab.swap_active_window_with(window.id) + def callback(tab: Optional[Tab], window: Optional[Window]) -> None: + if tab and window: + tab.swap_active_window_with(window.id) get_boss().visual_window_select_action(self, callback, 'Choose window to swap with') @ac('win', 'Move active window to the top (make it the first window)')