kitty/gen-go-code.py

298 lines
11 KiB
Python
Raw Normal View History

#!./kitty/launcher/kitty +launch
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
2022-08-24 12:41:08 +03:00
import json
2022-08-26 08:22:10 +03:00
import io
import os
2022-08-26 08:32:24 +03:00
from contextlib import contextmanager, suppress
2022-08-26 08:22:10 +03:00
from typing import Dict, Iterator, List, Tuple, Union
2022-08-17 07:04:03 +03:00
import kitty.constants as kc
2022-08-23 16:23:55 +03:00
from kittens.tui.operations import Mode
from kitty.rgb import color_names
from kitty.cli import OptionDict, OptionSpecSeq, parse_option_spec
from kitty.options.types import Options
from kitty.key_encoding import config_mod_map
2022-08-24 12:41:08 +03:00
from kitty.key_names import (
character_key_name_aliases, functional_key_name_aliases
)
from kitty.rc.base import RemoteCommand, all_command_names, command_for_name
2022-08-26 08:22:10 +03:00
changed: List[str] = []
2022-08-26 08:32:24 +03:00
# Utils {{{
def serialize_as_go_string(x: str) -> str:
return x.replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
2022-08-26 08:32:24 +03:00
def serialize_go_dict(x: Union[Dict[str, int], Dict[int, str], Dict[int, int], Dict[str, str]]) -> str:
ans = []
def s(x: Union[int, str]) -> str:
if isinstance(x, int):
return str(x)
return f'"{serialize_as_go_string(x)}"'
for k, v in x.items():
ans.append(f'{s(k)}: {s(v)}')
return '{' + ', '.join(ans) + '}'
2022-08-17 07:04:03 +03:00
def replace(template: str, **kw: str) -> str:
for k, v in kw.items():
template = template.replace(k, v)
return template
2022-08-26 08:32:24 +03:00
# }}}
2022-08-17 07:04:03 +03:00
2022-08-17 17:20:01 +03:00
go_type_map = {'bool-set': 'bool', 'bool-reset': 'bool', 'int': 'int', 'float': 'float64', '': 'string', 'list': '[]string', 'choices': 'string'}
go_getter_map = {
'bool-set': 'GetBool', 'bool-reset': 'GetBool', 'int': 'GetInt', 'float': 'GetFloat64', '': 'GetString',
'list': 'GetStringArray', 'choices': 'GetString'
}
2022-08-17 14:03:41 +03:00
class Option:
def __init__(self, cmd_name: str, x: OptionDict) -> None:
self.cmd_name = cmd_name
flags = sorted(x['aliases'], key=len)
short = ''
2022-08-17 14:03:41 +03:00
self.aliases = []
if len(flags) > 1 and not flags[0].startswith("--"):
short = flags[0][1:]
2022-08-17 14:03:41 +03:00
del flags[0]
self.short, self.long = short, x['name'].replace('_', '-')
for f in flags:
q = f[2:]
if q != self.long:
self.aliases.append(q)
self.usage = serialize_as_go_string(x['help'].strip())
self.type = x['type']
2022-08-17 11:03:24 +03:00
self.dest = x['dest']
self.default = x['default']
self.obj_dict = x
2022-08-17 14:03:41 +03:00
self.go_type = go_type_map[self.type]
self.go_var_name = self.long.replace('-', '_')
if self.go_var_name == 'type':
self.go_var_name += '_'
def to_flag_definition(self, base: str = 'ans.Flags()') -> str:
2022-08-17 17:20:01 +03:00
if self.type.startswith('bool-'):
defval = 'false' if self.type == 'bool-set' else 'true'
if self.short:
2022-08-17 17:20:01 +03:00
return f'{base}.BoolP("{self.long}", "{self.short}", {defval}, "{self.usage}")'
return f'{base}.Bool("{self.long}", {defval}, "{self.usage}")'
elif not self.type:
defval = f'''"{serialize_as_go_string(self.default or '')}"'''
if self.short:
return f'{base}.StringP("{self.long}", "{self.short}", {defval}, "{self.usage}")'
return f'{base}.String("{self.long}", {defval}, "{self.usage}")'
elif self.type == 'int':
if self.short:
return f'{base}.IntP("{self.long}", "{self.short}", {self.default or 0}, "{self.usage}")'
return f'{base}.Int("{self.long}", {self.default or 0}, "{self.usage}")'
elif self.type == 'float':
if self.short:
return f'{base}.Float64P("{self.long}", "{self.short}", {self.default or 0}, "{self.usage}")'
return f'{base}.Float64("{self.long}", {self.default or 0}, "{self.usage}")'
elif self.type == 'list':
defval = f'[]string{{"{serialize_as_go_string(self.default)}"}}' if self.default else '[]string{}'
if self.short:
return f'{base}.StringArrayP("{self.long}", "{self.short}", {defval}, "{self.usage}")'
return f'{base}.StringArray("{self.long}", {defval}, "{self.usage}")'
elif self.type == 'choices':
choices = sorted(self.obj_dict['choices'])
choices.remove(self.default or '')
choices.insert(0, self.default or '')
cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in choices)
if self.short:
return f'cli.ChoicesP({base}, "{self.long}", "{self.short}", "{self.usage}", {cx})'
return f'cli.Choices({base}, "{self.long}", "{self.usage}", {cx})'
else:
raise TypeError(f'Unknown type of CLI option: {self.type} for {self.long}')
def set_flag_value(self, cmd: str = 'cmd') -> str:
2022-08-17 17:20:01 +03:00
func = go_getter_map[self.type]
ans = f'{self.go_var_name}_temp, err := {cmd}.Flags().{func}("{self.long}")\n if err != nil {{ return err }}'
ans += f'\noptions_{self.cmd_name}.{self.go_var_name} = {self.go_var_name}_temp'
return ans
json_field_types: Dict[str, str] = {
'bool': 'bool', 'str': 'string', 'list.str': '[]string', 'dict.str': 'map[string]string', 'float': 'float64', 'int': 'int',
'scroll_amount': '[2]interface{}', 'spacing': 'interface{}', 'colors': 'interface{}',
}
def go_field_type(json_field_type: str) -> str:
q = json_field_types.get(json_field_type)
if q:
return q
if json_field_type.startswith('choices.'):
return 'string'
if '.' in json_field_type:
p, r = json_field_type.split('.', 1)
p = {'list': '[]', 'dict': 'map[string]'}[p]
return p + go_field_type(r)
raise TypeError(f'Unknown JSON field type: {json_field_type}')
class JSONField:
def __init__(self, line: str) -> None:
field_def = line.split(':', 1)[0]
self.required = False
self.field, self.field_type = field_def.split('/', 1)
if self.field.endswith('+'):
self.required = True
self.field = self.field[:-1]
self.struct_field_name = self.field[0].upper() + self.field[1:]
def go_declaration(self) -> str:
return self.struct_field_name + ' ' + go_field_type(self.field_type) + f'`json:"{self.field},omitempty"`'
2022-08-17 14:03:41 +03:00
def render_alias_map(alias_map: Dict[str, Tuple[str, ...]]) -> str:
if not alias_map:
return ''
amap = 'switch name {\n'
for name, aliases in alias_map.items():
for alias in aliases:
amap += f'\ncase "{alias}":\nname = "{name}"\n'
amap += '}'
return amap
2022-08-17 07:04:03 +03:00
def build_go_code(name: str, cmd: RemoteCommand, seq: OptionSpecSeq, template: str) -> str:
template = '\n' + template[len('//go:build exclude'):]
2022-08-17 07:04:03 +03:00
NO_RESPONSE_BASE = 'true' if cmd.no_response else 'false'
af: List[str] = []
a = af.append
2022-08-17 14:03:41 +03:00
alias_map = {}
od: List[str] = []
ov: List[str] = []
2022-08-17 07:04:03 +03:00
for x in seq:
if isinstance(x, str):
continue
o = Option(name, x)
2022-08-17 14:03:41 +03:00
if o.aliases:
alias_map[o.long] = tuple(o.aliases)
a(o.to_flag_definition())
2022-08-18 07:41:17 +03:00
if o.dest in ('no_response', 'response_timeout'):
2022-08-17 11:03:24 +03:00
continue
od.append(f'{o.go_var_name} {o.go_type}')
ov.append(o.set_flag_value())
jd: List[str] = []
for line in cmd.protocol_spec.splitlines():
line = line.strip()
if ':' not in line:
continue
f = JSONField(line)
jd.append(f.go_declaration())
2022-08-17 14:03:41 +03:00
2022-08-17 07:04:03 +03:00
ans = replace(
template,
CMD_NAME=name, __FILE__=__file__, CLI_NAME=name.replace('_', '-'),
SHORT_DESC=serialize_as_go_string(cmd.short_desc),
LONG_DESC=serialize_as_go_string(cmd.desc.strip()),
2022-08-25 15:46:03 +03:00
IS_ASYNC='true' if cmd.is_asynchronous else 'false',
2022-08-17 11:03:24 +03:00
NO_RESPONSE_BASE=NO_RESPONSE_BASE, ADD_FLAGS_CODE='\n'.join(af),
WAIT_TIMEOUT=str(cmd.response_timeout),
ALIAS_NORMALIZE_CODE=render_alias_map(alias_map),
OPTIONS_DECLARATION_CODE='\n'.join(od),
SET_OPTION_VALUES_CODE='\n'.join(ov),
JSON_DECLARATION_CODE='\n'.join(jd),
2022-08-22 12:41:00 +03:00
STRING_RESPONSE_IS_ERROR='true' if cmd.string_return_is_error else 'false',
2022-08-17 11:03:24 +03:00
)
return ans
2022-08-26 08:32:24 +03:00
# Constants {{{
def generate_color_names() -> str:
return 'package style\n\nvar ColorNames = map[string]RGBA{' + '\n'.join(
f'\t"{name}": RGBA{{ Red:{val.red}, Green:{val.green}, Blue:{val.blue} }},'
for name, val in color_names.items()
) + '\n}' + '\n\nvar ColorTable = [256]uint32{' + ', '.join(
f'{x}' for x in Options.color_table) + '}\n'
2022-08-24 12:41:08 +03:00
def load_ref_map() -> Dict[str, Dict[str, str]]:
with open('kitty/docs_ref_map_generated.h') as f:
raw = f.read()
raw = raw.split('{', 1)[1].split('}', 1)[0]
data = json.loads(bytes(bytearray(json.loads(f'[{raw}]'))))
return data # type: ignore
2022-08-26 08:22:10 +03:00
def generate_constants() -> str:
2022-08-24 12:41:08 +03:00
ref_map = load_ref_map()
2022-08-26 08:22:10 +03:00
dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help))
return f'''\
// auto-generated by {__file__} do no edit
package kitty
type VersionType struct {{
Major, Minor, Patch int
}}
2022-08-23 16:23:55 +03:00
const VersionString string = "{kc.str_version}"
const WebsiteBaseURL string = "{kc.website_base_url}"
const VCSRevision string = ""
const RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}"
const IsFrozenBuild bool = false
const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]}
var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}}
var DefaultPager []string = []string{{ {dp} }}
var FunctionalKeyNameAliases = map[string]string{serialize_go_dict(functional_key_name_aliases)}
var CharacterKeyNameAliases = map[string]string{serialize_go_dict(character_key_name_aliases)}
var ConfigModMap = map[string]uint16{serialize_go_dict(config_mod_map)}
2022-08-24 12:41:08 +03:00
var RefMap = map[string]string{serialize_go_dict(ref_map['ref'])}
var DocTitleMap = map[string]string{serialize_go_dict(ref_map['doc'])}
2022-08-26 08:32:24 +03:00
''' # }}}
2022-08-26 08:22:10 +03:00
2022-08-26 08:32:24 +03:00
# Boilerplate {{{
2022-08-26 08:22:10 +03:00
@contextmanager
def replace_if_needed(path: str) -> Iterator[io.StringIO]:
buf = io.StringIO()
yield buf
2022-08-26 08:32:24 +03:00
orig = ''
with suppress(FileNotFoundError), open(path, 'r') as f:
2022-08-26 08:22:10 +03:00
orig = f.read()
2022-08-26 08:32:24 +03:00
new = buf.getvalue()
2022-08-26 08:22:10 +03:00
if orig != new:
changed.append(path)
with open(path, 'w') as f:
f.write(new)
def update_at_commands() -> None:
with open('tools/cmd/at/template.go') as f:
template = f.read()
for name in all_command_names():
cmd = command_for_name(name)
2022-08-17 14:03:41 +03:00
opts = parse_option_spec(cmd.options_spec or '\n\n')[0]
code = build_go_code(name, cmd, opts, template)
2022-08-25 05:33:23 +03:00
dest = f'tools/cmd/at/cmd_{name}_generated.go'
if os.path.exists(dest):
os.remove(dest)
with open(dest, 'w') as f:
f.write(code)
2022-08-26 08:22:10 +03:00
def main() -> None:
with replace_if_needed('constants_generated.go') as f:
f.write(generate_constants())
with replace_if_needed('tools/utils/style/color-names_generated.go') as f:
f.write(generate_color_names())
2022-08-26 08:22:10 +03:00
update_at_commands()
print(json.dumps(changed, indent=2))
if __name__ == '__main__':
2022-08-26 08:32:24 +03:00
main() # }}}