conf.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  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. p('\n\n' + option_spec_as_rst(
  243. data['options'], message=data['help_text'], usage=data['usage'], appname=f'kitty +kitten {kitten}',
  244. heading_char='^'))
  245. # }}}
  246. def write_remote_control_protocol_docs() -> None: # {{{
  247. from kitty.rc.base import RemoteCommand, all_command_names, command_for_name
  248. field_pat = re.compile(r'\s*([^:]+?)\s*:\s*(.+)')
  249. def format_cmd(p: Callable[..., None], name: str, cmd: RemoteCommand) -> None:
  250. p(name)
  251. p('-' * 80)
  252. lines = (cmd.__doc__ or '').strip().splitlines()
  253. fields = []
  254. for line in lines:
  255. m = field_pat.match(line)
  256. if m is None:
  257. p(line)
  258. else:
  259. fields.append((m.group(1).split('/')[0], m.group(2)))
  260. if fields:
  261. p('\nFields are:\n')
  262. for (name, desc) in fields:
  263. if '+' in name:
  264. title = name.replace('+', ' (required)')
  265. else:
  266. title = name
  267. defval = cmd.get_default(name.replace('-', '_'), cmd)
  268. if defval is not cmd:
  269. title = f'{title} (default: {defval})'
  270. else:
  271. title = f'{title} (optional)'
  272. p(f':code:`{title}`')
  273. p(' ', desc), p()
  274. p(), p()
  275. with open('generated/rc.rst', 'w') as f:
  276. p = partial(print, file=f)
  277. for name in sorted(all_command_names()):
  278. cmd = command_for_name(name)
  279. if not cmd.__doc__:
  280. continue
  281. name = name.replace('_', '-')
  282. format_cmd(p, name, cmd)
  283. # }}}
  284. def replace_string(app: Any, docname: str, source: List[str]) -> None: # {{{
  285. src = source[0]
  286. for k, v in app.config.string_replacements.items():
  287. src = src.replace(k, v)
  288. source[0] = src
  289. # }}}
  290. # config file docs {{{
  291. class ConfLexer(RegexLexer): # type: ignore
  292. name = 'Conf'
  293. aliases = ['conf']
  294. filenames = ['*.conf']
  295. def map_flags(self: RegexLexer, val: str, start_pos: int) -> Iterator[Tuple[int, Any, str]]:
  296. expecting_arg = ''
  297. s = Shlex(val)
  298. from kitty.options.utils import allowed_key_map_options
  299. last_pos = 0
  300. while (tok := s.next_word())[0] > -1:
  301. x = tok[1]
  302. if tok[0] > last_pos:
  303. yield start_pos + last_pos, Whitespace, ' ' * (tok[0] - last_pos)
  304. last_pos = tok[0] + len(x)
  305. tok_start = start_pos + tok[0]
  306. if expecting_arg:
  307. yield tok_start, String, x
  308. expecting_arg = ''
  309. elif x.startswith('--'):
  310. expecting_arg = x[2:]
  311. k, sep, v = expecting_arg.partition('=')
  312. k = k.replace('-', '_')
  313. expecting_arg = k
  314. if expecting_arg not in allowed_key_map_options:
  315. yield tok_start, Error, x
  316. elif sep == '=':
  317. expecting_arg = ''
  318. yield tok_start, Name, x
  319. else:
  320. yield tok_start, Name, x
  321. else:
  322. break
  323. def mapargs(self: RegexLexer, match: 're.Match[str]') -> Iterator[Tuple[int, Any, str]]:
  324. start_pos = match.start()
  325. val = match.group()
  326. parts = val.split(maxsplit=1)
  327. if parts[0].startswith('--'):
  328. seen = 0
  329. for (pos, token, text) in self.map_flags(val, start_pos):
  330. yield pos, token, text
  331. seen += len(text)
  332. start_pos += seen
  333. val = val[seen:]
  334. parts = val.split(maxsplit=1)
  335. if not val:
  336. return
  337. yield start_pos, Literal, parts[0] # key spec
  338. if len(parts) == 1:
  339. return
  340. start_pos += len(parts[0])
  341. val = val[len(parts[0]):]
  342. m = re.match(r'(\s+)(\S+)', val)
  343. if m is None:
  344. return
  345. yield start_pos, Whitespace, m.group(1)
  346. yield start_pos + m.start(2), Name.Function, m.group(2) # action function
  347. yield start_pos + m.end(2), String, val[m.end(2):]
  348. tokens = {
  349. 'root': [
  350. (r'#.*?$', Comment.Single),
  351. (r'\s+$', Whitespace),
  352. (r'\s+', Whitespace),
  353. (r'(include)(\s+)(.+?)$', bygroups(Comment.Preproc, Whitespace, Name.Namespace)),
  354. (r'(map)(\s+)', bygroups(
  355. Keyword.Declaration, Whitespace), 'mapargs'),
  356. (r'(mouse_map)(\s+)(\S+)(\s+)(\S+)(\s+)(\S+)(\s+)', bygroups(
  357. Keyword.Declaration, Whitespace, String, Whitespace, Name.Variable, Whitespace, String, Whitespace), 'action'),
  358. (r'(symbol_map)(\s+)(\S+)(\s+)(.+?)$', bygroups(
  359. Keyword.Declaration, Whitespace, String, Whitespace, Literal)),
  360. (r'([a-zA-Z_0-9]+)(\s+)', bygroups(
  361. Name.Variable, Whitespace), 'args'),
  362. ],
  363. 'action': [
  364. (r'[a-z_0-9]+$', Name.Function, 'root'),
  365. (r'[a-z_0-9]+', Name.Function, 'args'),
  366. ],
  367. 'mapargs': [
  368. (r'.+$', mapargs, 'root'),
  369. ],
  370. 'args': [
  371. (r'\s+', Whitespace, 'args'),
  372. (r'\b(yes|no)\b$', Number.Bin, 'root'),
  373. (r'\b(yes|no)\b', Number.Bin, 'args'),
  374. (r'[+-]?[0-9]+\s*$', Number.Integer, 'root'),
  375. (r'[+-]?[0-9.]+\s*$', Number.Float, 'root'),
  376. (r'[+-]?[0-9]+', Number.Integer, 'args'),
  377. (r'[+-]?[0-9.]+', Number.Float, 'args'),
  378. (r'#[a-fA-F0-9]{3,6}\s*$', String, 'root'),
  379. (r'#[a-fA-F0-9]{3,6}\s*', String, 'args'),
  380. (r'.+', String, 'root'),
  381. ],
  382. }
  383. class SessionLexer(RegexLexer): # type: ignore
  384. name = 'Session'
  385. aliases = ['session']
  386. filenames = ['*.session']
  387. tokens = {
  388. 'root': [
  389. (r'#.*?$', Comment.Single),
  390. (r'[a-z][a-z0-9_]+', Name.Function, 'args'),
  391. ],
  392. 'args': [
  393. (r'.*?$', Literal, 'root'),
  394. ]
  395. }
  396. def link_role(
  397. name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []
  398. ) -> Tuple[List[nodes.reference], List[nodes.problematic]]:
  399. text = text.replace('\n', ' ')
  400. m = re.match(r'(.+)\s+<(.+?)>', text)
  401. if m is None:
  402. msg = inliner.reporter.error(f'link "{text}" not recognized', line=lineno)
  403. prb = inliner.problematic(rawtext, rawtext, msg)
  404. return [prb], [msg]
  405. text, url = m.group(1, 2)
  406. url = url.replace(' ', '')
  407. set_classes(options)
  408. node = nodes.reference(rawtext, text, refuri=url, **options)
  409. return [node], []
  410. opt_aliases: Dict[str, str] = {}
  411. shortcut_slugs: Dict[str, Tuple[str, str]] = {}
  412. def parse_opt_node(env: Any, sig: str, signode: Any) -> str:
  413. """Transform an option description into RST nodes."""
  414. count = 0
  415. firstname = ''
  416. for potential_option in sig.split(', '):
  417. optname = potential_option.strip()
  418. if count:
  419. signode += addnodes.desc_addname(', ', ', ')
  420. text = optname.split('.', 1)[-1]
  421. signode += addnodes.desc_name(text, text)
  422. if not count:
  423. firstname = optname
  424. signode['allnames'] = [optname]
  425. else:
  426. signode['allnames'].append(optname)
  427. opt_aliases[optname] = firstname
  428. count += 1
  429. if not firstname:
  430. raise ValueError(f'{sig} is not a valid opt')
  431. return firstname
  432. def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str:
  433. """Transform a shortcut description into RST nodes."""
  434. conf_name, text = sig.split('.', 1)
  435. signode += addnodes.desc_name(text, text)
  436. return sig
  437. def parse_action_node(env: Any, sig: str, signode: Any) -> str:
  438. """Transform an action description into RST nodes."""
  439. signode += addnodes.desc_name(sig, sig)
  440. return sig
  441. def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
  442. conf_name, opt = target.partition('.')[::2]
  443. if not opt:
  444. conf_name, opt = 'kitty', conf_name
  445. full_name = f'{conf_name}.{opt}'
  446. return title, opt_aliases.get(full_name, full_name)
  447. def process_action_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
  448. return title, target
  449. def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
  450. conf_name, slug = target.partition('.')[::2]
  451. if not slug:
  452. conf_name, slug = 'kitty', conf_name
  453. full_name = f'{conf_name}.{slug}'
  454. try:
  455. target, stitle = shortcut_slugs[full_name]
  456. except KeyError:
  457. logger.warning(f'Unknown shortcut: {target}', location=refnode)
  458. else:
  459. if not has_explicit_title:
  460. title = stitle
  461. return title, target
  462. def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
  463. app.add_lexer('conf', ConfLexer() if version_info[0] < 3 else ConfLexer)
  464. app.add_object_type(
  465. 'opt', 'opt',
  466. indextemplate="pair: %s; Config Setting",
  467. parse_node=parse_opt_node,
  468. )
  469. # Warn about opt references that could not be resolved
  470. opt_role = app.registry.domain_roles['std']['opt']
  471. opt_role.warn_dangling = True
  472. opt_role.process_link = process_opt_link
  473. app.add_object_type(
  474. 'shortcut', 'sc',
  475. indextemplate="pair: %s; Keyboard Shortcut",
  476. parse_node=parse_shortcut_node,
  477. )
  478. sc_role = app.registry.domain_roles['std']['sc']
  479. sc_role.warn_dangling = True
  480. sc_role.process_link = process_shortcut_link
  481. shortcut_slugs.clear()
  482. app.add_object_type(
  483. 'action', 'ac',
  484. indextemplate="pair: %s; Action",
  485. parse_node=parse_action_node,
  486. )
  487. ac_role = app.registry.domain_roles['std']['ac']
  488. ac_role.warn_dangling = True
  489. ac_role.process_link = process_action_link
  490. def generate_default_config(definition: Definition, name: str) -> None:
  491. with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
  492. print('.. highlight:: conf\n', file=f)
  493. f.write('\n'.join(definition.as_rst(name, shortcut_slugs)))
  494. conf_name = re.sub(r'^kitten-', '', name) + '.conf'
  495. with open(f'generated/conf/{conf_name}', 'w', encoding='utf-8') as f:
  496. text = '\n'.join(definition.as_conf(commented=True))
  497. print(text, file=f)
  498. from kitty.options.definition import definition
  499. generate_default_config(definition, 'kitty')
  500. from kittens.runner import get_kitten_conf_docs
  501. for kitten in all_kitten_names:
  502. defn = get_kitten_conf_docs(kitten)
  503. if defn is not None:
  504. generate_default_config(defn, f'kitten-{kitten}')
  505. from kitty.actions import as_rst
  506. with open('generated/actions.rst', 'w', encoding='utf-8') as f:
  507. f.write(as_rst())
  508. from kitty.rc.base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION
  509. with open('generated/matching.rst', 'w') as f:
  510. print('Matching windows', file=f)
  511. print('______________________________', file=f)
  512. w = 'm' + MATCH_WINDOW_OPTION[MATCH_WINDOW_OPTION.find('Match') + 1:]
  513. print('When matching windows,', w, file=f)
  514. print('Matching tabs', file=f)
  515. print('______________________________', file=f)
  516. w = 'm' + MATCH_TAB_OPTION[MATCH_TAB_OPTION.find('Match') + 1:]
  517. print('When matching tabs,', w, file=f)
  518. # }}}
  519. def add_html_context(app: Any, pagename: str, templatename: str, context: Any, doctree: Any, *args: Any) -> None:
  520. context['analytics_id'] = app.config.analytics_id
  521. if 'toctree' in context:
  522. # this is needed with furo to use all titles from pages
  523. # in the sidebar (global) toc
  524. original_toctee_function = context['toctree']
  525. def include_sub_headings(**kwargs: Any) -> Any:
  526. kwargs['titles_only'] = False
  527. return original_toctee_function(**kwargs)
  528. context['toctree'] = include_sub_headings
  529. @lru_cache
  530. def monkeypatch_man_writer() -> None:
  531. '''
  532. Monkeypatch the docutils man translator to be nicer
  533. '''
  534. from docutils.nodes import Element
  535. from docutils.writers.manpage import Table, Translator
  536. from sphinx.writers.manpage import ManualPageTranslator
  537. # Generate nicer tables https://sourceforge.net/p/docutils/bugs/475/
  538. class PatchedTable(Table): # type: ignore
  539. _options: list[str]
  540. def __init__(self) -> None:
  541. super().__init__()
  542. self.needs_border_removal = self._options == ['center']
  543. if self.needs_border_removal:
  544. self._options = ['box', 'center']
  545. def as_list(self) -> list[str]:
  546. ans: list[str] = super().as_list()
  547. if self.needs_border_removal:
  548. # remove side and top borders as we use box in self._options
  549. ans[2] = ans[2][1:]
  550. a, b = ans[2].rpartition('|')[::2]
  551. ans[2] = a + b
  552. if ans[3] == '_\n':
  553. del ans[3] # top border
  554. del ans[-2] # bottom border
  555. return ans
  556. def visit_table(self: ManualPageTranslator, node: object) -> None:
  557. setattr(self, '_active_table', PatchedTable())
  558. setattr(ManualPageTranslator, 'visit_table', visit_table)
  559. # Improve header generation
  560. def header(self: ManualPageTranslator) -> str:
  561. di = getattr(self, '_docinfo')
  562. di['ktitle'] = di['title'].replace('_', '-')
  563. th = (".TH \"%(ktitle)s\" %(manual_section)s"
  564. " \"%(date)s\" \"%(version)s\"") % di
  565. if di["manual_group"]:
  566. th += " \"%(manual_group)s\"" % di
  567. th += "\n"
  568. sh_tmpl: str = (".SH Name\n"
  569. "%(ktitle)s \\- %(subtitle)s\n")
  570. return th + sh_tmpl % di # type: ignore
  571. setattr(ManualPageTranslator, 'header', header)
  572. def visit_image(self: ManualPageTranslator, node: Element) -> None:
  573. pass
  574. def depart_image(self: ManualPageTranslator, node: Element) -> None:
  575. pass
  576. def depart_figure(self: ManualPageTranslator, node: Element) -> None:
  577. self.body.append(' (images not supported)\n')
  578. Translator.depart_figure(self, node)
  579. setattr(ManualPageTranslator, 'visit_image', visit_image)
  580. setattr(ManualPageTranslator, 'depart_image', depart_image)
  581. setattr(ManualPageTranslator, 'depart_figure', depart_figure)
  582. orig_astext = Translator.astext
  583. def astext(self: Translator) -> Any:
  584. b = []
  585. for line in self.body:
  586. if line.startswith('.SH'):
  587. x, y = line.split(' ', 1)
  588. parts = y.splitlines(keepends=True)
  589. parts[0] = parts[0].capitalize()
  590. line = x + ' ' + '\n'.join(parts)
  591. b.append(line)
  592. self.body = b
  593. return orig_astext(self)
  594. setattr(Translator, 'astext', astext)
  595. def setup_man_pages() -> None:
  596. from kittens.runner import get_kitten_cli_docs
  597. base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  598. for x in glob.glob(os.path.join(base, 'docs/kittens/*.rst')):
  599. kn = os.path.basename(x).rpartition('.')[0]
  600. if kn == 'custom':
  601. continue
  602. cd = get_kitten_cli_docs(kn) or {}
  603. khn = kn.replace('_', '-')
  604. man_pages.append((f'kittens/{kn}', 'kitten-' + khn, cd.get('short_desc', 'kitten Documentation'), [author], 1))
  605. monkeypatch_man_writer()
  606. def build_extra_man_pages() -> None:
  607. base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  608. kitten = os.environ.get('KITTEN_EXE_FOR_DOCS', os.path.join(base, 'kitty/launcher/kitten'))
  609. if not os.path.exists(kitten):
  610. kitten = os.path.join(base, 'kitty/launcher/kitty.app/Contents/MacOS/kitten')
  611. if not os.path.exists(kitten):
  612. subprocess.call(['find', os.path.join(base, 'kitty/launcher')])
  613. raise Exception(f'The kitten binary {kitten} is not built cannot generate man pages')
  614. raw = subprocess.check_output([kitten, '-h']).decode()
  615. started = 0
  616. names = set()
  617. for line in raw.splitlines():
  618. if line.strip() == '@':
  619. started = len(line.rstrip()[:-1])
  620. q = line.strip()
  621. if started and len(q.split()) == 1 and not q.startswith('-') and ':' not in q:
  622. if len(line) - len(line.lstrip()) == started:
  623. if not os.path.exists(os.path.join(base, f'docs/kittens/{q}.rst')):
  624. names.add(q)
  625. cwd = os.path.join(base, 'docs/_build/man')
  626. subprocess.check_call([kitten, '__generate_man_pages__'], cwd=cwd)
  627. subprocess.check_call([kitten, '__generate_man_pages__'] + list(names), cwd=cwd)
  628. if building_man_pages:
  629. setup_man_pages()
  630. def build_finished(*a: Any, **kw: Any) -> None:
  631. if building_man_pages:
  632. build_extra_man_pages()
  633. def setup(app: Any) -> None:
  634. os.makedirs('generated/conf', exist_ok=True)
  635. from kittens.runner import all_kitten_names
  636. kn = all_kitten_names()
  637. write_cli_docs(kn)
  638. write_remote_control_protocol_docs()
  639. write_conf_docs(app, kn)
  640. app.add_config_value('string_replacements', {}, True)
  641. app.connect('source-read', replace_string)
  642. app.add_config_value('analytics_id', '', 'env')
  643. app.connect('html-page-context', add_html_context)
  644. app.connect('build-finished', build_finished)
  645. app.add_lexer('session', SessionLexer() if version_info[0] < 3 else SessionLexer)
  646. app.add_role('link', link_role)
  647. app.add_role('commit', commit_role)