diff --git a/docs/changelog.rst b/docs/changelog.rst index 9b157f2cf..27011d9b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,9 @@ To update |kitty|, :doc:`follow the instructions `. 0.19.3 [future] ------------------- +- A new :doc:`broadcast ` kitten to type in all kitty windows + simultaneously (:iss:`1569`) + - Add a new mappable `select_tab` action to choose a tab to switch to even when the tab bar is hidden (:iss:`3115`) diff --git a/docs/kittens/broadcast.rst b/docs/kittens/broadcast.rst new file mode 100644 index 000000000..2b8d08453 --- /dev/null +++ b/docs/kittens/broadcast.rst @@ -0,0 +1,23 @@ +broadcast - type text in all kitty windows +================================================== + +The ``broadcast`` kitten can be used to type text simultaneously in +all kitty windows (or a subset as desired). + +To use it, simply create a mapping in :file:`kitty.conf` such as:: + + map F1 launch --allow-remote-control kitty +kitten broadcast + +Then press the :kbd:`F1` key and whatever you type in the newly created widow +will be sent to all kitty windows. + +You can use the options described below to control which windows +are selected. + +.. program:: kitty +kitten broadcast + + +Command Line Interface +-------------------------- + +.. include:: /generated/cli-kitten-broadcast.rst diff --git a/docs/remote-control.rst b/docs/remote-control.rst index d5d84089d..8c1cb755f 100644 --- a/docs/remote-control.rst +++ b/docs/remote-control.rst @@ -148,6 +148,19 @@ as the syntax for what follows :code:`kitty @` above. You do not need to enable remote control to use these mappings. +Broadcasting what you type to all kitty windows +-------------------------------------------------- + +As a simple illustration of the power of remote control, lets +have what we type sent to all open kitty windows. To do that define the +following mapping in :file:`kitty.conf`:: + + map F1 launch --allow-remote-control kitty +kitten broadcast + +Now press, F1 and start typing, what you type will be sent to all windows, +live, as you type it. + + Documentation for the remote control protocol ----------------------------------------------- diff --git a/kittens/broadcast/__init__.py b/kittens/broadcast/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kittens/broadcast/main.py b/kittens/broadcast/main.py new file mode 100644 index 000000000..6f13b2db3 --- /dev/null +++ b/kittens/broadcast/main.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2020, Kovid Goyal + +import sys +from base64 import standard_b64encode +from gettext import gettext as _ +from typing import Any, Dict, List, Optional, Tuple + +from kitty.cli import parse_args +from kitty.cli_stub import BroadcastCLIOptions +from kitty.key_encoding import RELEASE, key_defs as K +from kitty.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION +from kitty.remote_control import create_basic_command, encode_send +from kitty.typing import KeyEventType + +from ..tui.handler import Handler +from ..tui.loop import Loop +from ..tui.operations import styled + + +class Broadcast(Handler): + + def __init__(self, opts: BroadcastCLIOptions, initial_strings: List[str]) -> None: + self.opts = opts + self.initial_strings = initial_strings + self.payload = {'exclude_active': True, 'data': '', 'match': opts.match_tab, 'match_tab': opts.match_tab} + if not opts.match and not opts.match_tab: + self.payload['all'] = True + + def initialize(self) -> None: + self.print('Type the text to broadcast below, press', styled('Ctrl+c', fg='yellow'), 'to quit:') + for x in self.initial_strings: + self.write_broadcast_text(x) + + def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: + self.write_broadcast_text(text) + self.write(text) + + def on_interrupt(self) -> None: + self.quit_loop(0) + + def on_eot(self) -> None: + self.write_broadcast_text('\x04') + + def on_key(self, key_event: KeyEventType) -> None: + if key_event.type is not RELEASE and not key_event.mods: + if key_event.key is K['TAB']: + self.write_broadcast_text('\t') + self.write('\t') + elif key_event.key is K['BACKSPACE']: + self.write_broadcast_text('\177') + self.write('\x08\x1b[X') + elif key_event.key is K['ENTER']: + self.write_broadcast_text('\r') + self.print('') + + def write_broadcast_text(self, text: str) -> None: + self.write_broadcast_data('base64:' + standard_b64encode(text.encode('utf-8')).decode('ascii')) + + def write_broadcast_data(self, data: str) -> None: + payload = self.payload.copy() + payload['data'] = data + send = create_basic_command('send-text', payload, no_response=True) + self.write(encode_send(send)) + + +OPTIONS = (MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')).format +help_text = 'Broadcast typed text to all kitty windows. By default text is sent to all windows, unless one of the matching options is specified' +usage = '[initial text to send ...]' + + +def parse_broadcast_args(args: List[str]) -> Tuple[BroadcastCLIOptions, List[str]]: + return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten broadcast', result_class=BroadcastCLIOptions) + + +def main(args: List[str]) -> Optional[Dict[str, Any]]: + try: + opts, items = parse_broadcast_args(args[1:]) + except SystemExit as e: + if e.code != 0: + print(e.args[0], file=sys.stderr) + input(_('Press Enter to quit')) + return None + + print('Type text to be broadcast below, Ctrl-C to quit:', end='\r\n') + sys.stdout.flush() + loop = Loop() + handler = Broadcast(opts, items) + loop.loop(handler) + + +if __name__ == '__main__': + main(sys.argv) +elif __name__ == '__doc__': + cd = sys.cli_docs # type: ignore + cd['usage'] = usage + cd['options'] = OPTIONS + cd['help_text'] = help_text diff --git a/kitty/cli_stub.py b/kitty/cli_stub.py index fc4fdd664..39951865b 100644 --- a/kitty/cli_stub.py +++ b/kitty/cli_stub.py @@ -13,7 +13,7 @@ class CLIOptions: LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions -QueryTerminalCLIOptions = CLIOptions +QueryTerminalCLIOptions = BroadcastCLIOptions = CLIOptions def generate_stub() -> None: @@ -48,6 +48,9 @@ def do(otext=None, cls: str = 'CLIOptions', extra_fields: Sequence[str] = ()): from kittens.hints.main import OPTIONS do(OPTIONS(), 'HintsCLIOptions') + from kittens.broadcast.main import OPTIONS + do(OPTIONS(), 'BroadcastCLIOptions') + from kittens.icat.main import options_spec do(options_spec(), 'IcatCLIOptions')