Restore conf file generation in the new framework

This commit is contained in:
Kovid Goyal 2021-05-30 21:19:38 +05:30
parent 6d7df1c5e8
commit a059e49579
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 215 additions and 23 deletions

View File

@ -3,10 +3,12 @@
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@ -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:

View File

@ -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() # }}}