more work on porting rc command parsing to Go

This commit is contained in:
Kovid Goyal 2022-08-30 15:49:26 +05:30
parent 79c8862d4c
commit 6f4968305a
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
19 changed files with 291 additions and 27 deletions

View File

@ -3,6 +3,7 @@
import re import re
import subprocess
from typing import List from typing import List
from kitty.conf.generate import write_output from kitty.conf.generate import write_output
@ -11,14 +12,25 @@ from kitty.conf.generate import write_output
def patch_color_list(path: str, colors: List[str], name: str, spc: str = ' ') -> None: def patch_color_list(path: str, colors: List[str], name: str, spc: str = ' ') -> None:
with open(path, 'r+') as f: with open(path, 'r+') as f:
raw = f.read() raw = f.read()
colors = sorted(colors)
if path.endswith('.go'):
spc = '\t'
nraw = re.sub(
fr'(// {name}_COLORS_START).+?(\s+// {name}_COLORS_END)',
r'\1' + f'\n{spc}' + f'\n{spc}'.join(map(lambda x: f'"{x}":true,', colors)) + r'\2',
raw, flags=re.DOTALL | re.MULTILINE)
else:
nraw = re.sub( nraw = re.sub(
fr'(# {name}_COLORS_START).+?(\s+# {name}_COLORS_END)', fr'(# {name}_COLORS_START).+?(\s+# {name}_COLORS_END)',
r'\1' + f'\n{spc}' + f'\n{spc}'.join(map(lambda x: f'{x!r},', sorted(colors))) + r'\2', r'\1' + f'\n{spc}' + f'\n{spc}'.join(map(lambda x: f'{x!r},', colors)) + r'\2',
raw, flags=re.DOTALL | re.MULTILINE) raw, flags=re.DOTALL | re.MULTILINE)
if nraw != raw: if nraw != raw:
f.seek(0) f.seek(0)
f.truncate() f.truncate()
f.write(nraw) f.write(nraw)
f.flush()
if path.endswith('.go'):
subprocess.check_call(['gofmt', '-w', path])
def main() -> None: def main() -> None:
@ -34,6 +46,7 @@ def main() -> None:
elif opt.parser_func.__name__ in ('to_color', 'titlebar_color', 'macos_titlebar_color'): elif opt.parser_func.__name__ in ('to_color', 'titlebar_color', 'macos_titlebar_color'):
all_colors.append(opt.name) all_colors.append(opt.name)
patch_color_list('kitty/rc/set_colors.py', nullable_colors, 'NULLABLE') patch_color_list('kitty/rc/set_colors.py', nullable_colors, 'NULLABLE')
patch_color_list('tools/cmd/at/set_colors.go', nullable_colors, 'NULLABLE')
patch_color_list('kittens/themes/collection.py', all_colors, 'ALL', ' ' * 8) patch_color_list('kittens/themes/collection.py', all_colors, 'ALL', ' ' * 8)
from kittens.diff.options.definition import definition as kd from kittens.diff.options.definition import definition as kd

View File

@ -6,7 +6,7 @@ import json
import os import os
import sys import sys
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from typing import Dict, Iterator, List, Tuple, Union from typing import Dict, Iterator, List, Set, Tuple, Union
import kitty.constants as kc import kitty.constants as kc
from kittens.tui.operations import Mode from kittens.tui.operations import Mode
@ -123,19 +123,21 @@ def build_go_code(name: str, cmd: RemoteCommand, seq: OptionSpecSeq, template: s
field_types[f.field] = f.field_type field_types[f.field] = f.field_type
jd.append(f.go_declaration()) jd.append(f.go_declaration())
jc: List[str] = [] jc: List[str] = []
handled_fields: Set[str] = set()
try:
jc.extend(cmd.args.as_go_code(name, field_types, handled_fields))
except TypeError:
print(f'Cant parse args for cmd: {name}', file=sys.stderr)
for field in json_fields: for field in json_fields:
if field.field in option_map: if field.field in option_map:
o = option_map[field.field] o = option_map[field.field]
jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}') jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}')
elif field.field in handled_fields:
pass
else: else:
print(f'Cant map field: {field.field} for cmd: {name}', file=sys.stderr) print(f'Cant map field: {field.field} for cmd: {name}', file=sys.stderr)
continue print('TODO: test set_window_logo, set_window_background, set_font_size, send_text, env, scroll_window', file=sys.stderr)
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 argspec = cmd.args.spec
if argspec: if argspec:

View File

@ -172,6 +172,7 @@ class ArgsHandling:
minimum_count: int = -1 minimum_count: int = -1
first_rest: Optional[Tuple[str, str]] = None first_rest: Optional[Tuple[str, str]] = None
special_parse: str = '' special_parse: str = ''
args_choices: Optional[Callable[[], Iterable[str]]] = None
@property @property
def args_count(self) -> Optional[int]: def args_count(self) -> Optional[int]:
@ -193,6 +194,11 @@ class ArgsHandling:
yield '}' yield '}'
if self.minimum_count > -1: if self.minimum_count > -1:
yield f'if len(args) < {self.minimum_count} {{ return fmt.Errorf("%s", Must specify at least {self.minimum_count} arguments to {cmd_name}) }}' yield f'if len(args) < {self.minimum_count} {{ return fmt.Errorf("%s", Must specify at least {self.minimum_count} arguments to {cmd_name}) }}'
if self.args_choices:
achoices = tuple(self.args_choices())
yield 'achoices := map[string]bool{' + ' '.join(f'"{x}":true,' for x in achoices) + '}'
yield 'for _, a := range args {'
yield 'if !achoices[a] { return fmt.Errorf("Not a valid choice: %s. Allowed values are: %s", a, "' + ', '.join(achoices) + '") }'
if self.json_field: if self.json_field:
jf = self.json_field jf = self.json_field
dest = f'payload.{jf.capitalize()}' dest = f'payload.{jf.capitalize()}'
@ -207,6 +213,10 @@ class ArgsHandling:
if self.special_parse: if self.special_parse:
if self.special_parse.startswith('!'): if self.special_parse.startswith('!'):
yield f'io_data.multiple_payload_generator, err = {self.special_parse[1:]}' yield f'io_data.multiple_payload_generator, err = {self.special_parse[1:]}'
elif self.special_parse.startswith('+'):
fields, sp = self.special_parse[1:].split(':', 1)
handled_fields.update(set(fields.split(',')))
yield f'err = {sp}'
else: else:
yield f'{dest}, err = {self.special_parse}' yield f'{dest}, err = {self.special_parse}'
yield 'if err != nil { return err }' yield 'if err != nil { return err }'

View File

@ -101,7 +101,7 @@ Path to a file whose contents you wish to send. Note that in this case the file
are sent as is, not interpreted for escapes. are sent as is, not interpreted for escapes.
''' '''
no_response = True no_response = True
argspec = '[TEXT TO SEND]' args = RemoteCommand.Args(spec='[TEXT TO SEND]', json_field='data', special_parse='+session_id:parse_send_text(io_data, args)')
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
limit = 1024 limit = 1024

View File

@ -63,9 +63,8 @@ default=false
Don't wait for a response from kitty. This means that even if setting the background image Don't wait for a response from kitty. This means that even if setting the background image
failed, the command will exit with a success code. failed, the command will exit with a success code.
''' + '\n\n' + MATCH_WINDOW_OPTION ''' + '\n\n' + MATCH_WINDOW_OPTION
argspec = 'PATH_TO_PNG_IMAGE' args = RemoteCommand.Args(spec='PATH_TO_PNG_IMAGE', count=1, special_parse='!read_window_logo(args[0])', completion={
args_count = 1 'files': ('PNG Images', ('*.png',))})
args_completion = {'files': ('PNG Images', ('*.png',))}
images_in_flight: Dict[str, IO[bytes]] = {} images_in_flight: Dict[str, IO[bytes]] = {}
is_asynchronous = True is_asynchronous = True

View File

@ -37,8 +37,7 @@ By default, background opacity are only changed for the currently active window.
cause background opacity to be changed in all windows. cause background opacity to be changed in all windows.
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t') ''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
argspec = 'OPACITY' args = RemoteCommand.Args(spec='OPACITY', count=1, json_field='opacity')
args_count = 1
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
opacity = max(0.1, min(float(args[0]), 1.0)) opacity = max(0.1, min(float(args[0]), 1.0))

View File

@ -89,7 +89,8 @@ type=bool-set
Restore all colors to the values they had at kitty startup. Note that if you specify 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. 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') ''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
args = RemoteCommand.Args(spec='COLOR_OR_FILE ...', completion={'files': ('CONF files', ('*.conf',))}) args = RemoteCommand.Args(spec='COLOR_OR_FILE ...', json_field='colors', special_parse='parse_colors_and_files(args)', completion={
'files': ('CONF files', ('*.conf',))})
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
final_colors: Dict[str, Optional[int]] = {} final_colors: Dict[str, Optional[int]] = {}

View File

@ -41,8 +41,8 @@ type=bool-set
Change the default enabled layout value so that the new value takes effect for all newly created tabs Change the default enabled layout value so that the new value takes effect for all newly created tabs
as well. as well.
''' '''
argspec = 'LAYOUTS' args = RemoteCommand.Args(
args_completion = {'names': ('Layouts', layout_names)} spec='LAYOUT ...', minimum_count=1, json_field='layouts', completion={'names': ('Layouts', layout_names)}, args_choices=layout_names)
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if len(args) < 1: if len(args) < 1:

View File

@ -28,8 +28,7 @@ class SetFontSize(RemoteCommand):
' with a :code:`+` or :code:`-` increments the font size by the specified' ' with a :code:`+` or :code:`-` increments the font size by the specified'
' amount.' ' amount.'
) )
argspec = 'FONT_SIZE' args = RemoteCommand.Args(spec='FONT_SIZE', count=1, special_parse='+increment_op:parse_set_font_size(args[0], io_data)', json_field='size')
args_count = 1
options_spec = '''\ options_spec = '''\
--all -a --all -a
type=bool-set type=bool-set

View File

@ -95,7 +95,7 @@ type=bool-set
Also change the configured paddings and margins (i.e. the settings kitty will use for new Also change the configured paddings and margins (i.e. the settings kitty will use for new
windows). windows).
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t') ''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
argspec = 'MARGIN_OR_PADDING ...' args = RemoteCommand.Args(spec='MARGIN_OR_PADDING ...', minimum_count=1, json_field='settings', special_parse='parse_set_spacing(args)')
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if not args: if not args:

View File

@ -57,7 +57,7 @@ the keyword NONE to revert to using the default colors.
type=bool-set type=bool-set
Close the tab this command is run in, rather than the active tab. Close the tab this command is run in, rather than the active tab.
''' '''
argspec = 'COLORS' args = RemoteCommand.Args(spec='COLORS', json_field='colors', minimum_count=1, special_parse='parse_tab_colors(args)')
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
try: try:

View File

@ -28,7 +28,7 @@ class SetTabTitle(RemoteCommand):
' title of the currently active window in the tab is used.' ' title of the currently active window in the tab is used.'
) )
options_spec = MATCH_TAB_OPTION options_spec = MATCH_TAB_OPTION
argspec = 'TITLE ...' args = RemoteCommand.Args(spec='TITLE ...', json_field='title')
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'title': ' '.join(args), 'match': opts.match} return {'title': ' '.join(args), 'match': opts.match}

60
tools/cmd/at/send_text.go Normal file
View File

@ -0,0 +1,60 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package at
import (
"encoding/base64"
"errors"
"io"
"os"
"strings"
)
type generator_function func(io_data *rc_io_data) (bool, error)
func parse_send_text(io_data *rc_io_data, args []string) error {
generators := make([]generator_function, 0, 1)
var payload send_text_json_type = io_data.rc.Payload.(send_text_json_type)
if len(args) > 0 {
text := strings.Join(args, " ")
text_gen := func(io_data *rc_io_data) (bool, error) {
payload.Data = "text:" + text[:2048]
text = text[2048:]
return len(text) == 0, nil
}
generators = append(generators, text_gen)
}
if options_send_text.from_file != "" {
f, err := os.Open(options_send_text.from_file)
if err != nil {
return err
}
chunk := make([]byte, 2048)
file_gen := func(io_data *rc_io_data) (bool, error) {
n, err := f.Read(chunk)
if err != nil && !errors.Is(err, io.EOF) {
return false, err
}
payload.Data = "base64:" + base64.StdEncoding.EncodeToString(chunk[:n])
return n == 0, nil
}
generators = append(generators, file_gen)
}
io_data.multiple_payload_generator = func(io_data *rc_io_data) (bool, error) {
if len(generators) == 0 {
payload.Data = "text:"
return true, nil
}
finished, err := generators[0](io_data)
if finished {
generators = generators[1:]
finished = len(generators) == 0
}
return finished, err
}
return nil
}

View File

@ -0,0 +1,74 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package at
import (
"bufio"
"fmt"
"os"
"strings"
"kitty/tools/utils"
"kitty/tools/utils/style"
)
var nullable_colors = map[string]bool{
// generated by gen-config.py do not edit
// NULLABLE_COLORS_START
"active_border_color": true,
"cursor": true,
"cursor_text_color": true,
"selection_background": true,
"selection_foreground": true,
"tab_bar_background": true,
"tab_bar_margin_color": true,
"visual_bell_color": true,
// NULLABLE_COLORS_END
}
func set_color_in_color_map(key, val string, ans map[string]interface{}, check_nullable, skip_nullable bool) error {
if val == "none" {
if check_nullable && !nullable_colors[key] {
if skip_nullable {
return nil
}
return fmt.Errorf("The color %s cannot be set to none", key)
}
ans[key] = nil
} else {
col, err := style.ParseColor(val)
if err != nil {
return fmt.Errorf("%s is not a valid color", val)
}
ans[key] = col.AsRGB()
}
return nil
}
func parse_colors_and_files(args []string) (map[string]interface{}, error) {
ans := make(map[string]interface{}, len(args))
for _, arg := range args {
key, val, found := utils.Cut(strings.ToLower(arg), "=")
if found {
err := set_color_in_color_map(key, val, ans, true, false)
if err != nil {
return nil, err
}
} else {
path := utils.Expanduser(arg)
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
key, val, found := utils.Cut(scanner.Text(), " ")
if found {
set_color_in_color_map(strings.ToLower(key), strings.ToLower(strings.TrimSpace(val)), ans, true, true)
}
}
}
}
return ans, nil
}

View File

@ -0,0 +1,20 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package at
import (
"strconv"
)
func parse_set_font_size(arg string, io_data *rc_io_data) error {
payload := io_data.rc.Payload.(set_font_size_json_type)
if len(arg) > 0 && (arg[0] == '+' || arg[0] == '-') {
payload.Increment_op = arg[:1]
}
val, err := strconv.ParseFloat(arg, 64)
if err != nil {
return err
}
payload.Size = val
return nil
}

View File

@ -0,0 +1,53 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package at
import (
"fmt"
"strconv"
"strings"
"kitty/tools/utils"
)
func parse_set_spacing(args []string) (map[string]interface{}, error) {
ans := make(map[string]interface{}, len(args))
mapper := make(map[string][]string, 32)
types := [2]string{"margin", "padding"}
for _, q := range types {
mapper[q] = []string{q + "-left", q + "-top", q + "-right", q + "-bottom"}
mapper[q+"-h"] = []string{q + "-left", q + "-right"}
mapper[q+"-v"] = []string{q + "-top", q + "-bottom"}
mapper[q+"-left"] = []string{q + "left"}
mapper[q+"-right"] = []string{q + "right"}
mapper[q+"-top"] = []string{q + "top"}
mapper[q+"-bottom"] = []string{q + "bottom"}
}
for _, arg := range args {
k, v, found := utils.Cut(arg, "=")
if !found {
return nil, fmt.Errorf("%s is not a valid setting", arg)
}
k = strings.ToLower(k)
v = strings.ToLower(v)
which, found := mapper[k]
if !found {
return nil, fmt.Errorf("%s is not a valid edge specification", k)
}
if v == "default" {
for _, q := range which {
ans[q] = nil
}
} else {
val, err := strconv.ParseFloat(v, 64)
if err != nil {
return nil, fmt.Errorf("%s is not a number", v)
}
for _, q := range which {
ans[q] = val
}
}
}
return ans, nil
}

View File

@ -0,0 +1,30 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package at
import (
"fmt"
"strings"
"kitty/tools/utils"
)
var valid_color_names = map[string]bool{"active_fg": true, "active_bg": true, "inactive_fg": true, "inactive_bg": true}
func parse_tab_colors(args []string) (map[string]interface{}, error) {
ans := make(map[string]interface{}, len(args))
for _, arg := range args {
key, val, found := utils.Cut(strings.ToLower(arg), "=")
if !found {
return nil, fmt.Errorf("%s is not a valid setting", arg)
}
if !valid_color_names[key] {
return nil, fmt.Errorf("%s is not a valid color name", key)
}
err := set_color_in_color_map(key, val, ans, false, false)
if err != nil {
return nil, err
}
}
return ans, nil
}

View File

@ -49,6 +49,6 @@ func read_window_logo(path string) (func(io_data *rc_io_data) (bool, error), err
return false, err return false, err
} }
buf = buf[:n] buf = buf[:n]
return n == 0, nil return false, nil
}, nil }, nil
} }

View File

@ -70,6 +70,10 @@ func (self *RGBA) parse_rgb_strings(r string, g string, b string) bool {
return true return true
} }
func (self *RGBA) AsRGB() uint32 {
return uint32(self.Blue) | (uint32(self.Green) << 8) | (uint32(self.Red) << 16)
}
type color_type struct { type color_type struct {
is_numbered bool is_numbered bool
val RGBA val RGBA