Work on conversion of args parsing to go code

This commit is contained in:
Kovid Goyal 2022-08-30 00:21:59 +05:30
parent 441e4edfb2
commit 79c8862d4c
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
32 changed files with 274 additions and 66 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 []}

View File

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

View File

@ -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'}

View File

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

View File

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

View File

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

View File

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

View File

@ -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]] = {}

View File

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

View File

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

View File

@ -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
View 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
}

View File

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

View 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
}

View 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
}

View File

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

View File

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