diff --git a/gen-go-code.py b/gen-go-code.py index 3a2ea9332..8af3ba29f 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -113,12 +113,14 @@ def build_go_code(name: str, cmd: RemoteCommand, seq: OptionSpecSeq, template: s ov.append(o.set_flag_value(f'options_{name}')) jd: List[str] = [] json_fields = [] + field_types: Dict[str, str] = {} for line in cmd.protocol_spec.splitlines(): line = line.strip() if ':' not in line: continue f = JSONField(line) json_fields.append(f) + field_types[f.field] = f.field_type jd.append(f.go_declaration()) jc: List[str] = [] for field in json_fields: @@ -128,7 +130,16 @@ def build_go_code(name: str, cmd: RemoteCommand, seq: OptionSpecSeq, template: s else: print(f'Cant map field: {field.field} for cmd: {name}', file=sys.stderr) continue + try: + jc.extend(cmd.args.as_go_code(name, field_types)) + except TypeError: + print(f'Cant parse args for cmd: {name}', file=sys.stderr) + print('TODO: test set_window_logo, send_text, env, scroll_window', file=sys.stderr) + + argspec = cmd.args.spec + if argspec: + argspec = ' ' + argspec ans = replace( template, CMD_NAME=name, __FILE__=__file__, CLI_NAME=name.replace('_', '-'), @@ -141,7 +152,7 @@ def build_go_code(name: str, cmd: RemoteCommand, seq: OptionSpecSeq, template: s OPTIONS_DECLARATION_CODE='\n'.join(od), SET_OPTION_VALUES_CODE='\n'.join(ov), JSON_DECLARATION_CODE='\n'.join(jd), - JSON_INIT_CODE='\n'.join(jc), + JSON_INIT_CODE='\n'.join(jc), ARGSPEC=argspec, STRING_RESPONSE_IS_ERROR='true' if cmd.string_return_is_error else 'false', ) return ans diff --git a/kitty/complete.py b/kitty/complete.py index b9d3960dd..17422a836 100644 --- a/kitty/complete.py +++ b/kitty/complete.py @@ -492,7 +492,7 @@ def global_options_for_remote_cmd() -> Dict[str, OptionDict]: def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str], new_word: bool) -> None: aliases, alias_map = options_for_cmd(cmd_name) try: - args_completion = command_for_name(cmd_name).args_completion + args_completion = command_for_name(cmd_name).args.completion except KeyError: return args_completer: CompleteArgsFunc = basic_option_arg_completer diff --git a/kitty/rc/base.py b/kitty/rc/base.py index c7230623d..801a3042e 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -2,9 +2,10 @@ # License: GPLv3 Copyright: 2020, Kovid Goyal 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 @@ def send_error(self, error: str) -> None: 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 @@ def cancel_async_request(self, boss: 'Boss', window: Optional['Window'], payload 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: diff --git a/kitty/rc/close_tab.py b/kitty/rc/close_tab.py index f606d1e13..fae321ecf 100644 --- a/kitty/rc/close_tab.py +++ b/kitty/rc/close_tab.py @@ -46,7 +46,6 @@ class CloseTab(RemoteCommand): 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} diff --git a/kitty/rc/close_window.py b/kitty/rc/close_window.py index ab5ab764a..30567d528 100644 --- a/kitty/rc/close_window.py +++ b/kitty/rc/close_window.py @@ -38,7 +38,6 @@ class CloseWindow(RemoteCommand): 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} diff --git a/kitty/rc/create_marker.py b/kitty/rc/create_marker.py index 5da4ede5a..63ee3ab3d 100644 --- a/kitty/rc/create_marker.py +++ b/kitty/rc/create_marker.py @@ -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: diff --git a/kitty/rc/detach_tab.py b/kitty/rc/detach_tab.py index 90da785d6..6308abdbb 100644 --- a/kitty/rc/detach_tab.py +++ b/kitty/rc/detach_tab.py @@ -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} diff --git a/kitty/rc/detach_window.py b/kitty/rc/detach_window.py index b1f36cd0f..6c58b8315 100644 --- a/kitty/rc/detach_window.py +++ b/kitty/rc/detach_window.py @@ -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} diff --git a/kitty/rc/disable_ligatures.py b/kitty/rc/disable_ligatures.py index 99c6f00ad..080fe68bc 100644 --- a/kitty/rc/disable_ligatures.py +++ b/kitty/rc/disable_ligatures.py @@ -34,7 +34,7 @@ class DisableLigatures(RemoteCommand): 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: diff --git a/kitty/rc/env.py b/kitty/rc/env.py index 6fd847b43..6176aece6 100644 --- a/kitty/rc/env.py +++ b/kitty/rc/env.py @@ -12,24 +12,27 @@ 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 @@ def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: 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 diff --git a/kitty/rc/focus_tab.py b/kitty/rc/focus_tab.py index 344d832a5..93c0c597c 100644 --- a/kitty/rc/focus_tab.py +++ b/kitty/rc/focus_tab.py @@ -30,7 +30,6 @@ class FocusTab(RemoteCommand): 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} diff --git a/kitty/rc/focus_window.py b/kitty/rc/focus_window.py index 7c39238e1..ad68bcc9e 100644 --- a/kitty/rc/focus_window.py +++ b/kitty/rc/focus_window.py @@ -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 @@ class FocusWindow(RemoteCommand): ''' 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): diff --git a/kitty/rc/get_text.py b/kitty/rc/get_text.py index 887bd8096..404e3e313 100644 --- a/kitty/rc/get_text.py +++ b/kitty/rc/get_text.py @@ -68,7 +68,6 @@ class GetText(RemoteCommand): 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 { diff --git a/kitty/rc/goto_layout.py b/kitty/rc/goto_layout.py index 398f6868f..9e9332bf2 100644 --- a/kitty/rc/goto_layout.py +++ b/kitty/rc/goto_layout.py @@ -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: diff --git a/kitty/rc/kitten.py b/kitty/rc/kitten.py index 0d6399fb3..78d33d893 100644 --- a/kitty/rc/kitten.py +++ b/kitty/rc/kitten.py @@ -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: diff --git a/kitty/rc/launch.py b/kitty/rc/launch.py index 15558c7b4..e85c59592 100644 --- a/kitty/rc/launch.py +++ b/kitty/rc/launch.py @@ -67,7 +67,7 @@ class Launch(RemoteCommand): 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 []} diff --git a/kitty/rc/ls.py b/kitty/rc/ls.py index e7ffed755..97aa2a037 100644 --- a/kitty/rc/ls.py +++ b/kitty/rc/ls.py @@ -36,8 +36,6 @@ class LS(RemoteCommand): 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} diff --git a/kitty/rc/new_window.py b/kitty/rc/new_window.py index 943056c8f..db5b1f887 100644 --- a/kitty/rc/new_window.py +++ b/kitty/rc/new_window.py @@ -74,7 +74,7 @@ class NewWindow(RemoteCommand): 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'} diff --git a/kitty/rc/remove_marker.py b/kitty/rc/remove_marker.py index 16a6f0291..86eee1a8f 100644 --- a/kitty/rc/remove_marker.py +++ b/kitty/rc/remove_marker.py @@ -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} diff --git a/kitty/rc/resize_os_window.py b/kitty/rc/resize_os_window.py index 7b43b288c..3c3fbf65b 100644 --- a/kitty/rc/resize_os_window.py +++ b/kitty/rc/resize_os_window.py @@ -71,7 +71,6 @@ class ResizeOSWindow(RemoteCommand): 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 { diff --git a/kitty/rc/resize_window.py b/kitty/rc/resize_window.py index 00e10b7e5..345dafdaa 100644 --- a/kitty/rc/resize_window.py +++ b/kitty/rc/resize_window.py @@ -46,7 +46,6 @@ class ResizeWindow(RemoteCommand): 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: diff --git a/kitty/rc/scroll_window.py b/kitty/rc/scroll_window.py index 1917807f0..8e09a78af 100644 --- a/kitty/rc/scroll_window.py +++ b/kitty/rc/scroll_window.py @@ -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 @@ class ScrollWindow(RemoteCommand): 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: diff --git a/kitty/rc/set_colors.py b/kitty/rc/set_colors.py index af7de43dc..63fa7f456 100644 --- a/kitty/rc/set_colors.py +++ b/kitty/rc/set_colors.py @@ -89,8 +89,7 @@ class SetColors(RemoteCommand): 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]] = {} diff --git a/kitty/rc/set_window_logo.py b/kitty/rc/set_window_logo.py index 915129241..4a1d4ad4f 100644 --- a/kitty/rc/set_window_logo.py +++ b/kitty/rc/set_window_logo.py @@ -21,7 +21,7 @@ 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 @@ class SetWindowLogo(RemoteCommand): 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 diff --git a/kitty/rc/set_window_title.py b/kitty/rc/set_window_title.py index 9e32fa86f..09f17adb9 100644 --- a/kitty/rc/set_window_title.py +++ b/kitty/rc/set_window_title.py @@ -33,7 +33,7 @@ class SetWindowTitle(RemoteCommand): 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} diff --git a/kitty/rc/signal_child.py b/kitty/rc/signal_child.py index 02a4ae950..9648b0cd6 100644 --- a/kitty/rc/signal_child.py +++ b/kitty/rc/signal_child.py @@ -15,7 +15,7 @@ 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 @@ class SignalChild(RemoteCommand): 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 diff --git a/tools/cmd/at/env.go b/tools/cmd/at/env.go new file mode 100644 index 000000000..28d9c9a67 --- /dev/null +++ b/tools/cmd/at/env.go @@ -0,0 +1,20 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package at + +import ( + "kitty/tools/utils" +) + +func parse_key_val_args(args []string) map[string]string { + ans := make(map[string]string, len(args)) + for _, arg := range args { + key, value, found := utils.Cut(arg, "=") + if found { + ans[key] = value + } else { + ans[key+"="] = "" + } + } + return ans +} diff --git a/tools/cmd/at/main.go b/tools/cmd/at/main.go index fd6756c8c..6e46e4ba4 100644 --- a/tools/cmd/at/main.go +++ b/tools/cmd/at/main.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "os" + "strconv" "strings" "time" @@ -73,20 +74,37 @@ func simple_serializer(rc *utils.RemoteControlCmd) (ans []byte, err error) { type serializer_func func(rc *utils.RemoteControlCmd) ([]byte, error) type wrapped_serializer struct { - state int - serializer serializer_func + state int + serializer serializer_func + all_payloads_done bool } -func (self *wrapped_serializer) next(rc *utils.RemoteControlCmd) ([]byte, error) { +func (self *wrapped_serializer) next(io_data *rc_io_data) ([]byte, error) { const prefix = "\x1bP@kitty-cmd" const suffix = "\x1b\\" - defer func() { self.state++ }() switch self.state { case 0: + self.state++ return []byte(prefix), nil case 1: - return self.serializer(rc) + if io_data.multiple_payload_generator != nil { + is_last, err := io_data.multiple_payload_generator(io_data) + if err != nil { + return nil, err + } + if is_last { + self.all_payloads_done = true + } + } else { + self.all_payloads_done = true + } + return self.serializer(io_data.rc) case 2: + if self.all_payloads_done { + self.state++ + } else { + self.state = 0 + } return []byte(suffix), nil default: return make([]byte, 0), nil @@ -144,12 +162,13 @@ type Response struct { } type rc_io_data struct { - cmd *cobra.Command - rc *utils.RemoteControlCmd - serializer wrapped_serializer - send_keypresses bool - string_response_is_err bool - timeout time.Duration + cmd *cobra.Command + rc *utils.RemoteControlCmd + serializer wrapped_serializer + send_keypresses bool + string_response_is_err bool + timeout time.Duration + multiple_payload_generator func(io_data *rc_io_data) (bool, error) pending_chunks [][]byte } @@ -161,7 +180,7 @@ func (self *rc_io_data) next_chunk(limit_size bool) (chunk []byte, err error) { self.pending_chunks = self.pending_chunks[:len(self.pending_chunks)-1] return } - block, err := self.serializer.next(self.rc) + block, err := self.serializer.next(self) if err != nil && !errors.Is(err, io.EOF) { return } diff --git a/tools/cmd/at/scroll_window.go b/tools/cmd/at/scroll_window.go new file mode 100644 index 000000000..008be3759 --- /dev/null +++ b/tools/cmd/at/scroll_window.go @@ -0,0 +1,40 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package at + +import ( + "fmt" + "strconv" + "strings" +) + +func parse_scroll_amount(amt string) ([2]interface{}, error) { + var ans [2]interface{} + if amt == "start" || amt == "end" { + ans[0] = amt + ans[1] = nil + } else { + pages := strings.Contains(amt, "p") + unscroll := strings.Contains(amt, "u") + var mult float64 = 1 + if strings.HasSuffix(amt, "-") && !unscroll { + mult = -1 + q, err := strconv.ParseFloat(strings.TrimRight(amt, "+-plu"), 64) + if err != nil { + return ans, err + } + if !pages && q != float64(int(q)) { + return ans, fmt.Errorf("The number must be an integer") + } + ans[0] = q * mult + if pages { + ans[1] = "p" + } else if unscroll { + ans[1] = "u" + } else { + ans[1] = "l" + } + } + } + return ans, nil +} diff --git a/tools/cmd/at/set_window_logo.go b/tools/cmd/at/set_window_logo.go new file mode 100644 index 000000000..a753ff53b --- /dev/null +++ b/tools/cmd/at/set_window_logo.go @@ -0,0 +1,54 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package at + +import ( + "encoding/base64" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +func read_window_logo(path string) (func(io_data *rc_io_data) (bool, error), error) { + if strings.ToLower(path) == "none" { + return func(io_data *rc_io_data) (bool, error) { + io_data.rc.Payload = "-" + return true, nil + }, nil + } + + f, err := os.Open(path) + if err != nil { + return nil, err + } + buf := make([]byte, 2048) + n, err := f.Read(buf) + if err != nil && err != io.EOF { + f.Close() + return nil, err + } + buf = buf[:n] + + if http.DetectContentType(buf) != "image/png" { + f.Close() + return nil, fmt.Errorf("%s is not a PNG image", path) + } + + return func(io_data *rc_io_data) (bool, error) { + var payload set_window_logo_json_type = io_data.rc.Payload.(set_window_logo_json_type) + if len(buf) == 0 { + payload.Data = "" + return true, nil + } + payload.Data = base64.StdEncoding.EncodeToString(buf) + buf = buf[:cap(buf)] + n, err := f.Read(buf) + if err != nil && err != io.EOF { + return false, err + } + buf = buf[:n] + return n == 0, nil + }, nil +} diff --git a/tools/cmd/at/template.go b/tools/cmd/at/template.go index 3b4381a39..9197e432c 100644 --- a/tools/cmd/at/template.go +++ b/tools/cmd/at/template.go @@ -7,6 +7,8 @@ package at import ( + "fmt" + "strings" "time" "github.com/spf13/cobra" @@ -16,6 +18,9 @@ import ( "kitty/tools/utils" ) +var _ = fmt.Print +var _ = strings.Join + type options_CMD_NAME_type struct { OPTIONS_DECLARATION_CODE } @@ -88,7 +93,7 @@ func aliasNormalizeFunc_CMD_NAME(f *pflag.FlagSet, name string) pflag.Normalized func setup_CMD_NAME(root *cobra.Command) *cobra.Command { ans := cli.CreateCommand(&cobra.Command{ - Use: "CLI_NAME [options]", + Use: "CLI_NAME [options]" + "ARGSPEC", Short: "SHORT_DESC", Long: "LONG_DESC", RunE: run_CMD_NAME, diff --git a/tools/cmd/at/tty_io.go b/tools/cmd/at/tty_io.go index 1570dfeec..f86616b2a 100644 --- a/tools/cmd/at/tty_io.go +++ b/tools/cmd/at/tty_io.go @@ -44,7 +44,7 @@ func do_chunked_io(io_data *rc_io_data) (serialized_response []byte, err error) } lp.OnInitialize = func() (string, error) { - chunk, err := io_data.next_chunk(true) + chunk, err := io_data.next_chunk(false) if err != nil { return "", err } @@ -62,7 +62,7 @@ func do_chunked_io(io_data *rc_io_data) (serialized_response []byte, err error) } return nil } - chunk, err := io_data.next_chunk(true) + chunk, err := io_data.next_chunk(false) if err != nil { return err }