#!/usr/bin/env python # License: GPLv3 Copyright: 2021, Kovid Goyal import enum import re import sys from collections import defaultdict from collections.abc import Callable, Container, Iterable, Iterator, Sequence from contextlib import suppress from dataclasses import dataclass, fields from functools import lru_cache from typing import ( Any, Generic, Literal, NamedTuple, TypeVar, cast, get_args, ) import kitty.fast_data_types as defines from kitty.conf.utils import ( CurrentlyParsing, KeyAction, KeyFuncWrapper, currently_parsing, number_with_unit, percent, positive_float, positive_int, python_string, to_bool, to_cmdline, to_color, uniq, unit_float, ) from kitty.constants import is_macos from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_HOLLOW, CURSOR_UNDERLINE, NO_CURSOR_SHAPE, Color, Shlex, SingleKey from kitty.fonts import FontModification, FontSpec, ModificationType, ModificationUnit, ModificationValue from kitty.key_names import character_key_name_aliases, functional_key_name_aliases, get_key_name_lookup from kitty.rgb import color_as_int from kitty.types import FloatEdges, MouseEvent from kitty.utils import expandvars, log_error, resolve_abs_or_config_path, shlex_split KeyMap = dict[SingleKey, list['KeyDefinition']] MouseMap = dict[MouseEvent, str] KeySequence = tuple[SingleKey, ...] MINIMUM_FONT_SIZE = 4 default_tab_separator = ' ┇' mod_map = {'⌃': 'CONTROL', 'CTRL': 'CONTROL', '⇧': 'SHIFT', '⌥': 'ALT', 'OPTION': 'ALT', 'OPT': 'ALT', '⌘': 'SUPER', 'COMMAND': 'SUPER', 'CMD': 'SUPER', 'KITTY_MOD': 'KITTY'} character_key_name_aliases_with_ascii_lowercase: dict[str, str] = character_key_name_aliases.copy() for x in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': character_key_name_aliases_with_ascii_lowercase[x] = x.lower() sequence_sep = '>' mouse_button_map = {'left': 'b1', 'middle': 'b3', 'right': 'b2'} mouse_trigger_count_map = {'doubleclick': -3, 'click': -2, 'release': -1, 'press': 1, 'doublepress': 2, 'triplepress': 3} FuncArgsType = tuple[str, Sequence[Any]] func_with_args = KeyFuncWrapper[FuncArgsType]() DELETE_ENV_VAR = '_delete_this_env_var_' class MapType(enum.Enum): MAP = 'map' MOUSE_MAP = 'mouse_map' OPEN_ACTION = 'open_action' 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', 'mouse_handle_click', 'show_error', ) def shlex_parse(func: str, rest: str) -> FuncArgsType: return func, to_cmdline(rest) def parse_send_text_bytes(text: str) -> bytes: return defines.expand_ansi_c_escapes(text).encode('utf-8') @func_with_args('scroll_prompt_to_top') def scroll_prompt_to_top(func: str, rest: str) -> FuncArgsType: return func, [to_bool(rest) if rest else False] @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('send_key') def send_key(func: str, rest: str) -> FuncArgsType: return func, rest.split() @func_with_args('run_kitten', 'run_simple_kitten', 'kitten') def kitten_parse(func: str, rest: str) -> FuncArgsType: parts = to_cmdline(rest) if func == 'kitten': return func, parts return 'kitten', parts[1:] @func_with_args('open_url') def open_url_parse(func: str, rest: str) -> FuncArgsType: from urllib.parse import urlparse url = '' try: url = python_string(rest) tokens = urlparse(url) if not all((tokens.scheme, tokens.netloc,)): raise ValueError('Invalid URL') except Exception: log_error('Ignoring invalid URL string: ' + rest) return func, (url,) @func_with_args('goto_tab') def goto_tab_parse(func: str, rest: str) -> FuncArgsType: n = int(rest) if n < 0: n += 1 # goto_tab subtracts 1 from its argument, this maps both zero and -1 to previous tab for backwards compat. return func, (n,) @func_with_args('detach_window') def detach_window_parse(func: str, rest: str) -> FuncArgsType: if rest not in ('new', 'new-tab', 'new-tab-left', 'new-tab-right', 'ask', 'tab-prev', 'tab-left', 'tab-right'): log_error(f'Ignoring invalid detach_window argument: {rest}') rest = 'new' return func, (rest,) @func_with_args('close_window_with_confirmation') def close_window_with_confirmation(func: str, rest: str) -> FuncArgsType: ignore_shell = rest == 'ignore-shell' return func, (ignore_shell,) @func_with_args('detach_tab') def detach_tab_parse(func: str, rest: str) -> FuncArgsType: if rest not in ('new', 'ask'): log_error(f'Ignoring invalid detach_tab argument: {rest}') rest = 'new' return func, (rest,) @func_with_args( 'set_background_opacity', 'goto_layout', 'toggle_layout', 'toggle_tab', 'kitty_shell', 'show_kitty_doc', 'set_tab_title', 'push_keyboard_mode', 'dump_lines_with_attrs', 'set_window_title', ) 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, str | None, float]]: vals = rest.strip().split(maxsplit=1) if len(vals) != 2: log_error(f'Invalid change_font_size specification: {rest}, treating it as default') return func, (True, None, 0) c_all = vals[0].lower() == 'all' sign: str | None = 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 = ['reset', True] else: action = vals[0].lower() if action not in ('reset', 'scroll', 'scrollback', 'clear', 'to_cursor', 'to_cursor_scroll'): log_error(f'{action} is unknown for clear_terminal, using reset') action = 'reset' args = [action, 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('paste') def paste_parse(func: str, rest: str) -> FuncArgsType: text = '' try: text = defines.expand_ansi_c_escapes(rest) except Exception: log_error('Ignoring invalid paste string: ' + rest) return func, [text] @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(f'Invalid neighbor specification: {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 ('reset', 'taller', 'shorter', 'wider', 'narrower'): log_error(f'Invalid quality specification: {quality}') quality = 'wider' increment = 1 if len(vals) == 2: try: increment = int(vals[1]) except Exception: log_error(f'Invalid increment specification: {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: int | str = rest try: prest = int(prest) except Exception: if prest not in ('left', 'right', 'top', 'bottom'): log_error(f'Invalid move_window specification: {rest}') prest = 0 return func, [prest] @func_with_args('pipe') def pipe(func: str, rest: str) -> FuncArgsType: r = list(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: r = list(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: func, args = shlex_parse(func, rest) if len(args) < 1: log_error('Too few arguments to remote_control function') return func, args @func_with_args('remote_control_script') def remote_control_script(func: str, rest: str) -> FuncArgsType: func, args = shlex_parse(func, rest) if len(args) < 1: log_error('Too few arguments to remote_control_script function') return func, args @func_with_args('nth_os_window', 'nth_window', 'scroll_to_prompt', 'visual_window_select_action_trigger', 'next_layout') def single_integer_arg(func: str, rest: str) -> FuncArgsType: try: num = int(rest) except Exception: if rest: log_error(f'Invalid number for {func}: {rest}') num = -1 if func == 'scroll_to_prompt' else 1 return func, [num] @func_with_args('sleep') def sleep(func: str, sleep_time: str) -> FuncArgsType: mult = 1 sleep_time = sleep_time or '1' if sleep_time[-1] in 'shmd': mult = {'s': 1, 'm': 60, 'h': 3600, 'd': 24 * 3600}[sleep_time[-1]] sleep_time = sleep_time[:-1] return func, [abs(float(sleep_time)) * mult] @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(f'{where} is not a valid set of windows to disable ligatures in') if strategy not in ('never', 'always', 'cursor'): raise ValueError(f'{strategy} is not a valid disable ligatures 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, 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('Mark group number and text/regex are not specified in pairs: {}'.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(f'Mark group in marker specification is not an integer: {parts[i]}') sspec = parts[i + 1] if 'regex' not in ftype: sspec = re.escape(sspec) ans.append((color, sspec)) ftype = 'regex' spec: str | tuple[tuple[int, str], ...] = tuple(ans) elif ftype == 'function': spec = ' '.join(parts) else: raise ValueError(f'Unknown marker type: {ftype}') return ftype, spec, flags @func_with_args('toggle_marker') def toggle_marker(func: str, rest: str) -> FuncArgsType: parts = rest.split(maxsplit=1) if len(parts) != 2: raise ValueError(f'{rest} is not a valid marker specification') ftype, spec = parts parts = list(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(f'{rest} is not a valid scroll_to_mark destination') 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, 'move-end': defines.MOUSE_SELECTION_MOVE_END, 'rectangle': defines.MOUSE_SELECTION_RECTANGLE, 'word': defines.MOUSE_SELECTION_WORD, 'line': defines.MOUSE_SELECTION_LINE, 'line_from_point': defines.MOUSE_SELECTION_LINE_FROM_POINT, 'word_and_line_from_point': defines.MOUSE_SELECTION_WORD_AND_LINE_FROM_POINT, } setattr(mouse_selection, 'code_map', cmap) return func, [cmap[rest]] @func_with_args('load_config_file') def load_config_file(func: str, rest: str) -> FuncArgsType: return func, list(shlex_split(rest)) # }}} def parse_mods(parts: Iterable[str], sc: str) -> int | None: def map_mod(m: str) -> str: return mod_map.get(m, m) mods = 0 for m in parts: try: mods |= getattr(defines, f'GLFW_MOD_{map_mod(m.upper())}') except AttributeError: if m.upper() != 'NONE': log_error(f'Shortcut: {sc} has unknown modifier, ignoring') return None return mods def to_modifiers(val: str) -> int: return parse_mods(val.split('+'), val) or 0 def parse_shortcut(sc: str) -> SingleKey: if sc.endswith('+') and len(sc) > 1: sc = f'{sc[:-1]}plus' parts = sc.split('+') mods = 0 if len(parts) > 1: mods = parse_mods(parts[:-1], sc) or 0 if not mods: raise InvalidMods('Invalid shortcut') q = parts[-1] q = character_key_name_aliases_with_ascii_lowercase.get(q.upper(), q) is_native = False if q.startswith('0x'): try: key = int(q, 16) except Exception: key = 0 else: is_native = True else: try: key = ord(q) except Exception: uq = q.upper() uq = functional_key_name_aliases.get(uq, uq) x: int | None = getattr(defines, f'GLFW_FKEY_{uq}', None) if x is None: lf = get_key_name_lookup() key = lf(q, False) or 0 is_native = key > 0 else: key = x return SingleKey(mods, is_native, key or 0) def to_font_size(x: str) -> float: return max(MINIMUM_FONT_SIZE, float(x)) def disable_ligatures(x: str) -> int: cmap = {'never': 0, 'cursor': 1, 'always': 2} return cmap.get(x.lower(), 0) def box_drawing_scale(x: str) -> tuple[float, float, float, float]: ans = tuple(float(q.strip()) for q in x.split(',')) if len(ans) != 4: raise ValueError('Invalid box_drawing scale, must have four entries') return ans[0], ans[1], ans[2], ans[3] def cursor_text_color(x: str) -> Color | None: if x.lower() == 'background': return None return to_color(x) cshapes = { 'block': CURSOR_BLOCK, 'beam': CURSOR_BEAM, 'underline': CURSOR_UNDERLINE } cshapes_unfocused = { 'block': CURSOR_BLOCK, 'beam': CURSOR_BEAM, 'underline': CURSOR_UNDERLINE, 'hollow': CURSOR_HOLLOW, 'unchanged': NO_CURSOR_SHAPE, } def to_cursor_shape(x: str) -> int: try: return cshapes[x.lower()] except KeyError: raise ValueError( 'Invalid cursor shape: {} allowed values are {}'.format( x, ', '.join(cshapes) ) ) def to_cursor_unfocused_shape(x: str) -> int: try: return cshapes_unfocused[x.lower()] except KeyError: raise ValueError( 'Invalid unfocused cursor shape: {} allowed values are {}'.format( x, ', '.join(cshapes_unfocused) ) ) def cursor_trail_decay(x: str) -> tuple[float, float]: fast, slow = map(positive_float, x.split()) slow = max(slow, fast) return fast, slow def scrollback_lines(x: str) -> int: ans = int(x) if ans < 0: ans = 2 ** 32 - 1 return ans def scrollback_pager_history_size(x: str) -> int: ans = int(max(0, float(x)) * 1024 * 1024) return min(ans, 4096 * 1024 * 1024 - 1) # "single" for backwards compat url_style_map = {'none': 0, 'single': 1, 'straight': 1, 'double': 2, 'curly': 3, 'dotted': 4, 'dashed': 5} def url_style(x: str) -> int: return url_style_map.get(x, url_style_map['curly']) def url_prefixes(x: str) -> tuple[str, ...]: return tuple(a.lower() for a in x.replace(',', ' ').split()) def copy_on_select(raw: str) -> str: q = raw.lower() # boolean values special cased for backwards compat if q in ('y', 'yes', 'true', 'clipboard'): return 'clipboard' if q in ('n', 'no', 'false', ''): return '' return raw def window_size(val: str) -> tuple[int, str]: val = val.lower() unit = 'cells' if val.endswith('c') else 'px' return positive_int(val.rstrip('c')), unit def parse_layout_names(parts: Iterable[str]) -> list[str]: from kitty.layout.interface import all_layouts ans = [] for p in parts: p = p.lower() if p in ('*', 'all'): ans.extend(sorted(all_layouts)) continue name = p.partition(':')[0] if name not in all_layouts: raise ValueError(f'The window layout {p} is unknown') ans.append(p) return uniq(ans) def to_layout_names(raw: str) -> list[str]: return parse_layout_names(x.strip() for x in raw.split(',')) def window_border_width(x: str | int | float) -> tuple[float, str]: unit = 'pt' if isinstance(x, str): trailer = x[-2:] if trailer in ('px', 'pt'): unit = trailer val = float(x[:-2]) else: val = float(x) else: val = float(x) return max(0, val), unit def edge_width(x: str, converter: Callable[[str], float] = positive_float) -> FloatEdges: parts = str(x).split() num = len(parts) if num == 1: val = converter(parts[0]) return FloatEdges(val, val, val, val) if num == 2: v = converter(parts[0]) h = converter(parts[1]) return FloatEdges(h, v, h, v) if num == 3: top, h, bottom = map(converter, parts) return FloatEdges(h, top, h, bottom) top, right, bottom, left = map(converter, parts) return FloatEdges(left, top, right, bottom) def optional_edge_width(x: str) -> FloatEdges: return edge_width(x, float) def hide_window_decorations(x: str) -> int: if x == 'titlebar-only': return 0b10 if x == 'titlebar-and-corners': return 0b100 if to_bool(x): return 0b01 return 0b00 def resize_draw_strategy(x: str) -> int: cmap = {'static': 0, 'scale': 1, 'blank': 2, 'size': 3} return cmap.get(x.lower(), 0) def window_logo_scale(x: str) -> tuple[float, float]: parts = x.split(maxsplit=1) if len(parts) == 1: return positive_float(parts[0]), -1.0 return positive_float(parts[0]), positive_float(parts[1]) def resize_debounce_time(x: str) -> tuple[float, float]: parts = x.split(maxsplit=1) if len(parts) == 1: return positive_float(parts[0]), 0.5 return positive_float(parts[0]), positive_float(parts[1]) def visual_window_select_characters(x: str) -> str: import string valid_characters = string.digits + string.ascii_uppercase + "-=[]\\;',./`" ans = x.upper() ans_chars = set(ans) if not ans_chars.issubset(set(valid_characters)): raise ValueError(f'Invalid characters in visual_window_select_characters: {x} Only numbers (0-9) and alphabets (a-z,A-Z) are allowed. Ignoring.') if len(ans_chars) < len(x): raise ValueError(f'Invalid characters in visual_window_select_characters: {x} Contains identical numbers or alphabets, case insensitive. Ignoring.') return ans def tab_separator(x: str) -> str: for q in '\'"': if x.startswith(q) and x.endswith(q): x = x[1:-1] if not x: return '' break if not x.strip(): x = ('\xa0' * len(x)) if x else default_tab_separator return x def tab_bar_edge(x: str) -> int: return {'top': defines.TOP_EDGE, 'bottom': defines.BOTTOM_EDGE}.get(x.lower(), defines.BOTTOM_EDGE) def tab_font_style(x: str) -> tuple[bool, bool]: return { 'bold-italic': (True, True), 'bold': (True, False), 'italic': (False, True) }.get(x.lower().replace('_', '-'), (False, False)) def tab_bar_min_tabs(x: str) -> int: return max(1, positive_int(x)) def tab_fade(x: str) -> tuple[float, ...]: return tuple(map(unit_float, x.split())) def tab_activity_symbol(x: str) -> str: if x == 'none': return '' return tab_title_template(x) def bell_on_tab(x: str) -> str: xl = x.lower() if xl in ('yes', 'y', 'true'): return '🔔 ' if xl in ('no', 'n', 'false', 'none'): return '' return tab_title_template(x) def tab_title_template(x: str) -> str: if x: for q in '\'"': if x.startswith(q) and x.endswith(q): x = x[1:-1] break return x def active_tab_title_template(x: str) -> str | None: x = tab_title_template(x) return None if x == 'none' else x def text_fg_override_threshold(x: str) -> tuple[float, Literal['%', 'ratio']]: val, unit = number_with_unit(x, '%', 'ratio') return val, cast(Literal['%', 'ratio'], unit) ClearOn = Literal['next', 'focus'] default_clear_on: tuple[ClearOn, ...] = 'focus', 'next' all_clear_on = get_args(ClearOn) class NotifyOnCmdFinish(NamedTuple): when: str = 'never' duration: float = 5.0 action: str = 'notify' cmdline: tuple[str, ...] = () clear_on: tuple[ClearOn, ...] = default_clear_on def notify_on_cmd_finish(x: str) -> NotifyOnCmdFinish: parts = x.split(maxsplit=3) if parts[0] not in ('never', 'unfocused', 'invisible', 'always'): raise ValueError(f'Unknown notify_on_cmd_finish value: {parts[0]}') when = parts[0] duration = 5.0 if len(parts) > 1: duration = float(parts[1]) action = 'notify' cmdline: tuple[str, ...] = () clear_on = default_clear_on if len(parts) > 2: if parts[2] not in ('notify', 'bell', 'command'): raise ValueError(f'Unknown notify_on_cmd_finish action: {parts[2]}') action = parts[2] if action == 'notify': if len(parts) > 3: con: list[ClearOn] = [] for x in parts[3].split(): if x not in all_clear_on: raise ValueError( f'notify_on_cmd_finish: notify clear_on value "{x}" is invalid. Valid values are: {", ".join(all_clear_on)}') con.append(cast(ClearOn, x)) clear_on = tuple(con) elif action == 'command': if len(parts) > 3: cmdline = tuple(to_cmdline(parts[3])) else: raise ValueError('notify_on_cmd_finish `command` action needs a command line') return NotifyOnCmdFinish(when, duration, action, cmdline, clear_on) def config_or_absolute_path(x: str, env: dict[str, str] | None = None) -> str | None: if not x or x.lower() == 'none': return None return resolve_abs_or_config_path(x, env) def filter_notification(val: str, current_val: dict[str, str]) -> Iterable[tuple[str, str]]: yield val, '' def remote_control_password(val: str, current_val: dict[str, str]) -> Iterable[tuple[str, Sequence[str]]]: val = val.strip() if val: parts = to_cmdline(val, expand=False) if parts[0].startswith('-'): # this is done so in the future we can add --options to the cmd # line of remote_control_password raise ValueError('Passwords are not allowed to start with hyphens, ignoring this password') if len(parts) == 1: yield parts[0], () else: yield parts[0], tuple(parts[1:]) def clipboard_control(x: str) -> tuple[str, ...]: return tuple(x.lower().split()) def allow_hyperlinks(x: str) -> int: if x == 'ask': return 0b11 return 1 if to_bool(x) else 0 def titlebar_color(x: str) -> int: x = x.strip('"') if x == 'system': return 0 if x == 'background': return 1 try: return (color_as_int(to_color(x)) << 8) | 2 except ValueError: log_error(f'Ignoring invalid title bar color: {x}') return 0 def macos_titlebar_color(x: str) -> int: x = x.strip('"') if x == 'light': return -1 if x == 'dark': return -2 return titlebar_color(x) def macos_option_as_alt(x: str) -> int: x = x.lower() if x == 'both': return 0b11 if x == 'left': return 0b10 if x == 'right': return 0b01 if to_bool(x): return 0b11 return 0 class TabBarMarginHeight(NamedTuple): outer: float = 0 inner: float = 0 def __bool__(self) -> bool: return (self.outer + self.inner) > 0 def tab_bar_margin_height(x: str) -> TabBarMarginHeight: parts = x.split(maxsplit=1) if len(parts) != 2: log_error(f'Invalid tab_bar_margin_height: {x}, ignoring') return TabBarMarginHeight() ans = map(positive_float, parts) return TabBarMarginHeight(next(ans), next(ans)) def clone_source_strategies(x: str) -> frozenset[str]: return frozenset({'venv', 'conda', 'path', 'env_var'} & set(x.lower().split(','))) def clear_all_mouse_actions(val: str, dict_with_parse_results: dict[str, Any] | None = None) -> bool: ans = to_bool(val) if ans and dict_with_parse_results is not None: dict_with_parse_results['mouse_map'] = [None] return ans def clear_all_shortcuts(val: str, dict_with_parse_results: dict[str, Any] | None = None) -> bool: ans = to_bool(val) if ans and dict_with_parse_results is not None: dict_with_parse_results['map'] = [None] return ans def font_features(val: str) -> Iterable[tuple[str, tuple[defines.ParsedFontFeature, ...]]]: if val == 'none': return parts = val.split() if len(parts) < 2: log_error(f"Ignoring invalid font_features {val}") return if parts[0]: features = [] for feat in parts[1:]: try: features.append(defines.ParsedFontFeature(feat)) except ValueError: log_error(f'Ignoring invalid font feature: {feat}') yield parts[0], tuple(features) def modify_font(val: str) -> Iterable[tuple[str, FontModification]]: parts = val.split() pos, plen = 0, len(parts) if plen < 2: log_error(f"Ignoring invalid modify_font: {val}") return mtype: ModificationType | None = getattr(ModificationType, parts[pos], None) if mtype is None: log_error(f"Ignoring invalid modify_font with unknown modification type: {parts[pos]}") return pos += 1 font_name = '' if mtype is ModificationType.size: font_name = parts[pos] pos += 1 if plen - pos < 1: log_error(f"Ignoring invalid modify_font: {val}") return sz = parts[pos] pos += 1 munit = ModificationUnit.pt if sz.endswith('%'): munit = ModificationUnit.percent sz = sz[:-1] elif sz.endswith('px'): munit = ModificationUnit.pixel sz = sz[:-2] try: mvalue = float(sz) except Exception: log_error(f'Ignoring modify_font with invalid size: {sz}') return key = mtype.name if font_name: key += f':{font_name}' yield key, FontModification(mtype, ModificationValue(mvalue, munit), font_name) def env(val: str, current_val: dict[str, str]) -> Iterable[tuple[str, str]]: val = val.strip() if val: if '=' in val: key, v = val.split('=', 1) key, v = key.strip(), v.strip() if key: if v: v = expandvars(v, current_val) yield key, v else: yield val, DELETE_ENV_VAR def store_multiple(val: str, current_val: Container[str]) -> Iterable[tuple[str, str]]: val = val.strip() if val not in current_val: yield val, val def menu_map(val: str, current_val: Container[str]) -> Iterable[tuple[tuple[str, ...], str]]: parts = val.split(maxsplit=1) if len(parts) != 2: raise ValueError(f'Ignoring invalid menu action: {val}') if parts[0] != 'global': raise ValueError(f'Unknown menu type: {parts[0]}. Known types: global') start = 0 if parts[1].startswith('"'): start = 1 idx = parts[1].find('"', 1) if idx == -1: raise ValueError(f'The menu entry name in {val} must end with a double quote') else: idx = parts[1].find(' ') if idx == -1: raise ValueError(f'The menu entry {val} must have an action') location = ('global',) + tuple(parts[1][start:idx].split('::')) yield location, parts[1][idx+1:].lstrip() allowed_shell_integration_values = frozenset({'enabled', 'disabled', 'no-rc', 'no-cursor', 'no-title', 'no-prompt-mark', 'no-complete', 'no-cwd', 'no-sudo'}) def shell_integration(x: str) -> frozenset[str]: q = frozenset(x.lower().split()) if not q.issubset(allowed_shell_integration_values): log_error(f'Invalid shell integration options: {q - allowed_shell_integration_values}, ignoring') return q & allowed_shell_integration_values or frozenset({'invalid'}) return q def confirm_close(x: str) -> tuple[int, bool]: parts = x.split(maxsplit=1) num = int(parts[0]) allow_background = len(parts) > 1 and parts[1] == 'count-background' return num, allow_background def underline_exclusion(x: str) -> tuple[float, Literal['', 'px', 'pt']]: try: return float(x), '' except Exception: unit: Literal['pt', 'px'] = x[-2:] # type: ignore if unit not in ('px', 'pt'): raise ValueError(f'Invalid underline_exclusion with unrecognized unit: {x}') try: val = float(x[:-2]) except Exception: raise ValueError(f'Invalid underline_exclusion with non numberic value: {x}') return val, unit def paste_actions(x: str) -> frozenset[str]: s = frozenset({'quote-urls-at-prompt', 'confirm', 'filter', 'confirm-if-large', 'replace-dangerous-control-codes', 'replace-newline', 'no-op'}) q = frozenset(x.lower().split(',')) if not q.issubset(s): raise ValueError(f'Invalid paste actions: {q - s}, ignoring') return q 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, rest kitten_alias = action_alias def symbol_map_parser(val: str, min_size: int = 2) -> Iterable[tuple[tuple[int, int], str]]: parts = val.split() if len(parts) < min_size: raise ValueError('must have codepoints AND font name') family = ' '.join(parts[1:]) def to_chr(x: str) -> int: if not x.startswith('U+'): raise ValueError(f'{x} is not a unicode codepoint of the form U+number') return int(x[2:], 16) for x in parts[0].split(','): a_, b_ = x.replace('–', '-').partition('-')[::2] b_ = b_ or a_ a, b = map(to_chr, (a_, b_)) if b < a or max(a, b) > sys.maxunicode or min(a, b) < 1: raise ValueError(f'Invalid range: {a:x} - {b:x}') yield (a, b), family def symbol_map(val: str) -> Iterable[tuple[tuple[int, int], str]]: yield from symbol_map_parser(val) def narrow_symbols(val: str) -> Iterable[tuple[tuple[int, int], int]]: for x, y in symbol_map_parser(val, min_size=1): yield x, int(y or 1) def parse_key_action(action: str, action_type: MapType = MapType.MAP) -> KeyAction: parts = action.strip().split(maxsplit=1) func = parts[0] if len(parts) == 1: return KeyAction(func, ()) rest = parts[1] 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): name: str value: str replace_second_arg: bool = False class AliasMap: def __init__(self) -> None: self.aliases: dict[str, list[ActionAlias]] = {} def append(self, name: str, aa: ActionAlias) -> None: self.aliases.setdefault(name, []).append(aa) def update(self, aa: 'AliasMap') -> None: self.aliases.update(aa.aliases) @lru_cache(maxsize=256) def resolve_aliases(self, definition: str, map_type: MapType = MapType.MAP) -> tuple[KeyAction, ...]: return tuple(resolve_aliases_and_parse_actions(definition, self.aliases, map_type)) def build_action_aliases(raw: dict[str, str], first_arg_replacement: str = '') -> AliasMap: ans = AliasMap() if first_arg_replacement: for alias_name, rest in raw.items(): ans.append(first_arg_replacement, ActionAlias(alias_name, rest, True)) else: for alias_name, rest in raw.items(): ans.append(alias_name, ActionAlias(alias_name, rest)) return ans def resolve_aliases_and_parse_actions( defn: str, aliases: dict[str, list[ActionAlias]], map_type: MapType ) -> 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 = f'{possible_alias} {alias.value}{f" {parts[1]}" if len(parts) > 1 else ""}' new_aliases = aliases.copy() 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 = f'{alias.value} {rest}' if rest else alias.value new_aliases = aliases.copy() 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(fr'\s*{re.escape(sep)}\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: no_op_actions = frozenset(('noop', 'no-op', 'no_op')) map_type: MapType = MapType.MAP definition_location: CurrentlyParsing def __init__(self, definition: str = '') -> None: if definition in BaseDefinition.no_op_actions: definition = '' self.definition = definition self.definition_location = currently_parsing.__copy__() def pretty_repr(self, *fields: str) -> str: kwds = [] defaults = self.__class__() for f in fields: val = getattr(self, f) if val != getattr(defaults, f): kwds.append(f'{f}={val!r}') if self.definition: kwds.append(f'definition={self.definition!r}') return f'{self.__class__.__name__}({", ".join(kwds)})' def resolve_key_mods(kitty_mod: int, mods: int) -> int: return SingleKey(mods=mods).resolve_kitty_mod(kitty_mod).mods class MouseMapping(BaseDefinition): map_type: MapType = MapType.MOUSE_MAP def __init__( self, button: int = 0, mods: int = 0, repeat_count: int = 1, grabbed: bool = False, definition: str = '' ): super().__init__(definition) self.button = button self.mods = mods self.repeat_count = repeat_count self.grabbed = grabbed def __repr__(self) -> str: return self.pretty_repr('button', 'mods', 'repeat_count', 'grabbed') def resolve_and_copy(self, kitty_mod: int) -> 'MouseMapping': ans = MouseMapping( self.button, resolve_key_mods(kitty_mod, self.mods), self.repeat_count, self.grabbed, self.definition) ans.definition_location = self.definition_location return ans @property def trigger(self) -> MouseEvent: return MouseEvent(self.button, self.mods, self.repeat_count, self.grabbed) T = TypeVar('T') class LiteralField(Generic[T]): def __init__(self, vals: tuple[T, ...]): self._vals = vals def __set_name__(self, owner: object, name: str) -> None: self._name = "_" + name def __get__(self, obj: object, type: type | None = None) -> T: if obj is None: return self._vals[0] return getattr(obj, self._name, self._vals[0]) def __set__(self, obj: object, value: str) -> None: if value not in self._vals: raise KeyError(f'Invalid value for {self._name[1:]}: {value!r}') object.__setattr__(obj, self._name, value) OnUnknown = Literal['beep', 'end', 'ignore', 'passthrough'] OnAction = Literal['keep', 'end'] @dataclass(init=False, frozen=True) class KeyMapOptions: when_focus_on: str = '' new_mode: str = '' mode: str = '' on_unknown: LiteralField[OnUnknown] = LiteralField[OnUnknown](get_args(OnUnknown)) on_action: LiteralField[OnAction] = LiteralField[OnAction](get_args(OnAction)) default_key_map_options = KeyMapOptions() allowed_key_map_options = frozenset(f.name for f in fields(KeyMapOptions)) class KeyDefinition(BaseDefinition): def __init__( self, is_sequence: bool = False, trigger: SingleKey = SingleKey(), rest: tuple[SingleKey, ...] = (), definition: str = '', options: KeyMapOptions = default_key_map_options ): super().__init__(definition) self.is_sequence = is_sequence self.trigger = trigger self.rest = rest self.options = options @property def is_suitable_for_global_shortcut(self) -> bool: return not self.options.when_focus_on and not self.options.mode and not self.options.new_mode and not self.is_sequence @property def full_key_sequence_to_trigger(self) -> tuple[SingleKey, ...]: return (self.trigger,) + self.rest @property def unique_identity_within_keymap(self) -> tuple[tuple[SingleKey, ...], str]: return self.full_key_sequence_to_trigger, self.options.when_focus_on def __repr__(self) -> str: return self.pretty_repr('is_sequence', 'trigger', 'rest', 'options') def human_repr(self) -> str: ans = self.definition or 'no-op' if self.options.when_focus_on: ans = f'[--when-focus-on={self.options.when_focus_on}]{ans}' return ans def shift_sequence_and_copy(self) -> 'KeyDefinition': return KeyDefinition(self.is_sequence, self.trigger, self.rest[1:], self.definition, self.options) def resolve_and_copy(self, kitty_mod: int) -> 'KeyDefinition': def r(k: SingleKey) -> SingleKey: return k.resolve_kitty_mod(kitty_mod) ans = KeyDefinition( self.is_sequence, r(self.trigger), tuple(map(r, self.rest)), self.definition, self.options ) ans.definition_location = self.definition_location return ans class KeyboardMode: on_unknown: OnUnknown = get_args(OnUnknown)[0] on_action : OnAction = get_args(OnAction)[0] sequence_keys: list[defines.KeyEvent] | None = None def __init__(self, name: str = '') -> None: self.name = name self.keymap: KeyMap = defaultdict(list) KeyboardModeMap = dict[str, KeyboardMode] def parse_options_for_map(val: str) -> tuple[KeyMapOptions, str]: expecting_arg = '' ans = KeyMapOptions() s = Shlex(val) while (tok := s.next_word())[0] > -1: x = tok[1] if expecting_arg: object.__setattr__(ans, expecting_arg, x) expecting_arg = '' elif x.startswith('--'): expecting_arg = x[2:] k, sep, v = expecting_arg.partition('=') k = k.replace('-', '_') expecting_arg = k if expecting_arg not in allowed_key_map_options: raise KeyError(f'The map option {x} is unknown. Allowed options: {", ".join(allowed_key_map_options)}') if sep == '=': object.__setattr__(ans, k, v) expecting_arg = '' else: return ans, val[tok[0]:] return ans, '' def parse_map(val: str) -> Iterable[KeyDefinition]: parts = val.split(maxsplit=1) options = default_key_map_options if len(parts) == 2: sc, action = parts if sc.startswith('--'): options, leftover = parse_options_for_map(val) parts = leftover.split(maxsplit=1) if len(parts) == 1: sc, action = parts[0], '' else: sc = parts[0] action = ' '.join(parts[1:]) else: sc, action = val, '' sc, action = sc.strip().strip(sequence_sep), action.strip() if not sc: return is_sequence = sequence_sep in sc if is_sequence: trigger: SingleKey | None = None restl: list[SingleKey] = [] for part in sc.split(sequence_sep): try: mods, is_native, key = parse_shortcut(part) except InvalidMods: return if key == 0: if mods is not None: log_error(f'Shortcut: {sc} has unknown key, ignoring') return if trigger is None: trigger = SingleKey(mods, is_native, key) else: restl.append(SingleKey(mods, is_native, key)) rest = tuple(restl) else: try: mods, is_native, key = parse_shortcut(sc) except InvalidMods: return if key == 0: if mods is not None: log_error(f'Shortcut: {sc} has unknown key, ignoring') return if is_sequence: if trigger is not None: yield KeyDefinition(True, trigger, rest, definition=action, options=options) else: assert key is not None yield KeyDefinition(False, SingleKey(mods, is_native, key), definition=action, options=options) def parse_mouse_map(val: str) -> Iterable[MouseMapping]: parts = val.split(maxsplit=3) if len(parts) == 4: xbutton, event, modes, action = parts elif len(parts) > 2: xbutton, event, modes = parts action = '' else: log_error(f'Ignoring invalid mouse action: {val}') return kparts = xbutton.split('+') if len(kparts) > 1: mparts, obutton = kparts[:-1], kparts[-1].lower() mods = parse_mods(mparts, obutton) if mods is None: return else: obutton = parts[0].lower() mods = 0 try: b = mouse_button_map.get(obutton, obutton)[1:] button = getattr(defines, f'GLFW_MOUSE_BUTTON_{b}') except Exception: log_error(f'Mouse button: {xbutton} not recognized, ignoring') return try: count = mouse_trigger_count_map[event.lower()] except KeyError: log_error(f'Mouse event type: {event} not recognized, ignoring') return specified_modes = frozenset(modes.lower().split(',')) if specified_modes - {'grabbed', 'ungrabbed'}: log_error(f'Mouse modes: {modes} not recognized, ignoring') return for mode in sorted(specified_modes): yield MouseMapping(button, mods, count, mode == 'grabbed', definition=action) def parse_font_spec(spec: str) -> FontSpec: return FontSpec.from_setting(spec) JumpTypes = Literal['start', 'end', 'none', 'both'] class EasingFunction(NamedTuple): type: Literal['steps', 'linear', 'cubic-bezier', ''] = '' num_steps: int = 0 jump_type: JumpTypes = 'end' linear_x: tuple[float, ...] = () linear_y: tuple[float, ...] = () cubic_bezier_points: tuple[float, ...] = () def __repr__(self) -> str: fields = ', '.join(f'{f}={getattr(self, f)!r}' for f in self._fields if getattr(self, f) != self._field_defaults[f]) return f'kitty.options.utils.EasingFunction({fields})' def __bool__(self) -> bool: return bool(self.type) @classmethod def cubic_bezier(cls, params: str) -> 'EasingFunction': parts = params.replace(',', ' ').split() if len(parts) != 4: raise ValueError('cubic-bezier easing function must have four points') return cls(type='cubic-bezier', cubic_bezier_points=( unit_float(parts[0]), float(parts[1]), unit_float(parts[2]), float(parts[3]))) @classmethod def linear(cls, params: str) -> 'EasingFunction': parts = params.split(',') if len(parts) < 2: raise ValueError('Must specify at least two points for the linear easing function') xaxis: list[float] = [] yaxis: list[float] = [] def balance(end: float) -> None: extra = len(yaxis) - len(xaxis) if extra <= 0: return start = xaxis[-1] if xaxis else 0. delta = (end - start) / max(1, extra - 1) if delta <= 0.: raise ValueError(f'Linear easing curve must have strictly increasing points: {params} does not') if xaxis: for i in range(extra): xaxis.append(start + (i+1) * delta) else: for i in range(extra): xaxis.append(i * delta) def add_point(y: float, x: float | None = None) -> None: if x is None: yaxis.append(y) else: x = unit_float(x) balance(x) xaxis.append(x) yaxis.append(y) for r in parts: points = r.strip().split() y = unit_float(points[0]) if len(points) == 1: add_point(y) elif len(points) == 2: add_point(y, percent(points[1])) elif len(points) == 3: add_point(y, percent(points[1])) add_point(y, percent(points[2])) else: raise ValueError(f'{r} has too many points for a linear easing curve parameter') balance(1) return cls(type='linear', linear_x=tuple(xaxis), linear_y=tuple(yaxis)) @classmethod def steps(cls, params: str) -> 'EasingFunction': parts = params.replace(',', ' ').split() jump_type: JumpTypes = 'end' if len(parts) == 2: n = int(parts[0]) jt = parts[1] mapping: dict[str, JumpTypes] = { 'jump-start': 'start', 'start': 'start', 'end': 'end', 'jump-end': 'end', 'jump-none': 'none', 'jump-both': 'both' } try: jump_type = mapping[jt.lower()] except KeyError: raise KeyError(f'{jt} is not a valid jump type for a linear easing function') if jump_type == 'none': n = max(2, n) else: n = max(1, n) else: n = max(1, int(parts[0])) return cls(type='steps', jump_type=jump_type, num_steps=n) def parse_animation(spec: str, interval: float = -1.) -> tuple[float, EasingFunction, EasingFunction]: with suppress(Exception): interval = float(spec) return interval, EasingFunction(), EasingFunction() m = [EasingFunction(), EasingFunction()] def parse_func(func_name: str, params: str) -> None: idx = 1 if m[0] else 0 if m[idx]: raise ValueError(f'{spec} specified more than two easing functions') if func_name == 'cubic-bezier': m[idx] = EasingFunction.cubic_bezier(params) elif func_name == 'linear': m[idx] = EasingFunction.linear(params) elif func_name == 'steps': m[idx] = EasingFunction.steps(params) else: raise KeyError(f'{func_name} is not a valid easing function') for match in re.finditer(r'([-+.0-9a-zA-Z]+)(?:\(([^)]*)\)){0,1}', spec): func_name, params = match.group(1, 2) if params: parse_func(func_name, params) continue with suppress(Exception): interval = float(func_name) continue if func_name == 'ease-in-out': parse_func('cubic-bezier', '0.42, 0, 0.58, 1') elif func_name == 'linear': parse_func('cubic-bezier', '0, 0, 1, 1') elif func_name == 'ease': parse_func('cubic-bezier', '0.25, 0.1, 0.25, 1') elif func_name == 'ease-out': parse_func('cubic-bezier', '0, 0, 0.58, 1') elif func_name == 'ease-in': parse_func('cubic-bezier', '0.42, 0, 1, 1') elif func_name == 'step-start': parse_func('steps', '1, start') elif func_name == 'step-end': parse_func('steps', '1, end') else: raise KeyError(f'{func_name} is not a valid easing function') return interval, m[0], m[1] def cursor_blink_interval(spec: str) -> tuple[float, EasingFunction, EasingFunction]: return parse_animation(spec) class MouseHideWait(NamedTuple): hide_wait: float show_wait: float show_threshold: int scroll_show: bool def mouse_hide_wait(x: str) -> MouseHideWait: parts = x.split(maxsplit=3) if len(parts) != 1 and len(parts) != 4: log_error(f'Invalid mouse_hide_wait: {x}, ignoring') return MouseHideWait(3.0, 0.0, 40, True) if len(parts) == 1: return MouseHideWait(float(parts[0]), 0.0, 40, True) else: return MouseHideWait(float(parts[0]), float(parts[1]), int(parts[2]), to_bool(parts[3])) def visual_bell_duration(spec: str) -> tuple[float, EasingFunction, EasingFunction]: return parse_animation(spec, interval=0.) pointer_shape_names = ( # start pointer shape names (auto generated by gen-key-constants.py do not edit) 'arrow', 'beam', 'text', 'pointer', 'hand', 'help', 'wait', 'progress', 'crosshair', 'cell', 'vertical-text', 'move', 'e-resize', 'ne-resize', 'nw-resize', 'n-resize', 'se-resize', 'sw-resize', 's-resize', 'w-resize', 'ew-resize', 'ns-resize', 'nesw-resize', 'nwse-resize', 'zoom-in', 'zoom-out', 'alias', 'copy', 'not-allowed', 'no-drop', 'grab', 'grabbing', # end pointer shape names ) def pointer_shape_when_dragging(spec: str) -> tuple[str, str]: parts = spec.split(maxsplit=1) first = parts[0] if first not in pointer_shape_names: raise ValueError(f'{first} is not a valid pointer shape name') second = parts[1] if len(parts) > 1 else first if second not in pointer_shape_names: raise ValueError(f'{second} is not a valid pointer shape name') return first, second def transparent_background_colors(spec: str) -> tuple[tuple[Color, float], ...]: if not spec: return () ans: list[tuple[Color, float]] = [] seen: dict[Color, int] = {} for part in spec.split(): col, sep, alpha = part.partition('@') c = to_color(col) o = max(-1, min(float(alpha) if alpha else -1, 1)) if (idx := seen.get(c)) is not None: ans[idx] = c, o continue seen[c] = len(ans) ans.append((c, o)) return tuple(ans[:7]) def deprecated_hide_window_decorations_aliases(key: str, val: str, ans: dict[str, Any]) -> None: if not hasattr(deprecated_hide_window_decorations_aliases, key): setattr(deprecated_hide_window_decorations_aliases, key, True) log_error(f'The option {key} is deprecated. Use hide_window_decorations instead.') if to_bool(val): if is_macos and key == 'macos_hide_titlebar' or (not is_macos and key == 'x11_hide_window_decorations'): ans['hide_window_decorations'] = True def deprecated_macos_show_window_title_in_menubar_alias(key: str, val: str, ans: dict[str, Any]) -> None: if not hasattr(deprecated_macos_show_window_title_in_menubar_alias, key): setattr(deprecated_macos_show_window_title_in_menubar_alias, 'key', True) log_error(f'The option {key} is deprecated. Use macos_show_window_title_in menubar instead.') macos_show_window_title_in = ans.get('macos_show_window_title_in', 'all') if to_bool(val): if macos_show_window_title_in == 'none': macos_show_window_title_in = 'menubar' elif macos_show_window_title_in == 'window': macos_show_window_title_in = 'all' else: if macos_show_window_title_in == 'all': macos_show_window_title_in = 'window' elif macos_show_window_title_in == 'menubar': macos_show_window_title_in = 'none' ans['macos_show_window_title_in'] = macos_show_window_title_in def deprecated_send_text(key: str, val: str, ans: dict[str, Any]) -> None: parts = val.split(' ') def abort(msg: str) -> None: log_error(f'Send text: {val} is invalid ({msg}), ignoring') if len(parts) < 3: return abort('Incomplete') mode, sc = parts[:2] text = ' '.join(parts[2:]) key_str = f'{sc} send_text {mode} {text}' for k in parse_map(key_str): ans['map'].append(k) def deprecated_adjust_line_height(key: str, x: str, opts_dict: dict[str, Any]) -> None: fm = {'adjust_line_height': 'cell_height', 'adjust_baseline': 'baseline', 'adjust_column_width': 'cell_width'}[key] mtype = getattr(ModificationType, fm) if x.endswith('%'): ans = float(x[:-1].strip()) if ans < 0: log_error(f'Percentage adjustments of {key} must be positive numbers') return opts_dict['modify_font'][fm] = FontModification(mtype, ModificationValue(ans, ModificationUnit.percent)) else: opts_dict['modify_font'][fm] = FontModification(mtype, ModificationValue(int(x), ModificationUnit.pixel))