go_code.py 33 KB

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