diff --git a/kittens/diff/config_data.py b/kittens/diff/config_data.py index 57be6d52a..f1ec80fba 100644 --- a/kittens/diff/config_data.py +++ b/kittens/diff/config_data.py @@ -9,8 +9,9 @@ from gettext import gettext as _ from typing import Dict from kitty.conf.definition import OptionOrAction, option_func -from kitty.conf.utils import python_string, to_color, to_color_or_none -from kitty.utils import positive_int +from kitty.conf.utils import ( + positive_int, python_string, to_color, to_color_or_none +) # }}} diff --git a/kittens/tui/images.py b/kittens/tui/images.py index 787dd4e65..348f7e603 100644 --- a/kittens/tui/images.py +++ b/kittens/tui/images.py @@ -15,13 +15,12 @@ from typing import ( Sequence, Tuple, Union ) +from kitty.conf.utils import positive_float, positive_int from kitty.fast_data_types import create_canvas from kitty.typing import ( CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType ) -from kitty.utils import ( - ScreenSize, find_exe, fit_image, positive_float, positive_int -) +from kitty.utils import ScreenSize, find_exe, fit_image from .operations import cursor diff --git a/kitty/boss.py b/kitty/boss.py index 81af8ad53..2e5f6d97e 100755 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -23,7 +23,7 @@ from .config import ( KeyAction, SubSequenceMap, common_opts_as_dict, prepare_config_file_for_editing ) -from .config_data import MINIMUM_FONT_SIZE +from .options_types import MINIMUM_FONT_SIZE from .constants import ( appname, config_dir, is_macos, kitty_exe, supports_primary_selection ) diff --git a/kitty/conf/definition.py b/kitty/conf/definition.py index 596724ff3..740c6b094 100644 --- a/kitty/conf/definition.py +++ b/kitty/conf/definition.py @@ -174,7 +174,7 @@ def option_func(all_options: Dict[str, Any], all_groups: Dict[str, Sequence[str] return partial(option, all_options, group), partial(shortcut, all_options, group), partial(mouse_action, all_options, group), change_group, all_groups_ -OptionOrAction = Union[Option, Sequence[Shortcut], Sequence[MouseAction]] +OptionOrAction = Union[Option, List[Union[Shortcut, MouseAction]]] def merged_opts(all_options: Sequence[OptionOrAction], opt: Option, i: int) -> Generator[Option, None, None]: diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index f8fb2c48c..425587e75 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -7,7 +7,7 @@ import re import shlex from typing import ( Any, Callable, Dict, FrozenSet, Generator, Iterable, Iterator, List, - NamedTuple, Optional, Sequence, Tuple, Type, TypeVar, Union + NamedTuple, Optional, Sequence, Tuple, Type, TypeVar, Union, Set ) from ..rgb import Color, to_color as as_color @@ -24,6 +24,14 @@ class BadLine(NamedTuple): exception: Exception +def positive_int(x: ConvertibleToNumbers) -> int: + return max(0, int(x)) + + +def positive_float(x: ConvertibleToNumbers) -> float: + return max(0, float(x)) + + def to_color(x: str) -> Color: ans = as_color(x, validate=True) if ans is None: # this is only for type-checking @@ -342,3 +350,9 @@ def parse_kittens_key( return None ans = parse_kittens_func_args(action, funcs_with_args) return ans, parse_shortcut(sc) + + +def uniq(vals: Iterable[T]) -> List[T]: + seen: Set[T] = set() + seen_add = seen.add + return [x for x in vals if x not in seen and not seen_add(x)] diff --git a/kitty/config.py b/kitty/config.py index 0d1977f40..a9d7baa41 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -9,8 +9,8 @@ import sys from contextlib import contextmanager, suppress from functools import partial from typing import ( - Any, Callable, Dict, Generator, Iterable, List, NamedTuple, Optional, - Sequence, Set, Tuple, Type, Union, FrozenSet + Any, Callable, Dict, FrozenSet, Generator, Iterable, List, NamedTuple, + Optional, Sequence, Set, Tuple, Type, Union ) from . import fast_data_types as defines @@ -19,10 +19,11 @@ from .conf.utils import ( BadLine, init_config, key_func, load_config as _load_config, merge_dicts, parse_config_base, python_string, to_bool, to_cmdline ) -from .config_data import InvalidMods, all_options, parse_mods, parse_shortcut +from .config_data import all_options from .constants import cache_dir, defconf, is_macos from .fonts import FontFeature from .options_stub import Options as OptionsStub +from .options_types import InvalidMods, parse_mods, parse_shortcut from .types import MouseEvent, SingleKey from .typing import TypedDict from .utils import expandvars, log_error diff --git a/kitty/config_data.py b/kitty/config_data.py index d7ee1bc74..aade00f3e 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -3,106 +3,31 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal # Utils {{{ -import os from gettext import gettext as _ -from typing import ( - Callable, Dict, FrozenSet, Iterable, List, Optional, Set, - Tuple, TypeVar, Union -) +from typing import Dict from . import fast_data_types as defines from .conf.definition import OptionOrAction, option_func from .conf.utils import ( - choices, to_bool, to_cmdline, to_color, to_color_or_none, unit_float + choices, positive_float, positive_int, to_cmdline, to_color, + to_color_or_none, unit_float ) -from .constants import config_dir, is_macos -from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE -from .key_names import ( - character_key_name_aliases, functional_key_name_aliases, - get_key_name_lookup +from .constants import is_macos +from .options_types import ( + active_tab_title_template, adjust_line_height, allow_hyperlinks, + allow_remote_control, box_drawing_scale, clipboard_control, + config_or_absolute_path, copy_on_select, cursor_text_color, + default_tab_separator, disable_ligatures, edge_width, + hide_window_decorations, macos_option_as_alt, macos_titlebar_color, + optional_edge_width, resize_draw_strategy, scrollback_lines, + scrollback_pager_history_size, tab_activity_symbol, tab_bar_edge, + tab_bar_min_tabs, tab_fade, tab_font_style, tab_separator, + tab_title_template, to_cursor_shape, to_font_size, to_layout_names, + url_prefixes, url_style, window_border_width, window_size, to_modifiers ) -from .layout.interface import all_layouts -from .rgb import Color, color_as_int, color_as_sharp, color_from_int -from .types import FloatEdges, SingleKey -from .utils import log_error, positive_float, positive_int +from .rgb import color_as_sharp, color_from_int -class InvalidMods(ValueError): - pass - - -MINIMUM_FONT_SIZE = 4 -mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER', - '⌥': 'ALT', 'OPTION': 'ALT', 'KITTY_MOD': 'KITTY'} -character_key_name_aliases_with_ascii_lowercase = character_key_name_aliases.copy() -for x in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': - character_key_name_aliases_with_ascii_lowercase[x] = x.lower() - - -def parse_mods(parts: Iterable[str], sc: str) -> Optional[int]: - - def map_mod(m: str) -> str: - return mod_map.get(m, m) - - mods = 0 - for m in parts: - try: - mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper())) - except AttributeError: - if m.upper() != 'NONE': - log_error('Shortcut: {} has unknown modifier, ignoring'.format(sc)) - 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 = 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: Optional[int] = 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) - - -T = TypeVar('T') - - -def uniq(vals: Iterable[T]) -> List[T]: - seen: Set[T] = set() - seen_add = seen.add - return [x for x in vals if x not in seen and not seen_add(x)] # }}} # Groups {{{ @@ -306,11 +231,6 @@ o('bold_font', 'auto') o('italic_font', 'auto') o('bold_italic_font', 'auto') - -def to_font_size(x: str) -> float: - return max(MINIMUM_FONT_SIZE, float(x)) - - o('font_size', 11.0, long_text=_('Font size (in pts)'), option_type=to_font_size) o('force_ltr', False, long_text=_(""" @@ -331,16 +251,6 @@ support, because it will force kitty to always treat the text as LTR, which FriBidi expects for terminals.""")) -def adjust_line_height(x: str) -> Union[int, float]: - if x.endswith('%'): - ans = float(x[:-1].strip()) / 100.0 - if ans < 0: - log_error('Percentage adjustments of cell sizes must be positive numbers') - return 0 - return ans - return int(x) - - o('adjust_line_height', 0, option_type=adjust_line_height, long_text=_(''' Change the size of each character cell kitty renders. You can use either numbers, which are interpreted as pixels or percentages (number followed by %), which @@ -367,11 +277,6 @@ Syntax is:: ''')) -def disable_ligatures(x: str) -> int: - cmap = {'never': 0, 'cursor': 1, 'always': 2} - return cmap.get(x.lower(), 0) - - o('disable_ligatures', 'never', option_type=disable_ligatures, long_text=_(''' Choose how you want to handle multi-character ligatures. The default is to always render them. You can tell kitty to not render them when the cursor is @@ -439,13 +344,6 @@ You can do this with e.g.:: ''')) -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] - - o( 'box_drawing_scale', '0.001, 1, 1.5, 2', @@ -461,30 +359,6 @@ and very thick lines. g('cursor') # {{{ -cshapes = { - 'block': CURSOR_BLOCK, - 'beam': CURSOR_BEAM, - 'underline': CURSOR_UNDERLINE -} - - -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 cursor_text_color(x: str) -> Optional[Color]: - if x.lower() == 'background': - return None - return to_color(x) - - o('cursor', '#cccccc', _('Default cursor color'), option_type=to_color) o('cursor_text_color', '#111111', option_type=cursor_text_color, long_text=_(''' Choose the color of text under the cursor. If you want it rendered with the @@ -510,18 +384,6 @@ inactivity. Set to zero to never stop blinking. g('scrollback') # {{{ -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) - - o('scrollback_lines', 2000, option_type=scrollback_lines, long_text=_(''' Number of lines of history to keep in memory for scrolling back. Memory is allocated on demand. Negative numbers are (effectively) infinite scrollback. Note that using @@ -574,16 +436,6 @@ o('url_color', '#0087bd', option_type=to_color, long_text=_(''' The color and style for highlighting URLs on mouse-over. :code:`url_style` can be one of: none, single, double, curly''')) - -def url_style(x: str) -> int: - return url_style_map.get(x, url_style_map['curly']) - - -url_style_map = dict( - ((v, i) for i, v in enumerate('none single double curly'.split())) -) - - o('url_style', 'curly', option_type=url_style) o('open_url_with', 'default', option_type=to_cmdline, long_text=_(''' @@ -592,10 +444,6 @@ The special value :code:`default` means to use the operating system's default URL handler.''')) -def url_prefixes(x: str) -> Tuple[str, ...]: - return tuple(a.lower() for a in x.replace(',', ' ').split()) - - o('url_prefixes', 'http https file ftp gemini irc gopher mailto news git', option_type=url_prefixes, long_text=_(''' The set of URL prefixes to look for when detecting a URL under the mouse cursor.''')) @@ -605,16 +453,6 @@ with an underline and the mouse cursor becomes a hand over them. Even if this option is disabled, URLs are still clickable.''')) -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 - - o('copy_on_select', 'no', option_type=copy_on_select, long_text=_(''' Copy to clipboard or a private buffer on select. With this set to :code:`clipboard`, simply selecting text with the mouse will cause the text to @@ -747,30 +585,10 @@ number of cells instead of pixels. ''')) -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 - - o('initial_window_width', '640', option_type=window_size) o('initial_window_height', '400', option_type=window_size) -def to_layout_names(raw: str) -> List[str]: - parts = [x.strip().lower() for x in raw.split(',')] - ans: List[str] = [] - for p in parts: - if p in ('*', 'all'): - ans.extend(sorted(all_layouts)) - continue - name = p.partition(':')[0] - if name not in all_layouts: - raise ValueError('The window layout {} is unknown'.format(p)) - ans.append(p) - return uniq(ans) - - o('enabled_layouts', '*', option_type=to_layout_names, long_text=_(''' The enabled window layouts. A comma separated list of layout names. The special value :code:`all` means all layouts. The first listed layout will be used as the @@ -786,20 +604,6 @@ for vertical resizing. o('window_resize_step_lines', 2, option_type=positive_int) -def window_border_width(x: Union[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 - - o('window_border_width', '0.5pt', option_type=window_border_width, long_text=_(''' The width of window borders. Can be either in pixels (px) or pts (pt). Values in pts will be rounded to the nearest number of pixels based on screen @@ -815,27 +619,6 @@ a non-zero window margin overrides this and causes all borders to be drawn. ''')) -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) - - edge_desc = _( 'A single value sets all four sides. Two values set the vertical and horizontal sides.' ' Three values set top, horizontal and bottom. Four values set top, right, bottom and left.') @@ -875,14 +658,6 @@ zero and one, with zero being fully faded). ''')) -def hide_window_decorations(x: str) -> int: - if x == 'titlebar-only': - return 0b10 - if to_bool(x): - return 0b01 - return 0b00 - - o('hide_window_decorations', 'no', option_type=hide_window_decorations, long_text=_(''' Hide the window decorations (title-bar and window borders) with :code:`yes`. On macOS, :code:`titlebar-only` can be used to only hide the titlebar. @@ -897,11 +672,6 @@ operating system sends events corresponding to the start and end of a resize, this number is ignored.''')) -def resize_draw_strategy(x: str) -> int: - cmap = {'static': 0, 'scale': 1, 'blank': 2, 'size': 3} - return cmap.get(x.lower(), 0) - - o('resize_draw_strategy', 'static', option_type=resize_draw_strategy, long_text=_(''' Choose how kitty draws a window while a resize is in progress. A value of :code:`static` means draw the current window contents, mostly unchanged. @@ -926,31 +696,6 @@ OS windows, via the quit action). # }}} g('tabbar') # {{{ -default_tab_separator = ' ┇' - - -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': 1, 'bottom': 3}.get(x.lower(), 3) - - -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)) o('tab_bar_edge', 'bottom', option_type=tab_bar_edge, long_text=_(''' @@ -968,10 +713,6 @@ presents you with a list of tabs and allows for easy switching to a tab. ''')) -def tab_bar_min_tabs(x: str) -> int: - return max(1, positive_int(x)) - - o('tab_bar_min_tabs', 2, option_type=tab_bar_min_tabs, long_text=_(''' The minimum number of tabs that must exist before the tab bar is shown ''')) @@ -985,10 +726,6 @@ A value of :code:`last` will switch to the right-most tab. ''')) -def tab_fade(x: str) -> Tuple[float, ...]: - return tuple(map(unit_float, x.split())) - - o('tab_fade', '0.25 0.5 0.75 1', option_type=tab_fade, long_text=_(''' Control how each tab fades into the background when using :code:`fade` for the :opt:`tab_bar_style`. Each number is an alpha (between zero and one) that controls @@ -1006,31 +743,10 @@ as the :opt:`tab_bar_style`, can be one of: :code:`angled`, :code:`slanted`, or ''')) -def tab_activity_symbol(x: str) -> Optional[str]: - if x == 'none': - return None - return x or None - - o('tab_activity_symbol', 'none', option_type=tab_activity_symbol, long_text=_(''' Some text or a unicode symbol to show on the tab if a window in the tab that does not have focus has some activity.''')) - -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) -> Optional[str]: - x = tab_title_template(x) - return None if x == 'none' else x - - o('tab_title_template', '"{title}"', option_type=tab_title_template, long_text=_(''' A template to render the tab title. The default just renders the title. If you wish to include the tab-index as well, @@ -1086,16 +802,6 @@ default as it has a performance cost) ''')) -def config_or_absolute_path(x: str) -> Optional[str]: - if x.lower() == 'none': - return None - x = os.path.expanduser(x) - x = os.path.expandvars(x) - if not os.path.isabs(x): - x = os.path.join(config_dir, x) - return x - - o('background_image', 'none', option_type=config_or_absolute_path, long_text=_(''' Path to a background image. Must be in PNG format.''')) @@ -1189,12 +895,6 @@ terminal can fail silently because their stdout/stderr/stdin no longer work. ''')) -def allow_remote_control(x: str) -> str: - if x != 'socket-only': - x = 'y' if to_bool(x) else 'n' - return x - - o('allow_remote_control', 'no', option_type=allow_remote_control, long_text=_(''' Allow other programs to control kitty. If you turn this on other programs can control all aspects of kitty, including sending text to kitty windows, opening @@ -1249,10 +949,6 @@ Environment variables in the path are expanded. ''')) -def clipboard_control(x: str) -> FrozenSet[str]: - return frozenset(x.lower().split()) - - o('clipboard_control', 'write-clipboard write-primary', option_type=clipboard_control, long_text=_(''' Allow programs running in kitty to read and write from the clipboard. You can control exactly which actions are allowed. The set of possible actions is: @@ -1265,12 +961,6 @@ program, even one running on a remote server via SSH can read your clipboard. ''')) -def allow_hyperlinks(x: str) -> int: - if x == 'ask': - return 0b11 - return 1 if to_bool(x) else 0 - - o('allow_hyperlinks', 'yes', option_type=allow_hyperlinks, long_text=_(''' Process hyperlink (OSC 8) escape sequences. If disabled OSC 8 escape sequences are ignored. Otherwise they become clickable links, that you @@ -1293,15 +983,6 @@ key-presses, to colors, to various advanced features may not work. g('os') # {{{ -def macos_titlebar_color(x: str) -> int: - x = x.strip('"') - if x == 'system': - return 0 - if x == 'background': - return 1 - return (color_as_int(to_color(x)) << 8) | 2 - - o('wayland_titlebar_color', 'system', option_type=macos_titlebar_color, long_text=_(''' Change the color of the kitty window's titlebar on Wayland systems with client side window decorations such as GNOME. A value of :code:`system` means to use the default system color, @@ -1322,19 +1003,6 @@ probably better off just hiding the titlebar with :opt:`hide_window_decorations` ''')) -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 - - o('macos_option_as_alt', 'no', option_type=macos_option_as_alt, long_text=_(''' Use the option key as an alt key. With this set to :code:`no`, kitty will use the macOS native :kbd:`Option+Key` = unicode character behavior. This will diff --git a/kitty/key_names.py b/kitty/key_names.py index a1c62d28e..ff69bfc10 100644 --- a/kitty/key_names.py +++ b/kitty/key_names.py @@ -4,11 +4,10 @@ import sys from contextlib import suppress -from typing import Callable, Optional +from typing import Callable, Dict, Optional from .constants import is_macos - functional_key_name_aliases = { 'ESC': 'ESCAPE', 'PGUP': 'PAGE_UP', @@ -26,7 +25,7 @@ functional_key_name_aliases = { } -character_key_name_aliases = { +character_key_name_aliases: Dict[str, str] = { 'SPC': ' ', 'SPACE': ' ', 'STAR': '*', diff --git a/kitty/options_types.py b/kitty/options_types.py new file mode 100644 index 000000000..77b60ff53 --- /dev/null +++ b/kitty/options_types.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2021, Kovid Goyal + + +import os +from typing import ( + Callable, Dict, FrozenSet, Iterable, List, Optional, Tuple, Union +) + +import kitty.fast_data_types as defines +from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE + +from .conf.utils import ( + positive_float, positive_int, to_bool, to_color, uniq, unit_float +) +from .constants import config_dir +from .key_names import ( + character_key_name_aliases, functional_key_name_aliases, + get_key_name_lookup +) +from .layout.interface import all_layouts +from .rgb import Color, color_as_int +from .types import FloatEdges, SingleKey +from .utils import log_error + +MINIMUM_FONT_SIZE = 4 +default_tab_separator = ' ┇' +mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER', + '⌥': 'ALT', 'OPTION': 'ALT', '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() + + +class InvalidMods(ValueError): + pass + + +def parse_mods(parts: Iterable[str], sc: str) -> Optional[int]: + + def map_mod(m: str) -> str: + return mod_map.get(m, m) + + mods = 0 + for m in parts: + try: + mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper())) + except AttributeError: + if m.upper() != 'NONE': + log_error('Shortcut: {} has unknown modifier, ignoring'.format(sc)) + 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 = 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: Optional[int] = 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 adjust_line_height(x: str) -> Union[int, float]: + if x.endswith('%'): + ans = float(x[:-1].strip()) / 100.0 + if ans < 0: + log_error('Percentage adjustments of cell sizes must be positive numbers') + return 0 + return ans + return int(x) + + +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) -> Optional[Color]: + if x.lower() == 'background': + return None + return to_color(x) + + +cshapes = { + 'block': CURSOR_BLOCK, + 'beam': CURSOR_BEAM, + 'underline': CURSOR_UNDERLINE +} + + +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 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) + + +def url_style(x: str) -> int: + return url_style_map.get(x, url_style_map['curly']) + + +url_style_map = dict( + ((v, i) for i, v in enumerate('none single double curly'.split())) +) + + +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 to_layout_names(raw: str) -> List[str]: + parts = [x.strip().lower() for x in raw.split(',')] + ans: List[str] = [] + for p in parts: + if p in ('*', 'all'): + ans.extend(sorted(all_layouts)) + continue + name = p.partition(':')[0] + if name not in all_layouts: + raise ValueError('The window layout {} is unknown'.format(p)) + ans.append(p) + return uniq(ans) + + +def window_border_width(x: Union[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 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 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': 1, 'bottom': 3}.get(x.lower(), 3) + + +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) -> Optional[str]: + if x == 'none': + return None + return x or None + + +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) -> Optional[str]: + x = tab_title_template(x) + return None if x == 'none' else x + + +def config_or_absolute_path(x: str) -> Optional[str]: + if x.lower() == 'none': + return None + x = os.path.expanduser(x) + x = os.path.expandvars(x) + if not os.path.isabs(x): + x = os.path.join(config_dir, x) + return x + + +def allow_remote_control(x: str) -> str: + if x != 'socket-only': + x = 'y' if to_bool(x) else 'n' + return x + + +def clipboard_control(x: str) -> FrozenSet[str]: + return frozenset(x.lower().split()) + + +def allow_hyperlinks(x: str) -> int: + if x == 'ask': + return 0b11 + return 1 if to_bool(x) else 0 + + +def macos_titlebar_color(x: str) -> int: + x = x.strip('"') + if x == 'system': + return 0 + if x == 'background': + return 1 + return (color_as_int(to_color(x)) << 8) | 2 + + +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 diff --git a/kitty/session.py b/kitty/session.py index 2564a7b6b..17bc84ff6 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -7,7 +7,7 @@ import sys from typing import Generator, List, Optional, Sequence, Union from .cli_stub import CLIOptions -from .config_data import to_layout_names +from .options_types import to_layout_names, window_size from .constants import kitty_exe from .layout.interface import all_layouts from .options_stub import Options @@ -127,7 +127,6 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None) elif cmd == 'title': ans.set_next_title(rest) elif cmd == 'os_window_size': - from kitty.config_data import window_size w, h = map(window_size, rest.split(maxsplit=1)) ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h)) elif cmd == 'os_window_class': diff --git a/kitty/utils.py b/kitty/utils.py index a61976660..ada0e40da 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -680,11 +680,3 @@ class SSHConnectionData(NamedTuple): binary: str hostname: str port: Optional[int] = None - - -def positive_int(x: ConvertibleToNumbers) -> int: - return max(0, int(x)) - - -def positive_float(x: ConvertibleToNumbers) -> float: - return max(0, float(x))