go_code.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. #!./kitty/launcher/kitty +launch
  2. # License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
  3. import argparse
  4. import bz2
  5. import io
  6. import json
  7. import os
  8. import re
  9. import shlex
  10. import struct
  11. import subprocess
  12. import sys
  13. import tarfile
  14. from collections.abc import Iterator, Sequence
  15. from contextlib import contextmanager, suppress
  16. from functools import lru_cache
  17. from itertools import chain
  18. from typing import (
  19. Any,
  20. BinaryIO,
  21. Optional,
  22. TextIO,
  23. Union,
  24. )
  25. import kitty.constants as kc
  26. from kittens.tui.operations import Mode
  27. from kittens.tui.spinners import spinners
  28. from kitty.actions import get_all_actions
  29. from kitty.cli import (
  30. GoOption,
  31. go_options_for_seq,
  32. )
  33. from kitty.conf.generate import gen_go_code
  34. from kitty.conf.types import Definition
  35. from kitty.config import commented_out_default_config
  36. from kitty.guess_mime_type import known_extensions, text_mimes
  37. from kitty.key_encoding import config_mod_map
  38. from kitty.key_names import character_key_name_aliases, functional_key_name_aliases
  39. from kitty.options.types import Options
  40. from kitty.rc.base import RemoteCommand, all_command_names, command_for_name
  41. from kitty.remote_control import global_options_spec
  42. from kitty.rgb import color_names
  43. from kitty.simple_cli_definitions import CompletionSpec, parse_option_spec, serialize_as_go_string
  44. if __name__ == '__main__' and not __package__:
  45. import __main__
  46. __main__.__package__ = 'gen'
  47. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  48. changed: list[str] = []
  49. def newer(dest: str, *sources: str) -> bool:
  50. try:
  51. dtime = os.path.getmtime(dest)
  52. except OSError:
  53. return True
  54. for s in chain(sources, (__file__,)):
  55. with suppress(FileNotFoundError):
  56. if os.path.getmtime(s) >= dtime:
  57. return True
  58. return False
  59. # Utils {{{
  60. def serialize_go_dict(x: Union[dict[str, int], dict[int, str], dict[int, int], dict[str, str]]) -> str:
  61. ans = []
  62. def s(x: Union[int, str]) -> str:
  63. if isinstance(x, int):
  64. return str(x)
  65. return f'"{serialize_as_go_string(x)}"'
  66. for k, v in x.items():
  67. ans.append(f'{s(k)}: {s(v)}')
  68. return '{' + ', '.join(ans) + '}'
  69. def replace(template: str, **kw: str) -> str:
  70. for k, v in kw.items():
  71. template = template.replace(k, v)
  72. return template
  73. # }}}
  74. # {{{ Stringer
  75. @lru_cache(maxsize=1)
  76. def enum_parser() -> argparse.ArgumentParser:
  77. p = argparse.ArgumentParser()
  78. p.add_argument('--from-string-func-name')
  79. return p
  80. def stringify_file(path: str) -> None:
  81. with open(path) as f:
  82. src = f.read()
  83. types = {}
  84. constant_name_maps = {}
  85. for m in re.finditer(r'^type +(\S+) +\S+ +// *enum *(.*?)$', src, re.MULTILINE):
  86. args = m.group(2)
  87. types[m.group(1)] = enum_parser().parse_args(args=shlex.split(args) if args else [])
  88. def get_enum_def(src: str) -> None:
  89. type_name = q = ''
  90. constants = {}
  91. for line in src.splitlines():
  92. line = line.strip()
  93. if not line:
  94. continue
  95. parts = line.split()
  96. if not type_name:
  97. if len(parts) < 2 or parts[1] not in types:
  98. return
  99. type_name = parts[1]
  100. q = type_name + '_'
  101. constant_name = parts[0]
  102. a, sep, b = line.partition('//')
  103. if sep:
  104. string_val = b.strip()
  105. else:
  106. string_val = constant_name
  107. if constant_name.startswith(q):
  108. string_val = constant_name[len(q):]
  109. constants[constant_name] = serialize_as_go_string(string_val)
  110. if constants and type_name:
  111. constant_name_maps[type_name] = constants
  112. for m in re.finditer(r'^const +\((.+?)^\)', src, re.MULTILINE|re.DOTALL):
  113. get_enum_def(m.group(1))
  114. with replace_if_needed(path.replace('.go', '_stringer_generated.go')):
  115. print('package', os.path.basename(os.path.dirname(path)))
  116. print ('import "fmt"')
  117. print ('import "encoding/json"')
  118. print()
  119. for type_name, constant_map in constant_name_maps.items():
  120. print(f'func (self {type_name}) String() string ''{')
  121. print('switch self {')
  122. is_first = True
  123. for constant_name, string_val in constant_map.items():
  124. if is_first:
  125. print(f'default: return "{string_val}"')
  126. is_first = False
  127. else:
  128. print(f'case {constant_name}: return "{string_val}"')
  129. print('}}')
  130. print(f'func (self {type_name}) MarshalJSON() ([]byte, error) {{ return json.Marshal(self.String()) }}')
  131. fsname = types[type_name].from_string_func_name or (type_name + '_from_string')
  132. print(f'func {fsname}(x string) (ans {type_name}, err error) ''{')
  133. print('switch x {')
  134. for constant_name, string_val in constant_map.items():
  135. print(f'case "{string_val}": return {constant_name}, nil')
  136. print('}')
  137. print(f'err = fmt.Errorf("unknown value for enum {type_name}: %#v", x)')
  138. print('return')
  139. print('}')
  140. print(f'func (self *{type_name}) SetString(x string) error ''{')
  141. print(f's, err := {fsname}(x); if err == nil {{ *self = s }}; return err''}')
  142. print(f'func (self *{type_name}) UnmarshalJSON(data []byte) (err error)''{')
  143. print('var x string')
  144. print('if err = json.Unmarshal(data, &x); err != nil {return err}')
  145. print('return self.SetString(x)}')
  146. def stringify() -> None:
  147. for path in (
  148. 'tools/tui/graphics/command.go',
  149. 'tools/rsync/algorithm.go',
  150. 'kittens/transfer/ftc.go',
  151. ):
  152. stringify_file(path)
  153. # }}}
  154. # {{{ Bitfields
  155. def make_bitfields() -> None:
  156. from kitty.fast_data_types import SCALE_BITS, SUBSCALE_BITS, WIDTH_BITS
  157. from .bitfields import make_bitfield
  158. def mb(*args: str) -> None:
  159. output_path, ans = make_bitfield(*args)
  160. with replace_if_needed(output_path) as buf:
  161. print(ans, file=buf)
  162. mb(
  163. 'tools/vt', 'CellAttrs',
  164. 'decoration 3', 'bold 1', 'italic 1', 'reverse 1', 'strike 1', 'dim 1', 'hyperlink_id 16',
  165. )
  166. mb('tools/vt', 'Ch', 'is_idx 1', 'ch_or_idx 31')
  167. mb(
  168. 'tools/vt', 'MultiCell',
  169. 'is_multicell 1', 'natural_width 1', f'scale {SCALE_BITS}', f'subscale_n {SUBSCALE_BITS}', f'subscale_d {SUBSCALE_BITS}',
  170. f'width {WIDTH_BITS}', f'x {WIDTH_BITS + SCALE_BITS + 1}', f'y {SCALE_BITS + 1}', 'vertical_align 3',
  171. )
  172. mb('tools/vt', 'CellColor', 'is_idx 1', 'red 8', 'green 8', 'blue 8')
  173. mb('tools/vt', 'LineAttrs', 'prompt_kind 2',)
  174. # }}}
  175. # Completions {{{
  176. @lru_cache
  177. def kitten_cli_docs(kitten: str) -> Any:
  178. from kittens.runner import get_kitten_cli_docs
  179. return get_kitten_cli_docs(kitten)
  180. @lru_cache
  181. def go_options_for_kitten(kitten: str) -> tuple[Sequence[GoOption], Optional[CompletionSpec]]:
  182. kcd = kitten_cli_docs(kitten)
  183. if kcd:
  184. ospec = kcd['options']
  185. return (tuple(go_options_for_seq(parse_option_spec(ospec())[0])), kcd.get('args_completion'))
  186. return (), None
  187. def generate_kittens_completion() -> None:
  188. from kittens.runner import all_kitten_names, get_kitten_wrapper_of
  189. for kitten in sorted(all_kitten_names()):
  190. kn = 'kitten_' + kitten
  191. print(f'{kn} := plus_kitten.AddSubCommand(&cli.Command{{Name:"{kitten}", Group: "Kittens"}})')
  192. wof = get_kitten_wrapper_of(kitten)
  193. if wof:
  194. print(f'{kn}.ArgCompleter = cli.CompletionForWrapper("{serialize_as_go_string(wof)}")')
  195. print(f'{kn}.OnlyArgsAllowed = true')
  196. continue
  197. gopts, ac = go_options_for_kitten(kitten)
  198. if gopts or ac:
  199. for opt in gopts:
  200. print(opt.as_option(kn))
  201. if ac is not None:
  202. print(''.join(ac.as_go_code(kn + '.ArgCompleter', ' = ')))
  203. else:
  204. print(f'{kn}.HelpText = ""')
  205. @lru_cache
  206. def clone_safe_launch_opts() -> Sequence[GoOption]:
  207. from kitty.launch import clone_safe_opts, options_spec
  208. ans = []
  209. allowed = clone_safe_opts()
  210. for o in go_options_for_seq(parse_option_spec(options_spec())[0]):
  211. if o.obj_dict['name'] in allowed:
  212. ans.append(o)
  213. return tuple(ans)
  214. def completion_for_launch_wrappers(*names: str) -> None:
  215. for o in clone_safe_launch_opts():
  216. for name in names:
  217. print(o.as_option(name))
  218. def generate_completions_for_kitty() -> None:
  219. print('package completion\n')
  220. print('import "kitty/tools/cli"')
  221. print('import "kitty/tools/cmd/tool"')
  222. print('import "kitty/tools/cmd/at"')
  223. print('func kitty(root *cli.Command) {')
  224. # The kitty exe
  225. print('k := root.AddSubCommand(&cli.Command{'
  226. 'Name:"kitty", SubCommandIsOptional: true, ArgCompleter: cli.CompleteExecutableFirstArg, SubCommandMustBeFirst: true })')
  227. print('kt := root.AddSubCommand(&cli.Command{Name:"kitten", SubCommandMustBeFirst: true })')
  228. print('tool.KittyToolEntryPoints(kt)')
  229. for opt in go_options_for_seq(parse_option_spec()[0]):
  230. print(opt.as_option('k'))
  231. # kitty +
  232. print('plus := k.AddSubCommand(&cli.Command{Name:"+", Group:"Entry points", ShortDescription: "Various special purpose tools and kittens"})')
  233. # kitty +launch
  234. print('plus_launch := plus.AddSubCommand(&cli.Command{'
  235. 'Name:"launch", Group:"Entry points", ShortDescription: "Launch Python scripts", ArgCompleter: complete_plus_launch})')
  236. print('k.AddClone("", plus_launch).Name = "+launch"')
  237. # kitty +list-fonts
  238. print('plus_list_fonts := plus.AddSubCommand(&cli.Command{'
  239. 'Name:"list-fonts", Group:"Entry points", ShortDescription: "List all available monospaced fonts"})')
  240. print('k.AddClone("", plus_list_fonts).Name = "+list-fonts"')
  241. # kitty +runpy
  242. print('plus_runpy := plus.AddSubCommand(&cli.Command{'
  243. 'Name: "runpy", Group:"Entry points", ArgCompleter: complete_plus_runpy, ShortDescription: "Run Python code"})')
  244. print('k.AddClone("", plus_runpy).Name = "+runpy"')
  245. # kitty +open
  246. print('plus_open := plus.AddSubCommand(&cli.Command{'
  247. 'Name:"open", Group:"Entry points", ArgCompleter: complete_plus_open, ShortDescription: "Open files and URLs"})')
  248. print('for _, og := range k.OptionGroups { plus_open.OptionGroups = append(plus_open.OptionGroups, og.Clone(plus_open)) }')
  249. print('k.AddClone("", plus_open).Name = "+open"')
  250. # kitty +kitten
  251. print('plus_kitten := plus.AddSubCommand(&cli.Command{Name:"kitten", Group:"Kittens", SubCommandMustBeFirst: true})')
  252. generate_kittens_completion()
  253. print('k.AddClone("", plus_kitten).Name = "+kitten"')
  254. # @
  255. print('at.EntryPoint(k)')
  256. # clone-in-kitty, edit-in-kitty
  257. print('cik := root.AddSubCommand(&cli.Command{Name:"clone-in-kitty"})')
  258. completion_for_launch_wrappers('cik')
  259. print('}')
  260. print('func init() {')
  261. print('cli.RegisterExeForCompletion(kitty)')
  262. print('}')
  263. # }}}
  264. # rc command wrappers {{{
  265. json_field_types: dict[str, str] = {
  266. 'bool': 'bool', 'str': 'escaped_string', 'list.str': '[]escaped_string', 'dict.str': 'map[escaped_string]escaped_string', 'float': 'float64', 'int': 'int',
  267. 'scroll_amount': 'any', 'spacing': 'any', 'colors': 'any',
  268. }
  269. def go_field_type(json_field_type: str) -> str:
  270. json_field_type = json_field_type.partition('=')[0]
  271. q = json_field_types.get(json_field_type)
  272. if q:
  273. return q
  274. if json_field_type.startswith('choices.'):
  275. return 'string'
  276. if '.' in json_field_type:
  277. p, r = json_field_type.split('.', 1)
  278. p = {'list': '[]', 'dict': 'map[string]'}[p]
  279. return p + go_field_type(r)
  280. raise TypeError(f'Unknown JSON field type: {json_field_type}')
  281. class JSONField:
  282. def __init__(self, line: str, field_to_option_map: dict[str, str], option_map: dict[str, GoOption]) -> None:
  283. field_def = line.split(':', 1)[0]
  284. self.required = False
  285. self.field, self.field_type = field_def.split('/', 1)
  286. self.go_option_name = field_to_option_map.get(self.field, self.field)
  287. self.go_option_name = ''.join(x.capitalize() for x in self.go_option_name.split('_'))
  288. self.omitempty = True
  289. if fo := option_map.get(self.go_option_name):
  290. if fo.type in ('int', 'float') and float(fo.default or 0) != 0:
  291. self.omitempty = False
  292. self.field_type, self.special_parser = self.field_type.partition('=')[::2]
  293. if self.field.endswith('+'):
  294. self.required = True
  295. self.field = self.field[:-1]
  296. self.struct_field_name = self.field[0].upper() + self.field[1:]
  297. def go_declaration(self) -> str:
  298. omitempty = ',omitempty' if self.omitempty else ''
  299. return self.struct_field_name + ' ' + go_field_type(self.field_type) + f'`json:"{self.field}{omitempty}"`'
  300. def go_code_for_remote_command(name: str, cmd: RemoteCommand, template: str) -> str:
  301. template = '\n' + template[len('//go:build exclude'):]
  302. af: list[str] = []
  303. a = af.append
  304. af.extend(cmd.args.as_go_completion_code('ans'))
  305. od: list[str] = []
  306. option_map: dict[str, GoOption] = {}
  307. for o in rc_command_options(name):
  308. option_map[o.go_var_name] = o
  309. a(o.as_option('ans'))
  310. if o.go_var_name in ('NoResponse', 'ResponseTimeout'):
  311. continue
  312. od.append(o.struct_declaration())
  313. jd: list[str] = []
  314. json_fields = []
  315. field_types: dict[str, str] = {}
  316. for line in cmd.protocol_spec.splitlines():
  317. line = line.strip()
  318. if ':' not in line:
  319. continue
  320. f = JSONField(line, cmd.field_to_option_map or {}, option_map)
  321. json_fields.append(f)
  322. field_types[f.field] = f.field_type
  323. jd.append(f.go_declaration())
  324. jc: list[str] = []
  325. handled_fields: set[str] = set()
  326. jc.extend(cmd.args.as_go_code(name, field_types, handled_fields))
  327. unhandled = {}
  328. used_options = set()
  329. for field in json_fields:
  330. if field.go_option_name in option_map:
  331. o = option_map[field.go_option_name]
  332. used_options.add(field.go_option_name)
  333. optstring = f'options_{name}.{o.go_var_name}'
  334. if field.special_parser:
  335. optstring = f'{field.special_parser}({optstring})'
  336. if field.field_type == 'str':
  337. jc.append(f'payload.{field.struct_field_name} = escaped_string({optstring})')
  338. elif field.field_type == 'list.str':
  339. jc.append(f'payload.{field.struct_field_name} = escape_list_of_strings({optstring})')
  340. elif field.field_type == 'dict.str':
  341. jc.append(f'payload.{field.struct_field_name} = escape_dict_of_strings({optstring})')
  342. else:
  343. jc.append(f'payload.{field.struct_field_name} = {optstring}')
  344. elif field.field in handled_fields:
  345. pass
  346. else:
  347. unhandled[field.field] = field
  348. for x in tuple(unhandled):
  349. if x == 'match_window' and 'Match' in option_map and 'Match' not in used_options:
  350. used_options.add('Match')
  351. o = option_map['Match']
  352. field = unhandled[x]
  353. if field.field_type == 'str':
  354. jc.append(f'payload.{field.struct_field_name} = escaped_string(options_{name}.{o.go_var_name})')
  355. else:
  356. jc.append(f'payload.{field.struct_field_name} = options_{name}.{o.go_var_name}')
  357. del unhandled[x]
  358. if unhandled:
  359. raise SystemExit(f'Cant map fields: {", ".join(unhandled)} for cmd: {name}')
  360. if name != 'send_text':
  361. unused_options = set(option_map) - used_options - {'NoResponse', 'ResponseTimeout'}
  362. if unused_options:
  363. raise SystemExit(f'Unused options: {", ".join(unused_options)} for command: {name}')
  364. argspec = cmd.args.spec
  365. if argspec:
  366. argspec = ' ' + argspec
  367. NO_RESPONSE = 'true' if cmd.disallow_responses else 'false'
  368. ans = replace(
  369. template,
  370. CMD_NAME=name, __FILE__=__file__, CLI_NAME=name.replace('_', '-'),
  371. SHORT_DESC=serialize_as_go_string(cmd.short_desc),
  372. LONG_DESC=serialize_as_go_string(cmd.desc.strip()),
  373. IS_ASYNC='true' if cmd.is_asynchronous else 'false',
  374. NO_RESPONSE_BASE=NO_RESPONSE, ADD_FLAGS_CODE='\n'.join(af),
  375. WAIT_TIMEOUT=str(cmd.response_timeout),
  376. OPTIONS_DECLARATION_CODE='\n'.join(od),
  377. JSON_DECLARATION_CODE='\n'.join(jd),
  378. JSON_INIT_CODE='\n'.join(jc), ARGSPEC=argspec,
  379. STRING_RESPONSE_IS_ERROR='true' if cmd.string_return_is_error else 'false',
  380. STREAM_WANTED='true' if cmd.reads_streaming_data else 'false',
  381. )
  382. return ans
  383. # }}}
  384. # kittens {{{
  385. def generate_conf_parser(kitten: str, defn: Definition) -> None:
  386. with replace_if_needed(f'kittens/{kitten}/conf_generated.go'):
  387. print(f'package {kitten}')
  388. print(gen_go_code(defn))
  389. def generate_extra_cli_parser(name: str, spec: str) -> None:
  390. print('import "kitty/tools/cli"')
  391. go_opts = tuple(go_options_for_seq(parse_option_spec(spec)[0]))
  392. print(f'type {name}_options struct ''{')
  393. for opt in go_opts:
  394. print(opt.struct_declaration())
  395. print('}')
  396. print(f'func parse_{name}_args(args []string) (*{name}_options, []string, error) ''{')
  397. print(f'root := cli.Command{{Name: `{name}` }}')
  398. for opt in go_opts:
  399. print(opt.as_option('root'))
  400. print('cmd, err := root.ParseArgs(args)')
  401. print('if err != nil { return nil, nil, err }')
  402. print(f'var opts {name}_options')
  403. print('err = cmd.GetOptionValues(&opts)')
  404. print('if err != nil { return nil, nil, err }')
  405. print('return &opts, cmd.Args, nil')
  406. print('}')
  407. def kittens_needing_cli_parsers() -> Iterator[str]:
  408. for d in os.scandir('kittens'):
  409. if not d.is_dir(follow_symlinks=False):
  410. continue
  411. if os.path.exists(os.path.join(d.path, 'main.py')) and os.path.exists(os.path.join(d.path, 'main.go')):
  412. with open(os.path.join(d.path, 'main.py')) as f:
  413. raw = f.read()
  414. if 'options' in raw:
  415. yield d.name
  416. def kitten_clis() -> None:
  417. from kittens.runner import get_kitten_conf_docs, get_kitten_extra_cli_parsers
  418. for kitten in kittens_needing_cli_parsers():
  419. defn = get_kitten_conf_docs(kitten)
  420. if defn is not None:
  421. generate_conf_parser(kitten, defn)
  422. ecp = get_kitten_extra_cli_parsers(kitten)
  423. if ecp:
  424. for name, spec in ecp.items():
  425. with replace_if_needed(f'kittens/{kitten}/{name}_cli_generated.go'):
  426. print(f'package {kitten}')
  427. generate_extra_cli_parser(name, spec)
  428. with replace_if_needed(f'kittens/{kitten}/cli_generated.go'):
  429. od = []
  430. ser = []
  431. kcd = kitten_cli_docs(kitten)
  432. has_underscore = '_' in kitten
  433. print(f'package {kitten}')
  434. print('import "fmt"')
  435. print('import "kitty/tools/cli"')
  436. print('var _ = fmt.Sprintf')
  437. print('func create_cmd(root *cli.Command, run_func func(*cli.Command, *Options, []string)(int, error)) {')
  438. print('ans := root.AddSubCommand(&cli.Command{')
  439. print(f'Name: "{kitten}",')
  440. if kcd:
  441. print(f'ShortDescription: "{serialize_as_go_string(kcd["short_desc"])}",')
  442. if kcd['usage']:
  443. print(f'Usage: "[options] {serialize_as_go_string(kcd["usage"])}",')
  444. print(f'HelpText: "{serialize_as_go_string(kcd["help_text"])}",')
  445. print('Run: func(cmd *cli.Command, args []string) (int, error) {')
  446. print('opts := Options{}')
  447. print('err := cmd.GetOptionValues(&opts)')
  448. print('if err != nil { return 1, err }')
  449. print('return run_func(cmd, &opts, args)},')
  450. if has_underscore:
  451. print('Hidden: true,')
  452. print('})')
  453. gopts, ac = go_options_for_kitten(kitten)
  454. for opt in gopts:
  455. print(opt.as_option('ans'))
  456. od.append(opt.struct_declaration())
  457. ser.append('\n'.join(opt.as_string_for_commandline()))
  458. if ac is not None:
  459. print(''.join(ac.as_go_code('ans.ArgCompleter', ' = ')))
  460. if not kcd:
  461. print('specialize_command(ans)')
  462. if has_underscore:
  463. print("clone := root.AddClone(ans.Group, ans)")
  464. print('clone.Hidden = false')
  465. print(f'clone.Name = "{serialize_as_go_string(kitten.replace("_", "-"))}"')
  466. print('}')
  467. print('type Options struct {')
  468. print('\n'.join(od))
  469. print('}')
  470. print('func (opts Options) AsCommandLine() (ans []string) {')
  471. if ser:
  472. print('\t sval := ""')
  473. print('\t _ = sval')
  474. for x in ser:
  475. print('\t' + x)
  476. print('return')
  477. print('}')
  478. # }}}
  479. # Constants {{{
  480. def generate_spinners() -> str:
  481. ans = ['package tui', 'import "time"', 'func NewSpinner(name string) *Spinner {', 'var ans *Spinner', 'switch name {']
  482. a = ans.append
  483. for name, spinner in spinners.items():
  484. a(f'case "{serialize_as_go_string(name)}":')
  485. a('ans = &Spinner{')
  486. a(f'Name: "{serialize_as_go_string(name)}",')
  487. a(f'interval: {spinner["interval"]},')
  488. frames = ', '.join(f'"{serialize_as_go_string(x)}"' for x in spinner['frames'])
  489. a(f'frames: []string{{{frames}}},')
  490. a('}')
  491. a('}')
  492. a('if ans != nil {')
  493. a('ans.interval *= time.Millisecond')
  494. a('ans.current_frame = -1')
  495. a('ans.last_change_at = time.Now().Add(-ans.interval)')
  496. a('}')
  497. a('return ans}')
  498. return '\n'.join(ans)
  499. def generate_color_names() -> str:
  500. selfg = "" if Options.selection_foreground is None else Options.selection_foreground.as_sharp
  501. selbg = "" if Options.selection_background is None else Options.selection_background.as_sharp
  502. cursor = "" if Options.cursor is None else Options.cursor.as_sharp
  503. return 'package style\n\nvar ColorNames = map[string]RGBA{' + '\n'.join(
  504. f'\t"{name}": RGBA{{ Red:{val.red}, Green:{val.green}, Blue:{val.blue} }},'
  505. for name, val in color_names.items()
  506. ) + '\n}' + '\n\nvar ColorTable = [256]uint32{' + ', '.join(
  507. f'{x}' for x in Options.color_table) + '}\n' + f'''
  508. var DefaultColors = struct {{
  509. Foreground, Background, Cursor, SelectionFg, SelectionBg string
  510. }}{{
  511. Foreground: "{Options.foreground.as_sharp}",
  512. Background: "{Options.background.as_sharp}",
  513. Cursor: "{cursor}",
  514. SelectionFg: "{selfg}",
  515. SelectionBg: "{selbg}",
  516. }}
  517. '''
  518. def load_ref_map() -> dict[str, dict[str, str]]:
  519. with open('kitty/docs_ref_map_generated.h') as f:
  520. raw = f.read()
  521. raw = raw.split('{', 1)[1].split('}', 1)[0]
  522. data = json.loads(bytes(bytearray(json.loads(f'[{raw}]'))))
  523. return data # type: ignore
  524. def generate_constants() -> str:
  525. from kittens.hints.main import DEFAULT_REGEX
  526. from kittens.query_terminal.main import all_queries
  527. from kitty.colors import ThemeFile
  528. from kitty.config import option_names_for_completion
  529. from kitty.fast_data_types import FILE_TRANSFER_CODE
  530. from kitty.options.utils import allowed_shell_integration_values, url_style_map
  531. del sys.modules['kittens.hints.main']
  532. del sys.modules['kittens.query_terminal.main']
  533. ref_map = load_ref_map()
  534. with open('kitty/data-types.h') as dt:
  535. m = re.search(r'^#define IMAGE_PLACEHOLDER_CHAR (\S+)', dt.read(), flags=re.M)
  536. assert m is not None
  537. placeholder_char = int(m.group(1), 16)
  538. dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help))
  539. url_prefixes = ','.join(f'"{x}"' for x in Options.url_prefixes)
  540. option_names = '`' + '\n'.join(option_names_for_completion()) + '`'
  541. url_style = {v:k for k, v in url_style_map.items()}[Options.url_style]
  542. query_names = ', '.join(f'"{name}"' for name in all_queries)
  543. return f'''\
  544. package kitty
  545. type VersionType struct {{
  546. Major, Minor, Patch int
  547. }}
  548. const VersionString string = "{kc.str_version}"
  549. const WebsiteBaseURL string = "{kc.website_base_url}"
  550. const FileTransferCode int = {FILE_TRANSFER_CODE}
  551. const ImagePlaceholderChar rune = {placeholder_char}
  552. const SSHControlMasterTemplate = "{kc.ssh_control_master_template}"
  553. const RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}"
  554. var VCSRevision string = ""
  555. var IsFrozenBuild string = ""
  556. var IsStandaloneBuild string = ""
  557. const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]}
  558. const HintsDefaultRegex = `{DEFAULT_REGEX}`
  559. const DefaultTermName = `{Options.term}`
  560. const DefaultUrlStyle = `{url_style}`
  561. const DefaultUrlColor = `{Options.url_color.as_sharp}`
  562. var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}}
  563. var DefaultPager []string = []string{{ {dp} }}
  564. var FunctionalKeyNameAliases = map[string]string{serialize_go_dict(functional_key_name_aliases)}
  565. var CharacterKeyNameAliases = map[string]string{serialize_go_dict(character_key_name_aliases)}
  566. var ConfigModMap = map[string]uint16{serialize_go_dict(config_mod_map)}
  567. var RefMap = map[string]string{serialize_go_dict(ref_map['ref'])}
  568. var DocTitleMap = map[string]string{serialize_go_dict(ref_map['doc'])}
  569. var AllowedShellIntegrationValues = []string{{ {str(sorted(allowed_shell_integration_values))[1:-1].replace("'", '"')} }}
  570. var QueryNames = []string{{ {query_names} }}
  571. var CommentedOutDefaultConfig = "{serialize_as_go_string(commented_out_default_config())}"
  572. var KittyConfigDefaults = struct {{
  573. Term, Shell_integration, Select_by_word_characters, Url_excluded_characters, Shell string
  574. Wheel_scroll_multiplier int
  575. Url_prefixes []string
  576. }}{{
  577. Term: "{Options.term}", Shell_integration: "{' '.join(Options.shell_integration)}", Url_prefixes: []string{{ {url_prefixes} }},
  578. Select_by_word_characters: `{Options.select_by_word_characters}`, Wheel_scroll_multiplier: {Options.wheel_scroll_multiplier},
  579. Shell: "{Options.shell}", Url_excluded_characters: "{Options.url_excluded_characters}",
  580. }}
  581. const OptionNames = {option_names}
  582. const DarkThemeFileName = "{ThemeFile.dark.value}"
  583. const LightThemeFileName = "{ThemeFile.light.value}"
  584. const NoPreferenceThemeFileName = "{ThemeFile.no_preference.value}"
  585. ''' # }}}
  586. # Boilerplate {{{
  587. @contextmanager
  588. def replace_if_needed(path: str, show_diff: bool = False) -> Iterator[io.StringIO]:
  589. buf = io.StringIO()
  590. origb = sys.stdout
  591. sys.stdout = buf
  592. try:
  593. yield buf
  594. finally:
  595. sys.stdout = origb
  596. orig = ''
  597. with suppress(FileNotFoundError), open(path) as f:
  598. orig = f.read()
  599. new = buf.getvalue()
  600. new = f'// Code generated by {os.path.basename(__file__)}; DO NOT EDIT.\n\n' + new
  601. if orig != new:
  602. changed.append(path)
  603. if show_diff:
  604. with open(path + '.new', 'w') as f:
  605. f.write(new)
  606. subprocess.run(['diff', '-Naurp', path, f.name], stdout=open('/dev/tty', 'w'))
  607. os.remove(f.name)
  608. with open(path, 'w') as f:
  609. f.write(new)
  610. @lru_cache(maxsize=256)
  611. def rc_command_options(name: str) -> tuple[GoOption, ...]:
  612. cmd = command_for_name(name)
  613. return tuple(go_options_for_seq(parse_option_spec(cmd.options_spec or '\n\n')[0]))
  614. def update_at_commands() -> None:
  615. with open('tools/cmd/at/template.go') as f:
  616. template = f.read()
  617. for name in all_command_names():
  618. cmd = command_for_name(name)
  619. code = go_code_for_remote_command(name, cmd, template)
  620. dest = f'tools/cmd/at/cmd_{name}_generated.go'
  621. with replace_if_needed(dest) as f:
  622. f.write(code)
  623. struct_def = []
  624. opt_def = []
  625. for o in go_options_for_seq(parse_option_spec(global_options_spec())[0]):
  626. struct_def.append(o.struct_declaration())
  627. opt_def.append(o.as_option(depth=1, group="Global options"))
  628. sdef = '\n'.join(struct_def)
  629. odef = '\n'.join(opt_def)
  630. code = f'''
  631. package at
  632. import "kitty/tools/cli"
  633. type rc_global_options struct {{
  634. {sdef}
  635. }}
  636. var rc_global_opts rc_global_options
  637. func add_rc_global_opts(cmd *cli.Command) {{
  638. {odef}
  639. }}
  640. '''
  641. with replace_if_needed('tools/cmd/at/global_opts_generated.go') as f:
  642. f.write(code)
  643. def update_completion() -> None:
  644. with replace_if_needed('tools/cmd/completion/kitty_generated.go'):
  645. generate_completions_for_kitty()
  646. with replace_if_needed('tools/cmd/at/kitty_actions_generated.go'):
  647. print("package at")
  648. print("const KittyActionNames = `", end='')
  649. for grp, actions in get_all_actions().items():
  650. for ac in actions:
  651. print(ac.name)
  652. print('`')
  653. with replace_if_needed('tools/cmd/edit_in_kitty/launch_generated.go'):
  654. print('package edit_in_kitty')
  655. print('import "kitty/tools/cli"')
  656. print('func AddCloneSafeOpts(cmd *cli.Command) {')
  657. completion_for_launch_wrappers('cmd')
  658. print(''.join(CompletionSpec.from_string('type:file mime:text/* group:"Text files"').as_go_code('cmd.ArgCompleter', ' = ')))
  659. print('}')
  660. def define_enum(package_name: str, type_name: str, items: str, underlying_type: str = 'uint') -> str:
  661. actions = []
  662. for x in items.splitlines():
  663. x = x.strip()
  664. if x:
  665. actions.append(x)
  666. ans = [f'package {package_name}', 'import "strconv"', f'type {type_name} {underlying_type}', 'const (']
  667. stringer = [f'func (ac {type_name}) String() string ''{', 'switch(ac) {']
  668. for i, ac in enumerate(actions):
  669. stringer.append(f'case {ac}: return "{ac}"')
  670. if i == 0:
  671. ac = ac + f' {type_name} = iota'
  672. ans.append(ac)
  673. ans.append(')')
  674. stringer.append('}\nreturn strconv.Itoa(int(ac)) }')
  675. return '\n'.join(ans + stringer)
  676. def generate_readline_actions() -> str:
  677. return define_enum('readline', 'Action', '''\
  678. ActionNil
  679. ActionBackspace
  680. ActionDelete
  681. ActionMoveToStartOfLine
  682. ActionMoveToEndOfLine
  683. ActionMoveToStartOfDocument
  684. ActionMoveToEndOfDocument
  685. ActionMoveToEndOfWord
  686. ActionMoveToStartOfWord
  687. ActionCursorLeft
  688. ActionCursorRight
  689. ActionEndInput
  690. ActionAcceptInput
  691. ActionCursorUp
  692. ActionHistoryPreviousOrCursorUp
  693. ActionCursorDown
  694. ActionHistoryNextOrCursorDown
  695. ActionHistoryNext
  696. ActionHistoryPrevious
  697. ActionHistoryFirst
  698. ActionHistoryLast
  699. ActionHistoryIncrementalSearchBackwards
  700. ActionHistoryIncrementalSearchForwards
  701. ActionTerminateHistorySearchAndApply
  702. ActionTerminateHistorySearchAndRestore
  703. ActionClearScreen
  704. ActionAddText
  705. ActionAbortCurrentLine
  706. ActionStartKillActions
  707. ActionKillToEndOfLine
  708. ActionKillToStartOfLine
  709. ActionKillNextWord
  710. ActionKillPreviousWord
  711. ActionKillPreviousSpaceDelimitedWord
  712. ActionEndKillActions
  713. ActionYank
  714. ActionPopYank
  715. ActionNumericArgumentDigit0
  716. ActionNumericArgumentDigit1
  717. ActionNumericArgumentDigit2
  718. ActionNumericArgumentDigit3
  719. ActionNumericArgumentDigit4
  720. ActionNumericArgumentDigit5
  721. ActionNumericArgumentDigit6
  722. ActionNumericArgumentDigit7
  723. ActionNumericArgumentDigit8
  724. ActionNumericArgumentDigit9
  725. ActionNumericArgumentDigitMinus
  726. ActionCompleteForward
  727. ActionCompleteBackward
  728. ''')
  729. def generate_mimetypes() -> str:
  730. import mimetypes
  731. if not mimetypes.inited:
  732. mimetypes.init()
  733. ans = ['package utils', 'import "sync"', 'var only_once sync.Once', 'var builtin_types_map map[string]string',
  734. 'func set_builtins() {', 'builtin_types_map = map[string]string{',]
  735. for k, v in mimetypes.types_map.items():
  736. ans.append(f' "{serialize_as_go_string(k)}": "{serialize_as_go_string(v)}",')
  737. ans.append('}}')
  738. return '\n'.join(ans)
  739. def generate_textual_mimetypes() -> str:
  740. ans = ['package utils', 'var KnownTextualMimes = map[string]bool{',]
  741. for k in text_mimes:
  742. ans.append(f' "{serialize_as_go_string(k)}": true,')
  743. ans.append('}')
  744. ans.append('var KnownExtensions = map[string]string{')
  745. for k, v in known_extensions.items():
  746. ans.append(f' ".{serialize_as_go_string(k)}": "{serialize_as_go_string(v)}",')
  747. ans.append('}')
  748. return '\n'.join(ans)
  749. def write_compressed_data(data: bytes, d: BinaryIO) -> None:
  750. d.write(struct.pack('<I', len(data)))
  751. d.write(bz2.compress(data))
  752. def generate_unicode_names(src: TextIO, dest: BinaryIO) -> None:
  753. num_names, num_of_words = map(int, next(src).split())
  754. gob = io.BytesIO()
  755. gob.write(struct.pack('<II', num_names, num_of_words))
  756. for line in src:
  757. line = line.strip()
  758. if line:
  759. a, aliases = line.partition('\t')[::2]
  760. cp, name = a.partition(' ')[::2]
  761. ename = name.encode()
  762. record = struct.pack('<IH', int(cp), len(ename)) + ename
  763. if aliases:
  764. record += aliases.encode()
  765. gob.write(struct.pack('<H', len(record)) + record)
  766. write_compressed_data(gob.getvalue(), dest)
  767. def generate_ssh_kitten_data() -> None:
  768. files = {
  769. 'terminfo/kitty.terminfo', 'terminfo/x/' + Options.term,
  770. }
  771. for dirpath, dirnames, filenames in os.walk('shell-integration'):
  772. for f in filenames:
  773. path = os.path.join(dirpath, f)
  774. files.add(path.replace(os.sep, '/'))
  775. dest = 'tools/tui/shell_integration/data_generated.bin'
  776. def normalize(t: tarfile.TarInfo) -> tarfile.TarInfo:
  777. t.uid = t.gid = 0
  778. t.uname = t.gname = ''
  779. t.mtime = 0
  780. return t
  781. if newer(dest, *files):
  782. buf = io.BytesIO()
  783. with tarfile.open(fileobj=buf, mode='w') as tf:
  784. for f in sorted(files):
  785. tf.add(f, filter=normalize)
  786. with open(dest, 'wb') as d:
  787. write_compressed_data(buf.getvalue(), d)
  788. def start_simdgen() -> 'subprocess.Popen[bytes]':
  789. return subprocess.Popen(['go', 'run', 'generate.go'], cwd='tools/simdstring', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  790. def main(args: list[str]=sys.argv) -> None:
  791. simdgen_process = start_simdgen()
  792. with replace_if_needed('constants_generated.go') as f:
  793. f.write(generate_constants())
  794. with replace_if_needed('tools/utils/style/color-names_generated.go') as f:
  795. f.write(generate_color_names())
  796. with replace_if_needed('tools/tui/readline/actions_generated.go') as f:
  797. f.write(generate_readline_actions())
  798. with replace_if_needed('tools/tui/spinners_generated.go') as f:
  799. f.write(generate_spinners())
  800. with replace_if_needed('tools/utils/mimetypes_generated.go') as f:
  801. f.write(generate_mimetypes())
  802. with replace_if_needed('tools/utils/mimetypes_textual_generated.go') as f:
  803. f.write(generate_textual_mimetypes())
  804. if newer('tools/unicode_names/data_generated.bin', 'tools/unicode_names/names.txt'):
  805. with open('tools/unicode_names/data_generated.bin', 'wb') as dest, open('tools/unicode_names/names.txt') as src:
  806. generate_unicode_names(src, dest)
  807. generate_ssh_kitten_data()
  808. update_completion()
  809. update_at_commands()
  810. kitten_clis()
  811. stringify()
  812. make_bitfields()
  813. print(json.dumps(changed, indent=2))
  814. stdout, stderr = simdgen_process.communicate()
  815. if simdgen_process.wait() != 0:
  816. print('Failed to generate SIMD ASM', file=sys.stderr)
  817. sys.stdout.buffer.write(stdout)
  818. sys.stderr.buffer.write(stderr)
  819. raise SystemExit(simdgen_process.returncode)
  820. if __name__ == '__main__':
  821. import runpy
  822. m = runpy.run_path(os.path.dirname(os.path.abspath(__file__)))
  823. m['main']([sys.executable, 'go-code'])
  824. # }}}