Create an auto-generated stub file for the kitty Options object
This commit is contained in:
parent
ec8c96b8e6
commit
0f4e7921ee
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -3,14 +3,18 @@
|
||||
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@ -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:
|
||||
|
||||
34
kitty/options_stub.py
Normal file
34
kitty/options_stub.py
Normal file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
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()'
|
||||
])
|
||||
383
kitty/options_stub.pyi
Normal file
383
kitty/options_stub.pyi
Normal file
@ -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
|
||||
25
kitty/rgb.py
generated
25
kitty/rgb.py
generated
@ -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:])
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user