Move type parsers for config into own module

This commit is contained in:
Kovid Goyal 2021-05-24 22:29:28 +05:30
parent a4daa49f70
commit fe94f4cbb4
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
11 changed files with 392 additions and 372 deletions

View File

@ -9,8 +9,9 @@ from gettext import gettext as _
from typing import Dict from typing import Dict
from kitty.conf.definition import OptionOrAction, option_func from kitty.conf.definition import OptionOrAction, option_func
from kitty.conf.utils import python_string, to_color, to_color_or_none from kitty.conf.utils import (
from kitty.utils import positive_int positive_int, python_string, to_color, to_color_or_none
)
# }}} # }}}

View File

@ -15,13 +15,12 @@ from typing import (
Sequence, Tuple, Union Sequence, Tuple, Union
) )
from kitty.conf.utils import positive_float, positive_int
from kitty.fast_data_types import create_canvas from kitty.fast_data_types import create_canvas
from kitty.typing import ( from kitty.typing import (
CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType
) )
from kitty.utils import ( from kitty.utils import ScreenSize, find_exe, fit_image
ScreenSize, find_exe, fit_image, positive_float, positive_int
)
from .operations import cursor from .operations import cursor

View File

@ -23,7 +23,7 @@ from .config import (
KeyAction, SubSequenceMap, common_opts_as_dict, KeyAction, SubSequenceMap, common_opts_as_dict,
prepare_config_file_for_editing prepare_config_file_for_editing
) )
from .config_data import MINIMUM_FONT_SIZE from .options_types import MINIMUM_FONT_SIZE
from .constants import ( from .constants import (
appname, config_dir, is_macos, kitty_exe, supports_primary_selection appname, config_dir, is_macos, kitty_exe, supports_primary_selection
) )

View File

@ -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_ 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]: def merged_opts(all_options: Sequence[OptionOrAction], opt: Option, i: int) -> Generator[Option, None, None]:

View File

@ -7,7 +7,7 @@ import re
import shlex import shlex
from typing import ( from typing import (
Any, Callable, Dict, FrozenSet, Generator, Iterable, Iterator, List, 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 from ..rgb import Color, to_color as as_color
@ -24,6 +24,14 @@ class BadLine(NamedTuple):
exception: Exception 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: def to_color(x: str) -> Color:
ans = as_color(x, validate=True) ans = as_color(x, validate=True)
if ans is None: # this is only for type-checking if ans is None: # this is only for type-checking
@ -342,3 +350,9 @@ def parse_kittens_key(
return None return None
ans = parse_kittens_func_args(action, funcs_with_args) ans = parse_kittens_func_args(action, funcs_with_args)
return ans, parse_shortcut(sc) 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)]

View File

@ -9,8 +9,8 @@ import sys
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from functools import partial from functools import partial
from typing import ( from typing import (
Any, Callable, Dict, Generator, Iterable, List, NamedTuple, Optional, Any, Callable, Dict, FrozenSet, Generator, Iterable, List, NamedTuple,
Sequence, Set, Tuple, Type, Union, FrozenSet Optional, Sequence, Set, Tuple, Type, Union
) )
from . import fast_data_types as defines 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, BadLine, init_config, key_func, load_config as _load_config, merge_dicts,
parse_config_base, python_string, to_bool, to_cmdline 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 .constants import cache_dir, defconf, is_macos
from .fonts import FontFeature from .fonts import FontFeature
from .options_stub import Options as OptionsStub from .options_stub import Options as OptionsStub
from .options_types import InvalidMods, parse_mods, parse_shortcut
from .types import MouseEvent, SingleKey from .types import MouseEvent, SingleKey
from .typing import TypedDict from .typing import TypedDict
from .utils import expandvars, log_error from .utils import expandvars, log_error

View File

@ -3,106 +3,31 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
# Utils {{{ # Utils {{{
import os
from gettext import gettext as _ from gettext import gettext as _
from typing import ( from typing import Dict
Callable, Dict, FrozenSet, Iterable, List, Optional, Set,
Tuple, TypeVar, Union
)
from . import fast_data_types as defines from . import fast_data_types as defines
from .conf.definition import OptionOrAction, option_func from .conf.definition import OptionOrAction, option_func
from .conf.utils import ( 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 .constants import is_macos
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE from .options_types import (
from .key_names import ( active_tab_title_template, adjust_line_height, allow_hyperlinks,
character_key_name_aliases, functional_key_name_aliases, allow_remote_control, box_drawing_scale, clipboard_control,
get_key_name_lookup 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_as_sharp, color_from_int
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
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 {{{ # Groups {{{
@ -306,11 +231,6 @@ o('bold_font', 'auto')
o('italic_font', 'auto') o('italic_font', 'auto')
o('bold_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('font_size', 11.0, long_text=_('Font size (in pts)'), option_type=to_font_size)
o('force_ltr', False, long_text=_(""" 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.""")) 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=_(''' 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, 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 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=_(''' o('disable_ligatures', 'never', option_type=disable_ligatures, long_text=_('''
Choose how you want to handle multi-character ligatures. The default is to 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 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( o(
'box_drawing_scale', 'box_drawing_scale',
'0.001, 1, 1.5, 2', '0.001, 1, 1.5, 2',
@ -461,30 +359,6 @@ and very thick lines.
g('cursor') # {{{ 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', '#cccccc', _('Default cursor color'), option_type=to_color)
o('cursor_text_color', '#111111', option_type=cursor_text_color, long_text=_(''' 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 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') # {{{ 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=_(''' 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 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 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. The color and style for highlighting URLs on mouse-over.
:code:`url_style` can be one of: none, single, double, curly''')) :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('url_style', 'curly', option_type=url_style)
o('open_url_with', 'default', option_type=to_cmdline, long_text=_(''' 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.''')) 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=_(''' 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.''')) 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.''')) 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=_(''' 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 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 :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_width', '640', option_type=window_size)
o('initial_window_height', '400', 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=_(''' o('enabled_layouts', '*', option_type=to_layout_names, long_text=_('''
The enabled window layouts. A comma separated list of layout names. The special 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 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) 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=_(''' 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 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 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 = _( edge_desc = _(
'A single value sets all four sides. Two values set the vertical and horizontal sides.' '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.') ' 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=_(''' o('hide_window_decorations', 'no', option_type=hide_window_decorations, long_text=_('''
Hide the window decorations (title-bar and window borders) with :code:`yes`. 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. 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.''')) 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=_(''' o('resize_draw_strategy', 'static', option_type=resize_draw_strategy, long_text=_('''
Choose how kitty draws a window while a resize is in progress. 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. 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') # {{{ 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=_(''' 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=_(''' 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 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=_(''' 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 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 :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=_(''' 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 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.''')) 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=_(''' o('tab_title_template', '"{title}"', option_type=tab_title_template, long_text=_('''
A template to render the tab title. The default just renders A template to render the tab title. The default just renders
the title. If you wish to include the tab-index as well, 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=_(''' o('background_image', 'none', option_type=config_or_absolute_path, long_text=_('''
Path to a background image. Must be in PNG format.''')) 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=_(''' 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 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 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=_(''' 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 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: 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=_(''' o('allow_hyperlinks', 'yes', option_type=allow_hyperlinks, long_text=_('''
Process hyperlink (OSC 8) escape sequences. If disabled OSC 8 escape Process hyperlink (OSC 8) escape sequences. If disabled OSC 8 escape
sequences are ignored. Otherwise they become clickable links, that you 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') # {{{ 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=_(''' 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. 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, 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=_(''' 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 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 the macOS native :kbd:`Option+Key` = unicode character behavior. This will

View File

@ -4,11 +4,10 @@
import sys import sys
from contextlib import suppress from contextlib import suppress
from typing import Callable, Optional from typing import Callable, Dict, Optional
from .constants import is_macos from .constants import is_macos
functional_key_name_aliases = { functional_key_name_aliases = {
'ESC': 'ESCAPE', 'ESC': 'ESCAPE',
'PGUP': 'PAGE_UP', 'PGUP': 'PAGE_UP',
@ -26,7 +25,7 @@ functional_key_name_aliases = {
} }
character_key_name_aliases = { character_key_name_aliases: Dict[str, str] = {
'SPC': ' ', 'SPC': ' ',
'SPACE': ' ', 'SPACE': ' ',
'STAR': '*', 'STAR': '*',

347
kitty/options_types.py Normal file
View File

@ -0,0 +1,347 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

@ -7,7 +7,7 @@ import sys
from typing import Generator, List, Optional, Sequence, Union from typing import Generator, List, Optional, Sequence, Union
from .cli_stub import CLIOptions 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 .constants import kitty_exe
from .layout.interface import all_layouts from .layout.interface import all_layouts
from .options_stub import Options from .options_stub import Options
@ -127,7 +127,6 @@ def parse_session(raw: str, opts: Options, default_title: Optional[str] = None)
elif cmd == 'title': elif cmd == 'title':
ans.set_next_title(rest) ans.set_next_title(rest)
elif cmd == 'os_window_size': elif cmd == 'os_window_size':
from kitty.config_data import window_size
w, h = map(window_size, rest.split(maxsplit=1)) w, h = map(window_size, rest.split(maxsplit=1))
ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h)) ans.os_window_size = WindowSizes(WindowSize(*w), WindowSize(*h))
elif cmd == 'os_window_class': elif cmd == 'os_window_class':

View File

@ -680,11 +680,3 @@ class SSHConnectionData(NamedTuple):
binary: str binary: str
hostname: str hostname: str
port: Optional[int] = None 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))