diff --git a/kitty/rc/base.py b/kitty/rc/base.py index b9e8af99d..ff05701b9 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -108,6 +108,22 @@ class ParsingOfArgsFailed(ValueError): pass +class AsyncResponder: + + def __init__(self, payload_get: PayloadGetType, window: Optional[Window]) -> None: + self.async_id: str = payload_get('async_id', missing='') + self.peer_id: int = payload_get('peer_id', missing=0) + self.window_id: int = getattr(window, 'id', 0) + + def send_data(self, data: Any) -> None: + from kitty.remote_control import send_response_to_client + send_response_to_client(data=data, peer_id=self.peer_id, window_id=self.window_id, async_id=self.async_id) + + def send_error(self, error: str) -> None: + from kitty.remote_control import send_response_to_client + send_response_to_client(error=error, peer_id=self.peer_id, window_id=self.window_id, async_id=self.async_id) + + class RemoteCommand: name: str = '' @@ -121,6 +137,7 @@ class RemoteCommand: args_count: Optional[int] = None args_completion: Optional[Dict[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None defaults: Optional[Dict[str, Any]] = None + is_asynchronous: bool = False options_class: Type[RCOptions] = RCOptions def __init__(self) -> None: @@ -191,6 +208,9 @@ class RemoteCommand: windows += list(tab) return windows + def create_async_responder(self, payload_get: PayloadGetType, window: Optional[Window]) -> AsyncResponder: + return AsyncResponder(payload_get, window) + def message_to_kitty(self, global_opts: RCOptions, opts: Any, args: ArgsType) -> PayloadType: raise NotImplementedError() diff --git a/kitty/rc/select_window.py b/kitty/rc/select_window.py index 3e6c9c2b8..72f6a4de7 100644 --- a/kitty/rc/select_window.py +++ b/kitty/rc/select_window.py @@ -1,10 +1,8 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2020, Kovid Goyal - from typing import TYPE_CHECKING, Optional - from .base import ( MATCH_TAB_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window, no_response @@ -39,21 +37,20 @@ type=bool-set If specified the tab containing the window this command is run in is used instead of the active tab. ''' + is_asynchronous = True 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) + responder = self.create_async_responder(payload_get, window) def callback(tab: Optional['Tab'], window: Optional[Window]) -> None: - from kitty.remote_control import send_response_to_client if window: - send_response_to_client(data=window.id, peer_id=peer_id, window_id=window_id) + responder.send_data(window.id) else: - send_response_to_client(error='No window selected', peer_id=peer_id, window_id=window_id) + responder.send_error('No window selected') for tab in self.tabs_for_match_payload(boss, window, payload_get): if tab: boss.visual_window_select_action(tab, callback, 'Choose window') diff --git a/kitty/remote_control.py b/kitty/remote_control.py index 1f1fb5cde..9afedc1d3 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -6,6 +6,7 @@ import os import re import sys import types +from time import monotonic from contextlib import suppress from functools import partial from typing import ( @@ -24,6 +25,9 @@ from .typing import BossType, WindowType from .utils import TTYIO, parse_address_spec +active_async_requests: Dict[str, float] = {} + + def encode_response_for_peer(response: Any) -> bytes: import json return b'\x1bP@kitty-cmd' + json.dumps(response).encode('utf-8') + b'\x1b\\' @@ -40,7 +44,16 @@ def handle_cmd(boss: BossType, window: Optional[WindowType], serialized_cmd: str c = command_for_name(cmd['cmd']) payload = cmd.get('payload') or {} payload['peer_id'] = peer_id - + async_id = str(cmd.get('async', '')) + if async_id: + if 'cancel_async' in cmd: + active_async_requests.pop(async_id, None) + return None + active_async_requests[async_id] = monotonic() + payload['async_id'] = async_id + if len(active_async_requests) > 32: + oldest = next(iter(active_async_requests)) + del active_async_requests[oldest] try: ans = c.response_from_kitty(boss, window, PayloadGetter(c, payload)) except Exception: @@ -159,14 +172,20 @@ def parse_rc_args(args: List[str]) -> Tuple[RCOptions, List[str]]: return parse_args(args[1:], global_options_spec, 'command ...', msg, f'{appname} @', result_class=RCOptions) -def create_basic_command(name: str, payload: Any = None, no_response: bool = False) -> Dict[str, Any]: +def create_basic_command(name: str, payload: Any = None, no_response: bool = False, is_asynchronous: bool = False) -> Dict[str, Any]: ans = {'cmd': name, 'version': version, 'no_response': no_response} if payload is not None: ans['payload'] = payload + if is_asynchronous: + from kitty.short_uuid import uuid4 + ans['async'] = uuid4() return ans -def send_response_to_client(data: Any = None, error: str = '', peer_id: int = 0, window_id: int = 0) -> None: +def send_response_to_client(data: Any = None, error: str = '', peer_id: int = 0, window_id: int = 0, async_id: str = '') -> None: + ts = active_async_requests.pop(async_id, None) + if ts is None: + return if error: response: Dict[str, Union[bool, int, str]] = {'ok': False, 'error': error} else: @@ -203,13 +222,16 @@ def main(args: List[str]) -> None: response_timeout = c.response_timeout if hasattr(opts, 'response_timeout'): response_timeout = opts.response_timeout - send = create_basic_command(cmd, payload=payload, no_response=no_response) + send = create_basic_command(cmd, payload=payload, no_response=no_response, is_asynchronous=c.is_asynchronous) if not global_opts.to and 'KITTY_LISTEN_ON' in os.environ: global_opts.to = os.environ['KITTY_LISTEN_ON'] import socket try: response = do_io(global_opts.to, send, no_response, response_timeout) except (TimeoutError, socket.timeout): + send.pop('payload', None) + send['cancel_async'] = True + do_io(global_opts.to, send, True, 10) raise SystemExit(f'Timed out after {response_timeout} seconds waiting for response form kitty') if no_response: return