conf.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. #!/usr/bin/env python
  2. # vim:fileencoding=utf-8
  3. #
  4. # Configuration file for the Sphinx documentation builder.
  5. #
  6. # This file does only contain a selection of the most common options. For a
  7. # full list see the documentation:
  8. # https://www.sphinx-doc.org/en/master/config
  9. import glob
  10. import os
  11. import re
  12. import subprocess
  13. import sys
  14. import time
  15. from functools import lru_cache, partial
  16. from typing import Any, Callable, Dict, Iterable, Iterator, List, Tuple
  17. from docutils import nodes
  18. from docutils.parsers.rst.roles import set_classes
  19. from pygments.lexer import RegexLexer, bygroups # type: ignore
  20. from pygments.token import Comment, Error, Keyword, Literal, Name, Number, String, Whitespace # type: ignore
  21. from sphinx import addnodes, version_info
  22. from sphinx.util.logging import getLogger
  23. kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  24. if kitty_src not in sys.path:
  25. sys.path.insert(0, kitty_src)
  26. from kitty.conf.types import Definition, expand_opt_references # noqa
  27. from kitty.constants import str_version, website_url # noqa
  28. from kitty.fast_data_types import Shlex # noqa
  29. # config {{{
  30. # -- Project information -----------------------------------------------------
  31. project = 'kitty'
  32. copyright = time.strftime('%Y, Kovid Goyal')
  33. author = 'Kovid Goyal'
  34. building_man_pages = 'man' in sys.argv
  35. # The short X.Y version
  36. version = str_version
  37. # The full version, including alpha/beta/rc tags
  38. release = str_version
  39. logger = getLogger(__name__)
  40. # -- General configuration ---------------------------------------------------
  41. # If your documentation needs a minimal Sphinx version, state it here.
  42. #
  43. needs_sphinx = '1.7'
  44. # Add any Sphinx extension module names here, as strings. They can be
  45. # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
  46. # ones.
  47. extensions = [
  48. 'sphinx.ext.ifconfig',
  49. 'sphinx.ext.viewcode',
  50. 'sphinx.ext.githubpages',
  51. 'sphinx.ext.extlinks',
  52. 'sphinx_copybutton',
  53. 'sphinx_inline_tabs',
  54. "sphinxext.opengraph",
  55. ]
  56. # URL for OpenGraph tags
  57. ogp_site_url = website_url()
  58. # OGP needs a PNG image because of: https://github.com/wpilibsuite/sphinxext-opengraph/issues/96
  59. ogp_social_cards = {
  60. 'image': '../logo/kitty.png'
  61. }
  62. # Add any paths that contain templates here, relative to this directory.
  63. templates_path = ['_templates']
  64. # The suffix(es) of source filenames.
  65. # You can specify multiple suffix as a list of string:
  66. #
  67. # source_suffix = ['.rst', '.md']
  68. source_suffix = '.rst'
  69. # The master toctree document.
  70. master_doc = 'index'
  71. # The language for content autogenerated by Sphinx. Refer to documentation
  72. # for a list of supported languages.
  73. #
  74. # This is also used if you do content translation via gettext catalogs.
  75. # Usually you set "language" from the command line for these cases.
  76. language: str = 'en'
  77. # List of patterns, relative to source directory, that match files and
  78. # directories to ignore when looking for source files.
  79. # This pattern also affects html_static_path and html_extra_path .
  80. exclude_patterns = [
  81. '_build', 'Thumbs.db', '.DS_Store', 'basic.rst',
  82. 'generated/cli-*.rst', 'generated/conf-*.rst', 'generated/actions.rst'
  83. ]
  84. rst_prolog = '''
  85. .. |kitty| replace:: *kitty*
  86. .. |version| replace:: VERSION
  87. .. _tarball: https://github.com/kovidgoyal/kitty/releases/download/vVERSION/kitty-VERSION.tar.xz
  88. .. role:: italic
  89. '''.replace('VERSION', str_version)
  90. smartquotes_action = 'qe' # educate quotes and ellipses but not dashes
  91. def go_version(go_mod_path: str) -> str: # {{{
  92. with open(go_mod_path) as f:
  93. for line in f:
  94. if line.startswith('go '):
  95. return line.strip().split()[1]
  96. raise SystemExit(f'No Go version in {go_mod_path}')
  97. # }}}
  98. string_replacements = {
  99. '_kitty_install_cmd': 'curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin',
  100. '_build_go_version': go_version('../go.mod'),
  101. }
  102. # -- Options for HTML output -------------------------------------------------
  103. # The theme to use for HTML and HTML Help pages. See the documentation for
  104. # a list of builtin themes.
  105. #
  106. html_theme = 'furo'
  107. html_title = 'kitty'
  108. # Theme options are theme-specific and customize the look and feel of a theme
  109. # further. For a list of options available for each theme, see the
  110. # documentation.
  111. github_icon_path = 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z' # noqa
  112. html_theme_options: Dict[str, Any] = {
  113. 'sidebar_hide_name': True,
  114. 'navigation_with_keys': True,
  115. 'footer_icons': [
  116. {
  117. "name": "GitHub",
  118. "url": "https://github.com/kovidgoyal/kitty",
  119. "html": f"""
  120. <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16">
  121. <path fill-rule="evenodd" d="{github_icon_path}"></path>
  122. </svg>
  123. """,
  124. "class": "",
  125. },
  126. ],
  127. }
  128. # Add any paths that contain custom static files (such as style sheets) here,
  129. # relative to this directory. They are copied after the builtin static files,
  130. # so a file named "default.css" will overwrite the builtin "default.css".
  131. html_static_path = ['_static']
  132. html_favicon = html_logo = '../logo/kitty.svg'
  133. html_css_files = ['custom.css', 'timestamps.css']
  134. html_js_files = ['custom.js', 'timestamps.js']
  135. # Custom sidebar templates, must be a dictionary that maps document names
  136. # to template names.
  137. #
  138. # The default sidebars (for documents that don't match any pattern) are
  139. # defined by theme itself. Builtin themes are using these templates by
  140. # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
  141. # 'searchbox.html']``.
  142. #
  143. html_show_sourcelink = False
  144. html_show_sphinx = False
  145. manpages_url = 'https://man7.org/linux/man-pages/man{section}/{page}.{section}.html'
  146. # -- Options for manual page output ------------------------------------------
  147. # One entry per manual page. List of tuples
  148. # (source start file, name, description, authors, manual section).
  149. man_pages = [
  150. ('invocation', 'kitty', 'The fast, feature rich terminal emulator', [author], 1),
  151. ('conf', 'kitty.conf', 'Configuration file for kitty', [author], 5)
  152. ]
  153. # -- Options for Texinfo output ----------------------------------------------
  154. # Grouping the document tree into Texinfo files. List of tuples
  155. # (source start file, target name, title, author,
  156. # dir menu entry, description, category)
  157. texinfo_documents = [
  158. (master_doc, 'kitty', 'kitty Documentation',
  159. author, 'kitty', 'Cross-platform, fast, feature-rich, GPU based terminal',
  160. 'Miscellaneous'),
  161. ]
  162. # }}}
  163. # GitHub linking inline roles {{{
  164. extlinks = {
  165. 'iss': ('https://github.com/kovidgoyal/kitty/issues/%s', '#%s'),
  166. 'pull': ('https://github.com/kovidgoyal/kitty/pull/%s', '#%s'),
  167. 'disc': ('https://github.com/kovidgoyal/kitty/discussions/%s', '#%s'),
  168. }
  169. def commit_role(
  170. name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []
  171. ) -> Tuple[List[nodes.reference], List[nodes.problematic]]:
  172. ' Link to a github commit '
  173. try:
  174. commit_id = subprocess.check_output(
  175. f'git rev-list --max-count=1 {text}'.split()).decode('utf-8').strip()
  176. except Exception:
  177. msg = inliner.reporter.error(
  178. f'git commit id "{text}" not recognized.', line=lineno)
  179. prb = inliner.problematic(rawtext, rawtext, msg)
  180. return [prb], [msg]
  181. url = f'https://github.com/kovidgoyal/kitty/commit/{commit_id}'
  182. set_classes(options)
  183. short_id = subprocess.check_output(
  184. f'git rev-list --max-count=1 --abbrev-commit {commit_id}'.split()).decode('utf-8').strip()
  185. node = nodes.reference(rawtext, f'commit: {short_id}', refuri=url, **options)
  186. return [node], []
  187. # }}}
  188. # CLI docs {{{
  189. def write_cli_docs(all_kitten_names: Iterable[str]) -> None:
  190. from kittens.ssh.main import copy_message, option_text
  191. from kitty.cli import option_spec_as_rst
  192. with open('generated/ssh-copy.rst', 'w') as f:
  193. f.write(option_spec_as_rst(
  194. appname='copy', ospec=option_text, heading_char='^',
  195. usage='file-or-dir-to-copy ...', message=copy_message
  196. ))
  197. del sys.modules['kittens.ssh.main']
  198. from kitty.launch import options_spec as launch_options_spec
  199. with open('generated/launch.rst', 'w') as f:
  200. f.write(option_spec_as_rst(
  201. appname='launch', ospec=launch_options_spec, heading_char='_',
  202. message='''\
  203. Launch an arbitrary program in a new kitty window/tab. Note that
  204. if you specify a program-to-run you can use the special placeholder
  205. :code:`@selection` which will be replaced by the current selection.
  206. '''
  207. ))
  208. with open('generated/cli-kitty.rst', 'w') as f:
  209. f.write(option_spec_as_rst(appname='kitty').replace(
  210. 'kitty --to', 'kitty @ --to'))
  211. as_rst = partial(option_spec_as_rst, heading_char='_')
  212. from kitty.rc.base import all_command_names, command_for_name
  213. from kitty.remote_control import cli_msg, global_options_spec
  214. with open('generated/cli-kitten-at.rst', 'w') as f:
  215. p = partial(print, file=f)
  216. p('kitten @')
  217. p('-' * 80)
  218. p('.. program::', 'kitten @')
  219. p('\n\n' + as_rst(
  220. global_options_spec, message=cli_msg, usage='command ...', appname='kitten @'))
  221. from kitty.rc.base import cli_params_for
  222. for cmd_name in sorted(all_command_names()):
  223. func = command_for_name(cmd_name)
  224. p(f'.. _at-{func.name}:\n')
  225. p('kitten @', func.name)
  226. p('-' * 120)
  227. p('.. program::', 'kitten @', func.name)
  228. p('\n\n' + as_rst(*cli_params_for(func)))
  229. from kittens.runner import get_kitten_cli_docs
  230. for kitten in all_kitten_names:
  231. data = get_kitten_cli_docs(kitten)
  232. if data:
  233. with open(f'generated/cli-kitten-{kitten}.rst', 'w') as f:
  234. p = partial(print, file=f)
  235. p('.. program::', 'kitty +kitten', kitten)
  236. p('\nSource code for', kitten)
  237. p('-' * 72)
  238. scurl = f'https://github.com/kovidgoyal/kitty/tree/master/kittens/{kitten}'
  239. p(f'\nThe source code for this kitten is `available on GitHub <{scurl}>`_.')
  240. p('\nCommand Line Interface')
  241. p('-' * 72)
  242. appname = f'kitten {kitten}'
  243. if kitten in ('panel', 'broadcast', 'remote_file'):
  244. appname = 'kitty +' + appname
  245. p('\n\n' + option_spec_as_rst(
  246. data['options'], message=data['help_text'], usage=data['usage'], appname=appname, heading_char='^'))
  247. # }}}
  248. def write_color_names_table() -> None: # {{{
  249. from kitty.rgb import color_names
  250. def s(c: Any) -> str:
  251. return f'{c.red:02x}/{c.green:02x}/{c.blue:02x}'
  252. with open('generated/color-names.rst', 'w') as f:
  253. p = partial(print, file=f)
  254. p('=' * 50, '=' * 20)
  255. p('Name'.ljust(50), 'RGB value')
  256. p('=' * 50, '=' * 20)
  257. for name, col in color_names.items():
  258. p(name.ljust(50), s(col))
  259. p('=' * 50, '=' * 20)
  260. # }}}
  261. def write_remote_control_protocol_docs() -> None: # {{{
  262. from kitty.rc.base import RemoteCommand, all_command_names, command_for_name
  263. field_pat = re.compile(r'\s*([^:]+?)\s*:\s*(.+)')
  264. def format_cmd(p: Callable[..., None], name: str, cmd: RemoteCommand) -> None:
  265. p(name)
  266. p('-' * 80)
  267. lines = (cmd.__doc__ or '').strip().splitlines()
  268. fields = []
  269. for line in lines:
  270. m = field_pat.match(line)
  271. if m is None:
  272. p(line)
  273. else:
  274. fields.append((m.group(1).split('/')[0], m.group(2)))
  275. if fields:
  276. p('\nFields are:\n')
  277. for (name, desc) in fields:
  278. if '+' in name:
  279. title = name.replace('+', ' (required)')
  280. else:
  281. title = name
  282. defval = cmd.get_default(name.replace('-', '_'), cmd)
  283. if defval is not cmd:
  284. title = f'{title} (default: {defval})'
  285. else:
  286. title = f'{title} (optional)'
  287. p(f':code:`{title}`')
  288. p(' ', desc)
  289. p()
  290. p()
  291. p()
  292. with open('generated/rc.rst', 'w') as f:
  293. p = partial(print, file=f)
  294. for name in sorted(all_command_names()):
  295. cmd = command_for_name(name)
  296. if not cmd.__doc__:
  297. continue
  298. name = name.replace('_', '-')
  299. format_cmd(p, name, cmd)
  300. # }}}
  301. def replace_string(app: Any, docname: str, source: List[str]) -> None: # {{{
  302. src = source[0]
  303. for k, v in app.config.string_replacements.items():
  304. src = src.replace(k, v)
  305. source[0] = src
  306. # }}}
  307. # config file docs {{{
  308. class ConfLexer(RegexLexer): # type: ignore
  309. name = 'Conf'
  310. aliases = ['conf']
  311. filenames = ['*.conf']
  312. def map_flags(self: RegexLexer, val: str, start_pos: int) -> Iterator[Tuple[int, Any, str]]:
  313. expecting_arg = ''
  314. s = Shlex(val)
  315. from kitty.options.utils import allowed_key_map_options
  316. last_pos = 0
  317. while (tok := s.next_word())[0] > -1:
  318. x = tok[1]
  319. if tok[0] > last_pos:
  320. yield start_pos + last_pos, Whitespace, ' ' * (tok[0] - last_pos)
  321. last_pos = tok[0] + len(x)
  322. tok_start = start_pos + tok[0]
  323. if expecting_arg:
  324. yield tok_start, String, x
  325. expecting_arg = ''
  326. elif x.startswith('--'):
  327. expecting_arg = x[2:]
  328. k, sep, v = expecting_arg.partition('=')
  329. k = k.replace('-', '_')
  330. expecting_arg = k
  331. if expecting_arg not in allowed_key_map_options:
  332. yield tok_start, Error, x
  333. elif sep == '=':
  334. expecting_arg = ''
  335. yield tok_start, Name, x
  336. else:
  337. yield tok_start, Name, x
  338. else:
  339. break
  340. def mapargs(self: RegexLexer, match: 're.Match[str]') -> Iterator[Tuple[int, Any, str]]:
  341. start_pos = match.start()
  342. val = match.group()
  343. parts = val.split(maxsplit=1)
  344. if parts[0].startswith('--'):
  345. seen = 0
  346. for (pos, token, text) in self.map_flags(val, start_pos):
  347. yield pos, token, text
  348. seen += len(text)
  349. start_pos += seen
  350. val = val[seen:]
  351. parts = val.split(maxsplit=1)
  352. if not val:
  353. return
  354. yield start_pos, Literal, parts[0] # key spec
  355. if len(parts) == 1:
  356. return
  357. start_pos += len(parts[0])
  358. val = val[len(parts[0]):]
  359. m = re.match(r'(\s+)(\S+)', val)
  360. if m is None:
  361. return
  362. yield start_pos, Whitespace, m.group(1)
  363. yield start_pos + m.start(2), Name.Function, m.group(2) # action function
  364. yield start_pos + m.end(2), String, val[m.end(2):]
  365. tokens = {
  366. 'root': [
  367. (r'#.*?$', Comment.Single),
  368. (r'\s+$', Whitespace),
  369. (r'\s+', Whitespace),
  370. (r'(include)(\s+)(.+?)$', bygroups(Comment.Preproc, Whitespace, Name.Namespace)),
  371. (r'(map)(\s+)', bygroups(
  372. Keyword.Declaration, Whitespace), 'mapargs'),
  373. (r'(mouse_map)(\s+)(\S+)(\s+)(\S+)(\s+)(\S+)(\s+)', bygroups(
  374. Keyword.Declaration, Whitespace, String, Whitespace, Name.Variable, Whitespace, String, Whitespace), 'action'),
  375. (r'(symbol_map)(\s+)(\S+)(\s+)(.+?)$', bygroups(
  376. Keyword.Declaration, Whitespace, String, Whitespace, Literal)),
  377. (r'([a-zA-Z_0-9]+)(\s+)', bygroups(
  378. Name.Variable, Whitespace), 'args'),
  379. ],
  380. 'action': [
  381. (r'[a-z_0-9]+$', Name.Function, 'root'),
  382. (r'[a-z_0-9]+', Name.Function, 'args'),
  383. ],
  384. 'mapargs': [
  385. (r'.+$', mapargs, 'root'),
  386. ],
  387. 'args': [
  388. (r'\s+', Whitespace, 'args'),
  389. (r'\b(yes|no)\b$', Number.Bin, 'root'),
  390. (r'\b(yes|no)\b', Number.Bin, 'args'),
  391. (r'[+-]?[0-9]+\s*$', Number.Integer, 'root'),
  392. (r'[+-]?[0-9.]+\s*$', Number.Float, 'root'),
  393. (r'[+-]?[0-9]+', Number.Integer, 'args'),
  394. (r'[+-]?[0-9.]+', Number.Float, 'args'),
  395. (r'#[a-fA-F0-9]{3,6}\s*$', String, 'root'),
  396. (r'#[a-fA-F0-9]{3,6}\s*', String, 'args'),
  397. (r'.+', String, 'root'),
  398. ],
  399. }
  400. class SessionLexer(RegexLexer): # type: ignore
  401. name = 'Session'
  402. aliases = ['session']
  403. filenames = ['*.session']
  404. tokens = {
  405. 'root': [
  406. (r'#.*?$', Comment.Single),
  407. (r'[a-z][a-z0-9_]+', Name.Function, 'args'),
  408. ],
  409. 'args': [
  410. (r'.*?$', Literal, 'root'),
  411. ]
  412. }
  413. def link_role(
  414. name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []
  415. ) -> Tuple[List[nodes.reference], List[nodes.problematic]]:
  416. text = text.replace('\n', ' ')
  417. m = re.match(r'(.+)\s+<(.+?)>', text)
  418. if m is None:
  419. msg = inliner.reporter.error(f'link "{text}" not recognized', line=lineno)
  420. prb = inliner.problematic(rawtext, rawtext, msg)
  421. return [prb], [msg]
  422. text, url = m.group(1, 2)
  423. url = url.replace(' ', '')
  424. set_classes(options)
  425. node = nodes.reference(rawtext, text, refuri=url, **options)
  426. return [node], []
  427. opt_aliases: Dict[str, str] = {}
  428. shortcut_slugs: Dict[str, Tuple[str, str]] = {}
  429. def parse_opt_node(env: Any, sig: str, signode: Any) -> str:
  430. """Transform an option description into RST nodes."""
  431. count = 0
  432. firstname = ''
  433. for potential_option in sig.split(', '):
  434. optname = potential_option.strip()
  435. if count:
  436. signode += addnodes.desc_addname(', ', ', ')
  437. text = optname.split('.', 1)[-1]
  438. signode += addnodes.desc_name(text, text)
  439. if not count:
  440. firstname = optname
  441. signode['allnames'] = [optname]
  442. else:
  443. signode['allnames'].append(optname)
  444. opt_aliases[optname] = firstname
  445. count += 1
  446. if not firstname:
  447. raise ValueError(f'{sig} is not a valid opt')
  448. return firstname
  449. def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str:
  450. """Transform a shortcut description into RST nodes."""
  451. conf_name, text = sig.split('.', 1)
  452. signode += addnodes.desc_name(text, text)
  453. return sig
  454. def parse_action_node(env: Any, sig: str, signode: Any) -> str:
  455. """Transform an action description into RST nodes."""
  456. signode += addnodes.desc_name(sig, sig)
  457. return sig
  458. def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
  459. conf_name, opt = target.partition('.')[::2]
  460. if not opt:
  461. conf_name, opt = 'kitty', conf_name
  462. full_name = f'{conf_name}.{opt}'
  463. return title, opt_aliases.get(full_name, full_name)
  464. def process_action_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
  465. return title, target
  466. def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
  467. conf_name, slug = target.partition('.')[::2]
  468. if not slug:
  469. conf_name, slug = 'kitty', conf_name
  470. full_name = f'{conf_name}.{slug}'
  471. try:
  472. target, stitle = shortcut_slugs[full_name]
  473. except KeyError:
  474. logger.warning(f'Unknown shortcut: {target}', location=refnode)
  475. else:
  476. if not has_explicit_title:
  477. title = stitle
  478. return title, target
  479. def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
  480. app.add_lexer('conf', ConfLexer() if version_info[0] < 3 else ConfLexer)
  481. app.add_object_type(
  482. 'opt', 'opt',
  483. indextemplate="pair: %s; Config Setting",
  484. parse_node=parse_opt_node,
  485. )
  486. # Warn about opt references that could not be resolved
  487. opt_role = app.registry.domain_roles['std']['opt']
  488. opt_role.warn_dangling = True
  489. opt_role.process_link = process_opt_link
  490. app.add_object_type(
  491. 'shortcut', 'sc',
  492. indextemplate="pair: %s; Keyboard Shortcut",
  493. parse_node=parse_shortcut_node,
  494. )
  495. sc_role = app.registry.domain_roles['std']['sc']
  496. sc_role.warn_dangling = True
  497. sc_role.process_link = process_shortcut_link
  498. shortcut_slugs.clear()
  499. app.add_object_type(
  500. 'action', 'ac',
  501. indextemplate="pair: %s; Action",
  502. parse_node=parse_action_node,
  503. )
  504. ac_role = app.registry.domain_roles['std']['ac']
  505. ac_role.warn_dangling = True
  506. ac_role.process_link = process_action_link
  507. def generate_default_config(definition: Definition, name: str) -> None:
  508. with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
  509. print('.. highlight:: conf\n', file=f)
  510. f.write('\n'.join(definition.as_rst(name, shortcut_slugs)))
  511. conf_name = re.sub(r'^kitten-', '', name) + '.conf'
  512. with open(f'generated/conf/{conf_name}', 'w', encoding='utf-8') as f:
  513. text = '\n'.join(definition.as_conf(commented=True))
  514. print(text, file=f)
  515. from kitty.options.definition import definition
  516. generate_default_config(definition, 'kitty')
  517. from kittens.runner import get_kitten_conf_docs
  518. for kitten in all_kitten_names:
  519. defn = get_kitten_conf_docs(kitten)
  520. if defn is not None:
  521. generate_default_config(defn, f'kitten-{kitten}')
  522. from kitty.actions import as_rst
  523. with open('generated/actions.rst', 'w', encoding='utf-8') as f:
  524. f.write(as_rst())
  525. from kitty.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION
  526. with open('generated/matching.rst', 'w') as f:
  527. print('Matching windows', file=f)
  528. print('______________________________', file=f)
  529. w = 'm' + MATCH_WINDOW_OPTION[MATCH_WINDOW_OPTION.find('Match') + 1:]
  530. print('When matching windows,', w, file=f)
  531. print('Matching tabs', file=f)
  532. print('______________________________', file=f)
  533. w = 'm' + MATCH_TAB_OPTION[MATCH_TAB_OPTION.find('Match') + 1:]
  534. print('When matching tabs,', w, file=f)
  535. # }}}
  536. def add_html_context(app: Any, pagename: str, templatename: str, context: Any, doctree: Any, *args: Any) -> None:
  537. context['analytics_id'] = app.config.analytics_id
  538. if 'toctree' in context:
  539. # this is needed with furo to use all titles from pages
  540. # in the sidebar (global) toc
  541. original_toctee_function = context['toctree']
  542. def include_sub_headings(**kwargs: Any) -> Any:
  543. kwargs['titles_only'] = False
  544. return original_toctee_function(**kwargs)
  545. context['toctree'] = include_sub_headings
  546. @lru_cache
  547. def monkeypatch_man_writer() -> None:
  548. '''
  549. Monkeypatch the docutils man translator to be nicer
  550. '''
  551. from docutils.nodes import Element
  552. from docutils.writers.manpage import Table, Translator
  553. from sphinx.writers.manpage import ManualPageTranslator
  554. # Generate nicer tables https://sourceforge.net/p/docutils/bugs/475/
  555. class PatchedTable(Table): # type: ignore
  556. _options: list[str]
  557. def __init__(self) -> None:
  558. super().__init__()
  559. self.needs_border_removal = self._options == ['center']
  560. if self.needs_border_removal:
  561. self._options = ['box', 'center']
  562. def as_list(self) -> list[str]:
  563. ans: list[str] = super().as_list()
  564. if self.needs_border_removal:
  565. # remove side and top borders as we use box in self._options
  566. ans[2] = ans[2][1:]
  567. a, b = ans[2].rpartition('|')[::2]
  568. ans[2] = a + b
  569. if ans[3] == '_\n':
  570. del ans[3] # top border
  571. del ans[-2] # bottom border
  572. return ans
  573. def visit_table(self: ManualPageTranslator, node: object) -> None:
  574. setattr(self, '_active_table', PatchedTable())
  575. setattr(ManualPageTranslator, 'visit_table', visit_table)
  576. # Improve header generation
  577. def header(self: ManualPageTranslator) -> str:
  578. di = getattr(self, '_docinfo')
  579. di['ktitle'] = di['title'].replace('_', '-')
  580. th = (".TH \"%(ktitle)s\" %(manual_section)s"
  581. " \"%(date)s\" \"%(version)s\"") % di
  582. if di["manual_group"]:
  583. th += " \"%(manual_group)s\"" % di
  584. th += "\n"
  585. sh_tmpl: str = (".SH Name\n"
  586. "%(ktitle)s \\- %(subtitle)s\n")
  587. return th + sh_tmpl % di # type: ignore
  588. setattr(ManualPageTranslator, 'header', header)
  589. def visit_image(self: ManualPageTranslator, node: Element) -> None:
  590. pass
  591. def depart_image(self: ManualPageTranslator, node: Element) -> None:
  592. pass
  593. def depart_figure(self: ManualPageTranslator, node: Element) -> None:
  594. self.body.append(' (images not supported)\n')
  595. Translator.depart_figure(self, node)
  596. setattr(ManualPageTranslator, 'visit_image', visit_image)
  597. setattr(ManualPageTranslator, 'depart_image', depart_image)
  598. setattr(ManualPageTranslator, 'depart_figure', depart_figure)
  599. orig_astext = Translator.astext
  600. def astext(self: Translator) -> Any:
  601. b = []
  602. for line in self.body:
  603. if line.startswith('.SH'):
  604. x, y = line.split(' ', 1)
  605. parts = y.splitlines(keepends=True)
  606. parts[0] = parts[0].capitalize()
  607. line = x + ' ' + '\n'.join(parts)
  608. b.append(line)
  609. self.body = b
  610. return orig_astext(self)
  611. setattr(Translator, 'astext', astext)
  612. def setup_man_pages() -> None:
  613. from kittens.runner import get_kitten_cli_docs
  614. base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  615. for x in glob.glob(os.path.join(base, 'docs/kittens/*.rst')):
  616. kn = os.path.basename(x).rpartition('.')[0]
  617. if kn in ('custom', 'developing-builtin-kittens'):
  618. continue
  619. cd = get_kitten_cli_docs(kn) or {}
  620. khn = kn.replace('_', '-')
  621. man_pages.append((f'kittens/{kn}', 'kitten-' + khn, cd.get('short_desc', 'kitten Documentation'), [author], 1))
  622. monkeypatch_man_writer()
  623. def build_extra_man_pages() -> None:
  624. base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  625. kitten = os.environ.get('KITTEN_EXE_FOR_DOCS', os.path.join(base, 'kitty/launcher/kitten'))
  626. if not os.path.exists(kitten):
  627. kitten = os.path.join(base, 'kitty/launcher/kitty.app/Contents/MacOS/kitten')
  628. if not os.path.exists(kitten):
  629. subprocess.call(['find', os.path.join(base, 'kitty/launcher')])
  630. raise Exception(f'The kitten binary {kitten} is not built cannot generate man pages')
  631. raw = subprocess.check_output([kitten, '-h']).decode()
  632. started = 0
  633. names = set()
  634. for line in raw.splitlines():
  635. if line.strip() == '@':
  636. started = len(line.rstrip()[:-1])
  637. q = line.strip()
  638. if started and len(q.split()) == 1 and not q.startswith('-') and ':' not in q:
  639. if len(line) - len(line.lstrip()) == started:
  640. if not os.path.exists(os.path.join(base, f'docs/kittens/{q}.rst')):
  641. names.add(q)
  642. cwd = os.path.join(base, 'docs/_build/man')
  643. subprocess.check_call([kitten, '__generate_man_pages__'], cwd=cwd)
  644. subprocess.check_call([kitten, '__generate_man_pages__'] + list(names), cwd=cwd)
  645. if building_man_pages:
  646. setup_man_pages()
  647. def build_finished(*a: Any, **kw: Any) -> None:
  648. if building_man_pages:
  649. build_extra_man_pages()
  650. def setup(app: Any) -> None:
  651. os.makedirs('generated/conf', exist_ok=True)
  652. from kittens.runner import all_kitten_names
  653. kn = all_kitten_names()
  654. write_cli_docs(kn)
  655. write_remote_control_protocol_docs()
  656. write_color_names_table()
  657. write_conf_docs(app, kn)
  658. app.add_config_value('string_replacements', {}, True)
  659. app.connect('source-read', replace_string)
  660. app.add_config_value('analytics_id', '', 'env')
  661. app.connect('html-page-context', add_html_context)
  662. app.connect('build-finished', build_finished)
  663. app.add_lexer('session', SessionLexer() if version_info[0] < 3 else SessionLexer)
  664. app.add_role('link', link_role)
  665. app.add_role('commit', commit_role)