diff --git a/docs/conf.py b/docs/conf.py index c89529e98..80f5447ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,10 +32,9 @@ kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if kitty_src not in sys.path: sys.path.insert(0, kitty_src) -from kitty.conf.definition import Option, Shortcut, OptionOrAction, MouseAction # noqa +from kitty.conf.types import Definition # noqa from kitty.constants import str_version # noqa - # config {{{ # -- Project information ----------------------------------------------------- @@ -248,8 +247,8 @@ def add_html_context(app: Any, pagename: str, templatename: str, context: Any, * # CLI docs {{{ def write_cli_docs(all_kitten_names: Iterable[str]) -> None: - from kitty.launch import options_spec as launch_options_spec from kitty.cli import option_spec_as_rst + from kitty.launch import options_spec as launch_options_spec with open('generated/launch.rst', 'w') as f: f.write(option_spec_as_rst( appname='launch', ospec=launch_options_spec, heading_char='_', @@ -264,7 +263,7 @@ if you specify a program-to-run you can use the special placeholder 'kitty --to', 'kitty @ --to')) as_rst = partial(option_spec_as_rst, heading_char='_') from kitty.rc.base import all_command_names, command_for_name - from kitty.remote_control import global_options_spec, cli_msg + from kitty.remote_control import cli_msg, global_options_spec with open('generated/cli-kitty-at.rst', 'w') as f: p = partial(print, file=f) p('kitty @\n' + '-' * 80) @@ -293,7 +292,9 @@ if you specify a program-to-run you can use the special placeholder def write_remote_control_protocol_docs() -> None: # {{{ - from kitty.rc.base import all_command_names, command_for_name, RemoteCommand + from kitty.rc.base import ( + RemoteCommand, all_command_names, command_for_name + ) field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)') def format_cmd(p: Callable, name: str, cmd: RemoteCommand) -> None: @@ -447,87 +448,6 @@ def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str: return sig -def render_conf(conf_name: str, all_options: Iterable[OptionOrAction]) -> str: - from kitty.conf.definition import merged_opts, Group - ans = ['.. default-domain:: conf', ''] - a = ans.append - current_group: Optional[Group] = None - all_options_ = list(all_options) - kitty_mod = 'kitty_mod' - - def render_group(group: Group) -> None: - a('') - a(f'.. _conf-{conf_name}-{group.name}:') - a('') - a(group.short_text) - heading_level = '+' if '.' in group.name else '^' - a(heading_level * (len(group.short_text) + 20)) - a('') - if group.start_text: - a(group.start_text) - a('') - - def handle_group_end(group: Group) -> None: - if group.end_text: - assert current_group is not None - a(''), a(current_group.end_text) - - def handle_group(new_group: Group, new_group_is_shortcut: bool = False) -> None: - nonlocal current_group - if new_group is not current_group: - if current_group: - handle_group_end(current_group) - current_group = new_group - render_group(current_group) - - def handle_option(i: int, opt: Option) -> None: - nonlocal kitty_mod - if not opt.long_text or not opt.add_to_docs: - return - handle_group(opt.group) - if opt.name == 'kitty_mod': - kitty_mod = opt.defval_as_string - mopts = list(merged_opts(all_options_, opt, i)) - a('.. opt:: ' + ', '.join(conf_name + '.' + mo.name for mo in mopts)) - a('.. code-block:: conf') - a('') - sz = max(len(x.name) for x in mopts) - for mo in mopts: - a((' {:%ds} {}' % sz).format(mo.name, mo.defval_as_string)) - a('') - if opt.long_text: - a(expand_opt_references(conf_name, opt.long_text)) - a('') - - def handle_shortcuts(shortcuts: Sequence[Union[Shortcut, MouseAction]]) -> None: - sc = shortcuts[0] - handle_group(sc.group, True) - sc_text = f'{conf_name}.{sc.short_text}' - a('.. shortcut:: ' + sc_text) - shortcuts = [s for s in shortcuts if s.add_to_default] - shortcut_slugs[f'{conf_name}.{sc.name}'] = (sc_text, sc.key.replace('kitty_mod', kitty_mod)) - if shortcuts: - a('.. code-block:: conf') - a('') - for x in shortcuts: - if x.add_to_default: - a(' ' + x.line.replace('kitty_mod', kitty_mod)) - a('') - if sc.long_text: - a(expand_opt_references(conf_name, sc.long_text)) - a('') - - for i, opt in enumerate(all_options_): - if isinstance(opt, Option): - handle_option(i, opt) - else: - handle_shortcuts(opt) - - if current_group: - handle_group_end(current_group) - return '\n'.join(ans) - - def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: conf_name, opt = target.partition('.')[::2] if not opt: @@ -572,25 +492,24 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None: sc_role.warn_dangling = True sc_role.process_link = process_shortcut_link - def generate_default_config(all_options: Dict[str, OptionOrAction], name: str) -> None: - from kitty.conf.definition import as_conf_file + def generate_default_config(definition: Definition, name: str) -> None: with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f: print('.. highlight:: conf\n', file=f) - f.write(render_conf(name, all_options.values())) + f.write('\n'.join(definition.as_rst(name, shortcut_slugs))) conf_name = re.sub(r'^kitten-', '', name) + '.conf' with open(f'generated/conf/{conf_name}', 'w', encoding='utf-8') as f: - text = '\n'.join(as_conf_file(all_options.values())) + text = '\n'.join(definition.as_conf()) print(text, file=f) - from kitty.config_data import all_options - generate_default_config(all_options, 'kitty') + from kitty.options.definition import definition + generate_default_config(definition, 'kitty') from kittens.runner import get_kitten_conf_docs for kitten in all_kitten_names: - all_options = get_kitten_conf_docs(kitten) - if all_options: - generate_default_config(all_options, f'kitten-{kitten}') + definition = get_kitten_conf_docs(kitten) + if definition: + generate_default_config(definition, f'kitten-{kitten}') # }}} diff --git a/kittens/runner.py b/kittens/runner.py index 5de091763..4bac39847 100644 --- a/kittens/runner.py +++ b/kittens/runner.py @@ -7,11 +7,15 @@ import importlib import os import sys from functools import partial -from typing import Any, Dict, FrozenSet, List +from typing import Any, Dict, FrozenSet, List, TYPE_CHECKING, cast from kitty.types import run_once aliases = {'url_hints': 'hints'} +if TYPE_CHECKING: + from kitty.conf.types import Definition +else: + Definition = object def resolved_kitten(k: str) -> str: @@ -151,12 +155,12 @@ def get_kitten_cli_docs(kitten: str) -> Any: return ans -def get_kitten_conf_docs(kitten: str) -> Any: - setattr(sys, 'all_options', None) +def get_kitten_conf_docs(kitten: str) -> Definition: + setattr(sys, 'options_definition', None) run_kitten(kitten, run_name='__conf__') - ans = getattr(sys, 'all_options') - delattr(sys, 'all_options') - return ans + ans = getattr(sys, 'options_definition') + delattr(sys, 'options_definition') + return cast(Definition, ans) def main() -> None: diff --git a/kitty/conf/types.py b/kitty/conf/types.py index 01bd7f50e..4fa14869a 100644 --- a/kitty/conf/types.py +++ b/kitty/conf/types.py @@ -27,6 +27,19 @@ class Unset: unset = Unset() +def expand_opt_references(conf_name: str, text: str) -> str: + conf_name += '.' + + def expand(m: Match) -> str: + ref = m.group(1) + if '<' not in ref and '.' not in ref: + full_ref = conf_name + ref + return ':opt:`{} <{}>`'.format(ref, full_ref) + return str(m.group()) + + return re.sub(r':opt:`(.+?)`', expand, text) + + def remove_markup(text: str) -> str: def sub(m: Match) -> str: @@ -122,6 +135,27 @@ class Option: a('') return ans + def as_rst( + self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]], + kitty_mod: str, level: int = 0, option_group: List['Option'] = [] + ) -> List[str]: + ans: List[str] = [] + a = ans.append + if not self.documented: + return ans + mopts = [self] + option_group + a('.. opt:: ' + ', '.join(conf_name + '.' + mo.name for mo in mopts)) + a('.. code-block:: conf') + a('') + sz = max(len(x.name) for x in mopts) + for mo in mopts: + a((' {:%ds} {}' % sz).format(mo.name, mo.defval_as_string)) + a('') + if self.long_text: + a(expand_opt_references(conf_name, self.long_text)) + a('') + return ans + class MultiVal: @@ -160,17 +194,38 @@ class MultiOption: a('') return ans + def as_rst(self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]], kitty_mod: str, level: int = 0) -> List[str]: + ans: List[str] = [] + a = ans.append + a(f'.. opt:: {conf_name}.{self.name}') + a('.. code-block:: conf') + a('') + for k in self.items: + if k.documented: + a(f' {self.name:s} {k.defval_as_str}') + a('') + if self.long_text: + a(expand_opt_references(conf_name, self.long_text)) + a('') + return ans + class Mapping: add_to_default: bool + short_text: str long_text: str documented: bool setting_name: str + name: str @property def parseable_text(self) -> str: return '' + @property + def key_text(self) -> str: + return '' + def as_conf(self, commented: bool = False, level: int = 0) -> List[str]: ans: List[str] = [] if self.documented: @@ -181,6 +236,24 @@ class Mapping: a(''), a(render_block(self.long_text.strip())), a('') return ans + def as_rst(self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]], kitty_mod: str, level: int = 0) -> List[str]: + ans: List[str] = [] + sc_text = f'{conf_name}.{self.short_text}' + shortcut_slugs[f'{conf_name}.{self.name}'] = (sc_text, self.key_text.replace('kitty_mod', kitty_mod)) + if self.documented: + a = ans.append + a('.. shortcut:: ' + sc_text) + if self.add_to_default: + a('.. code-block:: conf') + a('') + a(' ' + self.setting_name + ' ' + self.parseable_text.replace('kitty_mod', kitty_mod)) + a('') + if self.long_text: + a(expand_opt_references(conf_name, self.long_text)) + a('') + + return ans + class ShortcutMapping(Mapping): setting_name: str = 'map' @@ -202,6 +275,10 @@ class ShortcutMapping(Mapping): def parseable_text(self) -> str: return f'{self.key} {self.action_def}' + @property + def key_text(self) -> str: + return self.key + class MouseMapping(Mapping): setting_name: str = 'mouse_map' @@ -226,6 +303,10 @@ class MouseMapping(Mapping): def parseable_text(self) -> str: return f'{self.button} {self.event} {self.modes} {self.action_def}' + @property + def key_text(self) -> str: + return self.button + NonGroups = Union[Option, MultiOption, ShortcutMapping, MouseMapping] GroupItem = Union[NonGroups, 'Group'] @@ -251,6 +332,7 @@ class Group: return len(self.items) def iter_all_with_coalesced_options(self) -> Iterator[Union[Tuple, GroupItem]]: + self.kitty_mod = 'kitty_mod' option_groups = {} current_group: List[Option] = [] coalesced = set() @@ -283,6 +365,42 @@ class Group: else: yield x + def as_rst(self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]], kitty_mod: str = 'kitty_mod', level: int = 0) -> List[str]: + ans: List[str] = [] + a = ans.append + if level: + a('') + a(f'.. _conf-{conf_name}-{self.name}:') + a('') + a(self.title) + heading_level = '+' if level > 1 else '^' + a(heading_level * (len(self.title) + 20)) + a('') + if self.start_text: + a(self.start_text) + a('') + else: + ans.extend(('.. default-domain:: conf', '')) + + if not level: + for opt in self.iter_all_non_groups(): + if isinstance(opt, Option) and opt.name == 'kitty_mod': + kitty_mod = opt.defval_as_string + break + for item in self.iter_all_with_coalesced_options(): + if isinstance(item, tuple): + option, option_group = item + lines = option.as_rst(conf_name, shortcut_slugs, kitty_mod, option_group=option_group) + else: + lines = item.as_rst(conf_name, shortcut_slugs, kitty_mod, level + 1) + ans.extend(lines) + + if level: + if self.end_text: + a('') + a(self.end_text) + return ans + def as_conf(self, commented: bool = False, level: int = 0) -> List[str]: ans: List[str] = [] a = ans.append @@ -470,3 +588,6 @@ class Definition: def as_conf(self, commented: bool = False) -> List[str]: return self.root_group.as_conf(commented) + + def as_rst(self, conf_name: str, shortcut_slugs: Dict[str, Tuple[str, str]]) -> List[str]: + return self.root_group.as_rst(conf_name, shortcut_slugs)