Move action parsing to option_types

This commit is contained in:
Kovid Goyal 2021-05-26 15:31:01 +05:30
parent 9f8a120664
commit bfbb85399e
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 315 additions and 313 deletions

View File

@ -4,333 +4,29 @@
import json import json
import os import os
import re
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from functools import partial from functools import partial
from typing import ( from typing import (
Any, Callable, Dict, FrozenSet, Generator, Iterable, List, Optional, Any, Callable, Dict, FrozenSet, Generator, Iterable, List, Optional,
Sequence, Tuple, Type, Union Sequence, Tuple, Type
) )
from . import fast_data_types as defines
from .conf.definition import as_conf_file, config_lines from .conf.definition import as_conf_file, config_lines
from .conf.utils import ( from .conf.utils import (
BadLine, init_config, load_config as _load_config, merge_dicts, BadLine, init_config, load_config as _load_config, merge_dicts,
parse_config_base, python_string, to_bool, to_cmdline parse_config_base, to_bool
) )
from .config_data import all_options from .config_data import all_options
from .constants import cache_dir, defconf, is_macos from .constants import cache_dir, defconf, is_macos
from .options_stub import Options as OptionsStub from .options_stub import Options as OptionsStub
from .options_types import ( from .options_types import (
FuncArgsType, KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, env,
env, font_features, func_with_args, kitten_alias, parse_key_action, font_features, kitten_alias, parse_map, parse_mouse_map, symbol_map
parse_map, parse_mouse_map, symbol_map
) )
from .typing import TypedDict from .typing import TypedDict
from .utils import log_error from .utils import log_error
@func_with_args(
'pass_selection_to_program', 'new_window', 'new_tab', 'new_os_window',
'new_window_with_cwd', 'new_tab_with_cwd', 'new_os_window_with_cwd',
'launch'
)
def shlex_parse(func: str, rest: str) -> FuncArgsType:
return func, to_cmdline(rest)
@func_with_args('combine')
def combine_parse(func: str, rest: str) -> FuncArgsType:
sep, rest = rest.split(maxsplit=1)
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
args = tuple(map(parse_key_action, filter(None, parts)))
return func, args
@func_with_args('send_text')
def send_text_parse(func: str, rest: str) -> FuncArgsType:
args = rest.split(maxsplit=1)
mode = ''
data = b''
if len(args) > 1:
mode = args[0]
try:
data = parse_send_text_bytes(args[1])
except Exception:
log_error('Ignoring invalid send_text string: ' + args[1])
return func, [mode, data]
@func_with_args('run_kitten', 'run_simple_kitten', 'kitten')
def kitten_parse(func: str, rest: str) -> FuncArgsType:
if func == 'kitten':
args = rest.split(maxsplit=1)
else:
args = rest.split(maxsplit=2)[1:]
func = 'kitten'
return func, args
@func_with_args('goto_tab')
def goto_tab_parse(func: str, rest: str) -> FuncArgsType:
args = (max(0, int(rest)), )
return func, args
@func_with_args('detach_window')
def detach_window_parse(func: str, rest: str) -> FuncArgsType:
if rest not in ('new', 'new-tab', 'ask'):
log_error('Ignoring invalid detach_window argument: {}'.format(rest))
rest = 'new'
return func, (rest,)
@func_with_args('detach_tab')
def detach_tab_parse(func: str, rest: str) -> FuncArgsType:
if rest not in ('new', 'ask'):
log_error('Ignoring invalid detach_tab argument: {}'.format(rest))
rest = 'new'
return func, (rest,)
@func_with_args('set_background_opacity', 'goto_layout', 'kitty_shell')
def simple_parse(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('set_font_size')
def float_parse(func: str, rest: str) -> FuncArgsType:
return func, (float(rest),)
@func_with_args('signal_child')
def signal_child_parse(func: str, rest: str) -> FuncArgsType:
import signal
signals = []
for q in rest.split():
try:
signum = getattr(signal, q.upper())
except AttributeError:
log_error(f'Unknown signal: {rest} ignoring')
else:
signals.append(signum)
return func, tuple(signals)
@func_with_args('change_font_size')
def parse_change_font_size(func: str, rest: str) -> Tuple[str, Tuple[bool, Optional[str], float]]:
vals = rest.strip().split(maxsplit=1)
if len(vals) != 2:
log_error('Invalid change_font_size specification: {}, treating it as default'.format(rest))
return func, (True, None, 0)
c_all = vals[0].lower() == 'all'
sign: Optional[str] = None
amt = vals[1]
if amt[0] in '+-':
sign = amt[0]
amt = amt[1:]
return func, (c_all, sign, float(amt.strip()))
@func_with_args('clear_terminal')
def clear_terminal(func: str, rest: str) -> FuncArgsType:
vals = rest.strip().split(maxsplit=1)
if len(vals) != 2:
log_error('clear_terminal needs two arguments, using defaults')
args: List[Union[str, bool]] = ['reset', 'active']
else:
args = [vals[0].lower(), vals[1].lower() == 'active']
return func, args
@func_with_args('copy_to_buffer')
def copy_to_buffer(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('paste_from_buffer')
def paste_from_buffer(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('neighboring_window')
def neighboring_window(func: str, rest: str) -> FuncArgsType:
rest = rest.lower()
rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest)
if rest not in ('left', 'right', 'top', 'bottom'):
log_error('Invalid neighbor specification: {}'.format(rest))
rest = 'right'
return func, [rest]
@func_with_args('resize_window')
def resize_window(func: str, rest: str) -> FuncArgsType:
vals = rest.strip().split(maxsplit=1)
if len(vals) > 2:
log_error('resize_window needs one or two arguments, using defaults')
args = ['wider', 1]
else:
quality = vals[0].lower()
if quality not in ('taller', 'shorter', 'wider', 'narrower'):
log_error('Invalid quality specification: {}'.format(quality))
quality = 'wider'
increment = 1
if len(vals) == 2:
try:
increment = int(vals[1])
except Exception:
log_error('Invalid increment specification: {}'.format(vals[1]))
args = [quality, increment]
return func, args
@func_with_args('move_window')
def move_window(func: str, rest: str) -> FuncArgsType:
rest = rest.lower()
rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest)
prest: Union[int, str] = rest
try:
prest = int(prest)
except Exception:
if prest not in ('left', 'right', 'top', 'bottom'):
log_error('Invalid move_window specification: {}'.format(rest))
prest = 0
return func, [prest]
@func_with_args('pipe')
def pipe(func: str, rest: str) -> FuncArgsType:
import shlex
r = shlex.split(rest)
if len(r) < 3:
log_error('Too few arguments to pipe function')
r = ['none', 'none', 'true']
return func, r
@func_with_args('set_colors')
def set_colors(func: str, rest: str) -> FuncArgsType:
import shlex
r = shlex.split(rest)
if len(r) < 1:
log_error('Too few arguments to set_colors function')
return func, r
@func_with_args('remote_control')
def remote_control(func: str, rest: str) -> FuncArgsType:
import shlex
r = shlex.split(rest)
if len(r) < 1:
log_error('Too few arguments to remote_control function')
return func, r
@func_with_args('nth_window')
def nth_window(func: str, rest: str) -> FuncArgsType:
try:
num = int(rest)
except Exception:
log_error('Invalid nth_window number: {}'.format(rest))
num = 1
return func, [num]
@func_with_args('disable_ligatures_in')
def disable_ligatures_in(func: str, rest: str) -> FuncArgsType:
parts = rest.split(maxsplit=1)
if len(parts) == 1:
where, strategy = 'active', parts[0]
else:
where, strategy = parts
if where not in ('active', 'all', 'tab'):
raise ValueError('{} is not a valid set of windows to disable ligatures in'.format(where))
if strategy not in ('never', 'always', 'cursor'):
raise ValueError('{} is not a valid disable ligatures strategy'.format(strategy))
return func, [where, strategy]
@func_with_args('layout_action')
def layout_action(func: str, rest: str) -> FuncArgsType:
parts = rest.split(maxsplit=1)
if not parts:
raise ValueError('layout_action must have at least one argument')
return func, [parts[0], tuple(parts[1:])]
def parse_marker_spec(ftype: str, parts: Sequence[str]) -> Tuple[str, Union[str, Tuple[Tuple[int, str], ...]], int]:
flags = re.UNICODE
if ftype in ('text', 'itext', 'regex', 'iregex'):
if ftype.startswith('i'):
flags |= re.IGNORECASE
if not parts or len(parts) % 2 != 0:
raise ValueError('No color specified in marker: {}'.format(' '.join(parts)))
ans = []
for i in range(0, len(parts), 2):
try:
color = max(1, min(int(parts[i]), 3))
except Exception:
raise ValueError('color {} in marker specification is not an integer'.format(parts[i]))
sspec = parts[i + 1]
if 'regex' not in ftype:
sspec = re.escape(sspec)
ans.append((color, sspec))
ftype = 'regex'
spec: Union[str, Tuple[Tuple[int, str], ...]] = tuple(ans)
elif ftype == 'function':
spec = ' '.join(parts)
else:
raise ValueError('Unknown marker type: {}'.format(ftype))
return ftype, spec, flags
@func_with_args('toggle_marker')
def toggle_marker(func: str, rest: str) -> FuncArgsType:
import shlex
parts = rest.split(maxsplit=1)
if len(parts) != 2:
raise ValueError('{} is not a valid marker specification'.format(rest))
ftype, spec = parts
parts = shlex.split(spec)
return func, list(parse_marker_spec(ftype, parts))
@func_with_args('scroll_to_mark')
def scroll_to_mark(func: str, rest: str) -> FuncArgsType:
parts = rest.split()
if not parts or not rest:
return func, [True, 0]
if len(parts) == 1:
q = parts[0].lower()
if q in ('prev', 'previous', 'next'):
return func, [q != 'next', 0]
try:
return func, [True, max(0, min(int(q), 3))]
except Exception:
raise ValueError('{} is not a valid scroll_to_mark destination'.format(rest))
return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))]
@func_with_args('mouse_selection')
def mouse_selection(func: str, rest: str) -> FuncArgsType:
cmap = getattr(mouse_selection, 'code_map', None)
if cmap is None:
cmap = {
'normal': defines.MOUSE_SELECTION_NORMAL,
'extend': defines.MOUSE_SELECTION_EXTEND,
'rectangle': defines.MOUSE_SELECTION_RECTANGLE,
'word': defines.MOUSE_SELECTION_WORD,
'line': defines.MOUSE_SELECTION_LINE,
'line_from_point': defines.MOUSE_SELECTION_LINE_FROM_POINT,
}
setattr(mouse_selection, 'code_map', cmap)
return func, [cmap[rest]]
def parse_send_text_bytes(text: str) -> bytes:
return python_string(text).encode('utf-8')
def parse_send_text(val: str, key_definitions: List[KeyDefinition]) -> None: def parse_send_text(val: str, key_definitions: List[KeyDefinition]) -> None:
parts = val.split(' ') parts = val.split(' ')

View File

@ -4,6 +4,7 @@
import os import os
import re
import sys import sys
from typing import ( from typing import (
Any, Callable, Dict, FrozenSet, Iterable, List, NamedTuple, Optional, Any, Callable, Dict, FrozenSet, Iterable, List, NamedTuple, Optional,
@ -14,7 +15,8 @@ import kitty.fast_data_types as defines
from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from .conf.utils import ( from .conf.utils import (
key_func, positive_float, positive_int, to_bool, to_color, uniq, unit_float key_func, positive_float, positive_int, python_string, to_bool, to_cmdline,
to_color, uniq, unit_float
) )
from .constants import config_dir from .constants import config_dir
from .fonts import FontFeature from .fonts import FontFeature
@ -53,6 +55,309 @@ class InvalidMods(ValueError):
pass pass
# Actions {{{
@func_with_args(
'pass_selection_to_program', 'new_window', 'new_tab', 'new_os_window',
'new_window_with_cwd', 'new_tab_with_cwd', 'new_os_window_with_cwd',
'launch'
)
def shlex_parse(func: str, rest: str) -> FuncArgsType:
return func, to_cmdline(rest)
@func_with_args('combine')
def combine_parse(func: str, rest: str) -> FuncArgsType:
sep, rest = rest.split(maxsplit=1)
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
args = tuple(map(parse_key_action, filter(None, parts)))
return func, args
def parse_send_text_bytes(text: str) -> bytes:
return python_string(text).encode('utf-8')
@func_with_args('send_text')
def send_text_parse(func: str, rest: str) -> FuncArgsType:
args = rest.split(maxsplit=1)
mode = ''
data = b''
if len(args) > 1:
mode = args[0]
try:
data = parse_send_text_bytes(args[1])
except Exception:
log_error('Ignoring invalid send_text string: ' + args[1])
return func, [mode, data]
@func_with_args('run_kitten', 'run_simple_kitten', 'kitten')
def kitten_parse(func: str, rest: str) -> FuncArgsType:
if func == 'kitten':
args = rest.split(maxsplit=1)
else:
args = rest.split(maxsplit=2)[1:]
func = 'kitten'
return func, args
@func_with_args('goto_tab')
def goto_tab_parse(func: str, rest: str) -> FuncArgsType:
args = (max(0, int(rest)), )
return func, args
@func_with_args('detach_window')
def detach_window_parse(func: str, rest: str) -> FuncArgsType:
if rest not in ('new', 'new-tab', 'ask'):
log_error('Ignoring invalid detach_window argument: {}'.format(rest))
rest = 'new'
return func, (rest,)
@func_with_args('detach_tab')
def detach_tab_parse(func: str, rest: str) -> FuncArgsType:
if rest not in ('new', 'ask'):
log_error('Ignoring invalid detach_tab argument: {}'.format(rest))
rest = 'new'
return func, (rest,)
@func_with_args('set_background_opacity', 'goto_layout', 'kitty_shell')
def simple_parse(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('set_font_size')
def float_parse(func: str, rest: str) -> FuncArgsType:
return func, (float(rest),)
@func_with_args('signal_child')
def signal_child_parse(func: str, rest: str) -> FuncArgsType:
import signal
signals = []
for q in rest.split():
try:
signum = getattr(signal, q.upper())
except AttributeError:
log_error(f'Unknown signal: {rest} ignoring')
else:
signals.append(signum)
return func, tuple(signals)
@func_with_args('change_font_size')
def parse_change_font_size(func: str, rest: str) -> Tuple[str, Tuple[bool, Optional[str], float]]:
vals = rest.strip().split(maxsplit=1)
if len(vals) != 2:
log_error('Invalid change_font_size specification: {}, treating it as default'.format(rest))
return func, (True, None, 0)
c_all = vals[0].lower() == 'all'
sign: Optional[str] = None
amt = vals[1]
if amt[0] in '+-':
sign = amt[0]
amt = amt[1:]
return func, (c_all, sign, float(amt.strip()))
@func_with_args('clear_terminal')
def clear_terminal(func: str, rest: str) -> FuncArgsType:
vals = rest.strip().split(maxsplit=1)
if len(vals) != 2:
log_error('clear_terminal needs two arguments, using defaults')
args: List[Union[str, bool]] = ['reset', 'active']
else:
args = [vals[0].lower(), vals[1].lower() == 'active']
return func, args
@func_with_args('copy_to_buffer')
def copy_to_buffer(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('paste_from_buffer')
def paste_from_buffer(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('neighboring_window')
def neighboring_window(func: str, rest: str) -> FuncArgsType:
rest = rest.lower()
rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest)
if rest not in ('left', 'right', 'top', 'bottom'):
log_error('Invalid neighbor specification: {}'.format(rest))
rest = 'right'
return func, [rest]
@func_with_args('resize_window')
def resize_window(func: str, rest: str) -> FuncArgsType:
vals = rest.strip().split(maxsplit=1)
if len(vals) > 2:
log_error('resize_window needs one or two arguments, using defaults')
args = ['wider', 1]
else:
quality = vals[0].lower()
if quality not in ('taller', 'shorter', 'wider', 'narrower'):
log_error('Invalid quality specification: {}'.format(quality))
quality = 'wider'
increment = 1
if len(vals) == 2:
try:
increment = int(vals[1])
except Exception:
log_error('Invalid increment specification: {}'.format(vals[1]))
args = [quality, increment]
return func, args
@func_with_args('move_window')
def move_window(func: str, rest: str) -> FuncArgsType:
rest = rest.lower()
rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest)
prest: Union[int, str] = rest
try:
prest = int(prest)
except Exception:
if prest not in ('left', 'right', 'top', 'bottom'):
log_error('Invalid move_window specification: {}'.format(rest))
prest = 0
return func, [prest]
@func_with_args('pipe')
def pipe(func: str, rest: str) -> FuncArgsType:
import shlex
r = shlex.split(rest)
if len(r) < 3:
log_error('Too few arguments to pipe function')
r = ['none', 'none', 'true']
return func, r
@func_with_args('set_colors')
def set_colors(func: str, rest: str) -> FuncArgsType:
import shlex
r = shlex.split(rest)
if len(r) < 1:
log_error('Too few arguments to set_colors function')
return func, r
@func_with_args('remote_control')
def remote_control(func: str, rest: str) -> FuncArgsType:
import shlex
r = shlex.split(rest)
if len(r) < 1:
log_error('Too few arguments to remote_control function')
return func, r
@func_with_args('nth_window')
def nth_window(func: str, rest: str) -> FuncArgsType:
try:
num = int(rest)
except Exception:
log_error('Invalid nth_window number: {}'.format(rest))
num = 1
return func, [num]
@func_with_args('disable_ligatures_in')
def disable_ligatures_in(func: str, rest: str) -> FuncArgsType:
parts = rest.split(maxsplit=1)
if len(parts) == 1:
where, strategy = 'active', parts[0]
else:
where, strategy = parts
if where not in ('active', 'all', 'tab'):
raise ValueError('{} is not a valid set of windows to disable ligatures in'.format(where))
if strategy not in ('never', 'always', 'cursor'):
raise ValueError('{} is not a valid disable ligatures strategy'.format(strategy))
return func, [where, strategy]
@func_with_args('layout_action')
def layout_action(func: str, rest: str) -> FuncArgsType:
parts = rest.split(maxsplit=1)
if not parts:
raise ValueError('layout_action must have at least one argument')
return func, [parts[0], tuple(parts[1:])]
def parse_marker_spec(ftype: str, parts: Sequence[str]) -> Tuple[str, Union[str, Tuple[Tuple[int, str], ...]], int]:
flags = re.UNICODE
if ftype in ('text', 'itext', 'regex', 'iregex'):
if ftype.startswith('i'):
flags |= re.IGNORECASE
if not parts or len(parts) % 2 != 0:
raise ValueError('No color specified in marker: {}'.format(' '.join(parts)))
ans = []
for i in range(0, len(parts), 2):
try:
color = max(1, min(int(parts[i]), 3))
except Exception:
raise ValueError('color {} in marker specification is not an integer'.format(parts[i]))
sspec = parts[i + 1]
if 'regex' not in ftype:
sspec = re.escape(sspec)
ans.append((color, sspec))
ftype = 'regex'
spec: Union[str, Tuple[Tuple[int, str], ...]] = tuple(ans)
elif ftype == 'function':
spec = ' '.join(parts)
else:
raise ValueError('Unknown marker type: {}'.format(ftype))
return ftype, spec, flags
@func_with_args('toggle_marker')
def toggle_marker(func: str, rest: str) -> FuncArgsType:
import shlex
parts = rest.split(maxsplit=1)
if len(parts) != 2:
raise ValueError('{} is not a valid marker specification'.format(rest))
ftype, spec = parts
parts = shlex.split(spec)
return func, list(parse_marker_spec(ftype, parts))
@func_with_args('scroll_to_mark')
def scroll_to_mark(func: str, rest: str) -> FuncArgsType:
parts = rest.split()
if not parts or not rest:
return func, [True, 0]
if len(parts) == 1:
q = parts[0].lower()
if q in ('prev', 'previous', 'next'):
return func, [q != 'next', 0]
try:
return func, [True, max(0, min(int(q), 3))]
except Exception:
raise ValueError('{} is not a valid scroll_to_mark destination'.format(rest))
return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))]
@func_with_args('mouse_selection')
def mouse_selection(func: str, rest: str) -> FuncArgsType:
cmap = getattr(mouse_selection, 'code_map', None)
if cmap is None:
cmap = {
'normal': defines.MOUSE_SELECTION_NORMAL,
'extend': defines.MOUSE_SELECTION_EXTEND,
'rectangle': defines.MOUSE_SELECTION_RECTANGLE,
'word': defines.MOUSE_SELECTION_WORD,
'line': defines.MOUSE_SELECTION_LINE,
'line_from_point': defines.MOUSE_SELECTION_LINE_FROM_POINT,
}
setattr(mouse_selection, 'code_map', cmap)
return func, [cmap[rest]]
# }}}
def parse_mods(parts: Iterable[str], sc: str) -> Optional[int]: def parse_mods(parts: Iterable[str], sc: str) -> Optional[int]:
def map_mod(m: str) -> str: def map_mod(m: str) -> str:
@ -423,7 +728,7 @@ def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]:
yield (a, b), family yield (a, b), family
def parse_key_action(action: str) -> Optional[KeyAction]: def parse_key_action(action: str, action_type: str = 'map') -> Optional[KeyAction]:
parts = action.strip().split(maxsplit=1) parts = action.strip().split(maxsplit=1)
func = parts[0] func = parts[0]
if len(parts) == 1: if len(parts) == 1:
@ -434,9 +739,11 @@ def parse_key_action(action: str) -> Optional[KeyAction]:
try: try:
func, args = parser(func, rest) func, args = parser(func, rest)
except Exception as err: except Exception as err:
log_error('Ignoring invalid key action: {} with err: {}'.format(action, err)) log_error(f'Ignoring invalid {action_type} action: {action} with err: {err}')
else: else:
return KeyAction(func, args) return KeyAction(func, args)
else:
log_error(f'Ignoring unknown {action_type} action: {action}')
return None return None
@ -583,12 +890,11 @@ def parse_mouse_map(val: str) -> Iterable[MouseMapping]:
log_error(f'Mouse modes: {modes} not recognized, ignoring') log_error(f'Mouse modes: {modes} not recognized, ignoring')
return return
try: try:
paction = parse_key_action(action) paction = parse_key_action(action, 'mouse_map')
except Exception: except Exception:
log_error(f'Invalid mouse action: {action}. Ignoring.') log_error(f'Invalid mouse action: {action}. Ignoring.')
return return
if paction is None: if paction is None:
log_error(f'Ignoring unknown mouse action: {action}')
return return
for mode in specified_modes: for mode in specified_modes:
yield MouseMapping(button, mods, count, mode == 'grabbed', paction) yield MouseMapping(button, mods, count, mode == 'grabbed', paction)