Allow running mappable actions via remote control

Saves me having to define a special remote control wrapper for every
mappable action.
This commit is contained in:
Kovid Goyal
2024-02-10 13:23:06 +05:30
parent ac7b6870a8
commit 54548931b5
7 changed files with 100 additions and 4 deletions

View File

@@ -48,6 +48,8 @@ Detailed list of changes
- kitten @ load-config: Allow (re)loading kitty.conf via remote control - 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 - 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 bracketed paste escape codes if the program in the destination window has
turned on bracketed paste. turned on bracketed paste.

View File

@@ -1550,7 +1550,7 @@ class Boss:
map kitty_mod+e combine : new_window : next_layout 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 consumed = False
if action_definition: if action_definition:
try: try:
@@ -1565,6 +1565,8 @@ class Boss:
if len(actions) > 1: if len(actions) > 1:
self.drain_actions(list(actions[1:]), window_for_dispatch, dispatch_type) self.drain_actions(list(actions[1:]), window_for_dispatch, dispatch_type)
except Exception as e: except Exception as e:
if raise_error:
raise
self.show_error('Key action failed', f'{actions[0].pretty()}\n{e}') self.show_error('Key action failed', f'{actions[0].pretty()}\n{e}')
consumed = True consumed = True
return consumed return consumed

View File

@@ -13,7 +13,7 @@ LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOpt
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions ErrorCLIOptions = UnicodeCLIOptions = RCOptions = RemoteFileCLIOptions = CLIOptions
QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = CLIOptions
ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = CLIOptions ThemesCLIOptions = TransferCLIOptions = LoadConfigRCOptions = ActionRCOptions = CLIOptions
def generate_stub() -> None: def generate_stub() -> None:

View File

@@ -321,7 +321,7 @@ def parse_launch_args(args: Optional[Sequence[str]] = None) -> LaunchSpec:
try: try:
opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec) opts, args = parse_args(result_class=LaunchCLIOptions, args=args, ospec=options_spec)
except SystemExit as e: except SystemExit as e:
raise ValueError from e raise ValueError(str(e)) from e
return LaunchSpec(opts, args) return LaunchSpec(opts, args)

77
kitty/rc/action.py Normal file
View File

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

View File

@@ -31,6 +31,11 @@ class RemoteControlError(Exception):
pass pass
class RemoteControlErrorWithoutTraceback(Exception):
hide_traceback = True
class MatchError(ValueError): class MatchError(ValueError):
hide_traceback = True hide_traceback = True
@@ -219,7 +224,10 @@ class ArgsHandling:
yield f'args = append(args, "{x}")' yield f'args = append(args, "{x}")'
yield '}' yield '}'
if self.minimum_count > -1: 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: if self.args_choices:
achoices = tuple(self.args_choices()) achoices = tuple(self.args_choices())
yield 'achoices := map[string]bool{' + ' '.join(f'"{x}":true,' for x in achoices) + '}' yield 'achoices := map[string]bool{' + ' '.join(f'"{x}":true,' for x in achoices) + '}'

View File

@@ -46,6 +46,13 @@ or a previous load-config-file command are respected. Use this option to have th
type=list type=list
Override individual configuration options, can be specified multiple times. Override individual configuration options, can be specified multiple times.
Syntax: :italic:`name=value`. For example: :option:`{appname} -o` font_size=20 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', args = RemoteCommand.Args(spec='CONF_FILE ...', json_field='paths',