diff --git a/kitty/config.py b/kitty/config.py index a5a2f1a37..7d5ddb112 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -4,333 +4,29 @@ import json import os -import re from contextlib import contextmanager, suppress from functools import partial from typing import ( 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.utils import ( 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 .constants import cache_dir, defconf, is_macos from .options_stub import Options as OptionsStub from .options_types import ( - FuncArgsType, KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, - env, font_features, func_with_args, kitten_alias, parse_key_action, - parse_map, parse_mouse_map, symbol_map + KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, env, + font_features, kitten_alias, parse_map, parse_mouse_map, symbol_map ) from .typing import TypedDict 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: parts = val.split(' ') diff --git a/kitty/options_types.py b/kitty/options_types.py index 75f316612..c2b6c5959 100644 --- a/kitty/options_types.py +++ b/kitty/options_types.py @@ -4,6 +4,7 @@ import os +import re import sys from typing import ( 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 .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 .fonts import FontFeature @@ -53,6 +55,309 @@ class InvalidMods(ValueError): 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 map_mod(m: str) -> str: @@ -423,7 +728,7 @@ def symbol_map(val: str) -> Iterable[Tuple[Tuple[int, int], str]]: 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) func = parts[0] if len(parts) == 1: @@ -434,9 +739,11 @@ def parse_key_action(action: str) -> Optional[KeyAction]: try: func, args = parser(func, rest) 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: return KeyAction(func, args) + else: + log_error(f'Ignoring unknown {action_type} action: {action}') return None @@ -583,12 +890,11 @@ def parse_mouse_map(val: str) -> Iterable[MouseMapping]: log_error(f'Mouse modes: {modes} not recognized, ignoring') return try: - paction = parse_key_action(action) + paction = parse_key_action(action, 'mouse_map') except Exception: log_error(f'Invalid mouse action: {action}. Ignoring.') return if paction is None: - log_error(f'Ignoring unknown mouse action: {action}') return for mode in specified_modes: yield MouseMapping(button, mods, count, mode == 'grabbed', paction)