Refactor configuration file parsing

Now the time for importing the kitty.config module has been halved, from
16ms from 32ms on my machine. Also, the new architecture will eventually
allow for auto generating a bunch of python-to-C boilerplate code.
This commit is contained in:
Kovid Goyal 2021-05-30 13:16:18 +05:30
parent dd5715ce79
commit 6d7df1c5e8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
47 changed files with 7051 additions and 2370 deletions

4
.gitattributes vendored
View File

@ -7,6 +7,10 @@ kitty/rgb.py linguist-generated=true
kitty/gl-wrapper.* linguist-generated=true
kitty/glfw-wrapper.* linguist-generated=true
kitty/parse-graphics-command.h linguist-generated=true
kitty/options/types.py linguist-generated=true
kitty/options/parse.py linguist-generated=true
kittens/diff/options/types.py linguist-generated=true
kittens/diff/options/parse.py linguist-generated=true
glfw/*.c linguist-vendored=true
glfw/*.h linguist-vendored=true
kittens/unicode_input/names.h linguist-generated=true

View File

@ -16,6 +16,10 @@ kitty/glfw-wrapper.c
kitty/emoji.h
kittens/unicode_input/names.h
kitty/parse-graphics-command.h
kitty/options/types.py
kitty/options/parse.py
kittens/diff/options/types.py
kittens/diff/options/parse.py
'''
p = subprocess.Popen([

342
gen-config.py Executable file
View File

@ -0,0 +1,342 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import inspect
import os
import pprint
import re
import textwrap
from typing import (
Any, Callable, Dict, List, Set, Tuple, Union, get_type_hints
)
from kitty.conf.types import Definition, MultiOption, Option, unset
def atoi(text: str) -> str:
return f'{int(text):08d}' if text.isdigit() else text
def natural_keys(text: str) -> Tuple[str, ...]:
return tuple(atoi(c) for c in re.split(r'(\d+)', text))
def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
class_lines: List[str] = []
tc_lines: List[str] = []
a = class_lines.append
t = tc_lines.append
a('class Options:')
t('class Parser:')
choices = {}
imports: Set[Tuple[str, str]] = set()
tc_imports: Set[Tuple[str, str]] = set()
def type_name(x: type) -> str:
ans = x.__name__
if x.__module__ and x.__module__ != 'builtins':
imports.add((x.__module__, x.__name__))
return ans
def option_type_as_str(x: Any) -> str:
if hasattr(x, '__name__'):
return type_name(x)
ans = repr(x)
ans = ans.replace('NoneType', 'None')
return ans
def option_type_data(option: Union[Option, MultiOption]) -> Tuple[Callable, str]:
func = option.parser_func
if func.__module__ == 'builtins':
return func, func.__name__
th = get_type_hints(func)
rettype = th['return']
typ = option_type_as_str(rettype)
if isinstance(option, MultiOption):
typ = typ[typ.index('[') + 1:-1]
typ = typ.replace('Tuple', 'Dict', 1)
return func, typ
is_mutiple_vars = {}
option_names = set()
def parser_function_declaration(option_name: str) -> None:
t('')
t(f' def {option_name}(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:')
for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)):
option_names.add(option.name)
parser_function_declaration(option.name)
if isinstance(option, MultiOption):
mval: Dict[str, Dict[str, Any]] = {'macos': {}, 'linux': {}, '': {}}
func, typ = option_type_data(option)
for val in option:
if val.add_to_default:
gr = mval[val.only]
for k, v in func(val.defval_as_str):
gr[k] = v
is_mutiple_vars[option.name] = typ, mval
sig = inspect.signature(func)
tc_imports.add((func.__module__, func.__name__))
if len(sig.parameters) == 1:
t(f' for k, v in {func.__name__}(val):')
t(f' ans["{option.name}"][k] = v')
else:
t(f' for k, v in {func.__name__}(val, ans["{option.name}"]):')
t(f' ans["{option.name}"][k] = v')
continue
if option.choices:
typ = 'typing.Literal[{}]'.format(', '.join(repr(x) for x in option.choices))
ename = f'choices_for_{option.name}'
choices[ename] = typ
typ = ename
func = str
else:
func, typ = option_type_data(option)
try:
params = inspect.signature(func).parameters
except Exception:
params = {}
if 'dict_with_parse_results' in params:
t(f' {func.__name__}(val, ans)')
else:
t(f' ans[{option.name!r}] = {func.__name__}(val)')
if func.__module__ != 'builtins':
tc_imports.add((func.__module__, func.__name__))
defval = repr(func(option.defval_as_string))
if option.macos_defval is not unset:
md = repr(func(option.macos_defval))
defval = f'{md} if is_macos else {defval}'
imports.add(('kitty.constants', 'is_macos'))
a(f' {option.name}: {typ} = {defval}')
if option.choices:
t(' val = val.lower()')
t(f' if val not in self.choices_for_{option.name}:')
t(f' raise ValueError(f"The value {{val}} is not a valid choice for {option.name}")')
t(f' ans["{option.name}"] = val')
t('')
t(f' choices_for_{option.name} = frozenset({option.choices!r})')
for option_name, (typ, mval) in is_mutiple_vars.items():
a(f' {option_name}: {typ} = ' '{}')
for parser, aliases in defn.deprecations.items():
for alias in aliases:
parser_function_declaration(alias)
tc_imports.add((parser.__module__, parser.__name__))
t(f' {parser.__name__}({alias!r}, val, ans)')
action_parsers = {}
def resolve_import(ftype: str) -> str:
if '.' in ftype:
fmod, ftype = ftype.rpartition('.')[::2]
else:
fmod = f'{loc}.options.utils'
imports.add((fmod, ftype))
return ftype
for aname, action in defn.actions.items():
option_names.add(aname)
action_parsers[aname] = func = action.parser_func
th = get_type_hints(func)
rettype = th['return']
typ = option_type_as_str(rettype)
typ = typ[typ.index('[') + 1:-1]
a(f' {aname}: typing.List[{typ}] = []')
for imp in action.imports:
resolve_import(imp)
for fname, ftype in action.fields.items():
ftype = resolve_import(ftype)
a(f' {fname}: {ftype} = ' '{}')
parser_function_declaration(aname)
t(f' for k in {func.__name__}(val):')
t(f' ans[{aname!r}].append(k)')
tc_imports.add((func.__module__, func.__name__))
a('')
a(' def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:')
a(' if options_dict is not None:')
a(' for key in option_names:')
a(' setattr(self, key, options_dict[key])')
a('')
a(' @property')
a(' def _fields(self) -> typing.Tuple[str, ...]:')
a(' return option_names')
a('')
a(' def __iter__(self) -> typing.Iterator[str]:')
a(' return iter(self._fields)')
a('')
a(' def __len__(self) -> int:')
a(' return len(self._fields)')
a('')
a(' def _copy_of_val(self, name: str) -> typing.Any:')
a(' ans = getattr(self, name)')
a(' if isinstance(ans, dict):\n ans = ans.copy()')
a(' elif isinstance(ans, list):\n ans = ans[:]')
a(' return ans')
a('')
a(' def _asdict(self) -> typing.Dict[str, typing.Any]:')
a(' return {k: self._copy_of_val(k) for k in self}')
a('')
a(' def _replace(self, **kw: typing.Any) -> "Options":')
a(' ans = Options()')
a(' for name in self:')
a(' setattr(ans, name, self._copy_of_val(name))')
a(' for name, val in kw.items():')
a(' setattr(ans, name, val)')
a(' return ans')
a('')
a(' def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:')
a(' k = option_names[key] if isinstance(key, int) else key')
a(' try:')
a(' return getattr(self, k)')
a(' except AttributeError:')
a(' pass')
a(' raise KeyError(f"No option named: {k}")')
a('')
a('')
a('defaults = Options()')
for option_name, (typ, mval) in is_mutiple_vars.items():
a(f'defaults.{option_name} = {mval[""]!r}')
if mval['macos']:
imports.add(('kitty.constants', 'is_macos'))
a('if is_macos:')
a(f' defaults.{option_name}.update({mval["macos"]!r}')
if mval['macos']:
imports.add(('kitty.constants', 'is_macos'))
a('if not is_macos:')
a(f' defaults.{option_name}.update({mval["linux"]!r}')
for aname, func in action_parsers.items():
a(f'defaults.{aname} = [')
only: Dict[str, List[Tuple[str, Callable]]] = {}
for sc in defn.iter_all_maps(aname):
if not sc.add_to_default:
continue
text = sc.parseable_text
if sc.only:
only.setdefault(sc.only, []).append((text, func))
for val in func(text):
a(f' {val!r},')
a(']')
if only:
imports.add(('kitty.constants', 'is_macos'))
for cond, items in only.items():
cond = 'is_macos' if cond == 'macos' else 'not is_macos'
a(f'if {cond}:')
for (text, func) in items:
for val in func(text):
a(f' defaults.{aname}.append({val!r})')
t('')
t('')
t('def create_result_dict() -> typing.Dict[str, typing.Any]:')
t(' return {')
for oname in is_mutiple_vars:
t(f' {oname!r}: {{}},')
for aname in defn.actions:
t(f' {aname!r}: [],')
t(' }')
t('')
t('')
t(f'actions = frozenset({tuple(defn.actions)!r})')
t('')
t('')
t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:')
t(' ans = {}')
t(' for k, v in defaults.items():')
t(' if isinstance(v, dict):')
t(' ans[k] = merge_dicts(v, vals.get(k, {}))')
t(' elif k in actions:')
t(' ans[k] = v + vals.get(k, [])')
t(' else:')
t(' ans[k] = vals.get(k, v)')
t(' return ans')
tc_imports.add(('kitty.conf.utils', 'merge_dicts'))
t('')
t('')
t('parser = Parser()')
t('')
t('')
t('def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:')
t(' func = getattr(parser, key, None)')
t(' if func is not None:')
t(' func(val, ans)')
t(' return True')
t(' return False')
preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', '']
a = preamble.append
def output_imports(imports: Set, add_module_imports: bool = True) -> None:
a('import typing')
seen_mods = {'typing'}
mmap: Dict[str, List[str]] = {}
for mod, name in imports:
mmap.setdefault(mod, []).append(name)
for mod, names in mmap.items():
names = sorted(names)
lines = textwrap.wrap(', '.join(names), 100)
if len(lines) == 1:
s = lines[0]
else:
s = '\n '.join(lines)
s = f'(\n {s}\n)'
a(f'from {mod} import {s}')
if add_module_imports and mod not in seen_mods:
a(f'import {mod}')
seen_mods.add(mod)
output_imports(imports)
a('')
if choices:
a('if typing.TYPE_CHECKING:')
for name, cdefn in choices.items():
a(f' {name} = {cdefn}')
a('else:')
for name in choices:
a(f' {name} = str')
a('')
a('option_names = ( # {{''{')
a(' ' + pprint.pformat(tuple(sorted(option_names, key=natural_keys)))[1:] + ' # }}''}')
class_def = '\n'.join(preamble + ['', ''] + class_lines)
preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', '']
a = preamble.append
output_imports(tc_imports, False)
return class_def, '\n'.join(preamble + ['', ''] + tc_lines)
def write_output(loc: str, defn: Definition) -> None:
cls, tc = generate_class(defn, loc)
with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f:
f.write(cls)
with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f:
f.write(tc)
def main() -> None:
from kitty.options.definition import definition
write_output('kitty', definition)
from kittens.diff.options.definition import definition as kd
write_output('kittens.diff', kd)
if __name__ == '__main__':
main()

View File

@ -3,21 +3,16 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
from typing import Any, Dict, FrozenSet, Iterable, Optional, Tuple, Type, Union
from typing import Any, Dict, Iterable, Optional
from kitty.cli_stub import DiffCLIOptions
from kitty.conf.definition import config_lines
from kitty.conf.utils import (
init_config as _init_config, key_func, load_config as _load_config,
merge_dicts, parse_config_base, parse_kittens_key, resolve_config
load_config as _load_config, parse_config_base, resolve_config
)
from kitty.constants import config_dir
from kitty.options_stub import DiffOptions
from kitty.rgb import color_as_sgr
from .config_data import all_options
defaults: Optional[DiffOptions] = None
from .options.types import Options as DiffOptions, defaults
formats: Dict[str, str] = {
'title': '',
@ -42,97 +37,27 @@ def set_formats(opts: DiffOptions) -> None:
formats['added_highlight'] = '48' + color_as_sgr(opts.highlight_added_bg)
func_with_args, args_funcs = key_func()
@func_with_args('scroll_by')
def parse_scroll_by(func: str, rest: str) -> Tuple[str, int]:
try:
return func, int(rest)
except Exception:
return func, 1
@func_with_args('scroll_to')
def parse_scroll_to(func: str, rest: str) -> Tuple[str, str]:
rest = rest.lower()
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
rest = 'start'
return func, rest
@func_with_args('change_context')
def parse_change_context(func: str, rest: str) -> Tuple[str, Union[int, str]]:
rest = rest.lower()
if rest in {'all', 'default'}:
return func, rest
try:
amount = int(rest)
except Exception:
amount = 5
return func, amount
@func_with_args('start_search')
def parse_start_search(func: str, rest: str) -> Tuple[str, Tuple[bool, bool]]:
rest_ = rest.lower().split()
is_regex = bool(rest_ and rest_[0] == 'regex')
is_backward = bool(len(rest_) > 1 and rest_[1] == 'backward')
return func, (is_regex, is_backward)
def special_handling(key: str, val: str, ans: Dict) -> bool:
if key == 'map':
x = parse_kittens_key(val, args_funcs)
if x is not None:
action, key_def = x
ans['key_definitions'][key_def] = action
return True
return False
def parse_config(lines: Iterable[str], check_keys: bool = True) -> Dict[str, Any]:
ans: Dict[str, Any] = {'key_definitions': {}}
defs: Optional[FrozenSet] = None
if check_keys:
defs = frozenset(defaults._fields) # type: ignore
parse_config_base(
lines,
defs,
all_options,
special_handling,
ans,
)
return ans
def merge_configs(defaults: Dict, vals: Dict) -> Dict:
ans = {}
for k, v in defaults.items():
if isinstance(v, dict):
newvals = vals.get(k, {})
ans[k] = merge_dicts(v, newvals)
else:
ans[k] = vals.get(k, v)
return ans
def parse_defaults(lines: Iterable[str], check_keys: bool = False) -> Dict[str, Any]:
return parse_config(lines, check_keys)
x = _init_config(config_lines(all_options), parse_defaults)
Options: Type[DiffOptions] = x[0]
defaults = x[1]
SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
defconf = os.path.join(config_dir, 'diff.conf')
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> DiffOptions:
return _load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=overrides)
from .options.parse import (
create_result_dict, merge_result_dicts, parse_conf_item
)
def parse_config(lines: Iterable[str]) -> Dict[str, Any]:
ans: Dict[str, Any] = create_result_dict()
parse_config_base(
lines,
parse_conf_item,
ans,
)
return ans
SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
defconf = os.path.join(config_dir, 'diff.conf')
opts_dict = _load_config(defaults, parse_config, merge_result_dicts, *paths, overrides=overrides)
opts = DiffOptions(opts_dict)
return opts
def init_config(args: DiffCLIOptions) -> DiffOptions:
@ -140,4 +65,6 @@ def init_config(args: DiffCLIOptions) -> DiffOptions:
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
opts = load_config(*config, overrides=overrides)
set_formats(opts)
for (sc, action) in opts.map:
opts.key_definitions[sc] = action
return opts

View File

@ -1,125 +0,0 @@
#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
# Utils {{{
from functools import partial
from gettext import gettext as _
from typing import Dict
from kitty.conf.definition import OptionOrAction, option_func
from kitty.conf.utils import (
positive_int, python_string, to_color, to_color_or_none
)
# }}}
all_options: Dict[str, OptionOrAction] = {}
o, k, m, g, all_groups = option_func(all_options, {
'colors': [_('Colors')],
'diff': [_('Diffing'), ],
'shortcuts': [_('Keyboard shortcuts')],
})
g('diff')
def syntax_aliases(raw: str) -> Dict[str, str]:
ans = {}
for x in raw.split():
a, b = x.partition(':')[::2]
if a and b:
ans[a.lower()] = b
return ans
o('syntax_aliases', 'pyj:py pyi:py recipe:py', option_type=syntax_aliases, long_text=_('''
File extension aliases for syntax highlight
For example, to syntax highlight :file:`file.xyz` as
:file:`file.abc` use a setting of :code:`xyz:abc`
'''))
o('num_context_lines', 3, option_type=positive_int, long_text=_('''
The number of lines of context to show around each change.'''))
o('diff_cmd', 'auto', long_text=_('''
The diff command to use. Must contain the placeholder :code:`_CONTEXT_`
which will be replaced by the number of lines of context. The default
is to search the system for either git or diff and use that, if found.
'''))
o('replace_tab_by', r'\x20\x20\x20\x20', option_type=python_string, long_text=_('''
The string to replace tabs with. Default is to use four spaces.'''))
g('colors')
o('pygments_style', 'default', long_text=_('''
The pygments color scheme to use for syntax highlighting.
See :link:`pygments colors schemes <https://help.farbox.com/pygments.html>` for a list of schemes.'''))
c = partial(o, option_type=to_color)
c('foreground', 'black', long_text=_('Basic colors'))
c('background', 'white')
c('title_fg', 'black', long_text=_('Title colors'))
c('title_bg', 'white')
c('margin_bg', '#fafbfc', long_text=_('Margin colors'))
c('margin_fg', '#aaaaaa')
c('removed_bg', '#ffeef0', long_text=_('Removed text backgrounds'))
c('highlight_removed_bg', '#fdb8c0')
c('removed_margin_bg', '#ffdce0')
c('added_bg', '#e6ffed', long_text=_('Added text backgrounds'))
c('highlight_added_bg', '#acf2bd')
c('added_margin_bg', '#cdffd8')
c('filler_bg', '#fafbfc', long_text=_('Filler (empty) line background'))
c('margin_filler_bg', 'none', option_type=to_color_or_none, long_text=_(
'Filler (empty) line background in margins, defaults to the filler background'))
c('hunk_margin_bg', '#dbedff', long_text=_('Hunk header colors'))
c('hunk_bg', '#f1f8ff')
c('search_bg', '#444', long_text=_('Highlighting'))
c('search_fg', 'white')
c('select_bg', '#b4d5fe')
o('select_fg', 'black', option_type=to_color_or_none)
g('shortcuts')
k('quit', 'q', 'quit', _('Quit'))
k('quit', 'esc', 'quit', _('Quit'))
k('scroll_down', 'j', 'scroll_by 1', _('Scroll down'))
k('scroll_down', 'down', 'scroll_by 1', _('Scroll down'))
k('scroll_up', 'k', 'scroll_by -1', _('Scroll up'))
k('scroll_up', 'up', 'scroll_by -1', _('Scroll up'))
k('scroll_top', 'home', 'scroll_to start', _('Scroll to top'))
k('scroll_bottom', 'end', 'scroll_to end', _('Scroll to bottom'))
k('scroll_page_down', 'page_down', 'scroll_to next-page', _('Scroll to next page'))
k('scroll_page_down', 'space', 'scroll_to next-page', _('Scroll to next page'))
k('scroll_page_up', 'page_up', 'scroll_to prev-page', _('Scroll to previous page'))
k('next_change', 'n', 'scroll_to next-change', _('Scroll to next change'))
k('prev_change', 'p', 'scroll_to prev-change', _('Scroll to previous change'))
k('all_context', 'a', 'change_context all', _('Show all context'))
k('default_context', '=', 'change_context default', _('Show default context'))
k('increase_context', '+', 'change_context 5', _('Increase context'))
k('decrease_context', '-', 'change_context -5', _('Decrease context'))
k('search_forward', '/', 'start_search regex forward', _('Search forward'))
k('search_backward', '?', 'start_search regex backward', _('Search backward'))
k('next_match', '.', 'scroll_to next-match', _('Scroll to next search match'))
k('prev_match', ',', 'scroll_to prev-match', _('Scroll to previous search match'))
k('next_match', '>', 'scroll_to next-match', _('Scroll to next search match'))
k('prev_match', '<', 'scroll_to prev-match', _('Scroll to previous search match'))
k('search_forward_simple', 'f', 'start_search substring forward', _('Search forward (no regex)'))
k('search_backward_simple', 'b', 'start_search substring backward', _('Search backward (no regex)'))

View File

@ -159,13 +159,12 @@ def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str
def main() -> None:
from .config import defaults
# kitty +runpy "from kittens.diff.highlight import main; main()" file
from .options.types import defaults
import sys
initialize_highlighter()
if defaults is not None:
with open(sys.argv[-1]) as f:
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
if highlighted is None:
raise SystemExit('Unknown filetype: {}'.format(sys.argv[-1]))
print(highlighted)
with open(sys.argv[-1]) as f:
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
if highlighted is None:
raise SystemExit('Unknown filetype: {}'.format(sys.argv[-1]))
print(highlighted)

View File

@ -19,11 +19,10 @@
from kitty.cli import CONFIG_HELP, parse_args
from kitty.cli_stub import DiffCLIOptions
from kitty.conf.utils import KittensKeyAction
from kitty.conf.utils import KeyAction
from kitty.constants import appname
from kitty.fast_data_types import wcswidth
from kitty.key_encoding import EventType, KeyEvent
from kitty.options_stub import DiffOptions
from kitty.utils import ScreenSize
from ..tui.handler import Handler
@ -33,10 +32,11 @@
from ..tui.operations import styled
from . import global_data
from .collect import (
Collection, create_collection, data_for_path, lines_for_path, sanitize,
set_highlight_data, add_remote_dir
Collection, add_remote_dir, create_collection, data_for_path,
lines_for_path, sanitize, set_highlight_data
)
from .config import init_config
from .options.types import Options as DiffOptions
from .patch import Differ, Patch, set_diff_command, worker_processes
from .render import (
ImagePlacement, ImageSupportWarning, Line, LineRef, Reference, render_diff
@ -95,14 +95,14 @@ def __init__(self, args: DiffCLIOptions, opts: DiffOptions, left: str, right: st
for key_def, action in self.opts.key_definitions.items():
self.add_shortcut(action, key_def)
def perform_action(self, action: KittensKeyAction) -> None:
def perform_action(self, action: KeyAction) -> None:
func, args = action
if func == 'quit':
self.quit_loop(0)
return
if self.state <= DIFFED:
if func == 'scroll_by':
return self.scroll_lines(int(args[0]))
return self.scroll_lines(int(args[0] or 0))
if func == 'scroll_to':
where = str(args[0])
if 'change' in where:
@ -122,7 +122,7 @@ def perform_action(self, action: KittensKeyAction) -> None:
elif to == 'default':
new_ctx = self.original_context_count
else:
new_ctx += int(to)
new_ctx += int(to or 0)
return self.change_context_count(new_ctx)
if func == 'start_search':
self.start_search(bool(args[0]), bool(args[1]))
@ -658,5 +658,5 @@ def main(args: List[str]) -> None:
cd['options'] = OPTIONS
cd['help_text'] = help_text
elif __name__ == '__conf__':
from .config_data import all_options
sys.all_options = all_options # type: ignore
from .options.definition import definition
sys.options_definition = definition # type: ignore

View File

View File

@ -0,0 +1,241 @@
from kitty.conf.types import Action, Definition
definition = Definition(
'kittens.diff',
Action('map', 'parse_map', {'key_definitions': 'kitty.conf.utils.KittensKeyMap'}, ['kitty.types.ParsedShortcut', 'kitty.conf.utils.KeyAction']),
)
agr = definition.add_group
egr = definition.end_group
opt = definition.add_option
map = definition.add_map
mma = definition.add_mouse_map
# diff {{{
agr('diff', 'Diffing')
opt('syntax_aliases', 'pyj:py pyi:py recipe:py',
option_type='syntax_aliases',
long_text='''
File extension aliases for syntax highlight For example, to syntax highlight
:file:`file.xyz` as :file:`file.abc` use a setting of :code:`xyz:abc`
'''
)
opt('num_context_lines', '3',
option_type='positive_int',
long_text='The number of lines of context to show around each change.'
)
opt('diff_cmd', 'auto',
long_text='''
The diff command to use. Must contain the placeholder :code:`_CONTEXT_` which
will be replaced by the number of lines of context. The default is to search the
system for either git or diff and use that, if found.
'''
)
opt('replace_tab_by', '\\x20\\x20\\x20\\x20',
option_type='python_string',
long_text='The string to replace tabs with. Default is to use four spaces.'
)
egr() # }}}
# colors {{{
agr('colors', 'Colors')
opt('pygments_style', 'default',
long_text='''
The pygments color scheme to use for syntax highlighting. See :link:`pygments
colors schemes <https://help.farbox.com/pygments.html>` for a list of schemes.
'''
)
opt('foreground', 'black',
option_type='to_color',
long_text='Basic colors'
)
opt('background', 'white',
option_type='to_color',
)
opt('title_fg', 'black',
option_type='to_color',
long_text='Title colors'
)
opt('title_bg', 'white',
option_type='to_color',
)
opt('margin_bg', '#fafbfc',
option_type='to_color',
long_text='Margin colors'
)
opt('margin_fg', '#aaaaaa',
option_type='to_color',
)
opt('removed_bg', '#ffeef0',
option_type='to_color',
long_text='Removed text backgrounds'
)
opt('highlight_removed_bg', '#fdb8c0',
option_type='to_color',
)
opt('removed_margin_bg', '#ffdce0',
option_type='to_color',
)
opt('added_bg', '#e6ffed',
option_type='to_color',
long_text='Added text backgrounds'
)
opt('highlight_added_bg', '#acf2bd',
option_type='to_color',
)
opt('added_margin_bg', '#cdffd8',
option_type='to_color',
)
opt('filler_bg', '#fafbfc',
option_type='to_color',
long_text='Filler (empty) line background'
)
opt('margin_filler_bg', 'none',
option_type='to_color_or_none',
long_text='Filler (empty) line background in margins, defaults to the filler background'
)
opt('hunk_margin_bg', '#dbedff',
option_type='to_color',
long_text='Hunk header colors'
)
opt('hunk_bg', '#f1f8ff',
option_type='to_color',
)
opt('search_bg', '#444',
option_type='to_color',
long_text='Highlighting'
)
opt('search_fg', 'white',
option_type='to_color',
)
opt('select_bg', '#b4d5fe',
option_type='to_color',
)
opt('select_fg', 'black',
option_type='to_color_or_none',
)
egr() # }}}
# shortcuts {{{
agr('shortcuts', 'Keyboard shortcuts')
map('Quit',
'quit q quit',
)
map('Quit',
'quit esc quit',
)
map('Scroll down',
'scroll_down j scroll_by 1',
)
map('Scroll down',
'scroll_down down scroll_by 1',
)
map('Scroll up',
'scroll_up k scroll_by -1',
)
map('Scroll up',
'scroll_up up scroll_by -1',
)
map('Scroll to top',
'scroll_top home scroll_to start',
)
map('Scroll to bottom',
'scroll_bottom end scroll_to end',
)
map('Scroll to next page',
'scroll_page_down page_down scroll_to next-page',
)
map('Scroll to next page',
'scroll_page_down space scroll_to next-page',
)
map('Scroll to previous page',
'scroll_page_up page_up scroll_to prev-page',
)
map('Scroll to next change',
'next_change n scroll_to next-change',
)
map('Scroll to previous change',
'prev_change p scroll_to prev-change',
)
map('Show all context',
'all_context a change_context all',
)
map('Show default context',
'default_context = change_context default',
)
map('Increase context',
'increase_context + change_context 5',
)
map('Decrease context',
'decrease_context - change_context -5',
)
map('Search forward',
'search_forward / start_search regex forward',
)
map('Search backward',
'search_backward ? start_search regex backward',
)
map('Scroll to next search match',
'next_match . scroll_to next-match',
)
map('Scroll to next search match',
'next_match > scroll_to next-match',
)
map('Scroll to previous search match',
'prev_match , scroll_to prev-match',
)
map('Scroll to previous search match',
'prev_match < scroll_to prev-match',
)
map('Search forward (no regex)',
'search_forward_simple f start_search substring forward',
)
map('Search backward (no regex)',
'search_backward_simple b start_search substring backward',
)
egr() # }}}

120
kittens/diff/options/parse.py generated Normal file
View File

@ -0,0 +1,120 @@
# generated by gen-config.py DO NOT edit
# vim:fileencoding=utf-8
import typing
from kitty.conf.utils import merge_dicts, positive_int, python_string, to_color, to_color_or_none
from kittens.diff.options.utils import parse_map, syntax_aliases
class Parser:
def added_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['added_bg'] = to_color(val)
def added_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['added_margin_bg'] = to_color(val)
def background(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['background'] = to_color(val)
def diff_cmd(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['diff_cmd'] = str(val)
def filler_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['filler_bg'] = to_color(val)
def foreground(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['foreground'] = to_color(val)
def highlight_added_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['highlight_added_bg'] = to_color(val)
def highlight_removed_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['highlight_removed_bg'] = to_color(val)
def hunk_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['hunk_bg'] = to_color(val)
def hunk_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['hunk_margin_bg'] = to_color(val)
def margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['margin_bg'] = to_color(val)
def margin_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['margin_fg'] = to_color(val)
def margin_filler_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['margin_filler_bg'] = to_color_or_none(val)
def num_context_lines(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['num_context_lines'] = positive_int(val)
def pygments_style(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['pygments_style'] = str(val)
def removed_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['removed_bg'] = to_color(val)
def removed_margin_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['removed_margin_bg'] = to_color(val)
def replace_tab_by(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['replace_tab_by'] = python_string(val)
def search_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['search_bg'] = to_color(val)
def search_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['search_fg'] = to_color(val)
def select_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['select_bg'] = to_color(val)
def select_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['select_fg'] = to_color_or_none(val)
def syntax_aliases(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['syntax_aliases'] = syntax_aliases(val)
def title_bg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['title_bg'] = to_color(val)
def title_fg(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['title_fg'] = to_color(val)
def map(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
for k in parse_map(val):
ans['map'].append(k)
def create_result_dict() -> typing.Dict[str, typing.Any]:
return {
'map': [],
}
actions = frozenset(('map',))
def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
ans = {}
for k, v in defaults.items():
if isinstance(v, dict):
ans[k] = merge_dicts(v, vals.get(k, {}))
elif k in actions:
ans[k] = v + vals.get(k, [])
else:
ans[k] = vals.get(k, v)
return ans
parser = Parser()
def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:
func = getattr(parser, key, None)
if func is not None:
func(val, ans)
return True
return False

141
kittens/diff/options/types.py generated Normal file
View File

@ -0,0 +1,141 @@
# generated by gen-config.py DO NOT edit
# vim:fileencoding=utf-8
import typing
from kitty.types import ParsedShortcut
import kitty.types
from kitty.conf.utils import KeyAction, KittensKeyMap
import kitty.conf.utils
from kitty.rgb import Color
import kitty.rgb
option_names = ( # {{{
'added_bg',
'added_margin_bg',
'background',
'diff_cmd',
'filler_bg',
'foreground',
'highlight_added_bg',
'highlight_removed_bg',
'hunk_bg',
'hunk_margin_bg',
'map',
'margin_bg',
'margin_fg',
'margin_filler_bg',
'num_context_lines',
'pygments_style',
'removed_bg',
'removed_margin_bg',
'replace_tab_by',
'search_bg',
'search_fg',
'select_bg',
'select_fg',
'syntax_aliases',
'title_bg',
'title_fg') # }}}
class Options:
added_bg: Color = Color(red=230, green=255, blue=237)
added_margin_bg: Color = Color(red=205, green=255, blue=216)
background: Color = Color(red=255, green=255, blue=255)
diff_cmd: str = 'auto'
filler_bg: Color = Color(red=250, green=251, blue=252)
foreground: Color = Color(red=0, green=0, blue=0)
highlight_added_bg: Color = Color(red=172, green=242, blue=189)
highlight_removed_bg: Color = Color(red=253, green=184, blue=192)
hunk_bg: Color = Color(red=241, green=248, blue=255)
hunk_margin_bg: Color = Color(red=219, green=237, blue=255)
margin_bg: Color = Color(red=250, green=251, blue=252)
margin_fg: Color = Color(red=170, green=170, blue=170)
margin_filler_bg: typing.Optional[kitty.rgb.Color] = None
num_context_lines: int = 3
pygments_style: str = 'default'
removed_bg: Color = Color(red=255, green=238, blue=240)
removed_margin_bg: Color = Color(red=255, green=220, blue=224)
replace_tab_by: str = ' '
search_bg: Color = Color(red=68, green=68, blue=68)
search_fg: Color = Color(red=255, green=255, blue=255)
select_bg: Color = Color(red=180, green=213, blue=254)
select_fg: typing.Optional[kitty.rgb.Color] = Color(red=0, green=0, blue=0)
syntax_aliases: typing.Dict[str, str] = {'pyj': 'py', 'pyi': 'py', 'recipe': 'py'}
title_bg: Color = Color(red=255, green=255, blue=255)
title_fg: Color = Color(red=0, green=0, blue=0)
map: typing.List[typing.Tuple[kitty.types.ParsedShortcut, kitty.conf.utils.KeyAction]] = []
key_definitions: KittensKeyMap = {}
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
if options_dict is not None:
for key in option_names:
setattr(self, key, options_dict[key])
@property
def _fields(self) -> typing.Tuple[str, ...]:
return option_names
def __iter__(self) -> typing.Iterator[str]:
return iter(self._fields)
def __len__(self) -> int:
return len(self._fields)
def _copy_of_val(self, name: str) -> typing.Any:
ans = getattr(self, name)
if isinstance(ans, dict):
ans = ans.copy()
elif isinstance(ans, list):
ans = ans[:]
return ans
def _asdict(self) -> typing.Dict[str, typing.Any]:
return {k: self._copy_of_val(k) for k in self}
def _replace(self, **kw: typing.Any) -> "Options":
ans = Options()
for name in self:
setattr(ans, name, self._copy_of_val(name))
for name, val in kw.items():
setattr(ans, name, val)
return ans
def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:
k = option_names[key] if isinstance(key, int) else key
try:
return getattr(self, k)
except AttributeError:
pass
raise KeyError(f"No option named: {k}")
defaults = Options()
defaults.map = [
(ParsedShortcut(mods=0, key_name='q'), KeyAction('quit')),
(ParsedShortcut(mods=0, key_name='ESCAPE'), KeyAction('quit')),
(ParsedShortcut(mods=0, key_name='j'), KeyAction('scroll_by', (1,))),
(ParsedShortcut(mods=0, key_name='DOWN'), KeyAction('scroll_by', (1,))),
(ParsedShortcut(mods=0, key_name='k'), KeyAction('scroll_by', (-1,))),
(ParsedShortcut(mods=0, key_name='UP'), KeyAction('scroll_by', (-1,))),
(ParsedShortcut(mods=0, key_name='HOME'), KeyAction('scroll_to', ('start',))),
(ParsedShortcut(mods=0, key_name='END'), KeyAction('scroll_to', ('end',))),
(ParsedShortcut(mods=0, key_name='PAGE_DOWN'), KeyAction('scroll_to', ('next-page',))),
(ParsedShortcut(mods=0, key_name=' '), KeyAction('scroll_to', ('next-page',))),
(ParsedShortcut(mods=0, key_name='PAGE_UP'), KeyAction('scroll_to', ('prev-page',))),
(ParsedShortcut(mods=0, key_name='n'), KeyAction('scroll_to', ('next-change',))),
(ParsedShortcut(mods=0, key_name='p'), KeyAction('scroll_to', ('prev-change',))),
(ParsedShortcut(mods=0, key_name='a'), KeyAction('change_context', ('all',))),
(ParsedShortcut(mods=0, key_name='='), KeyAction('change_context', ('default',))),
(ParsedShortcut(mods=0, key_name='+'), KeyAction('change_context', (5,))),
(ParsedShortcut(mods=0, key_name='-'), KeyAction('change_context', (-5,))),
(ParsedShortcut(mods=0, key_name='/'), KeyAction('start_search', (True, False))),
(ParsedShortcut(mods=0, key_name='?'), KeyAction('start_search', (True, True))),
(ParsedShortcut(mods=0, key_name='.'), KeyAction('scroll_to', ('next-match',))),
(ParsedShortcut(mods=0, key_name='>'), KeyAction('scroll_to', ('next-match',))),
(ParsedShortcut(mods=0, key_name=','), KeyAction('scroll_to', ('prev-match',))),
(ParsedShortcut(mods=0, key_name='<'), KeyAction('scroll_to', ('prev-match',))),
(ParsedShortcut(mods=0, key_name='f'), KeyAction('start_search', (False, False))),
(ParsedShortcut(mods=0, key_name='b'), KeyAction('start_search', (False, True))),
]

View File

@ -0,0 +1,62 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Any, Dict, Iterable, Sequence, Tuple, Union
from kitty.conf.utils import KittensKeyDefinition, key_func, parse_kittens_key
func_with_args, args_funcs = key_func()
FuncArgsType = Tuple[str, Sequence[Any]]
@func_with_args('scroll_by')
def parse_scroll_by(func: str, rest: str) -> Tuple[str, int]:
try:
return func, int(rest)
except Exception:
return func, 1
@func_with_args('scroll_to')
def parse_scroll_to(func: str, rest: str) -> Tuple[str, str]:
rest = rest.lower()
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
rest = 'start'
return func, rest
@func_with_args('change_context')
def parse_change_context(func: str, rest: str) -> Tuple[str, Union[int, str]]:
rest = rest.lower()
if rest in {'all', 'default'}:
return func, rest
try:
amount = int(rest)
except Exception:
amount = 5
return func, amount
@func_with_args('start_search')
def parse_start_search(func: str, rest: str) -> Tuple[str, Tuple[bool, bool]]:
rest_ = rest.lower().split()
is_regex = bool(rest_ and rest_[0] == 'regex')
is_backward = bool(len(rest_) > 1 and rest_[1] == 'backward')
return func, (is_regex, is_backward)
def syntax_aliases(raw: str) -> Dict[str, str]:
ans = {}
for x in raw.split():
a, b = x.partition(':')[::2]
if a and b:
ans[a.lower()] = b
return ans
def parse_map(val: str) -> Iterable[KittensKeyDefinition]:
x = parse_kittens_key(val, args_funcs)
if x is not None:
yield x

View File

@ -6,7 +6,7 @@
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Tuple
from kitty.fast_data_types import wcswidth
from kitty.options_stub import DiffOptions
from .options.types import Options as DiffOptions
from ..tui.operations import styled

View File

@ -11,7 +11,7 @@
from kitty.types import ParsedShortcut
from kitty.typing import (
AbstractEventLoop, BossType, Debug, ImageManagerType, KeyEventType,
KittensKeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType
KeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType
)
@ -46,15 +46,15 @@ def image_manager(self) -> ImageManagerType:
def asyncio_loop(self) -> AbstractEventLoop:
return self._tui_loop.asycio_loop
def add_shortcut(self, action: KittensKeyActionType, spec: Union[str, ParsedShortcut]) -> None:
def add_shortcut(self, action: KeyActionType, spec: Union[str, ParsedShortcut]) -> None:
if not hasattr(self, '_key_shortcuts'):
self._key_shortcuts: Dict[ParsedShortcut, KittensKeyActionType] = {}
self._key_shortcuts: Dict[ParsedShortcut, KeyActionType] = {}
if isinstance(spec, str):
from kitty.key_encoding import parse_shortcut
spec = parse_shortcut(spec)
self._key_shortcuts[spec] = action
def shortcut_action(self, key_event: KeyEventType) -> Optional[KittensKeyActionType]:
def shortcut_action(self, key_event: KeyEventType) -> Optional[KeyActionType]:
for sc, action in self._key_shortcuts.items():
if key_event.matches(sc):
return action

View File

@ -9,6 +9,6 @@ class CMD:
def generate_stub() -> None:
from kittens.tui.operations import as_type_stub
from kitty.conf.definition import save_type_stub
from kitty.conf.utils import save_type_stub
text = as_type_stub()
save_type_stub(text, __file__)

View File

@ -18,7 +18,7 @@
from .child import cached_process_data, cwd_of_process, default_env
from .cli import create_opts, parse_args
from .cli_stub import CLIOptions
from .conf.utils import BadLine, to_cmdline
from .conf.utils import BadLine, KeyAction, to_cmdline
from .config import common_opts_as_dict, prepare_config_file_for_editing
from .constants import (
appname, config_dir, is_macos, kitty_exe, supports_primary_selection
@ -37,8 +37,8 @@
from .keys import get_shortcut, shortcut_matches
from .layout.base import set_layout_options
from .notify import notification_activated
from .options_stub import Options
from .options.utils import MINIMUM_FONT_SIZE, KeyAction, SubSequenceMap
from .options.types import Options
from .options.utils import MINIMUM_FONT_SIZE, SubSequenceMap
from .os_window_size import initial_window_size_func
from .rgb import Color, color_from_int
from .session import Session, create_sessions, get_os_window_sizing_data

View File

@ -12,10 +12,10 @@
)
from .cli_stub import CLIOptions
from .conf.utils import resolve_config
from .options.utils import KeyAction, MouseMap
from .conf.utils import KeyAction, resolve_config
from .constants import appname, defconf, is_macos, is_wayland, str_version
from .options_stub import Options as OptionsStub
from .options.types import Options as KittyOpts, defaults
from .options.utils import MouseMap
from .types import MouseEvent, SingleKey
from .typing import BadLineType, SequenceMap, TypedDict
@ -836,13 +836,13 @@ def print_changes(defns: MouseMap, changes: Set[MouseEvent], text: str) -> None:
print_changes(final, changed, 'Changed mouse actions:')
def compare_opts(opts: OptionsStub) -> None:
from .config import defaults, load_config
def compare_opts(opts: KittyOpts) -> None:
from .config import load_config
print('\nConfig options different from defaults:')
default_opts = load_config()
ignored = ('keymap', 'sequence_map', 'mousemap', 'map', 'mouse_map')
changed_opts = [
f for f in sorted(defaults._fields) # type: ignore
f for f in sorted(defaults._fields)
if f not in ignored and getattr(opts, f) != getattr(defaults, f)
]
field_len = max(map(len, changed_opts)) if changed_opts else 20
@ -860,7 +860,7 @@ def compare_opts(opts: OptionsStub) -> None:
compare_keymaps(final, initial)
def create_opts(args: CLIOptions, debug_config: bool = False, accumulate_bad_lines: Optional[List[BadLineType]] = None) -> OptionsStub:
def create_opts(args: CLIOptions, debug_config: bool = False, accumulate_bad_lines: Optional[List[BadLineType]] = None) -> KittyOpts:
from .config import load_config
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
if debug_config:
@ -887,7 +887,7 @@ def create_opts(args: CLIOptions, debug_config: bool = False, accumulate_bad_lin
return opts
def create_default_opts() -> OptionsStub:
def create_default_opts() -> KittyOpts:
from .config import load_config
config = tuple(resolve_config(SYSTEM_CONF, defconf, ()))
opts = load_config(*config)

View File

@ -18,7 +18,7 @@ class CLIOptions:
def generate_stub() -> None:
from .cli import parse_option_spec, as_type_stub
from .conf.definition import save_type_stub
from .conf.utils import save_type_stub
text = 'import typing\n\n\n'
def do(otext=None, cls: str = 'CLIOptions', extra_fields: Sequence[str] = ()):

View File

@ -1,389 +0,0 @@
#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import re
from functools import partial
from typing import (
Any, Callable, Dict, Generator, Iterable, List, Match, Optional, Sequence,
Set, Tuple, Union, get_type_hints
)
from .utils import Choice, to_bool
class Group:
__slots__ = 'name', 'short_text', 'start_text', 'end_text'
def __init__(self, name: str, short_text: str, start_text: str = '', end_text: str = '') -> None:
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', 'long_text', 'option_type', 'defval_as_string', 'add_to_default', 'add_to_docs', 'line', 'is_multiple'
def __init__(self, name: str, group: Group, defval: str, option_type: Any, long_text: str, add_to_default: bool, add_to_docs: bool, is_multiple: bool):
self.name, self.group = name, group
self.long_text, self.option_type = long_text.strip(), option_type
self.defval_as_string = defval
self.add_to_default = add_to_default
self.add_to_docs = add_to_docs
self.is_multiple = is_multiple
self.line = self.name + ' ' + self.defval_as_string
def type_definition(self, imports: Set[Tuple[str, str]]) -> str:
def type_name(x: type) -> str:
ans = x.__name__
if x.__module__ and x.__module__ != 'builtins':
imports.add((x.__module__, x.__name__))
return ans
def option_type_as_str(x: Any) -> str:
if hasattr(x, '__name__'):
return type_name(x)
ans = repr(x)
ans = ans.replace('NoneType', 'None')
if self.is_multiple:
ans = ans[ans.index('[') + 1:-1]
ans = ans.replace('Tuple', 'Dict', 1)
return ans
if type(self.option_type) is type:
return type_name(self.option_type)
if isinstance(self.option_type, Choice):
return 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in self.option_type.all_choices))
th = get_type_hints(self.option_type)
try:
rettype = th['return']
except KeyError:
raise ValueError('The Option {} has an unknown option_type: {}'.format(self.name, self.option_type))
return option_type_as_str(rettype)
class Shortcut:
__slots__ = 'name', 'group', 'key', 'action_def', 'short_text', 'long_text', 'add_to_default', 'add_to_docs', 'line'
def __init__(self, name: str, group: Group, key: str, action_def: str, short_text: str, long_text: str, add_to_default: bool, add_to_docs: bool):
self.name, self.group, self.key, self.action_def = name, group, key, action_def
self.short_text, self.long_text = short_text, long_text
self.add_to_default = add_to_default
self.add_to_docs = add_to_docs
self.line = 'map ' + self.key + ' ' + self.action_def
class MouseAction:
__slots__ = 'name', 'group', 'button', 'event', 'modes', 'action_def', 'short_text', 'long_text', 'add_to_default', 'add_to_docs', 'line'
def __init__(
self, name: str, group: Group,
button: str, event: str, modes: str, action_def: str,
short_text: str, long_text: str, add_to_default: bool, add_to_docs: bool
):
self.name, self.group, self.button, self.event, self.action_def = name, group, button, event, action_def
self.modes, self.short_text, self.long_text = modes, short_text, long_text
self.add_to_default = add_to_default
self.add_to_docs = add_to_docs
self.line = f'mouse_map {self.button} {self.event} {self.modes} {self.action_def}'
@property
def key(self) -> str:
return self.button
def option(
all_options: Dict[str, Option],
group: Sequence[Group],
name: str,
defval: Any,
long_text: str = '',
option_type: Callable[[str], Any] = str,
add_to_default: bool = True,
add_to_docs: bool = True
) -> Option:
is_multiple = name.startswith('+')
if is_multiple:
name = name[1:]
defval_type = type(defval)
if defval_type is not str:
if option_type is str:
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)
ans = Option(name, group[0], defval, option_type, long_text, add_to_default, add_to_docs, is_multiple)
all_options[name] = ans
return ans
def shortcut(
all_options: Dict[str, List[Shortcut]],
group: Sequence[Group],
action_name: str,
key: str,
action_def: str,
short_text: str = '',
long_text: str = '',
add_to_default: bool = True,
add_to_docs: bool = True,
) -> Shortcut:
ans = Shortcut(action_name, group[0], key, action_def, short_text, long_text, add_to_default, add_to_docs)
key = 'sc-' + action_name
all_options.setdefault(key, []).append(ans)
return ans
def mouse_action(
all_options: Dict[str, List[MouseAction]],
group: Sequence[Group],
action_name: str,
button: str,
event: str,
modes: str,
action_def: str,
short_text: str = '',
long_text: str = '',
add_to_default: bool = True,
add_to_docs: bool = True,
) -> MouseAction:
ans = MouseAction(action_name, group[0], button, event, modes, action_def, short_text, long_text, add_to_default, add_to_docs)
key = 'ma-' + action_name
all_options.setdefault(key, []).append(ans)
return ans
def option_func(all_options: Dict[str, Any], all_groups: Dict[str, Sequence[str]]) -> Tuple[
Callable, Callable, Callable, Callable[[str], None], Dict[str, Group]]:
all_groups_ = {k: Group(k, *v) for k, v in all_groups.items()}
group: List[Optional[Group]] = [None]
def change_group(name: str) -> None:
group[0] = all_groups_[name]
return partial(option, all_options, group), partial(shortcut, all_options, group), partial(mouse_action, all_options, group), change_group, all_groups_
OptionOrAction = Union[Option, List[Union[Shortcut, MouseAction]]]
def merged_opts(all_options: Sequence[OptionOrAction], opt: Option, i: int) -> Generator[Option, None, None]:
yield opt
for k in range(i + 1, len(all_options)):
q = all_options[k]
if not isinstance(q, Option):
break
if not q.long_text and q.add_to_docs:
yield q
else:
break
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]) -> Generator[Tuple[List[str], int], None, None]:
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]) -> Generator[str, None, None]:
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))
def as_conf_file(all_options: Iterable[OptionOrAction]) -> List[str]:
ans = ['# vim:fileencoding=utf-8:ft=conf:foldmethod=marker', '']
a = ans.append
current_group: Optional[Group] = None
group_folds = []
all_options_ = list(all_options)
def render_group(group: Group, is_shortcut: bool) -> None:
a('#: ' + group.short_text + ' {{''{')
group_folds.append(group.name)
a('')
if group.start_text:
a(render_block(group.start_text))
a('')
def handle_group_end(group: Group, new_group_name: str = '', new_group_is_shortcut: bool = False) -> None:
if group.end_text:
a(''), a(render_block(group.end_text))
is_subgroup = new_group_name.startswith(group.name + '.')
while group_folds:
is_subgroup = new_group_name.startswith(group_folds[-1] + '.')
if is_subgroup:
break
a('#: }}''}'), a('')
del group_folds[-1]
def handle_group(new_group: Group, is_shortcut: bool = False) -> None:
nonlocal current_group
if new_group is not current_group:
if current_group:
handle_group_end(current_group, new_group.name, is_shortcut)
current_group = new_group
render_group(current_group, is_shortcut)
def handle_shortcut(shortcuts: Sequence[Union[Shortcut, MouseAction]]) -> None:
handle_group(shortcuts[0].group, True)
for sc in shortcuts:
if sc.add_to_default:
a(sc.line)
if sc.long_text:
a(''), a(render_block(sc.long_text.strip())), a('')
def handle_option(opt: Option) -> None:
if not opt.long_text or not opt.add_to_docs:
return
handle_group(opt.group)
mopts = list(merged_opts(all_options_, opt, i))
sz = max(len(x.name) for x in mopts)
for mo in mopts:
prefix = '' if mo.add_to_default else '# '
a('{}{} {}'.format(prefix, mo.name.ljust(sz), mo.defval_as_string))
a('')
a(render_block(opt.long_text))
a('')
for i, opt in enumerate(all_options_):
if isinstance(opt, Option):
handle_option(opt)
else:
handle_shortcut(opt)
if current_group:
handle_group_end(current_group)
while group_folds:
a('# }}''}')
del group_folds[-1]
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)
return ans
def config_lines(
all_options: Dict[str, OptionOrAction],
) -> Generator[str, None, None]:
for opt in all_options.values():
if isinstance(opt, Option):
if opt.add_to_default:
yield opt.line
else:
for sc in opt:
if sc.add_to_default:
yield sc.line
def as_type_stub(
all_options: Dict[str, OptionOrAction],
preamble_lines: Union[Tuple[str, ...], List[str], Iterable[str]] = (),
extra_fields: Union[Tuple[Tuple[str, str], ...], List[Tuple[str, str]], Iterable[Tuple[str, str]]] = (),
class_name: str = 'Options'
) -> str:
ans = ['import typing\n'] + list(preamble_lines) + ['', 'class {}:'.format(class_name)]
imports: Set[Tuple[str, str]] = set()
for name, val in all_options.items():
if isinstance(val, Option):
field_name = name.partition(' ')[0]
ans.append(' {}: {}'.format(field_name, val.type_definition(imports)))
for mod, name in imports:
ans.insert(0, 'from {} import {}'.format(mod, name))
ans.insert(0, 'import {}'.format(mod))
for field_name, type_def in extra_fields:
ans.append(' {}: {}'.format(field_name, type_def))
ans.append(' def __iter__(self) -> typing.Iterator[str]: pass')
ans.append(' def __len__(self) -> int: pass')
ans.append(' def __getitem__(self, k: typing.Union[int, str]) -> typing.Any: pass')
ans.append(' def _replace(self, **kw: typing.Any) -> {}: pass'.format(class_name))
return '\n'.join(ans) + '\n\n\n'
def save_type_stub(text: str, fpath: str) -> None:
fpath += 'i'
preamble = '# Update this file by running: ./test.py mypy\n\n'
try:
existing = open(fpath).read()
except FileNotFoundError:
existing = ''
current = preamble + text
if existing != current:
open(fpath, 'w').write(current)

272
kitty/conf/types.py Normal file
View File

@ -0,0 +1,272 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import builtins
import typing
from importlib import import_module
from typing import (
Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast
)
import kitty.conf.utils as generic_parsers
if typing.TYPE_CHECKING:
Only = typing.Literal['macos', 'linux', '']
else:
Only = str
class Unset:
def __bool__(self) -> bool:
return False
unset = Unset()
class Option:
def __init__(
self, name: str, defval: str, macos_default: Union[Unset, str], parser_func: Callable,
long_text: str, documented: bool, group: 'Group', choices: Tuple[str, ...]
):
self.name = name
self.defval_as_string = defval
self.macos_defval = macos_default
self.long_text = long_text
self.documented = documented
self.group = group
self.parser_func = parser_func
self.choices = choices
class MultiVal:
def __init__(self, val_as_str: str, add_to_default: bool, documented: bool, only: Only) -> None:
self.defval_as_str = val_as_str
self.documented = documented
self.only = only
self.add_to_default = add_to_default
class MultiOption:
def __init__(self, name: str, parser_func: Callable, long_text: str, group: 'Group'):
self.name = name
self.parser_func = parser_func
self.long_text = long_text
self.group = group
self.items: List[MultiVal] = []
def add_value(self, val_as_str: str, add_to_default: bool, documented: bool, only: Only) -> None:
self.items.append(MultiVal(val_as_str, add_to_default, documented, only))
def __iter__(self) -> Iterator[MultiVal]:
yield from self.items
class ShortcutMapping:
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
):
self.name = name
self.only = only
self.key = key
self.action_def = action_def
self.short_text = short_text
self.long_text = long_text
self.documented = documented
self.add_to_default = add_to_default
self.group = group
@property
def parseable_text(self) -> str:
return f'{self.key} {self.action_def}'
class MouseMapping:
def __init__(
self, name: str, button: str, event: str, modes: str, action_def: str,
short_text: str, long_text: str, add_to_default: bool, documented: bool, group: 'Group', only: Only
):
self.name = name
self.only = only
self.button = button
self.event = event
self.modes = modes
self.action_def = action_def
self.short_text = short_text
self.long_text = long_text
self.documented = documented
self.add_to_default = add_to_default
self.group = group
@property
def parseable_text(self) -> str:
return f'{self.button} {self.event} {self.modes} {self.action_def}'
NonGroups = Union[Option, MultiOption, ShortcutMapping, MouseMapping]
GroupItem = Union[NonGroups, 'Group']
class Group:
def __init__(self, name: str, title: str, start_text: str = '', parent: Optional['Group'] = None):
self.name = name
self.title = title
self.start_text = start_text
self.end_text = ''
self.items: List[GroupItem] = []
self.parent = parent
def append(self, item: GroupItem) -> None:
self.items.append(item)
def __iter__(self) -> Iterator[GroupItem]:
return iter(self.items)
def __len__(self) -> int:
return len(self.items)
def iter_all_non_groups(self) -> Iterator[NonGroups]:
for x in self:
if isinstance(x, Group):
yield from x.iter_all_non_groups()
else:
yield x
def resolve_import(name: str, module: Any = None) -> Callable:
ans = None
if name.count('.') > 1:
m = import_module(name.rpartition('.')[0])
ans = getattr(m, name.rpartition('.')[2])
else:
ans = getattr(builtins, name, None)
if not callable(ans):
ans = getattr(generic_parsers, name, None)
if not callable(ans):
ans = getattr(module, name)
if not callable(ans):
raise TypeError(f'{name} is not a function')
return cast(Callable, ans)
class Action:
def __init__(self, name: str, option_type: str, fields: Dict[str, str], imports: Iterable[str]):
self.name = name
self._parser_func = option_type
self.fields = fields
self.imports = frozenset(imports)
def resolve_imports(self, module: Any) -> 'Action':
self.parser_func = resolve_import(self._parser_func, module)
return self
class Definition:
def __init__(self, package: str, *actions: Action) -> None:
self.module_for_parsers = import_module(f'{package}.options.utils')
self.package = package
self.root_group = Group('', '')
self.current_group = self.root_group
self.option_map: Dict[str, Option] = {}
self.multi_option_map: Dict[str, MultiOption] = {}
self.shortcut_map: Dict[str, List[ShortcutMapping]] = {}
self.mouse_map: Dict[str, List[MouseMapping]] = {}
self.actions = {a.name: a.resolve_imports(self.module_for_parsers) for a in actions}
self.deprecations: Dict[Callable, Tuple[str, ...]] = {}
def iter_all_non_groups(self) -> Iterator[NonGroups]:
yield from self.root_group.iter_all_non_groups()
def iter_all_options(self) -> Iterator[Union[Option, MultiOption]]:
for x in self.iter_all_non_groups():
if isinstance(x, (Option, MultiOption)):
yield x
def iter_all_maps(self, which: str = 'map') -> Iterator[Union[ShortcutMapping, MouseMapping]]:
for x in self.iter_all_non_groups():
if isinstance(x, ShortcutMapping) and which == 'map':
yield x
elif isinstance(x, MouseMapping) and which == 'mouse_map':
yield x
def parser_func(self, name: str) -> Callable:
ans = getattr(builtins, name, None)
if callable(ans):
return cast(Callable, ans)
ans = getattr(generic_parsers, name, None)
if callable(ans):
return cast(Callable, ans)
ans = getattr(self.module_for_parsers, name)
if not callable(ans):
raise TypeError(f'{name} is not a function')
return cast(Callable, ans)
def add_group(self, name: str, title: str = '', start_text: str = '') -> None:
self.current_group = Group(name, title or name, start_text.strip(), self.current_group)
if self.current_group.parent is not None:
self.current_group.parent.append(self.current_group)
def end_group(self, end_text: str = '') -> None:
self.current_group.end_text = end_text.strip()
if self.current_group.parent is not None:
self.current_group = self.current_group.parent
def add_option(
self, name: str, defval: Union[str, float, int, bool],
option_type: str = 'str', long_text: str = '',
documented: bool = True, add_to_default: bool = False,
only: Only = '', macos_default: Union[Unset, str] = unset,
choices: Tuple[str, ...] = ()
) -> None:
if isinstance(defval, bool):
defval = 'yes' if defval else 'no'
else:
defval = str(defval)
is_multiple = name.startswith('+')
long_text = long_text.strip()
if is_multiple:
name = name[1:]
if macos_default is not unset:
raise TypeError(f'Cannot specify macos_default for is_multiple option: {name} use only instead')
is_new = name not in self.multi_option_map
if is_new:
self.multi_option_map[name] = MultiOption(name, self.parser_func(option_type), long_text, self.current_group)
mopt = self.multi_option_map[name]
if is_new:
self.current_group.append(mopt)
mopt.add_value(defval, add_to_default, documented, only)
return
opt = Option(name, defval, macos_default, self.parser_func(option_type), long_text, documented, self.current_group, choices)
self.current_group.append(opt)
self.option_map[name] = opt
def add_map(
self, short_text: str, defn: str, long_text: str = '', add_to_default: bool = True, documented: bool = True, only: Only = ''
) -> None:
name, key, action_def = defn.split(maxsplit=2)
sc = ShortcutMapping(name, key, action_def, short_text, long_text.strip(), add_to_default, documented, self.current_group, only)
self.current_group.append(sc)
self.shortcut_map.setdefault(name, []).append(sc)
def add_mouse_map(
self, short_text: str, defn: str, long_text: str = '', add_to_default: bool = True, documented: bool = True, only: Only = ''
) -> None:
name, button, event, modes, action_def = defn.split(maxsplit=4)
mm = MouseMapping(name, button, event, modes, action_def, short_text, long_text.strip(), add_to_default, documented, self.current_group, only)
self.current_group.append(mm)
self.mouse_map.setdefault(name, []).append(mm)
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:')

View File

@ -6,18 +6,26 @@
import re
import shlex
from typing import (
Any, Callable, Dict, FrozenSet, Generator, Iterable, Iterator, List,
NamedTuple, Optional, Sequence, Tuple, Type, TypeVar, Union, Set
Any, Callable, Dict, Generator, Iterable, List, NamedTuple, Optional,
Sequence, Set, Tuple, TypeVar, Union
)
from ..rgb import Color, to_color as as_color
from ..types import ParsedShortcut, ConvertibleToNumbers
from ..types import ConvertibleToNumbers, ParsedShortcut
from ..typing import Protocol
from ..utils import expandvars, log_error
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
ItemParser = Callable[[str, str, Dict[str, Any]], bool]
T = TypeVar('T')
class OptionsProtocol(Protocol):
def _asdict(self) -> Dict[str, Any]:
pass
class BadLine(NamedTuple):
number: int
line: str
@ -110,23 +118,12 @@ def choices(*choices: str) -> Choice:
return Choice(choices)
def create_type_converter(all_options: Dict) -> Callable[[str, Any], Any]:
from .definition import Option
def type_convert(name: str, val: Any) -> Any:
o = all_options.get(name)
if isinstance(o, Option):
val = o.option_type(val)
return val
return type_convert
def parse_line(
line: str,
type_convert: Callable[[str, Any], Any],
special_handling: Callable,
ans: Dict[str, Any], all_keys: Optional[FrozenSet[str]],
base_path_for_includes: str
parse_conf_item: ItemParser,
ans: Dict[str, Any],
base_path_for_includes: str,
accumulate_bad_lines: Optional[List[BadLine]] = None
) -> None:
line = line.strip()
if not line or line.startswith('#'):
@ -136,15 +133,13 @@ def parse_line(
log_error('Ignoring invalid config line: {}'.format(line))
return
key, val = m.groups()
if special_handling(key, val, ans):
return
if key == 'include':
val = os.path.expandvars(os.path.expanduser(val.strip()))
if not os.path.isabs(val):
val = os.path.join(base_path_for_includes, val)
try:
with open(val, encoding='utf-8', errors='replace') as include:
_parse(include, type_convert, special_handling, ans, all_keys)
_parse(include, parse_conf_item, ans, accumulate_bad_lines)
except FileNotFoundError:
log_error(
'Could not find included config file: {}, ignoring'.
@ -156,18 +151,14 @@ def parse_line(
format(val)
)
return
if all_keys is not None and key not in all_keys:
if not parse_conf_item(key, val, ans):
log_error('Ignoring unknown config key: {}'.format(key))
return
ans[key] = type_convert(key, val)
def _parse(
lines: Iterable[str],
type_convert: Callable[[str, Any], Any],
special_handling: Callable,
parse_conf_item: ItemParser,
ans: Dict[str, Any],
all_keys: Optional[FrozenSet[str]],
accumulate_bad_lines: Optional[List[BadLine]] = None
) -> None:
name = getattr(lines, 'name', None)
@ -179,8 +170,7 @@ def _parse(
for i, line in enumerate(lines):
try:
parse_line(
line, type_convert, special_handling, ans, all_keys,
base_path_for_includes
line, parse_conf_item, ans, base_path_for_includes, accumulate_bad_lines
)
except Exception as e:
if accumulate_bad_lines is None:
@ -190,62 +180,15 @@ def _parse(
def parse_config_base(
lines: Iterable[str],
all_option_names: Optional[FrozenSet],
all_options: Dict[str, Any],
special_handling: Callable,
parse_conf_item: ItemParser,
ans: Dict[str, Any],
accumulate_bad_lines: Optional[List[BadLine]] = None
) -> None:
_parse(
lines, create_type_converter(all_options), special_handling, ans, all_option_names, accumulate_bad_lines
lines, parse_conf_item, ans, accumulate_bad_lines
)
def create_options_class(all_keys: Iterable[str]) -> Type:
keys = tuple(sorted(all_keys))
slots = keys + ('_fields', )
def __init__(self: Any, kw: Dict[str, Any]) -> None:
for k, v in kw.items():
setattr(self, k, v)
def __iter__(self: Any) -> Iterator[str]:
return iter(keys)
def __len__(self: Any) -> int:
return len(keys)
def __getitem__(self: Any, i: Union[int, str]) -> Any:
if isinstance(i, int):
i = keys[i]
try:
return getattr(self, i)
except AttributeError:
raise KeyError('No option named: {}'.format(i))
def _asdict(self: Any) -> Dict[str, Any]:
return {k: getattr(self, k) for k in self._fields}
def _replace(self: Any, **kw: Dict) -> Any:
ans = self._asdict()
ans.update(kw)
return self.__class__(ans)
ans = type(
'Options', (), {
'__slots__': slots,
'__init__': __init__,
'_asdict': _asdict,
'_replace': _replace,
'__iter__': __iter__,
'__len__': __len__,
'__getitem__': __getitem__
}
)
ans._fields = keys # type: ignore
return ans
def merge_dicts(defaults: Dict, newvals: Dict) -> Dict:
ans = defaults.copy()
ans.update(newvals)
@ -264,14 +207,13 @@ def resolve_config(SYSTEM_CONF: str, defconf: str, config_files_on_cmd_line: Seq
def load_config(
Options: Type[T],
defaults: Any,
parse_config: Callable[[Iterable[str]], Dict[str, Any]],
merge_configs: Callable[[Dict, Dict], Dict],
*paths: str,
overrides: Optional[Iterable[str]] = None
) -> T:
ans: Dict = defaults._asdict()
defaults: OptionsProtocol,
parse_config: Callable[[Iterable[str]], Dict[str, Any]],
merge_configs: Callable[[Dict, Dict], Dict],
*paths: str,
overrides: Optional[Iterable[str]] = None
) -> Dict[str, Any]:
ans = defaults._asdict()
for path in paths:
if not path:
continue
@ -284,14 +226,7 @@ def load_config(
if overrides is not None:
vals = parse_config(overrides)
ans = merge_configs(ans, vals)
return Options(ans) # type: ignore
def init_config(default_config_lines: Iterable[str], parse_config: Callable) -> Tuple[Type, Any]:
defaults = parse_config(default_config_lines, check_keys=False)
Options = create_options_class(defaults.keys())
defaults = Options(defaults)
return Options, defaults
return ans
def key_func() -> Tuple[Callable[..., Callable], Dict[str, Callable]]:
@ -312,14 +247,21 @@ def w(f: Callable) -> Callable:
return func_with_args, ans
KittensKeyAction = Tuple[str, Tuple[str, ...]]
class KeyAction(NamedTuple):
func: str
args: Tuple[Union[str, float, bool, int, None], ...] = ()
def __repr__(self) -> str:
if self.args:
return f'KeyAction({self.func!r}, {self.args!r})'
return f'KeyAction({self.func!r})'
def parse_kittens_func_args(action: str, args_funcs: Dict[str, Callable]) -> KittensKeyAction:
def parse_kittens_func_args(action: str, args_funcs: Dict[str, Callable]) -> KeyAction:
parts = action.strip().split(' ', 1)
func = parts[0]
if len(parts) == 1:
return func, ()
return KeyAction(func, ())
rest = parts[1]
try:
@ -338,21 +280,38 @@ def parse_kittens_func_args(action: str, args_funcs: Dict[str, Callable]) -> Kit
if not isinstance(args, (list, tuple)):
args = (args, )
return func, tuple(args)
return KeyAction(func, tuple(args))
KittensKeyDefinition = Tuple[ParsedShortcut, KeyAction]
KittensKeyMap = Dict[ParsedShortcut, KeyAction]
def parse_kittens_key(
val: str, funcs_with_args: Dict[str, Callable]
) -> Optional[Tuple[KittensKeyAction, ParsedShortcut]]:
) -> Optional[KittensKeyDefinition]:
from ..key_encoding import parse_shortcut
sc, action = val.partition(' ')[::2]
if not sc or not action:
return None
ans = parse_kittens_func_args(action, funcs_with_args)
return ans, parse_shortcut(sc)
return parse_shortcut(sc), ans
def uniq(vals: Iterable[T]) -> List[T]:
seen: Set[T] = set()
seen_add = seen.add
return [x for x in vals if x not in seen and not seen_add(x)]
def save_type_stub(text: str, fpath: str) -> None:
fpath += 'i'
preamble = '# Update this file by running: ./test.py mypy\n\n'
try:
existing = open(fpath).read()
except FileNotFoundError:
existing = ''
current = preamble + text
if existing != current:
with open(fpath, 'w') as f:
f.write(current)

View File

@ -6,160 +6,28 @@
import os
from contextlib import contextmanager, suppress
from functools import partial
from typing import (
Any, Callable, Dict, FrozenSet, Generator, Iterable, List, Optional, Tuple,
Type
)
from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple
from .conf.definition import as_conf_file, config_lines
from .conf.utils import (
BadLine, init_config, load_config as _load_config, merge_dicts,
parse_config_base, to_bool
)
from .config_data import all_options
from .conf.utils import BadLine, load_config as _load_config, parse_config_base
from .constants import cache_dir, defconf
from .options.types import Options, defaults, option_names
from .options.utils import (
KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap,
deprecated_hide_window_decorations_aliases,
deprecated_macos_show_window_title_in_menubar_alias, deprecated_send_text,
env, font_features, kitten_alias, parse_map, parse_mouse_map, symbol_map
KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap
)
from .options_stub import Options as OptionsStub
from .typing import TypedDict
from .utils import log_error
SpecialHandlerFunc = Callable[[str, str, Dict[str, Any]], None]
special_handlers: Dict[str, SpecialHandlerFunc] = {}
def option_names_for_completion() -> Tuple[str, ...]:
return option_names
def special_handler(func: SpecialHandlerFunc) -> SpecialHandlerFunc:
special_handlers[func.__name__.partition('_')[2]] = func
return func
def deprecated_handler(*names: str) -> Callable[[SpecialHandlerFunc], SpecialHandlerFunc]:
def special_handler(func: SpecialHandlerFunc) -> SpecialHandlerFunc:
for name in names:
special_handlers[name] = func
return func
return special_handler
@special_handler
def handle_map(key: str, val: str, ans: Dict[str, Any]) -> None:
for k in parse_map(val):
ans['map'].append(k)
@special_handler
def handle_mouse_map(key: str, val: str, ans: Dict[str, Any]) -> None:
for ma in parse_mouse_map(val):
ans['mouse_map'].append(ma)
@special_handler
def handle_symbol_map(key: str, val: str, ans: Dict[str, Any]) -> None:
for k, v in symbol_map(val):
ans['symbol_map'][k] = v
@special_handler
def handle_font_features(key: str, val: str, ans: Dict[str, Any]) -> None:
for key, features in font_features(val):
ans['font_features'][key] = features
@special_handler
def handle_kitten_alias(key: str, val: str, ans: Dict[str, Any]) -> None:
for k, v in kitten_alias(val):
ans['kitten_alias'][k] = v
@special_handler
def handle_send_text(key: str, val: str, ans: Dict[str, Any]) -> None:
# For legacy compatibility
deprecated_send_text(key, val, ans)
@special_handler
def handle_clear_all_shortcuts(key: str, val: str, ans: Dict[str, Any]) -> None:
if to_bool(val):
ans['map'] = [None]
@deprecated_handler('x11_hide_window_decorations', 'macos_hide_titlebar')
def handle_deprecated_hide_window_decorations_aliases(key: str, val: str, ans: Dict[str, Any]) -> None:
deprecated_hide_window_decorations_aliases(key, val, ans)
@deprecated_handler('macos_show_window_title_in_menubar')
def handle_deprecated_macos_show_window_title_in_menubar_alias(key: str, val: str, ans: Dict[str, Any]) -> None:
deprecated_macos_show_window_title_in_menubar_alias(key, val, ans)
@special_handler
def handle_env(key: str, val: str, ans: Dict[str, Any]) -> None:
for key, val in env(val, ans['env']):
ans['env'][key] = val
def special_handling(key: str, val: str, ans: Dict[str, Any]) -> bool:
func = special_handlers.get(key)
if func is not None:
func(key, val, ans)
return True
def option_names_for_completion() -> Generator[str, None, None]:
yield from defaults
yield from special_handlers
def parse_config(lines: Iterable[str], check_keys: bool = True, accumulate_bad_lines: Optional[List[BadLine]] = None) -> Dict[str, Any]:
ans: Dict[str, Any] = {
'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'map': [],
'env': {}, 'kitten_alias': {}, 'font_features': {}, 'mouse_map': [],
'mousemap': {}
}
defs: Optional[FrozenSet] = None
if check_keys:
defs = frozenset(defaults._fields) # type: ignore
parse_config_base(
lines,
defs,
all_options,
special_handling,
ans,
accumulate_bad_lines=accumulate_bad_lines
)
return ans
def parse_defaults(lines: Iterable[str], check_keys: bool = False) -> Dict[str, Any]:
return parse_config(lines, check_keys)
xc = init_config(config_lines(all_options), parse_defaults)
Options: Type[OptionsStub] = xc[0]
defaults: OptionsStub = xc[1]
no_op_actions = frozenset({'noop', 'no-op', 'no_op'})
def merge_configs(defaults: Dict, vals: Dict) -> Dict:
ans = {}
for k, v in defaults.items():
if isinstance(v, dict):
newvals = vals.get(k, {})
ans[k] = merge_dicts(v, newvals)
elif k in ('map', 'mouse_map'):
ans[k] = v + vals.get(k, [])
else:
ans[k] = vals.get(k, v)
return ans
def build_ansi_color_table(opts: OptionsStub = defaults) -> List[int]:
def build_ansi_color_table(opts: Optional[Options] = None) -> List[int]:
if opts is None:
opts = defaults
def as_int(x: Tuple[int, int, int]) -> int:
return (x[0] << 16) | (x[1] << 8) | x[2]
@ -211,12 +79,8 @@ def cached_values_for(name: str) -> Generator[Dict, None, None]:
def commented_out_default_config() -> str:
ans = []
for line in as_conf_file(all_options.values()):
if line and line[0] != '#':
line = '# ' + line
ans.append(line)
return '\n'.join(ans)
from .options.definition import definition
return definition.as_conf(commented=True)
def prepare_config_file_for_editing() -> str:
@ -229,11 +93,11 @@ def prepare_config_file_for_editing() -> str:
return defconf
def finalize_keys(opts: OptionsStub) -> None:
def finalize_keys(opts: Options) -> None:
defns: List[KeyDefinition] = []
for d in getattr(opts, 'map'):
for d in opts.map:
if d is None: # clear_all_shortcuts
defns = []
defns = [] # type: ignore
else:
defns.append(d.resolve_and_copy(opts.kitty_mod, opts.kitten_alias))
keymap: KeyMap = {}
@ -260,13 +124,10 @@ def finalize_keys(opts: OptionsStub) -> None:
opts.sequence_map = sequence_map
def finalize_mouse_mappings(opts: OptionsStub) -> None:
def finalize_mouse_mappings(opts: Options) -> None:
defns: List[MouseMapping] = []
for d in getattr(opts, 'mouse_map'):
if d is None: # clear_all_shortcuts
defns = []
else:
defns.append(d.resolve_and_copy(opts.kitty_mod, opts.kitten_alias))
for d in opts.mouse_map:
defns.append(d.resolve_and_copy(opts.kitty_mod, opts.kitten_alias))
mousemap: MouseMap = {}
for defn in defns:
@ -278,17 +139,30 @@ def finalize_mouse_mappings(opts: OptionsStub) -> None:
opts.mousemap = mousemap
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumulate_bad_lines: Optional[List[BadLine]] = None) -> OptionsStub:
parser = parse_config
if accumulate_bad_lines is not None:
parser = partial(parse_config, accumulate_bad_lines=accumulate_bad_lines)
opts = _load_config(Options, defaults, parser, merge_configs, *paths, overrides=overrides)
def parse_config(lines: Iterable[str], accumulate_bad_lines: Optional[List[BadLine]] = None) -> Dict[str, Any]:
from .options.parse import create_result_dict, parse_conf_item
ans: Dict[str, Any] = create_result_dict()
parse_config_base(
lines,
parse_conf_item,
ans,
accumulate_bad_lines=accumulate_bad_lines
)
return ans
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumulate_bad_lines: Optional[List[BadLine]] = None) -> Options:
from .options.parse import merge_result_dicts
opts_dict = _load_config(defaults, partial(parse_config, accumulate_bad_lines=accumulate_bad_lines), merge_result_dicts, *paths, overrides=overrides)
opts = Options(opts_dict)
finalize_keys(opts)
finalize_mouse_mappings(opts)
# delete no longer needed definitions, replacing with empty placeholders
setattr(opts, 'kitten_alias', {})
setattr(opts, 'mouse_map', [])
setattr(opts, 'map', [])
opts.kitten_alias = {}
opts.mouse_map = []
opts.map = []
if opts.background_opacity < 1.0 and opts.macos_titlebar_color:
log_error('Cannot use both macos_titlebar_color and background_opacity')
opts.macos_titlebar_color = 0
@ -301,7 +175,7 @@ class KittyCommonOpts(TypedDict):
url_prefixes: Tuple[str, ...]
def common_opts_as_dict(opts: Optional[OptionsStub] = None) -> KittyCommonOpts:
def common_opts_as_dict(opts: Optional[Options] = None) -> KittyCommonOpts:
if opts is None:
opts = defaults
return {

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,13 @@
import pwd
import sys
from contextlib import suppress
from typing import NamedTuple, Optional, Set
from typing import NamedTuple, Optional, Set, TYPE_CHECKING
from .options_stub import Options
from .types import run_once
if TYPE_CHECKING:
from .options.types import Options
class Version(NamedTuple):
major: int
@ -149,7 +151,7 @@ def detect_if_wayland_ok() -> bool:
return True
def is_wayland(opts: Optional[Options] = None) -> bool:
def is_wayland(opts: Optional['Options'] = None) -> bool:
if is_macos:
return False
if opts is None:

View File

@ -8,7 +8,7 @@ import termios
from kitty.boss import Boss
from kitty.fonts import FontFeature
from kitty.fonts.render import FontObject
from kitty.options_stub import Options
from kitty.options.types import Options
# Constants {{{
MOUSE_SELECTION_LINE: int

View File

@ -7,7 +7,7 @@
from kitty.fast_data_types import coretext_all_fonts
from kitty.fonts import FontFeature
from kitty.options_stub import Options
from kitty.options.types import Options
from kitty.typing import CoreTextFont
from kitty.utils import log_error

View File

@ -11,7 +11,7 @@
FC_WEIGHT_REGULAR, FC_WIDTH_NORMAL, fc_list, fc_match as fc_match_impl,
fc_match_postscript_name, parse_font_feature
)
from kitty.options_stub import Options
from kitty.options.types import Options
from kitty.typing import FontConfigPattern
from kitty.utils import log_error

View File

@ -10,7 +10,6 @@
Any, Callable, Dict, Generator, List, Optional, Tuple, Union, cast
)
from kitty.config import defaults
from kitty.constants import is_macos
from kitty.fast_data_types import (
Screen, create_test_font_group, get_fallback_font, set_font_data,
@ -20,7 +19,7 @@
from kitty.fonts.box_drawing import (
BufType, render_box_char, render_missing_glyph
)
from kitty.options_stub import Options as OptionsStub
from kitty.options.types import Options, defaults
from kitty.typing import CoreTextFont, FontConfigPattern
from kitty.utils import log_error
@ -33,7 +32,7 @@
current_faces: List[Tuple[FontObject, bool, bool]] = []
def get_font_files(opts: OptionsStub) -> Dict[str, Any]:
def get_font_files(opts: Options) -> Dict[str, Any]:
if is_macos:
return get_font_files_coretext(opts)
return get_font_files_fontconfig(opts)
@ -136,7 +135,7 @@ def coalesce_symbol_maps(maps: Dict[Tuple[int, int], str]) -> Dict[Tuple[int, in
return dict(ans)
def create_symbol_map(opts: OptionsStub) -> Tuple[Tuple[int, int, int], ...]:
def create_symbol_map(opts: Options) -> Tuple[Tuple[int, int, int], ...]:
val = coalesce_symbol_maps(opts.symbol_map)
family_map: Dict[str, int] = {}
count = 0
@ -174,7 +173,7 @@ def face_str(f: Tuple[FontObject, bool, bool]) -> str:
log_error(face_str(face))
def set_font_family(opts: Optional[OptionsStub] = None, override_font_size: Optional[float] = None, debug_font_matching: bool = False) -> None:
def set_font_family(opts: Optional[Options] = None, override_font_size: Optional[float] = None, debug_font_matching: bool = False) -> None:
global current_faces
opts = opts or defaults
sz = override_font_size or opts.font_size

View File

@ -4,11 +4,12 @@
from typing import Optional, Union
from .conf.utils import KeyAction
from .fast_data_types import (
GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER,
GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER, KeyEvent
)
from .options.utils import KeyAction, KeyMap, SequenceMap, SubSequenceMap
from .options.utils import KeyMap, SequenceMap, SubSequenceMap
from .types import SingleKey
from .typing import ScreenType

View File

@ -13,7 +13,7 @@
from kitty.fast_data_types import (
Region, set_active_window, viewport_for_window
)
from kitty.options_stub import Options
from kitty.options.types import Options
from kitty.types import Edges, WindowGeometry
from kitty.typing import TypedDict, WindowType
from kitty.window_list import WindowGroup, WindowList

View File

@ -27,7 +27,7 @@
)
from .fonts.box_drawing import set_scale
from .fonts.render import set_font_family
from .options_stub import Options as OptionsStub
from .options.types import Options
from .os_window_size import initial_window_size_func
from .session import get_os_window_sizing_data
from .types import SingleKey
@ -98,13 +98,13 @@ def init_glfw_module(glfw_module: str, debug_keyboard: bool = False, debug_rende
raise SystemExit('GLFW initialization failed')
def init_glfw(opts: OptionsStub, debug_keyboard: bool = False, debug_rendering: bool = False) -> str:
def init_glfw(opts: Options, debug_keyboard: bool = False, debug_rendering: bool = False) -> str:
glfw_module = 'cocoa' if is_macos else ('wayland' if is_wayland(opts) else 'x11')
init_glfw_module(glfw_module, debug_keyboard, debug_rendering)
return glfw_module
def get_macos_shortcut_for(opts: OptionsStub, function: str = 'new_os_window') -> Optional[SingleKey]:
def get_macos_shortcut_for(opts: Options, function: str = 'new_os_window') -> Optional[SingleKey]:
ans = None
candidates = []
for k, v in opts.keymap.items():
@ -132,7 +132,7 @@ def set_x11_window_icon() -> None:
set_default_window_icon(path + '-128' + ext)
def _run_app(opts: OptionsStub, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
def _run_app(opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
global_shortcuts: Dict[str, SingleKey] = {}
if is_macos:
for ac in ('new_os_window', 'close_os_window', 'close_tab', 'edit_config_file', 'previous_tab',
@ -169,7 +169,7 @@ def __init__(self) -> None:
self.first_window_callback = lambda window_handle: None
self.initial_window_size_func = initial_window_size_func
def __call__(self, opts: OptionsStub, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
def __call__(self, opts: Options, args: CLIOptions, bad_lines: Sequence[BadLine] = ()) -> None:
set_scale(opts.box_drawing_scale)
set_options(opts, is_wayland(), args.debug_rendering, args.debug_font_fallback)
try:
@ -248,7 +248,7 @@ def expand_listen_on(listen_on: str, from_config_file: bool) -> str:
return listen_on
def setup_environment(opts: OptionsStub, cli_opts: CLIOptions) -> None:
def setup_environment(opts: Options, cli_opts: CLIOptions) -> None:
from_config_file = False
if not cli_opts.listen_on and opts.listen_on.startswith('unix:'):
cli_opts.listen_on = opts.listen_on

View File

@ -11,10 +11,10 @@
)
from urllib.parse import ParseResult, unquote, urlparse
from .conf.utils import to_cmdline_implementation
from .conf.utils import KeyAction, to_cmdline_implementation
from .constants import config_dir
from .guess_mime_type import guess_type
from .options.utils import KeyAction, parse_key_action
from .options.utils import parse_key_action
from .types import run_once
from .typing import MatchType
from .utils import expandvars, log_error

3325
kitty/options/definition.py Normal file

File diff suppressed because it is too large Load Diff

1291
kitty/options/parse.py generated Normal file

File diff suppressed because it is too large Load Diff

1040
kitty/options/types.py generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,32 +7,30 @@
import re
import sys
from typing import (
Any, Callable, Dict, FrozenSet, Iterable, List, NamedTuple, Optional,
Sequence, Tuple, Union
Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Sequence, Tuple,
Union
)
import kitty.fast_data_types as defines
from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from kitty.conf.utils import (
key_func, positive_float, positive_int, python_string, to_bool, to_cmdline,
to_color, uniq, unit_float
KeyAction, key_func, positive_float, positive_int, python_string, to_bool,
to_cmdline, to_color, uniq, unit_float
)
from kitty.constants import config_dir, is_macos
from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from kitty.fonts import FontFeature
from kitty.key_names import (
character_key_name_aliases, functional_key_name_aliases,
get_key_name_lookup
)
from kitty.layout.interface import all_layouts
from kitty.rgb import Color, color_as_int
from kitty.types import FloatEdges, MouseEvent, SingleKey
from kitty.utils import expandvars, log_error
KeyMap = Dict[SingleKey, 'KeyAction']
MouseMap = Dict[MouseEvent, 'KeyAction']
KeyMap = Dict[SingleKey, KeyAction]
MouseMap = Dict[MouseEvent, KeyAction]
KeySequence = Tuple[SingleKey, ...]
SubSequenceMap = Dict[KeySequence, 'KeyAction']
SubSequenceMap = Dict[KeySequence, KeyAction]
SequenceMap = Dict[SingleKey, SubSequenceMap]
MINIMUM_FONT_SIZE = 4
default_tab_separator = ''
@ -46,16 +44,6 @@
FuncArgsType = Tuple[str, Sequence[Any]]
class KeyAction(NamedTuple):
func: str
args: Tuple[Union[str, float, bool, int, None], ...] = ()
def __repr__(self) -> str:
if self.args:
return f'KeyAction({self.func!r}, {self.args!r})'
return f'KeyAction({self.func!r})'
class InvalidMods(ValueError):
pass
@ -512,6 +500,7 @@ def window_size(val: str) -> Tuple[int, str]:
def to_layout_names(raw: str) -> List[str]:
from kitty.layout.interface import all_layouts
parts = [x.strip().lower() for x in raw.split(',')]
ans: List[str] = []
for p in parts:
@ -762,7 +751,7 @@ def parse_key_action(action: str, action_type: str = 'map') -> Optional[KeyActio
class BaseDefinition:
action: KeyAction
def resolve_kitten_aliases(self, aliases: Dict[str, Sequence[str]]) -> KeyAction:
def resolve_kitten_aliases(self, aliases: Dict[str, List[str]]) -> KeyAction:
if not self.action.args or not aliases:
return self.action
kitten = self.action.args[0]
@ -789,7 +778,7 @@ def __init__(self, button: int, mods: int, repeat_count: int, grabbed: bool, act
def __repr__(self) -> str:
return f'MouseMapping({self.button}, {self.mods}, {self.repeat_count}, {self.grabbed}, {self.action})'
def resolve_and_copy(self, kitty_mod: int, aliases: Dict[str, Sequence[str]]) -> 'MouseMapping':
def resolve_and_copy(self, kitty_mod: int, aliases: Dict[str, List[str]]) -> 'MouseMapping':
return MouseMapping(self.button, defines.resolve_key_mods(kitty_mod, self.mods), self.repeat_count, self.grabbed, self.resolve_kitten_aliases(aliases))
@property
@ -808,7 +797,7 @@ def __init__(self, is_sequence: bool, action: KeyAction, mods: int, is_native: b
def __repr__(self) -> str:
return f'KeyDefinition({self.is_sequence}, {self.action}, {self.trigger.mods}, {self.trigger.is_native}, {self.trigger.key}, {self.rest})'
def resolve_and_copy(self, kitty_mod: int, aliases: Dict[str, Sequence[str]]) -> 'KeyDefinition':
def resolve_and_copy(self, kitty_mod: int, aliases: Dict[str, List[str]]) -> 'KeyDefinition':
def r(k: SingleKey) -> SingleKey:
mods = defines.resolve_key_mods(kitty_mod, k.mods)
return k._replace(mods=mods)

View File

@ -1,51 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
class Options:
pass
DiffOptions = Options
def generate_stub():
from .config_data import all_options
from .conf.definition import as_type_stub, save_type_stub
text = as_type_stub(
all_options,
preamble_lines=(
'from kitty.types import SingleKey',
'from kitty.options.utils import KeyAction, KeyMap, SequenceMap, MouseMap',
'from kitty.fonts import FontFeature',
),
extra_fields=(
('keymap', 'KeyMap'),
('sequence_map', 'SequenceMap'),
('mousemap', 'MouseMap'),
)
)
from kittens.diff.config_data import all_options
text += as_type_stub(
all_options,
class_name='DiffOptions',
preamble_lines=(
'from kitty.conf.utils import KittensKeyAction',
'from kitty.types import ParsedShortcut',
),
extra_fields=(
('key_definitions', 'typing.Dict[ParsedShortcut, KittensKeyAction]'),
)
)
save_type_stub(text, __file__)
if __name__ == '__main__':
import subprocess
subprocess.Popen([
'kitty', '+runpy',
'from kitty.options_stub import generate_stub; generate_stub()'
])

View File

@ -12,7 +12,7 @@
if TYPE_CHECKING:
from kitty.cli_stub import SetSpacingRCOptions as CLIOptions
from kitty.options_stub import Options
from kitty.options.types import Options
def patch_window_edges(w: Window, s: Dict[str, Optional[float]]) -> None:

View File

@ -10,7 +10,7 @@
from .options.utils import to_layout_names, window_size
from .constants import kitty_exe
from .layout.interface import all_layouts
from .options_stub import Options
from .options.types import Options
from .os_window_size import WindowSize, WindowSizeData, WindowSizes
from .typing import SpecialWindowInstance
from .utils import log_error, resolved_shell

View File

@ -7,7 +7,7 @@
from typing import TYPE_CHECKING, Dict, Generator, Optional, cast
if TYPE_CHECKING:
from .options_stub import Options
from .options.types import Options
def modify_key_bytes(keybytes: bytes, amt: int) -> bytes:

View File

@ -22,3 +22,4 @@
MatchType = str
Protocol = object
MouseEvent = dict
OptionsProtocol = object

View File

@ -12,11 +12,10 @@ from kittens.tui.loop import (
Debug as Debug, Loop as LoopType, MouseEvent as MouseEvent,
TermManager as TermManagerType
)
from kitty.conf.utils import KittensKeyAction as KittensKeyActionType
from .boss import Boss as BossType
from .child import Child as ChildType
from .conf.utils import BadLine as BadLineType
from .conf.utils import BadLine as BadLineType, KeyAction as KeyActionType
from .config import KittyCommonOpts
from .fast_data_types import (
CoreTextFont as CoreTextFont, FontConfigPattern as FontConfigPattern,
@ -25,7 +24,7 @@ from .fast_data_types import (
from .key_encoding import KeyEvent as KeyEventType
from .layout.base import Layout as LayoutType
from .options.utils import (
KeyAction as KeyActionType, KeyMap as KeyMap, SequenceMap as SequenceMap
KeyMap as KeyMap, SequenceMap as SequenceMap
)
from .rc.base import RemoteCommand as RemoteCommandType
from .session import Session as SessionType, Tab as SessionTab
@ -56,7 +55,7 @@ __all__ = (
'EdgeLiteral', 'MatchType', 'GRT_a', 'GRT_f', 'GRT_t', 'GRT_o', 'GRT_m', 'GRT_d',
'GraphicsCommandType', 'HandlerType', 'AbstractEventLoop', 'AddressFamily', 'Socket', 'CompletedProcess',
'PopenType', 'Protocol', 'TypedDict', 'MarkType', 'ImageManagerType', 'Debug', 'LoopType', 'MouseEvent',
'TermManagerType', 'KittensKeyActionType', 'BossType', 'ChildType', 'BadLineType',
'TermManagerType', 'BossType', 'ChildType', 'BadLineType',
'KeyActionType', 'KeyMap', 'KittyCommonOpts', 'SequenceMap', 'CoreTextFont', 'WindowSystemMouseEvent',
'FontConfigPattern', 'ScreenType', 'StartupCtx', 'KeyEventType', 'LayoutType', 'PowerlineStyle',
'RemoteCommandType', 'SessionType', 'SessionTab', 'SpecialWindowInstance', 'TabType', 'ScreenSize', 'WindowType'

View File

@ -14,19 +14,23 @@
from functools import lru_cache
from time import monotonic
from typing import (
Any, Callable, Dict, Generator, Iterable, List, Mapping, Match, NamedTuple,
Optional, Tuple, Union, cast
TYPE_CHECKING, Any, Callable, Dict, Generator, Iterable, List, Mapping,
Match, NamedTuple, Optional, Tuple, Union, cast
)
from .constants import (
appname, is_macos, is_wayland, read_kitty_resource, shell_path,
supports_primary_selection
)
from .options_stub import Options
from .rgb import Color, to_color
from .types import run_once
from .typing import AddressFamily, PopenType, Socket, StartupCtx
if TYPE_CHECKING:
from .options.types import Options
else:
Options = object
def expandvars(val: str, env: Mapping[str, str] = {}, fallback_to_os_env: bool = True) -> str:

View File

@ -35,7 +35,7 @@
)
from .keys import keyboard_mode_name
from .notify import NotificationCommand, handle_notification_cmd
from .options_stub import Options
from .options.types import Options
from .rgb import to_color
from .terminfo import get_capabilities
from .types import MouseEvent, ScreenGeometry, WindowGeometry
@ -1016,8 +1016,8 @@ def toggle_marker(self, ftype: str, spec: Union[str, Tuple[Tuple[int, str], ...]
self.current_marker_spec = key
def set_marker(self, spec: Union[str, Sequence[str]]) -> None:
from .options.utils import parse_marker_spec, toggle_marker
from .marks import marker_from_spec
from .options.utils import parse_marker_spec, toggle_marker
if isinstance(spec, str):
func, (ftype, spec_, flags) = toggle_marker('toggle_marker', spec)
else:

View File

@ -6,11 +6,12 @@
from unittest import TestCase
from kitty.config import (
Options, defaults, finalize_keys, finalize_mouse_mappings, merge_configs
Options, defaults, finalize_keys, finalize_mouse_mappings
)
from kitty.fast_data_types import (
Cursor, HistoryBuf, LineBuf, Screen, set_options
)
from kitty.options.parse import merge_result_dicts
from kitty.types import MouseEvent
@ -106,7 +107,7 @@ def set_options(self, options=None):
final_options = {'scrollback_pager_history_size': 1024, 'click_interval': 0.5}
if options:
final_options.update(options)
options = Options(merge_configs(defaults._asdict(), final_options))
options = Options(merge_result_dicts(defaults._asdict(), final_options))
finalize_keys(options)
finalize_mouse_mappings(options)
set_options(options)

View File

@ -68,8 +68,6 @@ def q(test: unittest.TestCase) -> bool:
def type_check() -> NoReturn:
from kitty.cli_stub import generate_stub # type:ignore
generate_stub()
from kitty.options_stub import generate_stub # type: ignore
generate_stub()
from kittens.tui.operations_stub import generate_stub # type: ignore
generate_stub()
os.execlp(sys.executable, 'python', '-m', 'mypy', '--pretty')

View File

@ -1110,9 +1110,9 @@ def src_ignore(parent: str, entries: Iterable[str]) -> List[str]:
if for_freeze:
shutil.copytree('kitty_tests', os.path.join(libdir, 'kitty_tests'))
if args.update_check_interval != 24.0:
with open(os.path.join(libdir, 'kitty/config_data.py'), 'r+', encoding='utf-8') as f:
with open(os.path.join(libdir, 'kitty/options/types.py'), 'r+', encoding='utf-8') as f:
raw = f.read()
nraw = raw.replace("update_check_interval', 24", "update_check_interval', {}".format(args.update_check_interval), 1)
nraw = raw.replace('update_check_interval: float = 24.0', f'update_check_interval: float = {args.update_check_interval!r}', 1)
if nraw == raw:
raise SystemExit('Failed to change the value of update_check_interval')
f.seek(0), f.truncate(), f.write(nraw)