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:
parent
dd5715ce79
commit
6d7df1c5e8
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -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
|
||||
|
||||
@ -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
342
gen-config.py
Executable 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()
|
||||
@ -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
|
||||
|
||||
@ -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)'))
|
||||
@ -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)
|
||||
|
||||
@ -19,11 +19,10 @@ from typing import (
|
||||
|
||||
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.loop import Loop
|
||||
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 @@ class DiffHandler(Handler):
|
||||
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 @@ class DiffHandler(Handler):
|
||||
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 @@ elif __name__ == '__doc__':
|
||||
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
|
||||
|
||||
0
kittens/diff/options/__init__.py
Normal file
0
kittens/diff/options/__init__.py
Normal file
241
kittens/diff/options/definition.py
Normal file
241
kittens/diff/options/definition.py
Normal 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
120
kittens/diff/options/parse.py
generated
Normal 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
141
kittens/diff/options/types.py
generated
Normal 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))),
|
||||
]
|
||||
62
kittens/diff/options/utils.py
Normal file
62
kittens/diff/options/utils.py
Normal 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
|
||||
@ -6,7 +6,7 @@ import re
|
||||
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
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ from typing import (
|
||||
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 @@ class Handler:
|
||||
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
|
||||
|
||||
@ -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__)
|
||||
|
||||
@ -18,7 +18,7 @@ from weakref import WeakValueDictionary
|
||||
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 .fast_data_types import (
|
||||
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
|
||||
|
||||
16
kitty/cli.py
16
kitty/cli.py
@ -12,10 +12,10 @@ from typing import (
|
||||
)
|
||||
|
||||
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 compare_mousemaps(final: MouseMap, initial: MouseMap) -> 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)
|
||||
|
||||
@ -18,7 +18,7 @@ QueryTerminalCLIOptions = BroadcastCLIOptions = ShowKeyCLIOptions = 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] = ()):
|
||||
|
||||
@ -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
272
kitty/conf/types.py
Normal 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:')
|
||||
@ -6,18 +6,26 @@ import os
|
||||
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 key_func() -> Tuple[Callable[..., Callable], Dict[str, 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)
|
||||
|
||||
206
kitty/config.py
206
kitty/config.py
@ -6,160 +6,28 @@ import json
|
||||
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 {
|
||||
|
||||
1349
kitty/config_data.py
1349
kitty/config_data.py
File diff suppressed because it is too large
Load Diff
@ -7,11 +7,13 @@ import os
|
||||
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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -7,7 +7,7 @@ from typing import Dict, Generator, Iterable, List, Optional, Tuple
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ from kitty.fast_data_types import (
|
||||
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
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ from typing import (
|
||||
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.fast_data_types import (
|
||||
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 @@ FontObject = Union[CoreTextFont, FontConfigPattern]
|
||||
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 dump_faces(ftypes: List[str], indices: Dict[str, int]) -> None:
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ from kitty.borders import BorderColor
|
||||
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
|
||||
|
||||
@ -27,7 +27,7 @@ from .fast_data_types import (
|
||||
)
|
||||
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 @@ class AppRunner:
|
||||
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
|
||||
|
||||
@ -11,10 +11,10 @@ from typing import (
|
||||
)
|
||||
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
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
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
1040
kitty/options/types.py
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,32 +7,30 @@ import os
|
||||
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 @@ func_with_args, args_funcs = key_func()
|
||||
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 @@ class MouseMapping(BaseDefinition):
|
||||
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 @@ class KeyDefinition(BaseDefinition):
|
||||
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)
|
||||
|
||||
@ -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()'
|
||||
])
|
||||
@ -12,7 +12,7 @@ from .base import (
|
||||
|
||||
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:
|
||||
|
||||
@ -10,7 +10,7 @@ from .cli_stub import CLIOptions
|
||||
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
|
||||
|
||||
@ -7,7 +7,7 @@ from binascii import hexlify, unhexlify
|
||||
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:
|
||||
|
||||
@ -22,3 +22,4 @@ PowerlineStyle = str
|
||||
MatchType = str
|
||||
Protocol = object
|
||||
MouseEvent = dict
|
||||
OptionsProtocol = object
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -14,19 +14,23 @@ from contextlib import suppress
|
||||
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:
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ from .fast_data_types import (
|
||||
)
|
||||
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 @@ class Window:
|
||||
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:
|
||||
|
||||
@ -6,11 +6,12 @@ import os
|
||||
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 @@ class BaseTest(TestCase):
|
||||
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)
|
||||
|
||||
@ -68,8 +68,6 @@ def filter_tests_by_module(suite: unittest.TestSuite, *names: str) -> unittest.T
|
||||
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')
|
||||
|
||||
4
setup.py
4
setup.py
@ -1110,9 +1110,9 @@ def package(args: Options, bundle_type: str) -> None:
|
||||
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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user