diff --git a/docs/changelog.rst b/docs/changelog.rst index 2cc345825..7b444e4ac 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -48,6 +48,8 @@ Detailed list of changes - kitten @ load-config: Allow (re)loading kitty.conf via remote control +- Remote control: Allow running mappable actions via remote control (`kitten @ action`) + - kitten @ send-text: Add a new option to automatically wrap the sent text in bracketed paste escape codes if the program in the destination window has turned on bracketed paste. diff --git a/kitty/boss.py b/kitty/boss.py index bd8fb4888..08fc5baf6 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -1550,7 +1550,7 @@ class Boss: map kitty_mod+e combine : new_window : next_layout ''') - def combine(self, action_definition: str, window_for_dispatch: Optional[Window] = None, dispatch_type: str = 'KeyPress') -> bool: + def combine(self, action_definition: str, window_for_dispatch: Optional[Window] = None, dispatch_type: str = 'KeyPress', raise_error: bool = False) -> bool: consumed = False if action_definition: try: @@ -1565,6 +1565,8 @@ class Boss: if len(actions) > 1: self.drain_actions(list(actions[1:]), window_for_dispatch, dispatch_type) except Exception as e: + if raise_error: + raise self.show_error('Key action failed', f'{actions[0].pretty()}\n{e}') consumed = True return consumed diff --git a/kitty/cli_stub.py b/kitty/cli_stub.py index 0cd4e3be1..a55b7dcf7 100644 --- a/kitty/cli_stub.py +++ b/kitty/cli_stub.py @@ -13,7 +13,7 @@ LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOpt HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions -ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = CLIOptions +ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = ActionRCOptions = CLIOptions def generate_stub() -> None: diff --git a/kitty/launch.py b/kitty/launch.py index 613f2cc0f..dc96e3ff6 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -321,7 +321,7 @@ def parse_launch_args(args: Optional[Sequence[str]] = None) -> LaunchSpec: try: opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec) except SystemExit as e: - raise ValueError from e + raise ValueError(str(e)) from e return LaunchSpec(opts, args) diff --git a/kitty/rc/action.py b/kitty/rc/action.py new file mode 100644 index 000000000..655537787 --- /dev/null +++ b/kitty/rc/action.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2020, Kovid Goyal + + +from typing import TYPE_CHECKING, Optional + +from .base import ( + MATCH_WINDOW_OPTION, + ArgsType, + Boss, + PayloadGetType, + PayloadType, + RCOptions, + RemoteCommand, + ResponseType, + Window, +) +from .base import ( + RemoteControlErrorWithoutTraceback as RemoteControlError, +) + +if TYPE_CHECKING: + from kitty.cli_stub import ActionRCOptions as CLIOptions + + +class Action(RemoteCommand): + + protocol_spec = __doc__ = ''' + action+/str: The action to perform. Of the form: action [optional args...] + match_window/str: Window to run the action on + self/bool: Whether to use the window this command is run in as the active window + ''' + + short_desc = 'Run the specified mappable action' + desc = ( + 'Run the specified mappable action. For a list of all available mappable actions, see :doc:`actions`.' + ' Any arguments for ACTION should follow the action. Note that parsing of arguments is action dependent' + ' so for best results specify all arguments as single string on the command line in the same format as you would' + ' use for that action in kitty.conf.' + ) + options_spec = '''\ +--self +type=bool-set +Run the action on the window this command is run in instead of the active window. + + +--no-response +type=bool-set +default=false +Don't wait for a response indicating the success of the action. Note that +using this option means that you will not be notified of failures. +''' + '\n\n' + MATCH_WINDOW_OPTION + + args = RemoteCommand.Args(spec='ACTION [ARGS FOR ACTION...]', json_field='action', minimum_count=1) + + def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: + return {'action': ' '.join(args), 'self': opts.self, 'match_window': opts.match} + + def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: + w = self.windows_for_match_payload(boss, window, payload_get) + if w: + window = w[0] + ac = payload_get('action') + if not ac: + raise RemoteControlError('Must specify an action') + + try: + consumed = boss.combine(str(ac), window, raise_error=True) + except (Exception, SystemExit) as e: + raise RemoteControlError(str(e)) + + if not consumed: + raise RemoteControlError(f'Unknown action: {ac}') + return None + + +action = Action() diff --git a/kitty/rc/base.py b/kitty/rc/base.py index 08a382aa3..b2199d671 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -31,6 +31,11 @@ class RemoteControlError(Exception): pass +class RemoteControlErrorWithoutTraceback(Exception): + + hide_traceback = True + + class MatchError(ValueError): hide_traceback = True @@ -219,7 +224,10 @@ class ArgsHandling: yield f'args = append(args, "{x}")' yield '}' if self.minimum_count > -1: - yield f'if len(args) < {self.minimum_count} {{ return fmt.Errorf("%s", "Must specify at least {self.minimum_count} arguments to {cmd_name}") }}' + if self.minimum_count == 1: + yield f'if len(args) < {self.minimum_count} {{ return fmt.Errorf("%s", "Must specify at least one argument to {cmd_name}") }}' + else: + yield f'if len(args) < {self.minimum_count} {{ return fmt.Errorf("%s", "Must specify at least {self.minimum_count} arguments to {cmd_name}") }}' if self.args_choices: achoices = tuple(self.args_choices()) yield 'achoices := map[string]bool{' + ' '.join(f'"{x}":true,' for x in achoices) + '}' diff --git a/kitty/rc/load_config.py b/kitty/rc/load_config.py index 59b5a9a31..fca346202 100644 --- a/kitty/rc/load_config.py +++ b/kitty/rc/load_config.py @@ -46,6 +46,13 @@ or a previous load-config-file command are respected. Use this option to have th type=list Override individual configuration options, can be specified multiple times. Syntax: :italic:`name=value`. For example: :option:`{appname} -o` font_size=20 + + +--no-response +type=bool-set +default=false +Don't wait for a response indicating the success of the action. Note that +using this option means that you will not be notified of failures. ''' args = RemoteCommand.Args(spec='CONF_FILE ...', json_field='paths',