From 6f4968305ae5d6e0fcb11dcfd870090bdc4a501f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 30 Aug 2022 15:49:26 +0530 Subject: [PATCH] more work on porting rc command parsing to Go --- gen-config.py | 21 +++++++-- gen-go-code.py | 18 ++++---- kitty/rc/base.py | 10 ++++ kitty/rc/send_text.py | 2 +- kitty/rc/set_background_image.py | 5 +- kitty/rc/set_background_opacity.py | 3 +- kitty/rc/set_colors.py | 3 +- kitty/rc/set_enabled_layouts.py | 4 +- kitty/rc/set_font_size.py | 3 +- kitty/rc/set_spacing.py | 2 +- kitty/rc/set_tab_color.py | 2 +- kitty/rc/set_tab_title.py | 2 +- tools/cmd/at/send_text.go | 60 ++++++++++++++++++++++++ tools/cmd/at/set_colors.go | 74 ++++++++++++++++++++++++++++++ tools/cmd/at/set_font_size.go | 20 ++++++++ tools/cmd/at/set_spacing.go | 53 +++++++++++++++++++++ tools/cmd/at/set_tab_color.go | 30 ++++++++++++ tools/cmd/at/set_window_logo.go | 2 +- tools/utils/style/wrapper.go | 4 ++ 19 files changed, 291 insertions(+), 27 deletions(-) create mode 100644 tools/cmd/at/send_text.go create mode 100644 tools/cmd/at/set_colors.go create mode 100644 tools/cmd/at/set_font_size.go create mode 100644 tools/cmd/at/set_spacing.go create mode 100644 tools/cmd/at/set_tab_color.go diff --git a/gen-config.py b/gen-config.py index d87a75d71..8cc3e4350 100755 --- a/gen-config.py +++ b/gen-config.py @@ -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() - 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', - raw, flags=re.DOTALL | re.MULTILINE) + 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},', 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 diff --git a/gen-go-code.py b/gen-go-code.py index 8af3ba29f..a0bcd3971 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -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: diff --git a/kitty/rc/base.py b/kitty/rc/base.py index 801a3042e..5cadd0752 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -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 }' diff --git a/kitty/rc/send_text.py b/kitty/rc/send_text.py index 974e57aee..a2b07d52f 100644 --- a/kitty/rc/send_text.py +++ b/kitty/rc/send_text.py @@ -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 diff --git a/kitty/rc/set_background_image.py b/kitty/rc/set_background_image.py index 25ff18bea..e91af9932 100644 --- a/kitty/rc/set_background_image.py +++ b/kitty/rc/set_background_image.py @@ -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 diff --git a/kitty/rc/set_background_opacity.py b/kitty/rc/set_background_opacity.py index 806ea0f20..dfabf9f4d 100644 --- a/kitty/rc/set_background_opacity.py +++ b/kitty/rc/set_background_opacity.py @@ -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)) diff --git a/kitty/rc/set_colors.py b/kitty/rc/set_colors.py index 63fa7f456..7115d1fb0 100644 --- a/kitty/rc/set_colors.py +++ b/kitty/rc/set_colors.py @@ -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]] = {} diff --git a/kitty/rc/set_enabled_layouts.py b/kitty/rc/set_enabled_layouts.py index 61938f472..272122702 100644 --- a/kitty/rc/set_enabled_layouts.py +++ b/kitty/rc/set_enabled_layouts.py @@ -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: diff --git a/kitty/rc/set_font_size.py b/kitty/rc/set_font_size.py index 9c680621f..6b41f35e3 100644 --- a/kitty/rc/set_font_size.py +++ b/kitty/rc/set_font_size.py @@ -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 diff --git a/kitty/rc/set_spacing.py b/kitty/rc/set_spacing.py index 60c1cd882..4573adc71 100644 --- a/kitty/rc/set_spacing.py +++ b/kitty/rc/set_spacing.py @@ -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: diff --git a/kitty/rc/set_tab_color.py b/kitty/rc/set_tab_color.py index cb5cb0bc0..5b7ace8bf 100644 --- a/kitty/rc/set_tab_color.py +++ b/kitty/rc/set_tab_color.py @@ -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: diff --git a/kitty/rc/set_tab_title.py b/kitty/rc/set_tab_title.py index b4efa28c1..82ff2eda0 100644 --- a/kitty/rc/set_tab_title.py +++ b/kitty/rc/set_tab_title.py @@ -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} diff --git a/tools/cmd/at/send_text.go b/tools/cmd/at/send_text.go new file mode 100644 index 000000000..afbdea247 --- /dev/null +++ b/tools/cmd/at/send_text.go @@ -0,0 +1,60 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +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 +} diff --git a/tools/cmd/at/set_colors.go b/tools/cmd/at/set_colors.go new file mode 100644 index 000000000..318802db3 --- /dev/null +++ b/tools/cmd/at/set_colors.go @@ -0,0 +1,74 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +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 +} diff --git a/tools/cmd/at/set_font_size.go b/tools/cmd/at/set_font_size.go new file mode 100644 index 000000000..57a794fd9 --- /dev/null +++ b/tools/cmd/at/set_font_size.go @@ -0,0 +1,20 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +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 +} diff --git a/tools/cmd/at/set_spacing.go b/tools/cmd/at/set_spacing.go new file mode 100644 index 000000000..d591a600a --- /dev/null +++ b/tools/cmd/at/set_spacing.go @@ -0,0 +1,53 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +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 +} diff --git a/tools/cmd/at/set_tab_color.go b/tools/cmd/at/set_tab_color.go new file mode 100644 index 000000000..96952b08b --- /dev/null +++ b/tools/cmd/at/set_tab_color.go @@ -0,0 +1,30 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +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 +} diff --git a/tools/cmd/at/set_window_logo.go b/tools/cmd/at/set_window_logo.go index a753ff53b..7e35a0448 100644 --- a/tools/cmd/at/set_window_logo.go +++ b/tools/cmd/at/set_window_logo.go @@ -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 } diff --git a/tools/utils/style/wrapper.go b/tools/utils/style/wrapper.go index 31c665901..c886ad6ee 100644 --- a/tools/utils/style/wrapper.go +++ b/tools/utils/style/wrapper.go @@ -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