Add support for more option types to Go conf file parsing

This commit is contained in:
Kovid Goyal 2023-03-15 15:17:38 +05:30
parent 3803d7e3c2
commit da38cb3254
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 245 additions and 4 deletions

View File

@ -442,8 +442,15 @@ def write_output(loc: str, defn: Definition) -> None:
f.write(f'{c}\n') 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:
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)' return f'*{ctype}', f'Parse{ctype}(val)'
p = parser_func.__name__ p = parser_func.__name__
if p == 'int': 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)' return 'float64', 'strconv.ParseFloat(val, 10, 64)'
if p == 'to_bool': if p == 'to_bool':
return 'bool', 'config.StringToBool(val), nil' 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) th = get_type_hints(parser_func)
rettype = th['return'] rettype = th['return']
return {int: 'int64', str: 'string', float: 'float64'}[rettype], f'{p}(val)' return {int: 'int64', str: 'string', float: 'float64'}[rettype], f'{p}(val)'
def gen_go_code(defn: Definition) -> str: def gen_go_code(defn: Definition) -> str:
lines = ['import "fmt"', 'import "strconv"', 'import "kitty/tools/config"', lines = ['import "fmt"', 'import "strconv"', 'import "kitty/tools/config"', 'import "kitty/tools/utils/style"',
'var _ = fmt.Println', 'var _ = config.StringToBool', 'var _ = strconv.Atoi'] 'var _ = fmt.Println', 'var _ = config.StringToBool', 'var _ = strconv.Atoi', 'var _ = style.ParseColor']
a = lines.append a = lines.append
choices = {} choices = {}
go_types = {} 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)): for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)):
name = option.name.capitalize() name = option.name.capitalize()
if isinstance(option, MultiOption): 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) multiopts.add(name)
else: else:
defaults[name] = option.parser_func(option.defval_as_string) 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('func NewConfig() *Config {')
a('return &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(): for name, pname in go_parsers.items():
if name in multiopts: if name in multiopts:
continue continue
@ -507,6 +528,15 @@ def gen_go_code(defn: Definition) -> str:
dval = f'{name}_{cval(d)}' if name in choices else f'`{d}`' dval = f'{name}_{cval(d)}' if name in choices else f'`{d}`'
elif isinstance(d, bool): elif isinstance(d, bool):
dval = repr(d).lower() 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: else:
dval = repr(d) dval = repr(d)
a(f'{name}: {dval},') a(f'{name}: {dval},')

185
tools/config/utils.go Normal file
View File

@ -0,0 +1,185 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
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
}

View File

@ -0,0 +1,26 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
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)
}
}
}