From a059e495793cd743704341d82d5e8f17514618f6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 30 May 2021 21:19:38 +0530 Subject: [PATCH] Restore conf file generation in the new framework --- kitty/conf/types.py | 202 +++++++++++++++++++++++++++++++++++- kitty/config.py | 2 +- kitty/options/definition.py | 34 +++--- 3 files changed, 215 insertions(+), 23 deletions(-) diff --git a/kitty/conf/types.py b/kitty/conf/types.py index e2e2141fb..36dd3d6d7 100644 --- a/kitty/conf/types.py +++ b/kitty/conf/types.py @@ -3,10 +3,12 @@ # License: GPLv3 Copyright: 2021, Kovid Goyal import builtins +import re import typing from importlib import import_module from typing import ( - Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast + Any, Callable, Dict, Iterable, Iterator, List, Match, Optional, Tuple, + Union, cast ) import kitty.conf.utils as generic_parsers @@ -25,6 +27,64 @@ class Unset: unset = Unset() +def remove_markup(text: str) -> str: + + def sub(m: Match) -> str: + if m.group(1) == 'ref': + return { + 'layouts': 'https://sw.kovidgoyal.net/kitty/index.html#layouts', + 'sessions': 'https://sw.kovidgoyal.net/kitty/index.html#sessions', + 'functional': 'https://sw.kovidgoyal.net/kitty/keyboard-protocol.html#functional-key-definitions', + }[m.group(2)] + return str(m.group(2)) + + return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL) + + +def iter_blocks(lines: Iterable[str]) -> Iterator[Tuple[List[str], int]]: + current_block: List[str] = [] + prev_indent = 0 + for line in lines: + indent_size = len(line) - len(line.lstrip()) + if indent_size != prev_indent or not line: + if current_block: + yield current_block, prev_indent + current_block = [] + prev_indent = indent_size + if not line: + yield [''], 100 + else: + current_block.append(line) + if current_block: + yield current_block, indent_size + + +def wrapped_block(lines: Iterable[str]) -> Iterator[str]: + wrapper = getattr(wrapped_block, 'wrapper', None) + if wrapper is None: + import textwrap + wrapper = textwrap.TextWrapper( + initial_indent='#: ', subsequent_indent='#: ', width=70, break_long_words=False + ) + setattr(wrapped_block, 'wrapper', wrapper) + for block, indent_size in iter_blocks(lines): + if indent_size > 0: + for line in block: + if not line: + yield line + else: + yield '#: ' + line + else: + for line in wrapper.wrap('\n'.join(block)): + yield line + + +def render_block(text: str) -> str: + text = remove_markup(text) + lines = text.splitlines() + return '\n'.join(wrapped_block(lines)) + + class Option: def __init__( @@ -40,6 +100,28 @@ class Option: self.parser_func = parser_func self.choices = choices + @property + def needs_coalescing(self) -> bool: + return self.documented and not self.long_text + + def as_conf(self, commented: bool = False, level: int = 0, option_group: List['Option'] = []) -> List[str]: + ans: List[str] = [] + a = ans.append + if not self.documented: + return ans + if option_group: + sz = max(len(self.name), max(len(o.name) for o in option_group)) + a(f'{self.name.ljust(sz)} {self.defval_as_string}') + for o in option_group: + a(f'{o.name.ljust(sz)} {o.defval_as_string}') + else: + a(f'{self.name} {self.defval_as_string}') + if self.long_text: + a('') + a(render_block(self.long_text)) + a('') + return ans + class MultiVal: @@ -65,8 +147,43 @@ class MultiOption: def __iter__(self) -> Iterator[MultiVal]: yield from self.items + def as_conf(self, commented: bool = False, level: int = 0) -> List[str]: + ans: List[str] = [] + a = ans.append + for k in self.items: + if k.documented: + prefix = '' if k.add_to_default else '# ' + a(f'{prefix}{self.name} {k.defval_as_str}') + if self.long_text: + a('') + a(render_block(self.long_text)) + a('') + return ans -class ShortcutMapping: + +class Mapping: + add_to_default: bool + long_text: str + documented: bool + setting_name: str + + @property + def parseable_text(self) -> str: + return '' + + def as_conf(self, commented: bool = False, level: int = 0) -> List[str]: + ans: List[str] = [] + if self.documented: + a = ans.append + if self.add_to_default: + a(self.setting_name + ' ' + self.parseable_text) + if self.long_text: + a(''), a(render_block(self.long_text.strip())), a('') + return ans + + +class ShortcutMapping(Mapping): + setting_name: str = 'map' def __init__( self, name: str, key: str, action_def: str, short_text: str, long_text: str, add_to_default: bool, documented: bool, group: 'Group', only: Only @@ -86,7 +203,8 @@ class ShortcutMapping: return f'{self.key} {self.action_def}' -class MouseMapping: +class MouseMapping(Mapping): + setting_name: str = 'mouse_map' def __init__( self, name: str, button: str, event: str, modes: str, action_def: str, @@ -139,6 +257,80 @@ class Group: else: yield x + def as_conf(self, commented: bool = False, level: int = 0) -> List[str]: + ans: List[str] = [] + a = ans.append + if level: + a('#: ' + self.title + ' {{''{') + a('') + if self.start_text: + a(render_block(self.start_text)) + a('') + else: + ans.extend(('# vim:fileencoding=utf-8:ft=conf:foldmethod=marker', '')) + + option_groups = {} + current_group: List[Option] = [] + coalesced = set() + for item in self: + if isinstance(item, Option): + if current_group: + if item.needs_coalescing: + current_group.append(item) + coalesced.add(id(item)) + continue + option_groups[id(current_group[0])] = current_group[1:] + current_group = [item] + else: + current_group.append(item) + if current_group: + option_groups[id(current_group[0])] = current_group[1:] + + for item in self: + if isinstance(item, Option): + if id(item) in coalesced: + continue + lines = item.as_conf(option_group=option_groups[id(item)]) + else: + lines = item.as_conf(commented, level + 1) + ans.extend(lines) + + if level: + if self.end_text: + a('') + a(render_block(self.end_text)) + a('#: }}''}') + a('') + else: + map_groups = [] + start: Optional[int] = None + count: Optional[int] = None + for i, line in enumerate(ans): + if line.startswith('map ') or line.startswith('mouse_map '): + if start is None: + start = i + count = 1 + else: + if count is not None: + count += 1 + else: + if start is not None and count is not None: + map_groups.append((start, count)) + start = count = None + for start, count in map_groups: + r = range(start, start + count) + sz = max(len(ans[i].split(' ', 3)[1]) for i in r) + for i in r: + line = ans[i] + parts = line.split(' ', 3) + parts[1] = parts[1].ljust(sz) + ans[i] = ' '.join(parts) + + if commented: + ans = [x if x.startswith('#') or not x.strip() else ('# ' + x) for x in ans] + + return ans + def resolve_import(name: str, module: Any = None) -> Callable: ans = None @@ -268,5 +460,5 @@ class Definition: def add_deprecation(self, parser_name: str, *aliases: str) -> None: self.deprecations[self.parser_func(parser_name)] = aliases - def as_conf(self, commented: bool = False) -> str: - raise NotImplementedError('TODO:') + def as_conf(self, commented: bool = False) -> List[str]: + return self.root_group.as_conf(commented) diff --git a/kitty/config.py b/kitty/config.py index 2d9fa7752..d75cd89d7 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -80,7 +80,7 @@ def cached_values_for(name: str) -> Generator[Dict, None, None]: def commented_out_default_config() -> str: from .options.definition import definition - return definition.as_conf(commented=True) + return '\n'.join(definition.as_conf(commented=True)) def prepare_config_file_for_editing() -> str: diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 6a67b36d8..9bf867752 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -539,8 +539,8 @@ agr('performance', 'Performance tuning') opt('repaint_delay', '10', option_type='positive_int', long_text=''' -Delay (in milliseconds) between screen updates. Decreasing it, increases frames- -per-second (FPS) at the cost of more CPU usage. The default value yields ~100 +Delay (in milliseconds) between screen updates. Decreasing it, increases frames-per-second +(FPS) at the cost of more CPU usage. The default value yields ~100 FPS which is more than sufficient for most uses. Note that to actually achieve 100 FPS you have to either set :opt:`sync_to_monitor` to no or use a monitor with a high refresh rate. Also, to minimize latency when there is pending input @@ -1127,7 +1127,7 @@ opt('mark3_foreground', 'black', opt('mark3_background', '#f274bc', option_type='to_color', - long_text='Color for marks of type 1 (violet)' + long_text='Color for marks of type 3 (violet)' ) opt('color16', '#000000', @@ -2459,8 +2459,8 @@ terminal programs, only change it if you know what you are doing, not because you read some advice on Stack Overflow to change it. The TERM variable is used by various programs to get information about the capabilities and behavior of the terminal. If you change it, depending on what programs you run, and how -different the terminal you are changing it to is, various things from key- -presses, to colors, to various advanced features may not work. +different the terminal you are changing it to is, various things from key-presses, +to colors, to various advanced features may not work. ''' ) egr() # }}} @@ -3092,26 +3092,26 @@ map('Increase font size', ) map('Increase font size', 'increase_font_size kitty_mod+plus change_font_size all +2.0', - documented=False, + documented=True, ) map('Increase font size', 'increase_font_size kitty_mod+kp_add change_font_size all +2.0', - documented=False, + documented=True, ) map('Increase font size', 'increase_font_size cmd+plus change_font_size all +2.0', only="macos", - documented=False, + documented=True, ) map('Increase font size', 'increase_font_size cmd+equal change_font_size all +2.0', only="macos", - documented=False, + documented=True, ) map('Increase font size', 'increase_font_size cmd+shift+equal change_font_size all +2.0', only="macos", - documented=False, + documented=True, ) map('Decrease font size', @@ -3123,12 +3123,12 @@ map('Decrease font size', map('Decrease font size', 'decrease_font_size cmd+minus change_font_size all -2.0', only="macos", - documented=False, + documented=True, ) map('Decrease font size', 'decrease_font_size cmd+shift+minus change_font_size all -2.0', only="macos", - documented=False, + documented=True, ) map('Reset font size', @@ -3137,7 +3137,7 @@ map('Reset font size', map('Reset font size', 'reset_font_size cmd+0 change_font_size all 0', only="macos", - documented=False, + documented=True, ) egr(''' To setup shortcuts for specific font sizes:: @@ -3291,7 +3291,7 @@ If you want to operate on all windows instead of just the current one, use :ital It is also possible to remap Ctrl+L to both scroll the current screen contents into the scrollback buffer and clear the screen, instead of just clearing the screen:: - map ctrl+l combine : clear_terminal scroll active : send_text normal,application \x0c + map ctrl+l combine : clear_terminal scroll active : send_text normal,application \\x0c ''' ) @@ -3306,7 +3306,7 @@ the client program when pressing specified shortcut keys. For example:: This will send "Special text" when you press the :kbd:`ctrl+alt+a` key combination. The text to be sent is a python string literal so you can use -escapes like :code:`\x1b` to send control codes or :code:`\u21fb` to send +escapes like :code:`\\x1b` to send control codes or :code:`\\u21fb` to send unicode characters (or you can just input the unicode characters directly as UTF-8 text). The first argument to :code:`send_text` is the keyboard modes in which to activate the shortcut. The possible values are :code:`normal` or :code:`application` or :code:`kitty` @@ -3317,8 +3317,8 @@ terminals, and :code:`kitty` refers to the special kitty extended keyboard proto Another example, that outputs a word and then moves the cursor to the start of the line (same as pressing the Home key):: - map ctrl+alt+a send_text normal Word\x1b[H - map ctrl+alt+a send_text application Word\x1bOH + map ctrl+alt+a send_text normal Word\\x1b[H + map ctrl+alt+a send_text application Word\\x1bOH ''' ) egr() # }}}