diff --git a/kitty/conf/definition.py b/kitty/conf/definition.py index d105cee37..50ad2d68f 100644 --- a/kitty/conf/definition.py +++ b/kitty/conf/definition.py @@ -4,11 +4,12 @@ import re from functools import partial +from typing import Any, Dict, List, Set, Tuple, Union, get_type_hints, Optional from .utils import to_bool -def to_string(x): +def to_string(x: str) -> str: return x @@ -33,6 +34,32 @@ class Option: self.add_to_docs = add_to_docs self.line = self.name + ' ' + self.defval_as_string + def type_definition(self, is_multiple: bool, imports: Set[Tuple[str, str]]) -> str: + + def type_name(x: type) -> str: + ans = x.__name__ + if x.__module__ and x.__module__ != 'builtins': + imports.add((x.__module__, x.__name__)) + if is_multiple: + ans = 'typing.Dict[str, str]' + return ans + + def option_type_as_str(x: Any) -> str: + if hasattr(x, '__name__'): + return type_name(x) + ans = repr(x) + ans = ans.replace('NoneType', 'None') + return ans + + if type(self.option_type) is type: + return type_name(self.option_type) + th = get_type_hints(self.option_type) + try: + rettype = th['return'] + except KeyError: + raise ValueError('The Option {} has an unknown option_type: {}'.format(self.name, self.option_type)) + return option_type_as_str(rettype) + class Shortcut: @@ -275,3 +302,18 @@ def config_lines(all_options): for sc in opt: if sc.add_to_default: yield sc.line + + +def as_type_stub(all_options: Dict[str, Union[Option, List[Shortcut]]], special_types: Optional[Dict[str, str]] = None) -> str: + ans = ['import typing\n', '', 'class Options:'] + imports: Set[Tuple[str, str]] = set() + overrides = special_types or {} + for name, val in all_options.items(): + if isinstance(val, Option): + is_multiple = ' ' in name + field_name = name.partition(' ')[0] + ans.append(' {}: {}'.format(field_name, overrides.get(field_name, val.type_definition(is_multiple, imports)))) + for mod, name in imports: + ans.insert(0, 'from {} import {}'.format(mod, name)) + ans.insert(0, 'import {}'.format(mod)) + return '\n'.join(ans) diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index 42b943cea..ba2e61c78 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -6,61 +6,77 @@ import os import re import shlex from collections import namedtuple +from typing import Callable, FrozenSet, List, Optional, Union, Dict, Any, Iterator, Type -from ..rgb import to_color as as_color +from ..rgb import Color, to_color as as_color from ..utils import log_error key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$') BadLine = namedtuple('BadLine', 'number line exception') -def to_color(x): - return as_color(x, validate=True) +def to_color(x: str) -> Color: + ans = as_color(x, validate=True) + if ans is None: # this is only for type-checking + ans = Color(0, 0, 0) + return ans -def to_color_or_none(x): +def to_color_or_none(x: str) -> Optional[Color]: return None if x.lower() == 'none' else to_color(x) -def positive_int(x): +ConvertibleToNumbers = Union[str, bytes, int, float] + + +def positive_int(x: ConvertibleToNumbers) -> int: return max(0, int(x)) -def positive_float(x): +def positive_float(x: ConvertibleToNumbers) -> float: return max(0, float(x)) -def unit_float(x): +def unit_float(x: ConvertibleToNumbers) -> float: return max(0, min(float(x), 1)) -def to_bool(x): +def to_bool(x: str) -> bool: return x.lower() in ('y', 'yes', 'true') -def to_cmdline(x): - return list(map( - lambda y: os.path.expandvars(os.path.expanduser(y)), shlex.split(x))) +def to_cmdline(x: str) -> List[str]: + return list( + map( + lambda y: os.path.expandvars(os.path.expanduser(y)), + shlex.split(x) + ) + ) -def python_string(text): +def python_string(text: str) -> str: import ast return ast.literal_eval("'''" + text.replace("'''", "'\\''") + "'''") -def choices(*choices): - defval = choices[0] - choices = frozenset(choices) +def choices(*choices) -> Callable[[str], str]: + defval: str = choices[0] + uc: FrozenSet[str] = frozenset(choices) - def choice(x): + def choice(x: str) -> str: x = x.lower() - if x not in choices: + if x not in uc: x = defval return x + return choice -def parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_includes): +def parse_line( + line: str, type_map: Dict[str, Any], special_handling: Callable, + ans: Dict[str, Any], all_keys: Optional[FrozenSet[str]], + base_path_for_includes: str +) -> None: line = line.strip() if not line or line.startswith('#'): return @@ -79,9 +95,15 @@ def parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_in with open(val, encoding='utf-8', errors='replace') as include: _parse(include, type_map, special_handling, ans, all_keys) except FileNotFoundError: - log_error('Could not find included config file: {}, ignoring'.format(val)) + log_error( + 'Could not find included config file: {}, ignoring'. + format(val) + ) except OSError: - log_error('Could not read from included config file: {}, ignoring'.format(val)) + log_error( + 'Could not read from included config file: {}, ignoring'. + format(val) + ) return if all_keys is not None and key not in all_keys: log_error('Ignoring unknown config key: {}'.format(key)) @@ -92,7 +114,14 @@ def parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_in ans[key] = val -def _parse(lines, type_map, special_handling, ans, all_keys, accumulate_bad_lines=None): +def _parse( + lines: Iterator[str], + type_map: Dict[str, Any], + special_handling: Callable, + ans: Dict[str, Any], + all_keys: Optional[FrozenSet[str]], + accumulate_bad_lines: Optional[List[BadLine]] = None +) -> None: name = getattr(lines, 'name', None) if name: base_path_for_includes = os.path.dirname(os.path.abspath(name)) @@ -101,7 +130,10 @@ def _parse(lines, type_map, special_handling, ans, all_keys, accumulate_bad_line base_path_for_includes = config_dir for i, line in enumerate(lines): try: - parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_includes) + parse_line( + line, type_map, special_handling, ans, all_keys, + base_path_for_includes + ) except Exception as e: if accumulate_bad_lines is None: raise @@ -109,16 +141,23 @@ def _parse(lines, type_map, special_handling, ans, all_keys, accumulate_bad_line def parse_config_base( - lines, defaults, type_map, special_handling, ans, check_keys=True, - accumulate_bad_lines=None + lines: Iterator[str], + defaults: Any, + type_map: Dict[str, Any], + special_handling: Callable, + ans: Dict[str, Any], + check_keys=True, + accumulate_bad_lines: Optional[List[BadLine]] = None ): - all_keys = defaults._asdict() if check_keys else None - _parse(lines, type_map, special_handling, ans, all_keys, accumulate_bad_lines) + all_keys: Optional[FrozenSet[str]] = defaults._asdict() if check_keys else None + _parse( + lines, type_map, special_handling, ans, all_keys, accumulate_bad_lines + ) -def create_options_class(keys): - keys = tuple(sorted(keys)) - slots = keys + ('_fields',) +def create_options_class(all_keys: Iterator[str]) -> Type: + keys = tuple(sorted(all_keys)) + slots = keys + ('_fields', ) def __init__(self, kw): for k, v in kw.items(): @@ -146,11 +185,18 @@ def create_options_class(keys): ans.update(kw) return self.__class__(ans) - ans = type('Options', (), { - '__slots__': slots, '__init__': __init__, '_asdict': _asdict, '_replace': _replace, '__iter__': __iter__, - '__len__': __len__, '__getitem__': __getitem__ - }) - ans._fields = keys + ans = type( + 'Options', (), { + '__slots__': slots, + '__init__': __init__, + '_asdict': _asdict, + '_replace': _replace, + '__iter__': __iter__, + '__len__': __len__, + '__getitem__': __getitem__ + } + ) + ans._fields = keys # type: ignore return ans @@ -171,7 +217,9 @@ def resolve_config(SYSTEM_CONF, defconf, config_files_on_cmd_line): yield defconf -def load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=None): +def load_config( + Options, defaults, parse_config, merge_configs, *paths, overrides=None +): ans = defaults._asdict() for path in paths: if not path: @@ -196,17 +244,20 @@ def init_config(default_config_lines, parse_config): def key_func(): - ans = {} + ans: Dict[str, Callable] = {} def func_with_args(*names): def w(f): for name in names: if ans.setdefault(name, f) is not f: - raise ValueError('the args_func {} is being redefined'.format(name)) + raise ValueError( + 'the args_func {} is being redefined'.format(name) + ) return f return w + return func_with_args, ans @@ -216,15 +267,17 @@ def parse_kittens_shortcut(sc): parts = list(filter(None, sc.rstrip('+').split('+') + ['+'])) else: parts = sc.split('+') - mods = parts[:-1] or None - if mods is not None: + qmods = parts[:-1] + if qmods: resolved_mods = 0 - for mod in mods: + for mod in qmods: m = config_mod_map.get(mod.upper()) if m is None: raise ValueError('Unknown shortcut modifiers: {}'.format(sc)) resolved_mods |= m - mods = resolved_mods + mods: Optional[int] = resolved_mods + else: + mods = None is_text = False rkey = parts[-1] tkey = text_match(rkey) @@ -260,7 +313,7 @@ def parse_kittens_func_args(action, args_funcs): raise ValueError('Unknown key action: {}'.format(action)) if not isinstance(args, (list, tuple)): - args = (args,) + args = (args, ) return func, tuple(args) diff --git a/kitty/config_data.py b/kitty/config_data.py index 30ecb55fa..846fb83f7 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -5,13 +5,15 @@ # Utils {{{ import os from gettext import gettext as _ -from typing import Dict, Union +from typing import ( + Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar, Union +) from . import fast_data_types as defines from .conf.definition import Option, Shortcut, option_func from .conf.utils import ( - choices, positive_float, positive_int, to_bool, to_cmdline, to_color, - to_color_or_none, unit_float + Color, choices, positive_float, positive_int, to_bool, 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 @@ -42,14 +44,17 @@ def parse_mods(parts, sc): return mods -def to_modifiers(val): +def to_modifiers(val: str) -> int: return parse_mods(val.split('+'), val) or 0 -def uniq(vals, result_type=list): - seen = set() +T = TypeVar('T') + + +def uniq(vals: Iterable[T]) -> List[T]: + seen: Set[T] = set() seen_add = seen.add - return result_type(x for x in vals if x not in seen and not seen_add(x)) + return [x for x in vals if x not in seen and not seen_add(x)] # }}} # Groups {{{ @@ -221,7 +226,7 @@ o('italic_font', 'auto') o('bold_italic_font', 'auto') -def to_font_size(x): +def to_font_size(x: str) -> float: return max(MINIMUM_FONT_SIZE, float(x)) @@ -245,7 +250,7 @@ support, because it will force kitty to always treat the text as LTR, which FriBidi expects for terminals.""")) -def adjust_line_height(x): +def adjust_line_height(x: str) -> Union[int, float]: if x.endswith('%'): ans = float(x[:-1].strip()) / 100.0 if ans < 0: @@ -281,7 +286,7 @@ Syntax is:: ''')) -def disable_ligatures(x): +def disable_ligatures(x: str) -> int: cmap = {'never': 0, 'cursor': 1, 'always': 2} return cmap.get(x.lower(), 0) @@ -345,11 +350,11 @@ You can do this with e.g.:: ''')) -def box_drawing_scale(x): - ans = tuple(float(x.strip()) for x in x.split(',')) +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 + return ans[0], ans[1], ans[2], ans[3] o( @@ -374,7 +379,7 @@ cshapes = { } -def to_cursor_shape(x): +def to_cursor_shape(x: str) -> int: try: return cshapes[x.lower()] except KeyError: @@ -385,9 +390,9 @@ def to_cursor_shape(x): ) -def cursor_text_color(x): +def cursor_text_color(x: str) -> Optional[Color]: if x.lower() == 'background': - return + return None return to_color(x) @@ -416,14 +421,14 @@ inactivity. Set to zero to never stop blinking. g('scrollback') # {{{ -def scrollback_lines(x): - x = int(x) - if x < 0: - x = 2 ** 32 - 1 - return x +def scrollback_lines(x: str) -> int: + ans = int(x) + if ans < 0: + ans = 2 ** 32 - 1 + return ans -def scrollback_pager_history_size(x): +def scrollback_pager_history_size(x: str) -> int: ans = int(max(0, float(x)) * 1024 * 1024) return min(ans, 4096 * 1024 * 1024 - 1) @@ -497,7 +502,7 @@ The special value :code:`default` means to use the operating system's default URL handler.''')) -def copy_on_select(raw): +def copy_on_select(raw: str) -> str: q = raw.lower() # boolean values special cased for backwards compat if q in ('y', 'yes', 'true', 'clipboard'): @@ -612,7 +617,7 @@ number of cells instead of pixels. ''')) -def window_size(val): +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 @@ -622,9 +627,9 @@ o('initial_window_width', '640', option_type=window_size) o('initial_window_height', '400', option_type=window_size) -def to_layout_names(raw): +def to_layout_names(raw: str) -> List[str]: parts = [x.strip().lower() for x in raw.split(',')] - ans = [] + ans: List[str] = [] for p in parts: if p in ('*', 'all'): ans.extend(sorted(all_layouts)) @@ -695,7 +700,7 @@ zero and one, with zero being fully faded). ''')) -def hide_window_decorations(x): +def hide_window_decorations(x: str) -> int: if x == 'titlebar-only': return 0b10 if to_bool(x): @@ -717,7 +722,7 @@ operating system sends events corresponding to the start and end of a resize, this number is ignored.''')) -def resize_draw_strategy(x): +def resize_draw_strategy(x: str) -> int: cmap = {'static': 0, 'scale': 1, 'blank': 2, 'size': 3} return cmap.get(x.lower(), 0) @@ -743,7 +748,7 @@ g('tabbar') # {{{ default_tab_separator = ' ┇' -def tab_separator(x): +def tab_separator(x: str) -> str: for q in '\'"': if x.startswith(q) and x.endswith(q): x = x[1:-1] @@ -753,11 +758,11 @@ def tab_separator(x): return x -def tab_bar_edge(x): +def tab_bar_edge(x: str) -> int: return {'top': 1, 'bottom': 3}.get(x.lower(), 3) -def tab_font_style(x): +def tab_font_style(x: str) -> Tuple[bool, bool]: return { 'bold-italic': (True, True), 'bold': (True, False), @@ -777,7 +782,12 @@ In the fade style, each tab's edges fade into the background color, in the separ separated by a configurable separator, and the powerline shows the tabs as a continuous line. ''')) -o('tab_bar_min_tabs', 2, option_type=lambda x: max(1, positive_int(x)), long_text=_(''' + +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 ''')) @@ -789,7 +799,7 @@ of :code:`last` will switch to the right-most tab. ''')) -def tab_fade(x): +def tab_fade(x: str) -> Tuple[float, ...]: return tuple(map(unit_float, x.split())) @@ -805,7 +815,7 @@ o('tab_separator', '"{}"'.format(default_tab_separator), option_type=tab_separat The separator between tabs in the tab bar when using :code:`separator` as the :opt:`tab_bar_style`.''')) -def tab_title_template(x): +def tab_title_template(x: str) -> str: if x: for q in '\'"': if x.startswith(q) and x.endswith(q): @@ -814,7 +824,7 @@ def tab_title_template(x): return x -def active_tab_title_template(x): +def active_tab_title_template(x: str) -> Optional[str]: x = tab_title_template(x) return None if x == 'none' else x @@ -865,9 +875,9 @@ default as it has a performance cost) ''')) -def config_or_absolute_path(x): +def config_or_absolute_path(x: str) -> Optional[str]: if x.lower() == 'none': - return + return None x = os.path.expanduser(x) x = os.path.expandvars(x) if not os.path.isabs(x): @@ -903,9 +913,10 @@ How much to dim text that has the DIM/FAINT attribute set. One means no dimming zero means fully dimmed (i.e. invisible).''')) -def selection_foreground(x): +def selection_foreground(x: str) -> Optional[Color]: if x.lower() != 'none': return to_color(x) + return None o('selection_foreground', '#000000', option_type=selection_foreground, long_text=_(''' @@ -974,7 +985,7 @@ terminal can fail silently because their stdout/stderr/stdin no longer work. ''')) -def allow_remote_control(x): +def allow_remote_control(x: str) -> str: if x != 'socket-only': x = 'y' if to_bool(x) else 'n' return x @@ -1020,7 +1031,12 @@ that relative paths are interpreted with respect to the kitty config directory. Environment variables in the path are expanded. ''')) -o('clipboard_control', 'write-clipboard write-primary', option_type=lambda x: frozenset(x.lower().split()), long_text=_(''' + +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: write-clipboard read-clipboard write-primary read-primary. You can @@ -1046,7 +1062,7 @@ key-presses, to colors, to various advanced features may not work. g('os') # {{{ -def macos_titlebar_color(x): +def macos_titlebar_color(x: str) -> int: x = x.strip('"') if x == 'system': return 0 @@ -1067,7 +1083,7 @@ probably better off just hiding the titlebar with :opt:`hide_window_decorations` ''')) -def macos_option_as_alt(x): +def macos_option_as_alt(x: str) -> int: x = x.lower() if x == 'both': return 0b11 diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 76f4fb7b0..003a18c64 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -402,7 +402,7 @@ def coretext_all_fonts() -> Tuple[Dict[str, Any], ...]: pass -def add_timer(callback: Callable[[int], None], interval: float, repeats: bool = True) -> int: +def add_timer(callback: Callable[[Optional[int]], bool], interval: float, repeats: bool = True) -> int: pass diff --git a/kitty/launch.py b/kitty/launch.py index dfb87c8f1..c24e30dac 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -3,14 +3,18 @@ # License: GPLv3 Copyright: 2019, Kovid Goyal -from kitty.cli import parse_args -from kitty.fast_data_types import set_clipboard_string -from kitty.utils import set_primary_selection +from functools import lru_cache +from typing import Dict + +from .child import Child +from .cli import parse_args +from .fast_data_types import set_clipboard_string +from .utils import set_primary_selection +@lru_cache(maxsize=2) def options_spec(): - if not hasattr(options_spec, 'ans'): - OPTIONS = ''' + return ''' --window-title --title The title to set for the new window. By default, title is controlled by the child process. @@ -124,8 +128,6 @@ screen. Create a marker that highlights text in the newly created window. The syntax is the same as for the :code:`toggle_marker` map action (see :doc:`/marks`). ''' - options_spec.ans = OPTIONS - return options_spec.ans def parse_launch_args(args=None): @@ -137,8 +139,8 @@ def parse_launch_args(args=None): return opts, args -def get_env(opts, active_child): - env = {} +def get_env(opts, active_child: Child) -> Dict[str, str]: + env: Dict[str, str] = {} if opts.copy_env and active_child: env.update(active_child.foreground_environ) for x in opts.env: diff --git a/kitty/layout.py b/kitty/layout.py index fd69dc19c..8e4c93bc5 100644 --- a/kitty/layout.py +++ b/kitty/layout.py @@ -5,7 +5,7 @@ from collections import namedtuple from functools import lru_cache, partial from itertools import islice, repeat -from typing import Callable, Dict, Generator, List, Optional, Tuple, Union +from typing import Callable, Dict, Generator, List, Optional, Tuple, Union, cast from .constants import WindowGeometry from .fast_data_types import ( @@ -1416,7 +1416,7 @@ class Splits(Layout): # Instantiation {{{ -all_layouts = {o.name: o for o in globals().values() if isinstance(o, type) and issubclass(o, Layout) and o is not Layout} +all_layouts = {cast(str, o.name): o for o in globals().values() if isinstance(o, type) and issubclass(o, Layout) and o is not Layout} class CreateLayoutObjectFor: diff --git a/kitty/options_stub.py b/kitty/options_stub.py new file mode 100644 index 000000000..5637bd588 --- /dev/null +++ b/kitty/options_stub.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2020, Kovid Goyal + +import os + + +class Options: + pass + + +def generate_stub(): + from .config_data import all_options + from .conf.definition import as_type_stub + text = as_type_stub( + all_options, + special_types={ + 'symbol_map': 'typing.Dict[typing.Tuple[int, int], str]' + } + ) + with open(__file__ + 'i', 'w') as f: + print( + '# Update this file by running: python {}'.format(os.path.relpath(os.path.abspath(__file__))), + file=f + ) + f.write(text) + + +if __name__ == '__main__': + import subprocess + subprocess.Popen([ + 'kitty', '+runpy', + 'from kitty.options_stub import generate_stub; generate_stub()' + ]) diff --git a/kitty/options_stub.pyi b/kitty/options_stub.pyi new file mode 100644 index 000000000..093dbd5f8 --- /dev/null +++ b/kitty/options_stub.pyi @@ -0,0 +1,383 @@ +# Update this file by running: python kitty/options_stub.py +import kitty.rgb +from kitty.rgb import Color +import typing + + +class Options: + font_family: str + bold_font: str + italic_font: str + bold_italic_font: str + font_size: float + force_ltr: bool + adjust_line_height: typing.Union[int, float] + adjust_column_width: typing.Union[int, float] + symbol_map: typing.Dict[typing.Tuple[int, int], str] + disable_ligatures: int + font_features: str + box_drawing_scale: typing.Tuple[float, float, float, float] + cursor: Color + cursor_text_color: typing.Union[kitty.rgb.Color, None] + cursor_shape: int + cursor_beam_thickness: float + cursor_underline_thickness: float + cursor_blink_interval: float + cursor_stop_blinking_after: float + scrollback_lines: int + scrollback_pager: typing.List[str] + scrollback_pager_history_size: int + wheel_scroll_multiplier: float + touch_scroll_multiplier: float + mouse_hide_wait: float + url_color: Color + url_style: int + open_url_modifiers: int + open_url_with: typing.List[str] + copy_on_select: str + strip_trailing_spaces: str + rectangle_select_modifiers: int + terminal_select_modifiers: int + select_by_word_characters: str + click_interval: float + focus_follows_mouse: bool + pointer_shape_when_grabbed: str + repaint_delay: int + input_delay: int + sync_to_monitor: bool + enable_audio_bell: bool + visual_bell_duration: float + window_alert_on_bell: bool + bell_on_tab: bool + command_on_bell: typing.List[str] + remember_window_size: bool + initial_window_width: typing.Tuple[int, str] + initial_window_height: typing.Tuple[int, str] + enabled_layouts: typing.List[str] + window_resize_step_cells: int + window_resize_step_lines: int + window_border_width: float + draw_minimal_borders: bool + window_margin_width: float + single_window_margin_width: float + window_padding_width: float + placement_strategy: str + active_border_color: typing.Union[kitty.rgb.Color, None] + inactive_border_color: Color + bell_border_color: Color + inactive_text_alpha: float + hide_window_decorations: int + resize_debounce_time: float + resize_draw_strategy: int + resize_in_steps: bool + tab_bar_edge: int + tab_bar_margin_width: float + tab_bar_style: str + tab_bar_min_tabs: int + tab_switch_strategy: str + tab_fade: typing.Tuple[float, ...] + tab_separator: str + tab_title_template: str + active_tab_title_template: typing.Union[str, None] + active_tab_foreground: Color + active_tab_background: Color + active_tab_font_style: typing.Tuple[bool, bool] + inactive_tab_foreground: Color + inactive_tab_background: Color + inactive_tab_font_style: typing.Tuple[bool, bool] + tab_bar_background: typing.Union[kitty.rgb.Color, None] + foreground: Color + background: Color + background_opacity: float + background_image: typing.Union[str, None] + background_image_layout: str + background_image_linear: bool + dynamic_background_opacity: bool + background_tint: float + dim_opacity: float + selection_foreground: typing.Union[kitty.rgb.Color, None] + selection_background: Color + color0: Color + color8: Color + color1: Color + color9: Color + color2: Color + color10: Color + color3: Color + color11: Color + color4: Color + color12: Color + color5: Color + color13: Color + color6: Color + color14: Color + color7: Color + color15: Color + mark1_foreground: Color + mark1_background: Color + mark2_foreground: Color + mark2_background: Color + mark3_foreground: Color + mark3_background: Color + color16: Color + color17: Color + color18: Color + color19: Color + color20: Color + color21: Color + color22: Color + color23: Color + color24: Color + color25: Color + color26: Color + color27: Color + color28: Color + color29: Color + color30: Color + color31: Color + color32: Color + color33: Color + color34: Color + color35: Color + color36: Color + color37: Color + color38: Color + color39: Color + color40: Color + color41: Color + color42: Color + color43: Color + color44: Color + color45: Color + color46: Color + color47: Color + color48: Color + color49: Color + color50: Color + color51: Color + color52: Color + color53: Color + color54: Color + color55: Color + color56: Color + color57: Color + color58: Color + color59: Color + color60: Color + color61: Color + color62: Color + color63: Color + color64: Color + color65: Color + color66: Color + color67: Color + color68: Color + color69: Color + color70: Color + color71: Color + color72: Color + color73: Color + color74: Color + color75: Color + color76: Color + color77: Color + color78: Color + color79: Color + color80: Color + color81: Color + color82: Color + color83: Color + color84: Color + color85: Color + color86: Color + color87: Color + color88: Color + color89: Color + color90: Color + color91: Color + color92: Color + color93: Color + color94: Color + color95: Color + color96: Color + color97: Color + color98: Color + color99: Color + color100: Color + color101: Color + color102: Color + color103: Color + color104: Color + color105: Color + color106: Color + color107: Color + color108: Color + color109: Color + color110: Color + color111: Color + color112: Color + color113: Color + color114: Color + color115: Color + color116: Color + color117: Color + color118: Color + color119: Color + color120: Color + color121: Color + color122: Color + color123: Color + color124: Color + color125: Color + color126: Color + color127: Color + color128: Color + color129: Color + color130: Color + color131: Color + color132: Color + color133: Color + color134: Color + color135: Color + color136: Color + color137: Color + color138: Color + color139: Color + color140: Color + color141: Color + color142: Color + color143: Color + color144: Color + color145: Color + color146: Color + color147: Color + color148: Color + color149: Color + color150: Color + color151: Color + color152: Color + color153: Color + color154: Color + color155: Color + color156: Color + color157: Color + color158: Color + color159: Color + color160: Color + color161: Color + color162: Color + color163: Color + color164: Color + color165: Color + color166: Color + color167: Color + color168: Color + color169: Color + color170: Color + color171: Color + color172: Color + color173: Color + color174: Color + color175: Color + color176: Color + color177: Color + color178: Color + color179: Color + color180: Color + color181: Color + color182: Color + color183: Color + color184: Color + color185: Color + color186: Color + color187: Color + color188: Color + color189: Color + color190: Color + color191: Color + color192: Color + color193: Color + color194: Color + color195: Color + color196: Color + color197: Color + color198: Color + color199: Color + color200: Color + color201: Color + color202: Color + color203: Color + color204: Color + color205: Color + color206: Color + color207: Color + color208: Color + color209: Color + color210: Color + color211: Color + color212: Color + color213: Color + color214: Color + color215: Color + color216: Color + color217: Color + color218: Color + color219: Color + color220: Color + color221: Color + color222: Color + color223: Color + color224: Color + color225: Color + color226: Color + color227: Color + color228: Color + color229: Color + color230: Color + color231: Color + color232: Color + color233: Color + color234: Color + color235: Color + color236: Color + color237: Color + color238: Color + color239: Color + color240: Color + color241: Color + color242: Color + color243: Color + color244: Color + color245: Color + color246: Color + color247: Color + color248: Color + color249: Color + color250: Color + color251: Color + color252: Color + color253: Color + color254: Color + color255: Color + shell: str + editor: str + close_on_child_death: bool + allow_remote_control: str + env: typing.Dict[str, str] + update_check_interval: float + startup_session: typing.Union[str, None] + clipboard_control: typing.FrozenSet[str] + term: str + macos_titlebar_color: int + macos_option_as_alt: int + macos_hide_from_tasks: bool + macos_quit_when_last_window_closed: bool + macos_window_resizable: bool + macos_thicken_font: float + macos_traditional_fullscreen: bool + macos_show_window_title_in: str + macos_custom_beam_cursor: bool + linux_display_server: str + kitty_mod: int + clear_all_shortcuts: bool + kitten_alias: str \ No newline at end of file diff --git a/kitty/rgb.py b/kitty/rgb.py index 397fd8404..d3de81a99 100644 --- a/kitty/rgb.py +++ b/kitty/rgb.py @@ -5,15 +5,16 @@ import re from collections import namedtuple from contextlib import suppress +from typing import Optional Color = namedtuple('Color', 'red green blue') -def alpha_blend_channel(top_color, bottom_color, alpha): +def alpha_blend_channel(top_color: int, bottom_color: int, alpha: float) -> int: return int(alpha * top_color + (1 - alpha) * bottom_color) -def alpha_blend(top_color, bottom_color, alpha): +def alpha_blend(top_color: Color, bottom_color: Color, alpha: float) -> Color: return Color( alpha_blend_channel(top_color.red, bottom_color.red, alpha), alpha_blend_channel(top_color.green, bottom_color.green, alpha), @@ -21,48 +22,50 @@ def alpha_blend(top_color, bottom_color, alpha): ) -def parse_single_color(c): +def parse_single_color(c: str) -> int: if len(c) == 1: c += c return int(c[:2], 16) -def parse_sharp(spec): +def parse_sharp(spec: str) -> Optional[Color]: if len(spec) in (3, 6, 9, 12): part_len = len(spec) // 3 colors = re.findall(r'[a-fA-F0-9]{%d}' % part_len, spec) return Color(*map(parse_single_color, colors)) + return None -def parse_rgb(spec): +def parse_rgb(spec: str) -> Optional[Color]: colors = spec.split('/') if len(colors) == 3: return Color(*map(parse_single_color, colors)) + return None -def color_from_int(x): +def color_from_int(x: int) -> Color: return Color((x >> 16) & 255, (x >> 8) & 255, x & 255) -def color_as_int(x): +def color_as_int(x: Color) -> int: return x.red << 16 | x.green << 8 | x.blue -def color_as_sharp(x): +def color_as_sharp(x: Color) -> str: return '#{:02x}{:02x}{:02x}'.format(*x) -def color_as_sgr(x): +def color_as_sgr(x: Color) -> str: return ':2:{}:{}:{}'.format(*x) -def to_color(raw, validate=False): +def to_color(raw: str, validate: bool = False) -> Optional[Color]: # See man XParseColor x = raw.strip().lower() ans = color_names.get(x) if ans is not None: return ans - val = None + val: Optional[Color] = None with suppress(Exception): if raw.startswith('#'): val = parse_sharp(raw[1:]) diff --git a/kitty/update_check.py b/kitty/update_check.py index 251fb1834..85074a5f8 100644 --- a/kitty/update_check.py +++ b/kitty/update_check.py @@ -7,6 +7,7 @@ import subprocess import time from collections import namedtuple from contextlib import suppress +from typing import Optional from urllib.request import urlopen from .config import atomic_save @@ -95,7 +96,7 @@ def process_current_release(raw): notify_new_version(release_version) -def run_worker(): +def run_worker() -> None: import time import random time.sleep(random.randint(1000, 4000) / 1000) @@ -103,7 +104,7 @@ def run_worker(): print(get_released_version()) -def update_check(timer_id=None): +def update_check(timer_id: Optional[int] = None) -> bool: try: p = subprocess.Popen([ kitty_exe(), '+runpy', @@ -113,10 +114,13 @@ def update_check(timer_id=None): log_error('Failed to run kitty for update check, with error: {}'.format(e)) return False monitor_pid(p.pid) - get_boss().set_update_check_process(p) - return True + boss = get_boss() + if boss is not None: + boss.set_update_check_process(p) + return True + return False -def run_update_check(interval=CHECK_INTERVAL): +def run_update_check(interval: int = CHECK_INTERVAL) -> None: if update_check(): add_timer(update_check, interval) diff --git a/kitty/utils.py b/kitty/utils.py index 1d6aedae1..528c2cd96 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -14,14 +14,13 @@ from collections import namedtuple from contextlib import suppress from functools import lru_cache from time import monotonic -from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, cast from .constants import ( appname, is_macos, is_wayland, shell_path, supports_primary_selection ) +from .options_stub import Options from .rgb import Color, to_color -if TYPE_CHECKING: - from .cli import Namespace # noqa BASE = os.path.dirname(os.path.abspath(__file__)) @@ -468,7 +467,7 @@ def func_name(f): return str(f) -def resolved_shell(opts: Optional['Namespace'] = None) -> List[str]: +def resolved_shell(opts: Optional[Options] = None) -> List[str]: ans = getattr(opts, 'shell', '.') if ans == '.': ans = [shell_path] @@ -478,7 +477,7 @@ def resolved_shell(opts: Optional['Namespace'] = None) -> List[str]: return ans -def read_shell_environment(opts: Optional['Namespace'] = None) -> Dict[str, str]: +def read_shell_environment(opts: Optional[Options] = None) -> Dict[str, str]: ans = getattr(read_shell_environment, 'ans', None) if ans is None: from .child import openpty, remove_blocking