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 subprocess
from typing import List
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:
with open(path, 'r+') as f:
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(
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)
if nraw != raw:
f.seek(0)
f.truncate()
f.write(nraw)
f.flush()
if path.endswith('.go'):
subprocess.check_call(['gofmt', '-w', path])
def main() -> None:
@ -34,6 +46,7 @@ def main() -> None:
elif opt.parser_func.__name__ in ('to_color', 'titlebar_color', 'macos_titlebar_color'):
all_colors.append(opt.name)
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)
from kittens.diff.options.definition import definition as kd

View File

@ -6,7 +6,7 @@ import json
import os
import sys
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
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
jd.append(f.go_declaration())
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:
if field.field in option_map:
o = option_map[field.field]
jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}')
elif field.field in handled_fields:
pass
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)
print('TODO: test set_window_logo, set_window_background, set_font_size, send_text, env, scroll_window', file=sys.stderr)
argspec = cmd.args.spec
if argspec:

View File

@ -172,6 +172,7 @@ class ArgsHandling:
minimum_count: int = -1
first_rest: Optional[Tuple[str, str]] = None
special_parse: str = ''
args_choices: Optional[Callable[[], Iterable[str]]] = None
@property
def args_count(self) -> Optional[int]:
@ -193,6 +194,11 @@ class ArgsHandling:
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.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:
jf = self.json_field
dest = f'payload.{jf.capitalize()}'
@ -207,6 +213,10 @@ class ArgsHandling:
if self.special_parse:
if self.special_parse.startswith('!'):
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:
yield f'{dest}, err = {self.special_parse}'
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.
'''
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:
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
failed, the command will exit with a success code.
''' + '\n\n' + MATCH_WINDOW_OPTION
argspec = 'PATH_TO_PNG_IMAGE'
args_count = 1
args_completion = {'files': ('PNG Images', ('*.png',))}
args = RemoteCommand.Args(spec='PATH_TO_PNG_IMAGE', count=1, special_parse='!read_window_logo(args[0])', completion={
'files': ('PNG Images', ('*.png',))})
images_in_flight: Dict[str, IO[bytes]] = {}
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.
''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t')
argspec = 'OPACITY'
args_count = 1
args = RemoteCommand.Args(spec='OPACITY', count=1, json_field='opacity')
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
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
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')
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:
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
as well.
'''
argspec = 'LAYOUTS'
args_completion = {'names': ('Layouts', layout_names)}
args = RemoteCommand.Args(
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:
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'
' amount.'
)
argspec = 'FONT_SIZE'
args_count = 1
args = RemoteCommand.Args(spec='FONT_SIZE', count=1, special_parse='+increment_op:parse_set_font_size(args[0], io_data)', json_field='size')
options_spec = '''\
--all -a
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
windows).
''' + '\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:
if not args:

View File

@ -57,7 +57,7 @@ the keyword NONE to revert to using the default colors.
type=bool-set
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:
try:

View File

@ -28,7 +28,7 @@ class SetTabTitle(RemoteCommand):
' title of the currently active window in the tab is used.'
)
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:
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
}
buf = buf[:n]
return n == 0, nil
return false, nil
}, nil
}

View File

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