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
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
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
- 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.

View file

@ -1550,7 +1550,7 @@ def user_menu_action(self, defn: str) -> None:
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 @@ def combine(self, action_definition: str, window_for_dispatch: Optional[Window]
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

View file

@ -13,7 +13,7 @@ class CLIOptions:
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:

View file

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

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
class RemoteControlErrorWithoutTraceback(Exception):
hide_traceback = True
class MatchError(ValueError):
hide_traceback = True
@ -219,7 +224,10 @@ def as_go_code(self, cmd_name: str, field_types: Dict[str, str], handled_fields:
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) + '}'

View file

@ -46,6 +46,13 @@ class LoadConfig(RemoteCommand):
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',