mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-24 01:08:10 +02:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
77
kitty/rc/action.py
Normal 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()
|
||||||
@@ -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) + '}'
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user