From 9dc9fb2012c857f647729cf52b4c6d15eb0e3601 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 3 Jun 2018 16:07:40 +0530 Subject: [PATCH] Start work on documenting the conf file --- docs/conf.py | 61 ++++++++++++++++- docs/conf.rst | 33 ++++++---- kitty/boss.py | 6 +- kitty/conf/definition.py | 137 +++++++++++++++++++++++++++++++++++++++ kitty/config.py | 25 +++---- kitty/config_data.py | 69 ++++++++++++++++++++ 6 files changed, 296 insertions(+), 35 deletions(-) create mode 100644 kitty/conf/definition.py create mode 100644 kitty/config_data.py diff --git a/docs/conf.py b/docs/conf.py index 0b12e3649..9f33cd188 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,6 +6,7 @@ # full list see the documentation: # http://www.sphinx-doc.org/en/master/config +import os import subprocess from collections import defaultdict from functools import partial @@ -80,7 +81,7 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'generated/cli-*'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'generated/cli-*', 'generated/conf-*'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' @@ -298,8 +299,66 @@ def write_cli_docs(): # }}} +# config file docs {{{ + +def render_group(a, group): + a(group.short_text) + a('^' * (len(group.short_text) + 20)) + a('') + if group.start_text: + a(group.start_text) + a('') + + +def render_conf(ref_prefix, all_options): + from kitty.conf.definition import merged_opts + ans = [] + a = ans.append + current_group = None + all_options = list(all_options) + for i, opt in enumerate(all_options): + if not opt.short_text: + continue + if opt.group is not current_group: + if current_group and current_group.end_text: + a(''), a(current_group.end_text) + current_group = opt.group + render_group(a, current_group) + mopts = list(merged_opts(all_options, opt, i)) + for mo in mopts: + a('.. _conf_{}_{}:'.format(ref_prefix, mo.name)) + a('') + a(opt.short_text) + a('_' * (len(opt.short_text) + 20)) + a('') + a('.. code-block:: ini') + 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(opt.long_text) + a('') + + return '\n'.join(ans) + + +def write_conf_docs(): + from kitty.config_data import all_options + with open('generated/conf-kitty.rst', 'w', encoding='utf-8') as f: + print('.. highlight:: ini\n', file=f) + f.write(render_conf('kitty', all_options.values())) +# }}} + + def setup(app): + try: + os.mkdir('generated') + except FileExistsError: + pass write_cli_docs() + write_conf_docs() app.add_role('iss', partial(num_role, 'issues')) app.add_role('pull', partial(num_role, 'pull')) app.add_role('commit', commit_role) diff --git a/docs/conf.rst b/docs/conf.rst index 849eca997..30807f7dd 100644 --- a/docs/conf.rst +++ b/docs/conf.rst @@ -1,25 +1,32 @@ +:tocdepth: 2 + Configuring kitty =============================== +.. highlight:: ini + |kitty| is highly customizable, everything from keyboard shortcuts, to painting -frames-per-second. See the heavily commented default config file below for an -overview of all customization possibilities. +frames-per-second. See below for an overview of all customization +possibilities. You can open the config file within kitty by pressing |sc_edit_config_file|. You can also display the current configuration by running ``kitty --debug-config``. - - -.. literalinclude:: ../kitty/kitty.conf - :language: ini - - .. _confloc: |kitty| looks for a config file in the OS config directories (usually -:file:`~/.config/kitty/kitty.conf` and additionally -:file:`~/Library/Preferences/kitty/kitty.conf` on macOS) but you can pass a -specific path via the :option:`kitty --config` option or use the ``KITTY_CONFIG_DIRECTORY`` -environment variable. See the :option:`kitty --config` option -for full details. +:file:`~/.config/kitty/kitty.conf`) but you can pass a specific path via the +:option:`kitty --config` option or use the ``KITTY_CONFIG_DIRECTORY`` +environment variable. See the :option:`kitty --config` option for full details. + +You can include secondary config files via the :code:`include` directive. If +you use a relative path for include, it is resolved with respect to the +location of the current config file. Note that environment variables are +expanded, so :code:`${USER}.conf` becomes :file:`name.conf` if +:code:`USER=name`. For example:: + + include other.conf + + +.. include:: /generated/conf-kitty.rst diff --git a/kitty/boss.py b/kitty/boss.py index a77c40c5b..bb9dbe54e 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -12,10 +12,8 @@ from weakref import WeakValueDictionary from .cli import create_opts, parse_args from .conf.utils import to_cmdline -from .config import ( - MINIMUM_FONT_SIZE, initial_window_size_func, - prepare_config_file_for_editing -) +from .config import initial_window_size_func, prepare_config_file_for_editing +from .config_data import MINIMUM_FONT_SIZE from .constants import ( appname, config_dir, set_boss, supports_primary_selection ) diff --git a/kitty/conf/definition.py b/kitty/conf/definition.py new file mode 100644 index 000000000..8800d1903 --- /dev/null +++ b/kitty/conf/definition.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + +import re +from functools import partial + +from .utils import to_bool + + +def to_string(x): + return x + + +class Group: + + __slots__ = 'name', 'short_text', 'start_text', 'end_text' + + def __init__(self, name, short_text, start_text='', end_text=''): + self.name, self.short_text = name, short_text.strip() + self.start_text, self.end_text = start_text.strip(), end_text.strip() + + +class Option: + + __slots__ = 'name', 'group', 'short_text', 'long_text', 'option_type', 'defval_as_string', 'defval', 'add_to_default' + + def __init__(self, name, group, defval, option_type, short_text, long_text, add_to_default): + self.name, self.group, self.short_text = name, group, short_text + self.long_text, self.option_type = long_text.strip(), option_type + self.defval_as_string, self.defval = defval, option_type(defval) + self.add_to_default = add_to_default + + +def option( + all_options, + group, + name, + defval, + short_text='', + long_text='', + option_type=to_string, + add_to_default=True +): + is_multiple = name.startswith('+') + if is_multiple: + name = name[1:] + defval_type = type(defval) + if defval_type is not str: + if option_type is to_string: + if defval_type is bool: + option_type = to_bool + else: + option_type = defval_type + if defval_type is bool: + defval = 'yes' if defval else 'no' + else: + defval = str(defval) + + key = name + if is_multiple: + key = name + ' ' + defval.partition(' ')[0] + ans = Option(name, group[0], defval, option_type, short_text, long_text, add_to_default) + all_options[key] = ans + return ans + + +def option_func(all_options, all_groups): + all_groups = {k: Group(k, *v) for k, v in all_groups.items()} + group = [None] + + def change_group(name): + group[0] = all_groups[name] + + return partial(option, all_options, group), change_group, all_groups + + +def merged_opts(all_options, opt, i): + yield opt + for k in range(i + 1, len(all_options)): + q = all_options[k] + if not q.short_text: + yield q + else: + break + + +def remove_markup(text): + return re.sub(r':(.+?):`(.+?)`', r'\2', text, flags=re.DOTALL) + + +def render_block(text): + text = remove_markup(text) + lines = text.splitlines() + return '\n'.join('#: ' + line for line in lines) + + +def render_group(a, group): + a('# ' + group.short_text + ' {{{') + a('') + if group.start_text: + a(render_block(group.start_text)) + a('') + + +def as_conf_file(all_options): + ans = [] + a = ans.append + current_group = None + all_options = list(all_options) + for i, opt in enumerate(all_options): + if not opt.short_text: + continue + if opt.group is not current_group: + if current_group: + if current_group.end_text: + a(''), a(current_group.end_text) + a('# }}}') + + current_group = opt.group + render_group(a, current_group) + mopts = list(merged_opts(all_options, opt, i)) + a(render_block(opt.short_text)) + a('') + if opt.long_text: + a(render_block(opt.long_text)) + a('') + sz = max(len(x.name) for x in mopts) + for mo in mopts: + prefix = '' if mo.add_to_default else '# ' + a(('{}{:%ds} {}' % sz).format(prefix, mo.name, mo.defval_as_string)) + a('') + if current_group: + if current_group.end_text: + a(''), a(current_group.end_text) + a('# }}}') + return ans diff --git a/kitty/config.py b/kitty/config.py index 03b07c1d4..86a0b9497 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -10,24 +10,19 @@ from collections import namedtuple from contextlib import contextmanager from . import fast_data_types as defines +from .conf.definition import as_conf_file from .conf.utils import ( init_config, key_func, load_config as _load_config, merge_dicts, parse_config_base, positive_float, positive_int, python_string, to_bool, to_cmdline, to_color, unit_float ) +from .config_data import all_options from .constants import cache_dir, defconf from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE from .layout import all_layouts from .rgb import color_as_int, color_from_int from .utils import log_error -MINIMUM_FONT_SIZE = 4 - - -def to_font_size(x): - return max(MINIMUM_FONT_SIZE, float(x)) - - cshapes = { 'block': CURSOR_BLOCK, 'beam': CURSOR_BEAM, @@ -378,7 +373,6 @@ type_map = { 'scrollback_lines': positive_int, 'scrollback_pager': to_cmdline, 'open_url_with': to_cmdline, - 'font_size': to_font_size, 'focus_follows_mouse': to_bool, 'cursor_shape': to_cursor_shape, 'open_url_modifiers': to_modifiers, @@ -587,15 +581,12 @@ def initial_window_size_func(opts, cached_values): def commented_out_default_config(): - with open(default_config_path, encoding='utf-8', errors='replace') as f: - config = f.read() - lines = [] - for line in config.splitlines(): - if line.strip() and not line.startswith('#'): + ans = [] + for line in as_conf_file(all_options.values()): + if line and line[0] != '#': line = '# ' + line - lines.append(line) - config = '\n'.join(lines) - return config + ans.append(line) + return '\n'.join(ans) def prepare_config_file_for_editing(): @@ -605,7 +596,7 @@ def prepare_config_file_for_editing(): os.makedirs(d) except FileExistsError: pass - with open(defconf, 'w') as f: + with open(defconf, 'w', encoding='utf-8') as f: f.write(commented_out_default_config()) return defconf diff --git a/kitty/config_data.py b/kitty/config_data.py new file mode 100644 index 000000000..61cba88b0 --- /dev/null +++ b/kitty/config_data.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + + +from gettext import gettext as _ + +from .conf.definition import option_func + +MINIMUM_FONT_SIZE = 4 + + +def to_font_size(x): + return max(MINIMUM_FONT_SIZE, float(x)) + + +all_options = {} + +o, g, all_groups = option_func(all_options, { + 'fonts': [ + _('Fonts'), + _('kitty has very powerful font management. You can configure individual\n' + 'font faces and even specify special fonts for particular characters.') + ], +}) +type_map = {o.name: o.option_type for o in all_options.values()} + + +g('fonts') # {{{ + +o( + 'font_family', + 'monospace', + _('Font family'), + long_text=_(''' +You can specify different fonts for the bold/italic/bold-italic variants. +By default they are derived automatically, by the OSes font system. Setting +them manually is useful for font families that have many weight variants like +Book, Medium, Thick, etc. For example:: + + font_family Operator Mono Book + bold_font Operator Mono Medium + italic_font Operator Mono Book Italic + bold_italic_font Operator Mono Medium Italic +''') +) +o('bold_font', 'auto') +o('italic_font', 'auto') +o('bold_italic_font', 'auto') + +o('font_size', 11.0, _('Font size (in pts)'), option_type=to_font_size) + +o( + '+symbol_map', + 'U+E0A0-U+E0A2,U+E0B0-U+E0B3 PowerlineSymbols', + _('Font character mapping'), + add_to_default=False, + long_text=_(''' +Map the specified unicode codepoints to a particular font. Useful if you need +special rendering for some symbols, such as for Powerline. Avoids the need for +patched fonts. Each unicode code point is specified in the form :code:`U+`. You can specify multiple code points, separated by commas and +ranges separated by hyphens. :code:`symbol_map` itself can be specified multiple times. +Syntax is:: + + symbol_map codepoints Font Family Name + +''')) +# }}}