A new remote control command to visually select a window

Fixes #4165
This commit is contained in:
Kovid Goyal
2021-10-30 13:56:48 +05:30
parent c541d7e349
commit 816360c275
7 changed files with 121 additions and 15 deletions

View File

@@ -31,6 +31,9 @@ To update |kitty|, :doc:`follow the instructions <binary>`.
- A new remote control command to :program:`change the tab color <kitty @
set-tab-color>` (:iss:`1287`)
- A new remote control command to :program:`visually select a window <kitty @
select-window>` (:iss:`4165`)
- Add support for reporting mouse events with pixel co-ordinates using the
``SGR_PIXEL_PROTOCOL`` introduced in xterm 359

View File

@@ -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

View File

@@ -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 */
};

View File

@@ -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

71
kitty/rc/select_window.py Normal file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
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()

View File

@@ -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'):

View File

@@ -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)')