mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-22 08:18:08 +02:00
Work on conversion of args parsing to go code
This commit is contained in:
@@ -2,9 +2,10 @@
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Iterable, Iterator, List,
|
||||
NoReturn, Optional, Tuple, Type, Union, cast
|
||||
NoReturn, Optional, Set, Tuple, Type, Union, cast
|
||||
)
|
||||
|
||||
from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec
|
||||
@@ -160,27 +161,98 @@ class AsyncResponder:
|
||||
send_response_to_client(error=error, peer_id=self.peer_id, window_id=self.window_id, async_id=self.async_id)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArgsHandling:
|
||||
|
||||
json_field: str = ''
|
||||
count: Optional[int] = None
|
||||
spec: str = ''
|
||||
completion: Optional[Dict[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None
|
||||
value_if_unspecified: Tuple[str, ...] = ()
|
||||
minimum_count: int = -1
|
||||
first_rest: Optional[Tuple[str, str]] = None
|
||||
special_parse: str = ''
|
||||
|
||||
@property
|
||||
def args_count(self) -> Optional[int]:
|
||||
if not self.spec:
|
||||
return 0
|
||||
return self.count
|
||||
|
||||
def as_go_code(self, cmd_name: str, field_types: Dict[str, str], handled_fields: Set[str]) -> Iterator[str]:
|
||||
c = self.args_count
|
||||
if c == 0:
|
||||
yield f'if len(args) != 0 {{ return fmt.Errorf("%s", "Unknown extra argument(s) supplied to {cmd_name}") }}'
|
||||
return
|
||||
if c is not None:
|
||||
yield f'if len(args) != {c} {{ return fmt.Errorf("%s", "Must specify exactly {c} argument(s) for {cmd_name}") }}'
|
||||
if self.value_if_unspecified:
|
||||
yield 'if len(args) == 0 {'
|
||||
for x in self.value_if_unspecified:
|
||||
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.json_field:
|
||||
jf = self.json_field
|
||||
dest = f'payload.{jf.capitalize()}'
|
||||
jt = field_types[jf]
|
||||
if self.first_rest:
|
||||
yield f'payload.{self.first_rest[0].capitalize()} = args[0]'
|
||||
yield f'payload.{self.first_rest[1].capitalize()} = args[1:]'
|
||||
handled_fields.add(self.first_rest[0])
|
||||
handled_fields.add(self.first_rest[1])
|
||||
return
|
||||
handled_fields.add(self.json_field)
|
||||
if self.special_parse:
|
||||
if self.special_parse.startswith('!'):
|
||||
yield f'io_data.multiple_payload_generator, err = {self.special_parse[1:]}'
|
||||
else:
|
||||
yield f'{dest}, err = {self.special_parse}'
|
||||
yield 'if err != nil { return err }'
|
||||
return
|
||||
if jt == 'list.str':
|
||||
yield f'{dest} = args'
|
||||
return
|
||||
if jt == 'str':
|
||||
if c == 1:
|
||||
yield f'{dest} = args[0]'
|
||||
else:
|
||||
yield f'{dest} = strings.Join(args, " ")'
|
||||
return
|
||||
if jt.startswith('choices.'):
|
||||
yield f'if len(args) != 1 {{ return fmt.Errorf("%s", "Must specify exactly 1 argument for {cmd_name}") }}'
|
||||
choices = ", ".join((f'"{x}"' for x in jt.split('.')[1:]))
|
||||
yield 'switch(args[0]) {'
|
||||
yield f'case {choices}:\n\t{dest} = args[0]'
|
||||
yield f'default: return fmt.Errorf("%s is not a valid choice. Allowed values: %s", args[0], `{choices}`)'
|
||||
yield '}'
|
||||
return
|
||||
if jt == 'dict.str':
|
||||
yield f'{dest} = parse_key_val_args(args)'
|
||||
raise TypeError(f'Unknown args handling for cmd: {cmd_name}')
|
||||
|
||||
|
||||
class RemoteCommand:
|
||||
Args = ArgsHandling
|
||||
|
||||
name: str = ''
|
||||
short_desc: str = ''
|
||||
desc: str = ''
|
||||
argspec: str = '...'
|
||||
args: ArgsHandling = ArgsHandling()
|
||||
options_spec: Optional[str] = None
|
||||
no_response: bool = False
|
||||
response_timeout: float = 10. # seconds
|
||||
string_return_is_error: bool = False
|
||||
args_count: Optional[int] = None
|
||||
args_completion: Optional[Dict[str, Tuple[str, Union[Callable[[], Iterable[str]], Tuple[str, ...]]]]] = None
|
||||
defaults: Optional[Dict[str, Any]] = None
|
||||
is_asynchronous: bool = False
|
||||
options_class: Type[RCOptions] = RCOptions
|
||||
protocol_spec: str = ''
|
||||
argspec = args_count = args_completion = ArgsHandling()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.desc = self.desc or self.short_desc
|
||||
self.name = self.__class__.__module__.split('.')[-1].replace('_', '-')
|
||||
self.args_count = 0 if not self.argspec else self.args_count
|
||||
|
||||
def fatal(self, msg: str) -> NoReturn:
|
||||
if running_in_kitty():
|
||||
@@ -259,21 +331,21 @@ class RemoteCommand:
|
||||
|
||||
|
||||
def cli_params_for(command: RemoteCommand) -> Tuple[Callable[[], str], str, str, str]:
|
||||
return (command.options_spec or '\n').format, command.argspec, command.desc, f'{appname} @ {command.name}'
|
||||
return (command.options_spec or '\n').format, command.args.spec, command.desc, f'{appname} @ {command.name}'
|
||||
|
||||
|
||||
def parse_subcommand_cli(command: RemoteCommand, args: ArgsType) -> Tuple[Any, ArgsType]:
|
||||
opts, items = parse_args(args[1:], *cli_params_for(command), result_class=command.options_class)
|
||||
if command.args_count is not None and command.args_count != len(items):
|
||||
if command.args_count == 0:
|
||||
if command.args.args_count is not None and command.args.args_count != len(items):
|
||||
if command.args.args_count == 0:
|
||||
raise SystemExit(f'Unknown extra argument(s) supplied to {command.name}')
|
||||
raise SystemExit(f'Must specify exactly {command.args_count} argument(s) for {command.name}')
|
||||
raise SystemExit(f'Must specify exactly {command.args.args_count} argument(s) for {command.name}')
|
||||
return opts, items
|
||||
|
||||
|
||||
def display_subcommand_help(func: RemoteCommand) -> None:
|
||||
with suppress(SystemExit):
|
||||
parse_args(['--help'], (func.options_spec or '\n').format, func.argspec, func.desc, func.name)
|
||||
parse_args(['--help'], (func.options_spec or '\n').format, func.args.spec, func.desc, func.name)
|
||||
|
||||
|
||||
def command_for_name(cmd_name: str) -> RemoteCommand:
|
||||
|
||||
@@ -46,7 +46,6 @@ Close the tab of the window this command is run in, rather than the active tab.
|
||||
type=bool-set
|
||||
Do not return an error if no tabs are matched to be closed.
|
||||
'''
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'match': opts.match, 'self': opts.self, 'ignore_no_match': opts.ignore_no_match}
|
||||
|
||||
@@ -38,7 +38,6 @@ Close the window this command is run in, rather than the active window.
|
||||
type=bool-set
|
||||
Do not return an error if no windows are matched to be closed.
|
||||
'''
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'match': opts.match, 'self': opts.self, 'ignore_no_match': opts.ignore_no_match}
|
||||
|
||||
@@ -32,7 +32,7 @@ class CreateMarker(RemoteCommand):
|
||||
type=bool-set
|
||||
Apply marker to the window this command is run in, rather than the active window.
|
||||
'''
|
||||
argspec = 'MARKER SPECIFICATION'
|
||||
args = RemoteCommand.Args(spec='MARKER SPECIFICATION', json_field='marker_spec', minimum_count=2)
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
if len(args) < 2:
|
||||
|
||||
@@ -30,7 +30,6 @@ class DetachTab(RemoteCommand):
|
||||
type=bool-set
|
||||
Detach the tab this command is run in, rather than the active tab.
|
||||
'''
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'match': opts.match, 'target_tab': opts.target_tab, 'self': opts.self}
|
||||
|
||||
@@ -36,7 +36,6 @@ class DetachWindow(RemoteCommand):
|
||||
type=bool-set
|
||||
Detach the window this command is run in, rather than the active window.
|
||||
''')
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'match': opts.match, 'target_tab': opts.target_tab, 'self': opts.self}
|
||||
|
||||
@@ -34,7 +34,7 @@ By default, ligatures are only affected in the active window. This option will
|
||||
cause ligatures to be changed in all windows.
|
||||
|
||||
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
|
||||
argspec = 'STRATEGY'
|
||||
args = RemoteCommand.Args(spec='STRATEGY', count=1, json_field='strategy')
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
if not args:
|
||||
|
||||
@@ -12,24 +12,27 @@ from .base import (
|
||||
class Env(RemoteCommand):
|
||||
|
||||
protocol_spec = __doc__ = '''
|
||||
env+/dict.str: Dictionary of environment variables to values. Empty values cause the variable to be removed.
|
||||
env+/dict.str: Dictionary of environment variables to values. When a env var ends with = it is removed from the environment.
|
||||
'''
|
||||
|
||||
short_desc = 'Change environment variables seen by future children'
|
||||
desc = (
|
||||
'Change the environment variables that will be seen in newly launched windows.'
|
||||
' Similar to the :opt:`env` option in :file:`kitty.conf`, but affects running kitty instances.'
|
||||
' Empty values cause the environment variable to be removed.'
|
||||
' If no = is present, the variable is removed from the environment.'
|
||||
)
|
||||
argspec = 'env_var1=val env_var2=val ...'
|
||||
args = RemoteCommand.Args(spec='env_var1=val env_var2=val ...', minimum_count=1, json_field='env')
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: Any, args: ArgsType) -> PayloadType:
|
||||
if len(args) < 1:
|
||||
self.fatal('Must specify at least one env var to set')
|
||||
env = {}
|
||||
for x in args:
|
||||
key, val = x.split('=', 1)
|
||||
env[key] = val
|
||||
if '=' in x:
|
||||
key, val = x.split('=', 1)
|
||||
env[key] = val
|
||||
else:
|
||||
env[x + '='] = ''
|
||||
return {'env': env}
|
||||
|
||||
def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType:
|
||||
@@ -38,10 +41,10 @@ class Env(RemoteCommand):
|
||||
new_env = payload_get('env') or {}
|
||||
env = default_env().copy()
|
||||
for k, v in new_env.items():
|
||||
if v:
|
||||
env[k] = expandvars(v, env)
|
||||
else:
|
||||
if k.endswith('='):
|
||||
env.pop(k, None)
|
||||
else:
|
||||
env[k] = expandvars(v or '', env)
|
||||
set_default_env(env)
|
||||
return None
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ 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.
|
||||
'''
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'match': opts.match, 'no_response': opts.no_response}
|
||||
|
||||
@@ -23,7 +23,6 @@ class FocusWindow(RemoteCommand):
|
||||
|
||||
short_desc = 'Focus the specified window'
|
||||
desc = 'Focus the specified window, if no window is specified, focus the window this command is run inside.'
|
||||
argspec = ''
|
||||
options_spec = MATCH_WINDOW_OPTION + '''\n\n
|
||||
--no-response
|
||||
type=bool-set
|
||||
@@ -33,7 +32,7 @@ the command will exit with a success code.
|
||||
'''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'match': opts.match, 'no_response': opts.no_response}
|
||||
return {'match': opts.match}
|
||||
|
||||
def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType:
|
||||
for window in self.windows_for_match_payload(boss, window, payload_get):
|
||||
|
||||
@@ -68,7 +68,6 @@ Clear the selection in the matched window, if any.
|
||||
type=bool-set
|
||||
Get text from the window this command is run in, rather than the active window.
|
||||
'''
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {
|
||||
|
||||
@@ -30,9 +30,7 @@ class GotoLayout(RemoteCommand):
|
||||
' You can use special match value :code:`all` to set the layout in all tabs.'
|
||||
)
|
||||
options_spec = MATCH_TAB_OPTION
|
||||
argspec = 'LAYOUT_NAME'
|
||||
args_count = 1
|
||||
args_completion = {'names': ('Layouts', layout_names)}
|
||||
args = RemoteCommand.Args(spec='LAYOUT_NAME', count=1, completion={'names': ('Layouts', layout_names)}, json_field='layout')
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
if len(args) != 1:
|
||||
|
||||
@@ -30,7 +30,7 @@ class Kitten(RemoteCommand):
|
||||
' is printed out to stdout.'
|
||||
)
|
||||
options_spec = MATCH_WINDOW_OPTION
|
||||
argspec = 'kitten_name'
|
||||
args = RemoteCommand.Args(spec='kitten_name', json_field='kitten', minimum_count=1, first_rest=('kitten', 'args'))
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
if len(args) < 1:
|
||||
|
||||
@@ -67,7 +67,7 @@ type=bool-set
|
||||
If specified the tab containing the window this command is run in is used
|
||||
instead of the active tab
|
||||
''' + '\n\n' + launch_options_spec().replace(':option:`launch', ':option:`kitty @ launch')
|
||||
argspec = '[CMD ...]'
|
||||
args = RemoteCommand.Args(spec='[CMD ...]', json_field='args')
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
ans = {'args': args or []}
|
||||
|
||||
@@ -36,8 +36,6 @@ type=bool-set
|
||||
Show all environment variables in output, not just differing ones.
|
||||
'''
|
||||
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'all_env_vars': opts.all_env_vars}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ Don't wait for a response giving the id of the newly opened window. Note that
|
||||
using this option means that you will not be notified of failures and that
|
||||
the id of the new window will not be printed out.
|
||||
'''
|
||||
argspec = '[CMD ...]'
|
||||
args = RemoteCommand.Args(spec='[CMD ...]', json_field='args')
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
ans = {'args': args or [], 'type': 'window'}
|
||||
|
||||
@@ -26,7 +26,6 @@ class RemoveMarker(RemoteCommand):
|
||||
type=bool-set
|
||||
Apply marker to the window this command is run in, rather than the active window.
|
||||
'''
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {'match': opts.match, 'self': opts.self}
|
||||
|
||||
@@ -71,7 +71,6 @@ 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.
|
||||
'''
|
||||
argspec = ''
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
return {
|
||||
|
||||
@@ -46,7 +46,6 @@ The special value :code:`reset` will reset the layout to its default configurati
|
||||
type=bool-set
|
||||
Resize the window this command is run in, rather than the active window.
|
||||
'''
|
||||
argspec = ''
|
||||
string_return_is_error = True
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
|
||||
@@ -31,7 +31,6 @@ class ScrollWindow(RemoteCommand):
|
||||
' will scroll up 2 pages and :code:`0.5p`will scroll down half page. :code:`3u` will *unscroll* by 3 lines, which means that 3 lines will move from the'
|
||||
' scrollback buffer onto the top of the screen.'
|
||||
)
|
||||
argspec = 'SCROLL_AMOUNT'
|
||||
options_spec = MATCH_WINDOW_OPTION + '''\n
|
||||
--no-response
|
||||
type=bool-set
|
||||
@@ -39,6 +38,7 @@ 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='SCROLL_AMOUNT', count=1, special_parse='parse_scroll_amount(args[0])', json_field='amount')
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
if len(args) < 1:
|
||||
|
||||
@@ -89,8 +89,7 @@ type=bool-set
|
||||
Restore all colors to the values they had at kitty startup. Note that if you specify
|
||||
this option, any color arguments are ignored and :option:`kitty @ set-colors --configured` and :option:`kitty @ set-colors --all` are implied.
|
||||
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
|
||||
argspec = 'COLOR_OR_FILE ...'
|
||||
args_completion = {'files': ('CONF files', ('*.conf',))}
|
||||
args = RemoteCommand.Args(spec='COLOR_OR_FILE ...', completion={'files': ('CONF files', ('*.conf',))})
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
final_colors: Dict[str, Optional[int]] = {}
|
||||
|
||||
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class SetWindowLogo(RemoteCommand):
|
||||
protocol_spec = __doc__ = '''
|
||||
data+/str: Chunk of at most 512 bytes of PNG data, base64 encoded. Must send an empty chunk to indicate end of image. \
|
||||
data+/str: Chunk of PNG data, base64 encoded no more than 2048 bytes. Must send an empty chunk to indicate end of image. \
|
||||
Or the special value :code:`-` to indicate image must be removed.
|
||||
position/str: The logo position as a string, empty string means default
|
||||
alpha/float: The logo alpha between :code:`0` and :code:`1`. :code:`-1` means use default
|
||||
@@ -59,9 +59,8 @@ default=false
|
||||
Don't wait for a response from kitty. This means that even if setting the image
|
||||
failed, the command will exit with a success code.
|
||||
'''
|
||||
argspec = 'PATH_TO_PNG_IMAGE'
|
||||
args_count = 1
|
||||
args_completion = {'files': ('PNG Images', ('*.png',))}
|
||||
args = RemoteCommand.Args(spec='PATH_TO_PNG_IMAGE', count=1, json_field='data', special_parse='!read_window_logo(args[0])', completion={
|
||||
'files': ('PNG Images', ('*.png',))})
|
||||
images_in_flight: Dict[str, IO[bytes]] = {}
|
||||
is_asynchronous = True
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ type=bool-set
|
||||
By default, the title will be permanently changed and programs running in the window will not be able to change it
|
||||
again. If you want to allow other programs to change it afterwards, use this option.
|
||||
''' + '\n\n' + MATCH_WINDOW_OPTION
|
||||
argspec = '[TITLE ...]'
|
||||
args = RemoteCommand.Args(json_field='title', spec='[TITLE ...]')
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
ans = {'match': opts.match, 'temporary': opts.temporary}
|
||||
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
class SignalChild(RemoteCommand):
|
||||
|
||||
protocol_spec = __doc__ = '''
|
||||
signals/list.str: The signals, a list of names, such as :code:`SIGTERM`, :code:`SIGKILL`, :code:`SIGUSR1`, etc.
|
||||
signals+/list.str: The signals, a list of names, such as :code:`SIGTERM`, :code:`SIGKILL`, :code:`SIGUSR1`, etc.
|
||||
match/str: Which windows to send the signals to
|
||||
'''
|
||||
|
||||
@@ -35,7 +35,7 @@ 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
|
||||
argspec = '[SIGNAL_NAME ...]'
|
||||
args = RemoteCommand.Args(json_field='signals', spec='[SIGNAL_NAME ...]', value_if_unspecified=('SIGINT',))
|
||||
|
||||
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
|
||||
# defaults to signal the window this command is run in
|
||||
|
||||
Reference in New Issue
Block a user