diff --git a/kitty/conf/generate.py b/kitty/conf/generate.py index 044b8fd09..0f4661110 100644 --- a/kitty/conf/generate.py +++ b/kitty/conf/generate.py @@ -442,8 +442,15 @@ def write_output(loc: str, defn: Definition) -> None: f.write(f'{c}\n') -def go_type_data(parser_func: ParserFuncType, ctype: str) -> Tuple[str, str]: +def go_type_data(parser_func: ParserFuncType, ctype: str, is_multiple: bool = False) -> Tuple[str, str]: if ctype: + if ctype == 'string': + if is_multiple: + return 'string', '[]string{val}, nil' + return 'string', 'val, nil' + if ctype.startswith('strdict_'): + _, rsep, fsep = ctype.split('_', 2) + return 'map[string]string', f'config.ParseStrDict(val, `{rsep}`, `{fsep}`)' return f'*{ctype}', f'Parse{ctype}(val)' p = parser_func.__name__ if p == 'int': @@ -454,14 +461,26 @@ def go_type_data(parser_func: ParserFuncType, ctype: str) -> Tuple[str, str]: return 'float64', 'strconv.ParseFloat(val, 10, 64)' if p == 'to_bool': return 'bool', 'config.StringToBool(val), nil' + if p == 'to_color': + return 'style.RGBA', 'style.ParseColor(val)' + if p == 'to_color_or_none': + return 'style.NullableColor', 'style.ParseColorOrNone(val)' + if p == 'positive_int': + return 'uint64', 'strconv.ParseUint(val, 10, 64)' + if p == 'positive_float': + return 'float64', 'config.PositiveFloat(val, 10, 64)' + if p == 'unit_float': + return 'float64', 'config.UnitFloat(val, 10, 64)' + if p == 'python_string': + return 'string', 'config.StringLiteral(val, 10, 64)' th = get_type_hints(parser_func) rettype = th['return'] return {int: 'int64', str: 'string', float: 'float64'}[rettype], f'{p}(val)' def gen_go_code(defn: Definition) -> str: - lines = ['import "fmt"', 'import "strconv"', 'import "kitty/tools/config"', - 'var _ = fmt.Println', 'var _ = config.StringToBool', 'var _ = strconv.Atoi'] + lines = ['import "fmt"', 'import "strconv"', 'import "kitty/tools/config"', 'import "kitty/tools/utils/style"', + 'var _ = fmt.Println', 'var _ = config.StringToBool', 'var _ = strconv.Atoi', 'var _ = style.ParseColor'] a = lines.append choices = {} go_types = {} @@ -471,7 +490,7 @@ def gen_go_code(defn: Definition) -> str: for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)): name = option.name.capitalize() if isinstance(option, MultiOption): - go_types[name], go_parsers[name] = go_type_data(option.parser_func, option.ctype) + go_types[name], go_parsers[name] = go_type_data(option.parser_func, option.ctype, True) multiopts.add(name) else: defaults[name] = option.parser_func(option.defval_as_string) @@ -497,6 +516,8 @@ def gen_go_code(defn: Definition) -> str: a('func NewConfig() *Config {') a('return &Config{') + from kitty.cli import serialize_as_go_string + from kitty.fast_data_types import Color for name, pname in go_parsers.items(): if name in multiopts: continue @@ -507,6 +528,15 @@ def gen_go_code(defn: Definition) -> str: dval = f'{name}_{cval(d)}' if name in choices else f'`{d}`' elif isinstance(d, bool): dval = repr(d).lower() + elif isinstance(d, dict): + dval = 'map[string]string{' + for k, v in d.items(): + dval += f'"{serialize_as_go_string(k)}": "{serialize_as_go_string(v)}",' + dval += '}' + elif isinstance(d, Color): + dval = f'style.RGBA{{Red:{d.red}, Green: {d.green}, Blue: {d.blue}}}' + if 'NullableColor' in go_types[name]: + dval = f'style.NullableColor{{Color:{dval}}}' else: dval = repr(d) a(f'{name}: {dval},') diff --git a/tools/config/utils.go b/tools/config/utils.go new file mode 100644 index 000000000..b8e9c7a94 --- /dev/null +++ b/tools/config/utils.go @@ -0,0 +1,185 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package config + +import ( + "fmt" + "kitty/tools/utils" + "strconv" + "strings" +) + +var _ = fmt.Print + +func ParseStrDict(val, record_sep, field_sep string) (map[string]string, error) { + ans := make(map[string]string) + for _, record := range strings.Split(val, record_sep) { + key, val, found := strings.Cut(record, field_sep) + if found { + ans[key] = val + } + } + return ans, nil +} + +func PositiveFloat(val string) (ans float64, err error) { + ans, err = strconv.ParseFloat(val, 64) + if err == nil { + ans = utils.Max(0, ans) + } + return +} + +func UnitFloat(val string) (ans float64, err error) { + ans, err = strconv.ParseFloat(val, 64) + if err == nil { + ans = utils.Max(0, utils.Min(ans, 1)) + } + return +} + +func StringLiteral(val string) (string, error) { + ans := strings.Builder{} + ans.Grow(len(val)) + var buf [8]rune + bufcount := 0 + buflimit := 0 + var prefix rune + type State int + const ( + normal State = iota + backslash + octal + hex + ) + var state State + decode := func(base int) { + text := string(buf[:bufcount]) + num, _ := strconv.ParseUint(text, base, 32) + ans.WriteRune(rune(num)) + state = normal + bufcount = 0 + buflimit = 0 + prefix = 0 + } + + write_invalid_buf := func() { + ans.WriteByte('\\') + ans.WriteRune(prefix) + for _, r := range buf[:bufcount] { + ans.WriteRune(r) + } + state = normal + bufcount = 0 + buflimit = 0 + prefix = 0 + } + + var dispatch_ch_recurse func(rune) + + dispatch_ch := func(ch rune) { + switch state { + case normal: + switch ch { + case '\\': + state = backslash + default: + ans.WriteRune(ch) + } + case octal: + switch ch { + case '0', '1', '2', '3', '4', '5', '6', '7': + if bufcount >= buflimit { + decode(8) + dispatch_ch_recurse(ch) + } else { + buf[bufcount] = ch + bufcount++ + } + default: + decode(8) + dispatch_ch_recurse(ch) + } + case hex: + switch ch { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'E', 'f', 'F': + buf[bufcount] = ch + bufcount++ + if bufcount >= buflimit { + decode(16) + } + default: + write_invalid_buf() + dispatch_ch_recurse(ch) + } + case backslash: + switch ch { + case '\n': + case '\\': + ans.WriteRune('\\') + state = normal + case '\'', '"': + ans.WriteRune(ch) + state = normal + case 'a': + ans.WriteRune('\a') + state = normal + case 'b': + ans.WriteRune('\b') + state = normal + case 'f': + ans.WriteRune('\f') + state = normal + case 'n': + ans.WriteRune('\n') + state = normal + case 'r': + ans.WriteRune('\r') + state = normal + case 't': + ans.WriteRune('\t') + state = normal + case 'v': + ans.WriteRune('\v') + state = normal + case '0', '1', '2', '3', '4', '5', '6', '7': + buf[0] = ch + bufcount = 1 + buflimit = 3 + state = octal + case 'x': + bufcount = 0 + buflimit = 2 + state = hex + prefix = ch + case 'u': + bufcount = 0 + buflimit = 4 + state = hex + prefix = ch + case 'U': + bufcount = 0 + buflimit = 8 + state = hex + prefix = ch + default: + ans.WriteByte('\\') + ans.WriteRune(ch) + state = normal + } + } + } + dispatch_ch_recurse = dispatch_ch + for _, ch := range val { + dispatch_ch(ch) + } + switch state { + case octal: + decode(8) + case hex: + write_invalid_buf() + case backslash: + ans.WriteRune('\\') + } + return ans.String(), nil +} diff --git a/tools/config/utils_test.go b/tools/config/utils_test.go new file mode 100644 index 000000000..11288f913 --- /dev/null +++ b/tools/config/utils_test.go @@ -0,0 +1,26 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package config + +import ( + "fmt" + "testing" +) + +var _ = fmt.Print + +func TestStringLiteralParsing(t *testing.T) { + for q, expected := range map[string]string{ + `abc`: `abc`, + `a\nb\M`: "a\nb\\M", + `a\x20\x1\u1234\123\12|`: "a \\x1\u1234\123\x0a|", + } { + actual, err := StringLiteral(q) + if err != nil { + t.Fatal(err) + } + if expected != actual { + t.Fatalf("Failed with input: %#v\n%#v != %#v", q, expected, actual) + } + } +}