mirror of
https://github.com/kovidgoyal/kitty.git
synced 2024-09-20 02:57:19 +03:00
Work on conversion of args parsing to go code
This commit is contained in:
parent
441e4edfb2
commit
79c8862d4c
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 @@ 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:
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 @@ 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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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):
|
||||
|
@ -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 {
|
||||
|
@ -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 @@ 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 []}
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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'}
|
||||
|
@ -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 @@ 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 {
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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]] = {}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
20
tools/cmd/at/env.go
Normal file
20
tools/cmd/at/env.go
Normal file
@ -0,0 +1,20 @@
|
||||
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
40
tools/cmd/at/scroll_window.go
Normal file
40
tools/cmd/at/scroll_window.go
Normal file
@ -0,0 +1,40 @@
|
||||
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
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
|
||||
}
|
54
tools/cmd/at/set_window_logo.go
Normal file
54
tools/cmd/at/set_window_logo.go
Normal file
@ -0,0 +1,54 @@
|
||||
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
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
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user