Allow action_alias to work with any action

This commit is contained in:
Kovid Goyal 2021-11-23 14:43:36 +05:30
parent ee2520e036
commit a97a05b1ec
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 205 additions and 108 deletions

View File

@ -1951,8 +1951,18 @@ class Boss:
def format_bad_line(bad_line: BadLine) -> str:
return f'{bad_line.number}:{bad_line.exception} in line: {bad_line.line}\n'
msg = '\n'.join(map(format_bad_line, bad_lines)).rstrip()
self.show_error(_('Errors in kitty.conf'), msg)
groups: Dict[str, List[BadLine]] = {}
for bl in bad_lines:
groups.setdefault(bl.file, []).append(bl)
ans: List[str] = []
a = ans.append
for file in sorted(groups):
if file:
a(f'In file {file}:')
[a(format_bad_line(x)) for x in groups[file]]
msg = '\n'.join(ans).rstrip()
self.show_error(_('Errors parsing configuration'), msg)
@ac('misc', '''
Change colors in the specified windows

View File

@ -269,6 +269,11 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
a('if not is_macos:')
a(f' defaults.{option_name}.update({mval["linux"]!r}')
def resolve_action(a: Any) -> Any:
if hasattr(a, 'resolve_aliases_and_parse'):
a.resolve_aliases_and_parse({})
return a
for aname, func in action_parsers.items():
a(f'defaults.{aname} = [')
only: Dict[str, List[Tuple[str, Callable[..., Any]]]] = {}
@ -281,7 +286,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
else:
for val in func(text):
a(f' # {sc.name}')
a(f' {val!r},')
a(f' {resolve_action(val)!r},')
a(']')
if only:
imports.add(('kitty.constants', 'is_macos'))
@ -290,7 +295,7 @@ def generate_class(defn: Definition, loc: str) -> Tuple[str, str]:
a(f'if {cond}:')
for (text, func) in items:
for val in func(text):
a(f' defaults.{aname}.append({val!r})')
a(f' defaults.{aname}.append({resolve_action(val)!r})')
t('')
t('')

View File

@ -4,13 +4,14 @@
import os
import re
import shlex
from contextlib import contextmanager
from typing import (
Any, Callable, Dict, Generator, Iterable, List, NamedTuple, Optional,
Sequence, Set, Tuple, TypeVar, Union, Generic
Any, Callable, Dict, Generator, Generic, Iterable, Iterator, List,
NamedTuple, Optional, Sequence, Set, Tuple, TypeVar, Union
)
from ..rgb import to_color as as_color
from ..fast_data_types import Color
from ..rgb import to_color as as_color
from ..types import ConvertibleToNumbers, ParsedShortcut
from ..typing import Protocol
from ..utils import expandvars, log_error
@ -30,6 +31,7 @@ class BadLine(NamedTuple):
number: int
line: str
exception: Exception
file: str
def positive_int(x: ConvertibleToNumbers) -> int:
@ -118,6 +120,40 @@ def choices(*choices: str) -> Choice:
return Choice(choices)
class CurrentlyParsing:
__slots__ = 'line', 'number', 'file'
def __init__(self, line: str = '', number: int = -1, file: str = ''):
self.line = line
self.number = number
self.file = file
def __copy__(self) -> 'CurrentlyParsing':
return CurrentlyParsing(self.line, self.number, self.file)
@contextmanager
def set_line(self, line: str, number: int) -> Iterator['CurrentlyParsing']:
orig = self.line, self.number
self.line = line
self.number = number
try:
yield self
finally:
self.line, self.number = orig
@contextmanager
def set_file(self, file: str) -> Iterator['CurrentlyParsing']:
orig = self.file
self.file = file
try:
yield self
finally:
self.file = orig
currently_parsing = CurrentlyParsing()
def parse_line(
line: str,
parse_conf_item: ItemParser,
@ -139,7 +175,8 @@ def parse_line(
val = os.path.join(base_path_for_includes, val)
try:
with open(val, encoding='utf-8', errors='replace') as include:
_parse(include, parse_conf_item, ans, accumulate_bad_lines)
with currently_parsing.set_file(val):
_parse(include, parse_conf_item, ans, accumulate_bad_lines)
except FileNotFoundError:
log_error(
'Could not find included config file: {}, ignoring'.
@ -169,13 +206,12 @@ def _parse(
base_path_for_includes = config_dir
for i, line in enumerate(lines):
try:
parse_line(
line, parse_conf_item, ans, base_path_for_includes, accumulate_bad_lines
)
with currently_parsing.set_line(line, i + 1):
parse_line(line, parse_conf_item, ans, base_path_for_includes, accumulate_bad_lines)
except Exception as e:
if accumulate_bad_lines is None:
raise
accumulate_bad_lines.append(BadLine(i + 1, line.rstrip(), e))
accumulate_bad_lines.append(BadLine(i + 1, line.rstrip(), e, currently_parsing.file))
def parse_config_base(
@ -219,13 +255,15 @@ def load_config(
continue
try:
with open(path, encoding='utf-8', errors='replace') as f:
vals = parse_config(f)
with currently_parsing.set_file(path):
vals = parse_config(f)
except (FileNotFoundError, PermissionError):
continue
found_paths.append(path)
ans = merge_configs(ans, vals)
if overrides is not None:
vals = parse_config(overrides)
with currently_parsing.set_file('<override>'):
vals = parse_config(overrides)
ans = merge_configs(ans, vals)
return ans, tuple(found_paths)

View File

@ -91,13 +91,20 @@ def prepare_config_file_for_editing() -> str:
return defconf
def finalize_keys(opts: Options, alias_map: Dict[str, List[ActionAlias]]) -> None:
def finalize_keys(opts: Options, alias_map: Dict[str, List[ActionAlias]], accumulate_bad_lines: Optional[List[BadLine]] = None) -> None:
defns: List[KeyDefinition] = []
for d in opts.map:
if d is None: # clear_all_shortcuts
defns = [] # type: ignore
else:
defns.append(d.resolve_and_copy(opts.kitty_mod, alias_map))
try:
defns.append(d.resolve_and_copy(opts.kitty_mod, alias_map))
except Exception as err:
if accumulate_bad_lines is None:
log_error(f'Ignoring map with invalid action: {d.unresolved_action}. Error: {err}')
else:
accumulate_bad_lines.append(BadLine(d.definition_location.number, d.definition_location.line, err, d.definition_location.file))
keymap: KeyMap = {}
sequence_map: SequenceMap = {}
@ -122,13 +129,19 @@ def finalize_keys(opts: Options, alias_map: Dict[str, List[ActionAlias]]) -> Non
opts.sequence_map = sequence_map
def finalize_mouse_mappings(opts: Options, alias_map: Dict[str, List[ActionAlias]]) -> None:
def finalize_mouse_mappings(opts: Options, alias_map: Dict[str, List[ActionAlias]], accumulate_bad_lines: Optional[List[BadLine]] = None) -> None:
defns: List[MouseMapping] = []
for d in opts.mouse_map:
if d is None: # clear_all_mouse_actions
defns = [] # type: ignore
else:
defns.append(d.resolve_and_copy(opts.kitty_mod, alias_map))
try:
defns.append(d.resolve_and_copy(opts.kitty_mod, alias_map))
except Exception as err:
if accumulate_bad_lines is None:
log_error(f'Ignoring mouse_map with invalid action: {d.unresolved_action}. Error: {err}')
else:
accumulate_bad_lines.append(BadLine(d.definition_location.number, d.definition_location.line, err, d.definition_location.file))
mousemap: MouseMap = {}
for defn in defns:
@ -161,8 +174,8 @@ def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumula
alias_map = build_action_aliases(opts.kitten_alias, 'kitten')
alias_map.update(build_action_aliases(opts.action_alias))
finalize_keys(opts, alias_map)
finalize_mouse_mappings(opts, alias_map)
finalize_keys(opts, alias_map, accumulate_bad_lines)
finalize_mouse_mappings(opts, alias_map, accumulate_bad_lines)
# delete no longer needed definitions, replacing with empty placeholders
opts.kitten_alias = {}
opts.action_alias = {}

View File

@ -2875,10 +2875,9 @@ opt('+action_alias', 'launch_tab launch --type=tab --cwd=current',
add_to_default=False,
long_text='''
Define aliases to avoid repeating the same options in multiple mappings. Aliases
can be defined for the :ref:`launch <action-launch>`, :ref:`kitten <action-kitten>`
and :ref:`action-remote_control` actions. Aliases are expanded recursively.
For example, the above alias allows you to create mappings to launch a new tab without
duplication::
can be defined for any action. Aliases are expanded recursively.
For example, the above alias allows you to create mappings to launch a new tab
in the current working directory without duplication::
map f1 launch_tab vim
map f2 launch_tab emacs

View File

@ -584,10 +584,10 @@ class Options:
window_padding_width: FloatEdges = FloatEdges(left=0, top=0, right=0, bottom=0)
window_resize_step_cells: int = 2
window_resize_step_lines: int = 2
action_alias: typing.Dict[str, typing.List[str]] = {}
action_alias: typing.Dict[str, str] = {}
env: typing.Dict[str, str] = {}
font_features: typing.Dict[str, typing.Tuple[kitty.fonts.FontFeature, ...]] = {}
kitten_alias: typing.Dict[str, typing.List[str]] = {}
kitten_alias: typing.Dict[str, str] = {}
symbol_map: typing.Dict[typing.Tuple[int, int], str] = {}
watcher: typing.Dict[str, str] = {}
map: typing.List[kitty.options.utils.KeyDefinition] = []

View File

@ -12,8 +12,9 @@ from typing import (
import kitty.fast_data_types as defines
from kitty.conf.utils import (
KeyAction, KeyFuncWrapper, positive_float, positive_int, python_string,
to_bool, to_cmdline, to_color, uniq, unit_float
CurrentlyParsing, KeyAction, KeyFuncWrapper, currently_parsing,
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 (
@ -765,11 +766,11 @@ def watcher(val: str, current_val: Container[str]) -> Iterable[Tuple[str, str]]:
yield val, val
def action_alias(val: str) -> Iterable[Tuple[str, List[str]]]:
def action_alias(val: str) -> Iterable[Tuple[str, str]]:
parts = val.split(maxsplit=1)
if len(parts) > 1:
alias_name, rest = parts
yield alias_name, to_cmdline(rest)
yield alias_name, rest
kitten_alias = action_alias
@ -802,98 +803,122 @@ def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
yield (a, b), family
def parse_combine(rest: str, action_type: str = 'map') -> Iterator[KeyAction]:
sep, rest = rest.split(maxsplit=1)
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
for x in parts:
if x:
yield from parse_key_actions(x, action_type)
def parse_key_actions(action: str, action_type: str = 'map') -> Iterator[KeyAction]:
def parse_key_action(action: str, action_type: str = 'map') -> KeyAction:
parts = action.strip().split(maxsplit=1)
func = parts[0]
if len(parts) == 1:
yield KeyAction(func, ())
return
return KeyAction(func, ())
rest = parts[1]
if func == 'combine':
yield from parse_combine(rest, action_type)
else:
parser = func_with_args.get(func) or shlex_parse
try:
func, args = parser(func, rest)
except Exception as err:
log_error(f'Ignoring invalid {action_type} action: {action} with err: {err}')
else:
yield KeyAction(func, tuple(args))
parser = func_with_args.get(func)
if parser is None:
raise KeyError(f'Unknown action: {func}')
func, args = parser(func, rest)
return KeyAction(func, tuple(args))
class ActionAlias(NamedTuple):
func_name: str
args: Tuple[str, ...]
second_arg_test: Optional[Callable[[Any], bool]] = None
name: str
value: str
is_recursive: bool
replace_second_arg: bool = False
def build_action_aliases(raw: Dict[str, List[str]], first_arg_replacement: str = '') -> Dict[str, List[ActionAlias]]:
def build_action_aliases(raw: Dict[str, str], first_arg_replacement: str = '') -> Dict[str, List[ActionAlias]]:
ans: Dict[str, List[ActionAlias]] = {}
if first_arg_replacement:
for alias_name, args in raw.items():
ans.setdefault(first_arg_replacement, []).append(ActionAlias(first_arg_replacement, tuple(args), alias_name.__eq__))
for alias_name, rest in raw.items():
is_recursive = alias_name == rest.split(maxsplit=1)[0]
ans.setdefault(first_arg_replacement, []).append(ActionAlias(alias_name, rest, is_recursive, True))
else:
for alias_name, args in raw.items():
ans[alias_name] = [ActionAlias(args[0], tuple(args[1:]))]
for alias_name, rest in raw.items():
is_recursive = alias_name == rest.split(maxsplit=1)[0]
ans[alias_name] = [ActionAlias(alias_name, rest, is_recursive)]
return ans
def resolve_aliases_in_action(action: KeyAction, aliases: Dict[str, List[ActionAlias]]) -> KeyAction:
for alias in aliases.get(action.func, ()):
if alias.second_arg_test is None:
new_action = action._replace(func=alias.func_name, args=alias.args + action.args)
if new_action.func == action.func:
def resolve_aliases_and_parse_actions(
defn: str, aliases: Dict[str, List[ActionAlias]], map_type: str
) -> Iterator[KeyAction]:
parts = defn.split(maxsplit=1)
if len(parts) == 1:
possible_alias = defn
rest = ''
else:
possible_alias = parts[0]
rest = parts[1]
for alias in aliases.get(possible_alias, ()):
if alias.replace_second_arg: # kitten_alias
if not rest:
continue
parts = rest.split(maxsplit=1)
if parts[0] != alias.name:
continue
new_defn = possible_alias + ' ' + alias.value + ((' ' + parts[1]) if len(parts) > 1 else '')
new_aliases = aliases
if alias.is_recursive:
new_aliases = aliases.copy()
new_aliases.pop(alias.func_name)
else:
new_aliases = aliases
return resolve_aliases_in_action(new_action, new_aliases)
if action.args and alias.second_arg_test(action.args[0]):
new_action = action._replace(func=alias.func_name, args=alias.args + action.args[1:])
if new_action.func == action.func and new_action.args and alias.second_arg_test(new_action.args[0]):
new_aliases[possible_alias] = [a for a in aliases[possible_alias] if a is not alias]
yield from resolve_aliases_and_parse_actions(new_defn, new_aliases, map_type)
return
else: # action_alias
new_defn = alias.value + ((' ' + rest) if rest else '')
new_aliases = aliases
if alias.is_recursive:
new_aliases = aliases.copy()
new_aliases[action.func] = [x for x in aliases[action.func] if x is not alias]
else:
new_aliases = aliases
return resolve_aliases_in_action(new_action, new_aliases)
return action
new_aliases.pop(possible_alias)
yield from resolve_aliases_and_parse_actions(new_defn, new_aliases, map_type)
return
if possible_alias == 'combine':
sep, rest = rest.split(maxsplit=1)
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
for x in parts:
if x:
yield from resolve_aliases_and_parse_actions(x, aliases, map_type)
else:
yield parse_key_action(defn, map_type)
class BaseDefinition:
actions: Tuple[KeyAction, ...]
actions: Tuple[KeyAction, ...] = ()
no_op_actions = frozenset(('noop', 'no-op', 'no_op'))
map_type: str = 'map'
definition_location: CurrentlyParsing
def __init__(self, unresolved_action: str) -> None:
self.unresolved_action = unresolved_action
self.definition_location = currently_parsing.__copy__()
@property
def is_no_op(self) -> bool:
return len(self.actions) == 1 and self.actions[0].func in self.no_op_actions
def resolve_aliases(self, aliases: Dict[str, List[ActionAlias]]) -> Tuple[KeyAction, ...]:
self.actions = tuple(resolve_aliases_in_action(a, aliases) for a in self.actions)
return self.actions
def resolve_aliases_and_parse(self, aliases: Dict[str, List[ActionAlias]]) -> None:
if self.unresolved_action:
self.actions = tuple(resolve_aliases_and_parse_actions(
self.unresolved_action, aliases, self.map_type))
self.unresolved_action = ''
class MouseMapping(BaseDefinition):
map_type: str = 'mouse_map'
def __init__(self, button: int, mods: int, repeat_count: int, grabbed: bool, actions: Tuple[KeyAction, ...]):
def __init__(self, button: int, mods: int, repeat_count: int, grabbed: bool, actions: Tuple[KeyAction, ...] = (), unresolved_action: str = ''):
super().__init__(unresolved_action)
self.button = button
self.mods = mods
self.actions = actions
self.repeat_count = repeat_count
self.grabbed = grabbed
self.actions = actions
def __repr__(self) -> str:
return f'MouseMapping({self.button}, {self.mods}, {self.repeat_count}, {self.grabbed}, {self.actions})'
def resolve_and_copy(self, kitty_mod: int, aliases: Dict[str, List[ActionAlias]]) -> 'MouseMapping':
return MouseMapping(self.button, defines.resolve_key_mods(kitty_mod, self.mods), self.repeat_count, self.grabbed, self.resolve_aliases(aliases))
ans = MouseMapping(
self.button, defines.resolve_key_mods(kitty_mod, self.mods), self.repeat_count, self.grabbed, self.actions, self.unresolved_action)
ans.resolve_aliases_and_parse(aliases)
return ans
@property
def trigger(self) -> MouseEvent:
@ -902,7 +927,11 @@ class MouseMapping(BaseDefinition):
class KeyDefinition(BaseDefinition):
def __init__(self, is_sequence: bool, actions: Tuple[KeyAction, ...], mods: int, is_native: bool, key: int, rest: Tuple[SingleKey, ...] = ()):
def __init__(
self, is_sequence: bool, actions: Tuple[KeyAction, ...], mods: int, is_native: bool, key: int,
rest: Tuple[SingleKey, ...] = (), unresolved_action: str = ''
):
super().__init__(unresolved_action)
self.is_sequence = is_sequence
self.actions = actions
self.trigger = SingleKey(mods, is_native, key)
@ -915,10 +944,12 @@ class KeyDefinition(BaseDefinition):
def r(k: SingleKey) -> SingleKey:
mods = defines.resolve_key_mods(kitty_mod, k.mods)
return k._replace(mods=mods)
return KeyDefinition(
self.is_sequence, self.resolve_aliases(aliases),
defines.resolve_key_mods(kitty_mod, self.trigger.mods),
self.trigger.is_native, self.trigger.key, tuple(map(r, self.rest)))
ans = KeyDefinition(
self.is_sequence, self.actions, defines.resolve_key_mods(kitty_mod, self.trigger.mods),
self.trigger.is_native, self.trigger.key, tuple(map(r, self.rest)), self.unresolved_action
)
ans.resolve_aliases_and_parse(aliases)
return ans
def parse_map(val: str) -> Iterable[KeyDefinition]:
@ -956,18 +987,12 @@ def parse_map(val: str) -> Iterable[KeyDefinition]:
if mods is not None:
log_error(f'Shortcut: {sc} has unknown key, ignoring')
return
try:
paction = tuple(parse_key_actions(action))
except Exception:
log_error(f'Invalid shortcut action: {action}. Ignoring.')
if is_sequence:
if trigger is not None:
yield KeyDefinition(True, (), trigger[0], trigger[1], trigger[2], rest, unresolved_action=action)
else:
if paction:
if is_sequence:
if trigger is not None:
yield KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest)
else:
assert key is not None
yield KeyDefinition(False, paction, mods, is_native, key)
assert key is not None
yield KeyDefinition(False, (), mods, is_native, key, unresolved_action=action)
def parse_mouse_map(val: str) -> Iterable[MouseMapping]:
@ -1000,15 +1025,8 @@ def parse_mouse_map(val: str) -> Iterable[MouseMapping]:
if specified_modes - {'grabbed', 'ungrabbed'}:
log_error(f'Mouse modes: {modes} not recognized, ignoring')
return
try:
paction = tuple(parse_key_actions(action, 'mouse_map'))
except Exception:
log_error(f'Invalid mouse action: {action}. Ignoring.')
return
if not paction:
return
for mode in sorted(specified_modes):
yield MouseMapping(button, mods, count, mode == 'grabbed', paction)
yield MouseMapping(button, mods, count, mode == 'grabbed', unresolved_action=action)
def deprecated_hide_window_decorations_aliases(key: str, val: str, ans: Dict[str, Any]) -> None:

View File

@ -27,7 +27,10 @@ class TestConfParsing(BaseTest):
del bad_lines[:]
del self.error_messages[:]
ans = load_config(overrides=lines, accumulate_bad_lines=bad_lines)
self.ae(len(bad_lines), bad_line_num)
if bad_line_num:
self.ae(len(bad_lines), bad_line_num)
else:
self.assertFalse(bad_lines)
return ans
def keys_for_func(opts, name):
@ -51,6 +54,7 @@ class TestConfParsing(BaseTest):
opts = p('pointer_shape_when_grabbed XXX', bad_line_num=1)
self.ae(opts.pointer_shape_when_grabbed, defaults.pointer_shape_when_grabbed)
# test the aliasing options
opts = p('env A=1', 'env B=x$A', 'env C=', 'env D', 'clear_all_shortcuts y', 'kitten_alias a b --moo', 'map f1 kitten a arg')
self.ae(opts.env, {'A': '1', 'B': 'x1', 'C': '', 'D': DELETE_ENV_VAR})
ka = tuple(opts.keymap.values())[0][0]
@ -77,10 +81,20 @@ class TestConfParsing(BaseTest):
self.ae(ka.func, 'launch')
self.ae(ka.args, ('--moo', 'XXX'))
opts = p('clear_all_shortcuts y', 'action_alias cfs change_font_size current', 'map f1 cfs +2')
ka = tuple(opts.keymap.values())[0][0]
self.ae(ka.func, 'change_font_size')
self.ae(ka.args, (False, '+', 2.0))
opts = p('clear_all_shortcuts y', 'action_alias la launch --moo', 'map f1 combine : new_window : la ')
ka = tuple(opts.keymap.values())[0]
self.ae((ka[0].func, ka[1].func), ('new_window', 'launch'))
opts = p('clear_all_shortcuts y', 'action_alias cc combine : new_window : launch --moo', 'map f1 cc XXX')
ka = tuple(opts.keymap.values())[0]
self.ae((ka[0].func, ka[1].func), ('new_window', 'launch'))
self.ae(ka[1].args, ('--moo', 'XXX'))
opts = p('kitty_mod alt')
self.ae(opts.kitty_mod, to_modifiers('alt'))
self.ae(next(keys_for_func(opts, 'next_layout')).mods, opts.kitty_mod)