kitty/gen-go-code.py

483 lines
17 KiB
Python
Raw Normal View History

#!./kitty/launcher/kitty +launch
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
2022-08-26 08:22:10 +03:00
import io
import json
import os
import subprocess
2022-09-07 08:25:07 +03:00
import sys
2022-08-26 08:32:24 +03:00
from contextlib import contextmanager, suppress
from functools import lru_cache
from typing import Dict, Iterator, List, Set, 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
2022-11-16 10:18:13 +03:00
from kittens.tui.spinners import spinners
from kitty.cli import (
2022-09-17 10:02:02 +03:00
CompletionSpec, GoOption, go_options_for_seq, parse_option_spec,
2022-09-25 09:53:52 +03:00
serialize_as_go_string,
)
from kitty.key_encoding import config_mod_map
2022-09-25 09:53:52 +03:00
from kitty.key_names import character_key_name_aliases, functional_key_name_aliases
from kitty.options.types import Options
2022-08-24 12:41:08 +03:00
from kitty.rc.base import RemoteCommand, all_command_names, command_for_name
2022-09-25 09:53:52 +03:00
from kitty.remote_control import global_options_spec
from kitty.rgb import color_names
2022-08-26 08:22:10 +03:00
changed: List[str] = []
2022-08-26 08:32:24 +03:00
# Utils {{{
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
# Completions {{{
2022-09-17 09:29:41 +03:00
def generate_kittens_completion() -> None:
2022-09-25 09:53:52 +03:00
from kittens.runner import (
all_kitten_names, get_kitten_cli_docs, get_kitten_wrapper_of,
)
for kitten in sorted(all_kitten_names()):
2022-09-17 09:29:41 +03:00
kn = 'kitten_' + kitten
print(f'{kn} := plus_kitten.AddSubCommand(&cli.Command{{Name:"{kitten}", Group: "Kittens"}})')
wof = get_kitten_wrapper_of(kitten)
if wof:
print(f'{kn}.ArgCompleter = cli.CompletionForWrapper("{serialize_as_go_string(wof)}")')
2022-09-26 09:24:57 +03:00
print(f'{kn}.OnlyArgsAllowed = true')
continue
2022-09-17 09:29:41 +03:00
kcd = get_kitten_cli_docs(kitten)
if kcd:
ospec = kcd['options']
for opt in go_options_for_seq(parse_option_spec(ospec())[0]):
print(opt.as_option(kn))
2022-09-17 09:29:41 +03:00
ac = kcd.get('args_completion')
if ac is not None:
print(''.join(ac.as_go_code(kn + '.ArgCompleter', ' = ')))
2022-09-17 09:29:41 +03:00
else:
print(f'{kn}.HelpText = ""')
2022-09-17 09:29:41 +03:00
2022-09-17 10:02:02 +03:00
def completion_for_launch_wrappers(*names: str) -> None:
from kitty.launch import clone_safe_opts, options_spec
opts = tuple(go_options_for_seq(parse_option_spec(options_spec())[0]))
allowed = clone_safe_opts()
for o in opts:
2022-09-25 09:53:52 +03:00
if o.obj_dict['name'] in allowed:
2022-09-17 10:02:02 +03:00
for name in names:
print(o.as_option(name))
2022-09-17 10:02:02 +03:00
2022-09-07 08:25:07 +03:00
def generate_completions_for_kitty() -> None:
from kitty.config import option_names_for_completion
2022-09-07 08:25:07 +03:00
print('package completion\n')
print('import "kitty/tools/cli"')
2022-09-30 11:30:01 +03:00
print('import "kitty/tools/cmd/tool"')
print('import "kitty/tools/cmd/at"')
conf_names = ', '.join((f'"{serialize_as_go_string(x)}"' for x in option_names_for_completion()))
print('var kitty_option_names_for_completion = []string{' + conf_names + '}')
print('func kitty(root *cli.Command) {')
2022-09-17 10:02:02 +03:00
# The kitty exe
print('k := root.AddSubCommand(&cli.Command{'
'Name:"kitty", SubCommandIsOptional: true, ArgCompleter: cli.CompleteExecutableFirstArg, SubCommandMustBeFirst: true })')
2022-09-30 08:53:58 +03:00
print('kt := root.AddSubCommand(&cli.Command{Name:"kitty-tool", SubCommandMustBeFirst: true })')
2022-09-30 11:30:01 +03:00
print('tool.KittyToolEntryPoints(kt)')
2022-09-16 11:38:22 +03:00
for opt in go_options_for_seq(parse_option_spec()[0]):
print(opt.as_option('k'))
2022-09-17 10:02:02 +03:00
# kitty +
print('plus := k.AddSubCommand(&cli.Command{Name:"+", Group:"Entry points", ShortDescription: "Various special purpose tools and kittens"})')
print('plus_launch := plus.AddSubCommand(&cli.Command{'
'Name:"launch", Group:"Entry points", ShortDescription: "Launch Python scripts", ArgCompleter: complete_plus_launch})')
print('k.AddClone("", plus_launch).Name = "+launch"')
print('plus_runpy := plus.AddSubCommand(&cli.Command{'
'Name: "runpy", Group:"Entry points", ArgCompleter: complete_plus_runpy, ShortDescription: "Run Python code"})')
print('k.AddClone("", plus_runpy).Name = "+runpy"')
print('plus_open := plus.AddSubCommand(&cli.Command{'
'Name:"open", Group:"Entry points", ArgCompleter: complete_plus_open, ShortDescription: "Open files and URLs"})')
print('for _, og := range k.OptionGroups { plus_open.OptionGroups = append(plus_open.OptionGroups, og.Clone(plus_open)) }')
print('k.AddClone("", plus_open).Name = "+open"')
2022-09-17 10:02:02 +03:00
# kitty +kitten
print('plus_kitten := plus.AddSubCommand(&cli.Command{Name:"kitten", Group:"Kittens", SubCommandMustBeFirst: true})')
2022-09-17 09:29:41 +03:00
generate_kittens_completion()
print('k.AddClone("", plus_kitten).Name = "+kitten"')
2022-09-17 09:29:41 +03:00
# @
print('at.EntryPoint(k)')
2022-09-17 10:02:02 +03:00
# clone-in-kitty, edit-in-kitty
print('cik := root.AddSubCommand(&cli.Command{Name:"clone-in-kitty"})')
print('eik := root.AddSubCommand(&cli.Command{Name:"edit-in-kitty"})')
2022-09-17 10:02:02 +03:00
completion_for_launch_wrappers('cik', 'eik')
print(''.join(CompletionSpec.from_string('type:file mime:text/* group:"Text files"').as_go_code('eik.ArgCompleter', ' = ')))
2022-09-17 10:02:02 +03:00
2022-09-07 08:25:07 +03:00
print('}')
print('func init() {')
print('cli.RegisterExeForCompletion(kitty)')
2022-09-07 08:25:07 +03:00
print('}')
# }}}
2022-09-07 08:25:07 +03:00
# rc command wrappers {{{
json_field_types: Dict[str, str] = {
'bool': 'bool', 'str': 'string', 'list.str': '[]string', 'dict.str': 'map[string]string', 'float': 'float64', 'int': 'int',
'scroll_amount': 'any', 'spacing': 'any', 'colors': 'any',
}
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-09-09 12:40:38 +03:00
def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> str:
template = '\n' + template[len('//go:build exclude'):]
NO_RESPONSE_BASE = 'false'
2022-08-17 07:04:03 +03:00
af: List[str] = []
a = af.append
af.extend(cmd.args.as_go_completion_code('ans'))
od: List[str] = []
option_map: Dict[str, GoOption] = {}
2022-09-09 12:40:38 +03:00
for o in rc_command_options(name):
2022-09-25 09:53:52 +03:00
option_map[o.go_var_name] = o
a(o.as_option('ans'))
if o.go_var_name in ('NoResponse', 'ResponseTimeout'):
2022-08-17 11:03:24 +03:00
continue
2022-09-25 09:53:52 +03:00
od.append(o.struct_declaration())
jd: List[str] = []
json_fields = []
field_types: Dict[str, str] = {}
for line in cmd.protocol_spec.splitlines():
line = line.strip()
if ':' not in line:
continue
f = JSONField(line)
json_fields.append(f)
field_types[f.field] = f.field_type
jd.append(f.go_declaration())
jc: List[str] = []
handled_fields: Set[str] = set()
jc.extend(cmd.args.as_go_code(name, field_types, handled_fields))
2022-08-30 18:35:31 +03:00
unhandled = {}
used_options = set()
for field in json_fields:
2022-08-30 18:35:31 +03:00
oq = (cmd.field_to_option_map or {}).get(field.field, field.field)
2022-09-25 09:53:52 +03:00
oq = ''.join(x.capitalize() for x in oq.split('_'))
2022-08-30 18:35:31 +03:00
if oq in option_map:
o = option_map[oq]
used_options.add(oq)
jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}')
elif field.field in handled_fields:
pass
else:
2022-08-30 18:35:31 +03:00
unhandled[field.field] = field
for x in tuple(unhandled):
2022-09-25 09:53:52 +03:00
if x == 'match_window' and 'Match' in option_map and 'Match' not in used_options:
used_options.add('Match')
o = option_map['Match']
2022-08-30 18:35:31 +03:00
field = unhandled[x]
jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}')
del unhandled[x]
if unhandled:
raise SystemExit(f'Cant map fields: {", ".join(unhandled)} for cmd: {name}')
if name != 'send_text':
2022-09-25 09:53:52 +03:00
unused_options = set(option_map) - used_options - {'NoResponse', 'ResponseTimeout'}
if unused_options:
raise SystemExit(f'Unused options: {", ".join(unused_options)} for command: {name}')
argspec = cmd.args.spec
if argspec:
argspec = ' ' + argspec
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),
OPTIONS_DECLARATION_CODE='\n'.join(od),
JSON_DECLARATION_CODE='\n'.join(jd),
JSON_INIT_CODE='\n'.join(jc), ARGSPEC=argspec,
2022-08-22 12:41:00 +03:00
STRING_RESPONSE_IS_ERROR='true' if cmd.string_return_is_error else 'false',
STREAM_WANTED='true' if cmd.reads_streaming_data else 'false',
2022-08-17 11:03:24 +03:00
)
return ans
# }}}
2022-08-26 08:32:24 +03:00
# Constants {{{
2022-11-16 10:18:13 +03:00
def generate_spinners() -> str:
ans = ['package tui', 'import "time"', 'func NewSpinner(name string) *Spinner {', 'var ans *Spinner', 'switch name {']
a = ans.append
for name, spinner in spinners.items():
a(f'case "{serialize_as_go_string(name)}":')
a('ans = &Spinner{')
a(f'Name: "{serialize_as_go_string(name)}",')
a(f'interval: {spinner["interval"]},')
frames = ', '.join(f'"{serialize_as_go_string(x)}"' for x in spinner['frames'])
a(f'frames: []string{{{frames}}},')
a('}')
a('}')
a('if ans != nil {')
a('ans.interval *= time.Millisecond')
a('ans.current_frame = -1')
a('ans.last_change_at = time.Now().Add(-ans.interval)')
a('}')
a('return ans}')
return '\n'.join(ans)
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'''\
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 IsStandaloneBuild bool = false
2022-08-23 16:23:55 +03:00
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, show_diff: bool = False) -> Iterator[io.StringIO]:
2022-08-26 08:22:10 +03:00
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()
new = f'// Code generated by {os.path.basename(__file__)}; DO NOT EDIT.\n\n' + new
2022-08-26 08:22:10 +03:00
if orig != new:
changed.append(path)
if show_diff:
with open(path + '.new', 'w') as f:
f.write(new)
subprocess.run(['diff', '-Naurp', path, f.name], stdout=open('/dev/tty', 'w'))
os.remove(f.name)
2022-08-26 08:22:10 +03:00
with open(path, 'w') as f:
f.write(new)
2022-09-09 12:40:38 +03:00
@lru_cache(maxsize=256)
def rc_command_options(name: str) -> Tuple[GoOption, ...]:
cmd = command_for_name(name)
return tuple(go_options_for_seq(parse_option_spec(cmd.options_spec or '\n\n')[0]))
2022-08-26 08:22:10 +03:00
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-09-09 12:40:38 +03:00
code = go_code_for_remote_command(name, cmd, template)
2022-08-25 05:33:23 +03:00
dest = f'tools/cmd/at/cmd_{name}_generated.go'
with replace_if_needed(dest) as f:
f.write(code)
2022-09-25 09:53:52 +03:00
struct_def = []
opt_def = []
for o in go_options_for_seq(parse_option_spec(global_options_spec())[0]):
struct_def.append(o.struct_declaration())
opt_def.append(o.as_option(depth=1, group="Global options"))
sdef = '\n'.join(struct_def)
odef = '\n'.join(opt_def)
code = f'''
package at
import "kitty/tools/cli"
type rc_global_options struct {{
{sdef}
}}
var rc_global_opts rc_global_options
func add_rc_global_opts(cmd *cli.Command) {{
{odef}
}}
'''
with replace_if_needed('tools/cmd/at/global_opts_generated.go') as f:
f.write(code)
def update_completion() -> None:
2022-09-07 08:25:07 +03:00
orig = sys.stdout
try:
with replace_if_needed('tools/cmd/completion/kitty_generated.go') as f:
2022-09-07 08:25:07 +03:00
sys.stdout = f
generate_completions_for_kitty()
finally:
sys.stdout = orig
2022-11-08 17:11:03 +03:00
def define_enum(package_name: str, type_name: str, items: str, underlying_type: str = 'uint') -> str:
actions = []
2022-11-08 17:11:03 +03:00
for x in items.splitlines():
x = x.strip()
if x:
actions.append(x)
ans = [f'package {package_name}', 'import "strconv"', f'type {type_name} {underlying_type}', 'const (']
stringer = [f'func (ac {type_name}) String() string ''{', 'switch(ac) {']
for i, ac in enumerate(actions):
stringer.append(f'case {ac}: return "{ac}"')
if i == 0:
ac = ac + f' {type_name} = iota'
ans.append(ac)
ans.append(')')
stringer.append('}\nreturn strconv.Itoa(int(ac)) }')
return '\n'.join(ans + stringer)
def generate_readline_actions() -> str:
return define_enum('readline', 'Action', '''\
ActionNil
ActionBackspace
ActionDelete
ActionMoveToStartOfLine
ActionMoveToEndOfLine
ActionMoveToStartOfDocument
ActionMoveToEndOfDocument
ActionMoveToEndOfWord
ActionMoveToStartOfWord
ActionCursorLeft
ActionCursorRight
ActionEndInput
ActionAcceptInput
ActionCursorUp
ActionHistoryPreviousOrCursorUp
ActionCursorDown
ActionHistoryNextOrCursorDown
ActionHistoryNext
ActionHistoryPrevious
ActionHistoryFirst
ActionHistoryLast
ActionHistoryIncrementalSearchBackwards
ActionHistoryIncrementalSearchForwards
ActionTerminateHistorySearchAndApply
ActionTerminateHistorySearchAndRestore
ActionClearScreen
ActionAddText
ActionAbortCurrentLine
ActionStartKillActions
ActionKillToEndOfLine
ActionKillToStartOfLine
ActionKillNextWord
ActionKillPreviousWord
ActionKillPreviousSpaceDelimitedWord
ActionEndKillActions
ActionYank
ActionPopYank
ActionNumericArgumentDigit0
ActionNumericArgumentDigit1
ActionNumericArgumentDigit2
ActionNumericArgumentDigit3
ActionNumericArgumentDigit4
ActionNumericArgumentDigit5
ActionNumericArgumentDigit6
ActionNumericArgumentDigit7
ActionNumericArgumentDigit8
ActionNumericArgumentDigit9
ActionNumericArgumentDigitMinus
2022-11-10 13:05:36 +03:00
ActionCompleteForward
ActionCompleteBackward
2022-11-08 17:11:03 +03:00
''')
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())
with replace_if_needed('tools/tui/readline/actions_generated.go') as f:
f.write(generate_readline_actions())
2022-11-16 10:18:13 +03:00
with replace_if_needed('tools/tui/spinners_generated.go') as f:
f.write(generate_spinners())
update_completion()
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() # }}}