More typing work

Also use a mypy based linter when editing
This commit is contained in:
Kovid Goyal
2020-03-08 11:02:14 +05:30
parent 9b32f18109
commit cc1336a616
43 changed files with 333 additions and 196 deletions

View File

@@ -7,28 +7,28 @@
# full list see the documentation: # full list see the documentation:
# https://www.sphinx-doc.org/en/master/config # https://www.sphinx-doc.org/en/master/config
import importlib
import os import os
import re import re
import subprocess import subprocess
import sys import sys
import time import time
from functools import partial from functools import partial
from typing import Optional, Dict, Tuple
from docutils import nodes from docutils import nodes
from docutils.parsers.rst.roles import set_classes from docutils.parsers.rst.roles import set_classes
from pygments.lexer import RegexLexer, bygroups from pygments.lexer import RegexLexer, bygroups # type: ignore
from pygments.token import ( from pygments.token import ( # type: ignore
Comment, Keyword, Literal, Name, Number, String, Whitespace Comment, Keyword, Literal, Name, Number, String, Whitespace
) )
from sphinx import addnodes from sphinx import addnodes # type: ignore
from sphinx.environment.adapters.toctree import TocTree from sphinx.environment.adapters.toctree import TocTree # type: ignore
from sphinx.util.logging import getLogger from sphinx.util.logging import getLogger # type: ignore
kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if kitty_src not in sys.path: if kitty_src not in sys.path:
sys.path.insert(0, kitty_src) sys.path.insert(0, kitty_src)
str_version = importlib.import_module('kitty.constants').str_version from kitty.constants import str_version # noqa
# config {{{ # config {{{
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
@@ -77,7 +77,7 @@ master_doc = 'index'
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = None language: Optional[str] = None
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
@@ -205,7 +205,8 @@ def num_role(which, name, rawtext, text, lineno, inliner, options={}, content=[]
def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]): def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
' Link to a github commit ' ' Link to a github commit '
try: try:
commit_id = subprocess.check_output(f'git rev-list --max-count=1 --skip=# {text}'.split()).decode('utf-8').strip() commit_id = subprocess.check_output(
f'git rev-list --max-count=1 --skip=# {text}'.split()).decode('utf-8').strip()
except Exception: except Exception:
msg = inliner.reporter.error( msg = inliner.reporter.error(
f'GitHub commit id "{text}" not recognized.', line=lineno) f'GitHub commit id "{text}" not recognized.', line=lineno)
@@ -213,7 +214,8 @@ def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
return [prb], [msg] return [prb], [msg]
url = f'https://github.com/kovidgoyal/kitty/commit/{commit_id}' url = f'https://github.com/kovidgoyal/kitty/commit/{commit_id}'
set_classes(options) set_classes(options)
short_id = subprocess.check_output(f'git rev-list --max-count=1 --abbrev-commit --skip=# {commit_id}'.split()).decode('utf-8').strip() short_id = subprocess.check_output(
f'git rev-list --max-count=1 --abbrev-commit --skip=# {commit_id}'.split()).decode('utf-8').strip()
node = nodes.reference(rawtext, f'commit: {short_id}', refuri=url, **options) node = nodes.reference(rawtext, f'commit: {short_id}', refuri=url, **options)
return [node], [] return [node], []
# }}} # }}}
@@ -409,8 +411,8 @@ def expand_opt_references(conf_name, text):
return re.sub(r':opt:`(.+?)`', expand, text) return re.sub(r':opt:`(.+?)`', expand, text)
opt_aliases = {} opt_aliases: Dict[str, str] = {}
shortcut_slugs = {} shortcut_slugs: Dict[str, Tuple[str, str]] = {}
def parse_opt_node(env, sig, signode): def parse_opt_node(env, sig, signode):

View File

@@ -10,6 +10,7 @@ from contextlib import suppress
from functools import partial from functools import partial
from gettext import gettext as _ from gettext import gettext as _
from weakref import WeakValueDictionary from weakref import WeakValueDictionary
from typing import Optional
from .child import cached_process_data, cwd_of_process from .child import cached_process_data, cwd_of_process
from .cli import create_opts, parse_args from .cli import create_opts, parse_args
@@ -43,8 +44,8 @@ from .window import Window
def notification_activated(identifier: str) -> None: def notification_activated(identifier: str) -> None:
if identifier == 'new-version': if identifier == 'new-version':
from .update_check import notification_activated from .update_check import notification_activated as do
notification_activated() do()
def listen_on(spec: str) -> int: def listen_on(spec: str) -> int:
@@ -57,7 +58,7 @@ def listen_on(spec: str) -> int:
return s.fileno() return s.fileno()
def data_for_at(w: Window, arg: str, add_wrap_markers: bool = False) -> str: def data_for_at(w: Window, arg: str, add_wrap_markers: bool = False) -> Optional[str]:
def as_text(**kw) -> str: def as_text(**kw) -> str:
kw['add_wrap_markers'] = add_wrap_markers kw['add_wrap_markers'] = add_wrap_markers
return w.as_text(**kw) return w.as_text(**kw)
@@ -80,6 +81,7 @@ def data_for_at(w: Window, arg: str, add_wrap_markers: bool = False) -> str:
return as_text(as_ansi=True, alternate_screen=True) return as_text(as_ansi=True, alternate_screen=True)
if arg == '@ansi_alternate_scrollback': if arg == '@ansi_alternate_scrollback':
return as_text(as_ansi=True, alternate_screen=True, add_history=True) return as_text(as_ansi=True, alternate_screen=True, add_history=True)
return None
class DumpCommands: # {{{ class DumpCommands: # {{{
@@ -935,7 +937,7 @@ class Boss:
stdin = stdin.encode('utf-8') stdin = stdin.encode('utf-8')
return env, stdin return env, stdin
def data_for_at(self, which, window=None, add_wrap_markers=False): def data_for_at(self, which, window=None, add_wrap_markers=False) -> Optional[str]:
return data_for_at(window or self.active_window, which, add_wrap_markers=add_wrap_markers) return data_for_at(window or self.active_window, which, add_wrap_markers=add_wrap_markers)
def special_window_for_cmd(self, cmd, window=None, stdin=None, cwd_from=None, as_overlay=False): def special_window_for_cmd(self, cmd, window=None, stdin=None, cwd_from=None, as_overlay=False):

View File

@@ -7,7 +7,7 @@ import os
import sys import sys
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from typing import DefaultDict, List from typing import DefaultDict, List, Optional
import kitty.fast_data_types as fast_data_types import kitty.fast_data_types as fast_data_types
@@ -158,7 +158,8 @@ def openpty():
class Child: class Child:
child_fd = pid = None child_fd: Optional[int] = None
pid: Optional[int] = None
forked = False forked = False
def __init__(self, argv, cwd, opts, stdin=None, env=None, cwd_from=None, allow_remote_control=False): def __init__(self, argv, cwd, opts, stdin=None, env=None, cwd_from=None, allow_remote_control=False):

View File

@@ -7,13 +7,13 @@ import re
import sys import sys
from collections import deque from collections import deque
from typing import ( from typing import (
Any, Callable, Dict, FrozenSet, Iterator, List, Optional, Tuple, Type, Any, Callable, Dict, FrozenSet, Iterator, List, Optional, Sequence, Tuple,
TypeVar, Union, cast Type, TypeVar, Union, cast
) )
from .cli_stub import CLIOptions
from .conf.utils import resolve_config from .conf.utils import resolve_config
from .constants import appname, defconf, is_macos, is_wayland, str_version from .constants import appname, defconf, is_macos, is_wayland, str_version
from .cli_stub import CLIOptions
from .options_stub import Options as OptionsStub from .options_stub import Options as OptionsStub
try: try:
@@ -135,7 +135,8 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
disabled: OptionSpecSeq = [] disabled: OptionSpecSeq = []
mpat = re.compile('([a-z]+)=(.+)') mpat = re.compile('([a-z]+)=(.+)')
current_cmd: OptionDict = { current_cmd: OptionDict = {
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(), 'type': '', 'condition': False, 'default': None 'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(),
'type': '', 'condition': False, 'default': None
} }
empty_cmd = current_cmd empty_cmd = current_cmd
@@ -150,7 +151,8 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
if line.startswith('--'): if line.startswith('--'):
parts = line.split(' ') parts = line.split(' ')
current_cmd = { current_cmd = {
'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': '', 'choices': frozenset(), 'type': '', 'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': '',
'choices': frozenset(), 'type': '',
'default': None, 'condition': True 'default': None, 'condition': True
} }
state = METADATA state = METADATA
@@ -335,7 +337,13 @@ class PrintHelpForSeq:
print_help_for_seq = PrintHelpForSeq() print_help_for_seq = PrintHelpForSeq()
def seq_as_rst(seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: Optional[str], heading_char: str = '-') -> str: def seq_as_rst(
seq: OptionSpecSeq,
usage: Optional[str],
message: Optional[str],
appname: Optional[str],
heading_char: str = '-'
) -> str:
import textwrap import textwrap
blocks: List[str] = [] blocks: List[str] = []
a = blocks.append a = blocks.append
@@ -382,7 +390,7 @@ def seq_as_rst(seq: OptionSpecSeq, usage: Optional[str], message: Optional[str],
return text return text
def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str) -> str: def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, extra_fields: Sequence[str] = ()) -> str:
from itertools import chain from itertools import chain
ans: List[str] = ['class {}:'.format(class_name)] ans: List[str] = ['class {}:'.format(class_name)]
for opt in chain(seq, disabled): for opt in chain(seq, disabled):
@@ -401,6 +409,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str) -
else: else:
raise ValueError('Unknown CLI option type: {}'.format(otype)) raise ValueError('Unknown CLI option type: {}'.format(otype))
ans.append(' {}: {}'.format(name, t)) ans.append(' {}: {}'.format(name, t))
for x in extra_fields:
ans.append(' {}'.format(x))
return '\n'.join(ans) + '\n\n\n' return '\n'.join(ans) + '\n\n\n'

View File

@@ -3,6 +3,9 @@
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Sequence
class CLIOptions: class CLIOptions:
pass pass
@@ -17,9 +20,9 @@ def generate_stub() -> None:
from .conf.definition import save_type_stub from .conf.definition import save_type_stub
text = 'import typing\n\n\n' text = 'import typing\n\n\n'
def do(otext=None, cls: str = 'CLIOptions'): def do(otext=None, cls: str = 'CLIOptions', extra_fields: Sequence[str] = ()):
nonlocal text nonlocal text
text += as_type_stub(*parse_option_spec(otext), class_name=cls) text += as_type_stub(*parse_option_spec(otext), class_name=cls, extra_fields=extra_fields)
do() do()
@@ -27,7 +30,7 @@ def generate_stub() -> None:
do(options_spec(), 'LaunchCLIOptions') do(options_spec(), 'LaunchCLIOptions')
from .remote_control import global_options_spec from .remote_control import global_options_spec
do(global_options_spec(), 'RCOptions') do(global_options_spec(), 'RCOptions', extra_fields=['no_command_response: typing.Optional[bool]'])
from kittens.ask.main import option_text from kittens.ask.main import option_text
do(option_text(), 'AskCLIOptions') do(option_text(), 'AskCLIOptions')

View File

@@ -6,7 +6,7 @@ import os
import shlex import shlex
import sys import sys
from functools import lru_cache from functools import lru_cache
from typing import Tuple from typing import Callable, Dict, Tuple
from kittens.runner import all_kitten_names, get_kitten_cli_docs from kittens.runner import all_kitten_names, get_kitten_cli_docs
@@ -33,7 +33,8 @@ taking the results from kitty's completion system and converting
them into something your shell will understand. them into something your shell will understand.
''' '''
parsers, serializers = {}, {} parsers: Dict[str, Callable] = {}
serializers: Dict[str, Callable] = {}
def debug(*a, **kw): def debug(*a, **kw):

View File

@@ -7,7 +7,7 @@ import re
import shlex import shlex
from collections import namedtuple from collections import namedtuple
from typing import ( from typing import (
Any, Callable, Dict, FrozenSet, Iterator, List, Optional, Sequence, Tuple, Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Sequence, Tuple,
Type, Union Type, Union
) )
@@ -118,7 +118,7 @@ def parse_line(
def _parse( def _parse(
lines: Iterator[str], lines: Iterable[str],
type_convert: Callable[[str, Any], Any], type_convert: Callable[[str, Any], Any],
special_handling: Callable, special_handling: Callable,
ans: Dict[str, Any], ans: Dict[str, Any],
@@ -144,7 +144,7 @@ def _parse(
def parse_config_base( def parse_config_base(
lines: Iterator[str], lines: Iterable[str],
defaults: Any, defaults: Any,
type_convert: Callable[[str, Any], Any], type_convert: Callable[[str, Any], Any],
special_handling: Callable, special_handling: Callable,
@@ -158,7 +158,7 @@ def parse_config_base(
) )
def create_options_class(all_keys: Iterator[str]) -> Type: def create_options_class(all_keys: Iterable[str]) -> Type:
keys = tuple(sorted(all_keys)) keys = tuple(sorted(all_keys))
slots = keys + ('_fields', ) slots = keys + ('_fields', )
@@ -223,10 +223,10 @@ def resolve_config(SYSTEM_CONF: str, defconf: str, config_files_on_cmd_line: Seq
def load_config( def load_config(
Options: Type, Options: Type,
defaults: Any, defaults: Any,
parse_config: Callable[[Iterator[str]], Dict[str, Any]], parse_config: Callable[[Iterable[str]], Dict[str, Any]],
merge_configs: Callable[[Dict, Dict], Dict], merge_configs: Callable[[Dict, Dict], Dict],
*paths: str, *paths: str,
overrides: Optional[Iterator[str]] = None overrides: Optional[Iterable[str]] = None
): ):
ans: Dict = defaults._asdict() ans: Dict = defaults._asdict()
for path in paths: for path in paths:
@@ -244,7 +244,7 @@ def load_config(
return Options(ans) return Options(ans)
def init_config(default_config_lines: Iterator[str], parse_config: Callable): def init_config(default_config_lines: Iterable[str], parse_config: Callable):
defaults = parse_config(default_config_lines, check_keys=False) defaults = parse_config(default_config_lines, check_keys=False)
Options = create_options_class(defaults.keys()) Options = create_options_class(defaults.keys())
defaults = Options(defaults) defaults = Options(defaults)

View File

@@ -10,8 +10,8 @@ from collections import namedtuple
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from functools import partial from functools import partial
from typing import ( from typing import (
TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Sequence, Tuple, Type, TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional,
cast Sequence, Set, Tuple, Type, cast
) )
from . import fast_data_types as defines from . import fast_data_types as defines
@@ -310,7 +310,7 @@ def scroll_to_mark(func, rest):
return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))] return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))]
def parse_key_action(action): def parse_key_action(action: str) -> Optional[KeyAction]:
parts = action.strip().split(maxsplit=1) parts = action.strip().split(maxsplit=1)
func = parts[0] func = parts[0]
if len(parts) == 1: if len(parts) == 1:
@@ -324,9 +324,10 @@ def parse_key_action(action):
log_error('Ignoring invalid key action: {} with err: {}'.format(action, err)) log_error('Ignoring invalid key action: {} with err: {}'.format(action, err))
else: else:
return KeyAction(func, args) return KeyAction(func, args)
return None
all_key_actions = set() all_key_actions: Set[str] = set()
sequence_sep = '>' sequence_sep = '>'
@@ -454,16 +455,17 @@ def parse_send_text(val, key_definitions):
return parse_key(key_str, key_definitions) return parse_key(key_str, key_definitions)
special_handlers = {} SpecialHandlerFunc = Callable[[str, str, Dict[str, Any]], None]
special_handlers: Dict[str, SpecialHandlerFunc] = {}
def special_handler(func): def special_handler(func: SpecialHandlerFunc) -> SpecialHandlerFunc:
special_handlers[func.__name__.partition('_')[2]] = func special_handlers[func.__name__.partition('_')[2]] = func
return func return func
def deprecated_handler(*names): def deprecated_handler(*names: str) -> Callable[[SpecialHandlerFunc], SpecialHandlerFunc]:
def special_handler(func): def special_handler(func: SpecialHandlerFunc) -> SpecialHandlerFunc:
for name in names: for name in names:
special_handlers[name] = func special_handlers[name] = func
return func return func
@@ -588,7 +590,7 @@ def option_names_for_completion():
yield from special_handlers yield from special_handlers
def parse_config(lines: Iterator[str], check_keys=True, accumulate_bad_lines: Optional[List[BadLine]] = None): def parse_config(lines: Iterable[str], check_keys=True, accumulate_bad_lines: Optional[List[BadLine]] = None):
ans: Dict[str, Any] = { ans: Dict[str, Any] = {
'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [], 'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [],
'env': {}, 'kitten_aliases': {}, 'font_features': {} 'env': {}, 'kitten_aliases': {}, 'font_features': {}
@@ -777,7 +779,7 @@ def finalize_keys(opts: OptionsStub) -> None:
opts.sequence_map = sequence_map opts.sequence_map = sequence_map
def load_config(*paths: str, overrides: Optional[Iterator[str]] = None, accumulate_bad_lines: Optional[List[BadLine]] = None) -> OptionsStub: def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumulate_bad_lines: Optional[List[BadLine]] = None) -> OptionsStub:
parser = parse_config parser = parse_config
if accumulate_bad_lines is not None: if accumulate_bad_lines is not None:
parser = partial(parse_config, accumulate_bad_lines=accumulate_bad_lines) parser = partial(parse_config, accumulate_bad_lines=accumulate_bad_lines)

View File

@@ -175,4 +175,4 @@ supports_primary_selection = not is_macos
def running_in_kitty(set_val: Optional[bool] = None) -> bool: def running_in_kitty(set_val: Optional[bool] = None) -> bool:
if set_val is not None: if set_val is not None:
setattr(running_in_kitty, 'ans', set_val) setattr(running_in_kitty, 'ans', set_val)
return getattr(running_in_kitty, 'ans', False) return bool(getattr(running_in_kitty, 'ans', False))

View File

@@ -348,7 +348,11 @@ StartupCtx = NewType('StartupCtx', int)
Display = NewType('Display', int) Display = NewType('Display', int)
def init_x11_startup_notification(display: Display, window_id: int, startup_id: Optional[str] = None) -> StartupCtx: def init_x11_startup_notification(
display: Display,
window_id: int,
startup_id: Optional[str] = None
) -> StartupCtx:
pass pass
@@ -387,7 +391,10 @@ def default_color_table() -> Tuple[int, ...]:
FontConfigPattern = Dict[str, Union[str, int, bool, float]] FontConfigPattern = Dict[str, Union[str, int, bool, float]]
def fc_list(spacing: int = -1, allow_bitmapped_fonts: bool = False) -> Tuple[FontConfigPattern, ...]: def fc_list(
spacing: int = -1,
allow_bitmapped_fonts: bool = False
) -> Tuple[FontConfigPattern, ...]:
pass pass
@@ -407,7 +414,11 @@ def coretext_all_fonts() -> Tuple[Dict[str, Any], ...]:
pass pass
def add_timer(callback: Callable[[Optional[int]], bool], interval: float, repeats: bool = True) -> int: def add_timer(
callback: Callable[[Optional[int]], bool],
interval: float,
repeats: bool = True
) -> int:
pass pass
@@ -419,7 +430,9 @@ def add_window(os_window_id: int, tab_id: int, title: str) -> int:
pass pass
def compile_program(which: int, vertex_shader: str, fragment_shader: str) -> int: def compile_program(
which: int, vertex_shader: str, fragment_shader: str
) -> int:
pass pass
@@ -431,7 +444,10 @@ def set_titlebar_color(os_window_id: int, color: int) -> bool:
pass pass
def add_borders_rect(os_window_id: int, tab_id: int, left: int, top: int, right: int, bottom: int, color: int) -> None: def add_borders_rect(
os_window_id: int, tab_id: int, left: int, top: int, right: int,
bottom: int, color: int
) -> None:
pass pass
@@ -443,16 +459,30 @@ def os_window_has_background_image(os_window_id: int) -> bool:
pass pass
def dbus_send_notification(app_name: str, icon: str, summary: str, body: str, action_name: str, timeout: int = -1) -> int: def dbus_send_notification(
app_name: str,
icon: str,
summary: str,
body: str,
action_name: str,
timeout: int = -1
) -> int:
pass pass
def cocoa_send_notification(identifier: Optional[str], title: str, informative_text: str, path_to_img: Optional[str], subtitle: Optional[str] = None) -> None: def cocoa_send_notification(
identifier: Optional[str],
title: str,
informative_text: str,
path_to_img: Optional[str],
subtitle: Optional[str] = None
) -> None:
pass pass
def create_os_window( def create_os_window(
get_window_size: Callable[[int, int, int, int, float, float], Tuple[int, int]], get_window_size: Callable[[int, int, int, int, float, float], Tuple[int,
int]],
pre_show_callback: Callable[[object], None], pre_show_callback: Callable[[object], None],
title: str, title: str,
wm_class_name: str, wm_class_name: str,
@@ -464,11 +494,16 @@ def create_os_window(
pass pass
def update_window_title(os_window_id: int, tab_id: int, window_id: int, title: str) -> None: def update_window_title(
os_window_id: int, tab_id: int, window_id: int, title: str
) -> None:
pass pass
def update_window_visibility(os_window_id: int, tab_id: int, window_id: int, window_idx: int, visible: bool) -> None: def update_window_visibility(
os_window_id: int, tab_id: int, window_id: int, window_idx: int,
visible: bool
) -> None:
pass pass
@@ -501,7 +536,12 @@ def set_default_window_icon(data: bytes, width: int, height: int) -> None:
pass pass
def set_custom_cursor(cursor_type: int, images: Tuple[Tuple[bytes, int, int], ...], x: int = 0, y: int = 0) -> None: def set_custom_cursor(
cursor_type: int,
images: Tuple[Tuple[bytes, int, int], ...],
x: int = 0,
y: int = 0
) -> None:
pass pass
@@ -542,8 +582,10 @@ def set_clipboard_string(data: bytes) -> None:
def set_background_image( def set_background_image(
path: Optional[str], os_window_ids: Tuple[int, ...], path: Optional[str],
configured: bool = True, layout_name: Optional[str] = None os_window_ids: Tuple[int, ...],
configured: bool = True,
layout_name: Optional[str] = None
) -> None: ) -> None:
pass pass
@@ -564,7 +606,20 @@ def patch_global_colors(spec: Dict[str, int], configured: bool) -> None:
pass pass
def os_window_font_size(os_window_id: int, new_sz: float = -1., force: bool = False) -> float: class ColorProfile:
pass
def patch_color_profiles(
spec: Dict[str, int], cursor_text_color: Optional[Union[bool, int]],
profiles: Tuple[ColorProfile, ...], change_configured: bool
) -> None:
pass
def os_window_font_size(
os_window_id: int, new_sz: float = -1., force: bool = False
) -> float:
pass pass
@@ -692,7 +747,9 @@ class Region:
pass pass
def viewport_for_window(os_window_id: int) -> Tuple[Region, Region, int, int, int, int]: def viewport_for_window(
os_window_id: int
) -> Tuple[Region, Region, int, int, int, int]:
pass pass
@@ -716,14 +773,10 @@ def open_tty(read_with_timeout: bool = False) -> Tuple[int, TermiosPtr]:
def parse_input_from_terminal( def parse_input_from_terminal(
text_callback: Callable[[str], None], text_callback: Callable[[str], None], dcs_callback: Callable[[str], None],
dcs_callback: Callable[[str], None], csi_callback: Callable[[str], None], osc_callback: Callable[[str], None],
csi_callback: Callable[[str], None], pm_callback: Callable[[str], None], apc_callback: Callable[[str], None],
osc_callback: Callable[[str], None], data: str, in_bracketed_paste: bool
pm_callback: Callable[[str], None],
apc_callback: Callable[[str], None],
data: str,
in_bracketed_paste: bool
): ):
pass pass
@@ -732,7 +785,9 @@ class Line:
pass pass
def test_shape(line: Line, path: Optional[str] = None, index: int = 0) -> List[Tuple[int, int, int, Tuple[int, ...]]]: def test_shape(line: Line,
path: Optional[str] = None,
index: int = 0) -> List[Tuple[int, int, int, Tuple[int, ...]]]:
pass pass
@@ -744,17 +799,21 @@ def sprite_map_set_limits(w: int, h: int) -> None:
pass pass
def set_send_sprite_to_gpu(func: Callable[[int, int, int, bytes], None]) -> None: def set_send_sprite_to_gpu(
func: Callable[[int, int, int, bytes], None]
) -> None:
pass pass
def set_font_data( def set_font_data(
box_drawing_func: Callable[[int, int, int, float], Tuple[int, Union[bytearray, bytes]]], box_drawing_func: Callable[[int, int, int, float],
prerender_func: Callable[[int, int, int, int, int, float, float, float, float], Tuple[int, ...]], Tuple[int, Union[bytearray, bytes]]],
descriptor_for_idx: Callable[[int], Tuple[dict, bool, bool]], prerender_func: Callable[
[int, int, int, int, int, float, float, float, float],
Tuple[int, ...]], descriptor_for_idx: Callable[[int], Tuple[dict, bool,
bool]],
bold: int, italic: int, bold_italic: int, num_symbol_fonts: int, bold: int, italic: int, bold_italic: int, num_symbol_fonts: int,
symbol_maps: Tuple[Tuple[int, int, int], ...], symbol_maps: Tuple[Tuple[int, int, int], ...], font_sz_in_pts: float,
font_sz_in_pts: float,
font_feature_settings: Dict[str, Tuple[bytes, ...]] font_feature_settings: Dict[str, Tuple[bytes, ...]]
): ):
pass pass
@@ -764,7 +823,8 @@ def get_fallback_font(text: str, bold: bool, italic: bool):
pass pass
def create_test_font_group(sz: float, dpix: float, dpiy: float) -> Tuple[int, int]: def create_test_font_group(sz: float, dpix: float,
dpiy: float) -> Tuple[int, int]:
pass pass
@@ -772,30 +832,35 @@ class Screen:
pass pass
def set_tab_bar_render_data(os_window_id: int, xstart: float, ystart: float, dx: float, dy: float, screen: Screen) -> None: def set_tab_bar_render_data(
os_window_id: int, xstart: float, ystart: float, dx: float, dy: float,
screen: Screen
) -> None:
pass pass
def set_window_render_data( def set_window_render_data(
os_window_id: int, tab_id: int, window_id: int, window_idx: int, os_window_id: int, tab_id: int, window_id: int, window_idx: int,
xstart: float, ystart: float, dx: float, dy: float, xstart: float, ystart: float, dx: float, dy: float, screen: Screen,
screen: Screen, left: int, top: int, right: int, bottom: int
left: int, top: int, right: int, bottom: int
): ):
pass pass
def truncate_point_for_length(text: str, num_cells: int, start_pos: int = 0) -> int: def truncate_point_for_length(
text: str, num_cells: int, start_pos: int = 0
) -> int:
pass pass
class ChildMonitor: class ChildMonitor:
def __init__( def __init__(
self, self,
death_notify: Callable[[int], None], death_notify: Callable[[int], None],
dump_callback: Optional[Callable], dump_callback: Optional[Callable],
talk_fd: int = -1, listen_fd: int = -1 talk_fd: int = -1,
listen_fd: int = -1
): ):
pass pass

View File

@@ -45,10 +45,14 @@ def all_fonts_map(monospaced=True):
def list_fonts() -> Generator[ListedFont, None, None]: def list_fonts() -> Generator[ListedFont, None, None]:
for fd in fc_list(): for fd in fc_list():
f = fd.get('family') f = fd.get('family')
if f: if f and isinstance(f, str):
fn = fd.get('full_name') or (f + ' ' + fd.get('style', '')).strip() fn_ = fd.get('full_name')
if fn_:
fn = str(fn_)
else:
fn = (f + ' ' + str(fd.get('style', ''))).strip()
is_mono = fd.get('spacing') in ('MONO', 'DUAL') is_mono = fd.get('spacing') in ('MONO', 'DUAL')
yield {'family': f, 'full_name': fn, 'postscript_name': fd.get('postscript_name', ''), 'is_monospace': is_mono} yield {'family': f, 'full_name': fn, 'postscript_name': str(fd.get('postscript_name', '')), 'is_monospace': is_mono}
def family_name_to_key(family): def family_name_to_key(family):

View File

@@ -5,7 +5,8 @@
import ctypes import ctypes
import sys import sys
from functools import partial from functools import partial
from math import ceil, pi, cos, floor from math import ceil, cos, floor, pi
from typing import Any, List, Tuple
from kitty.config import defaults from kitty.config import defaults
from kitty.constants import is_macos from kitty.constants import is_macos
@@ -22,7 +23,7 @@ if is_macos:
else: else:
from .fontconfig import get_font_files, font_for_family from .fontconfig import get_font_files, font_for_family
current_faces = None current_faces: List[Tuple[Any, bool, bool]] = []
def coalesce_symbol_maps(maps): def coalesce_symbol_maps(maps):

View File

@@ -11,10 +11,9 @@ from .terminfo import key_as_bytes, modify_key_bytes
from .utils import base64_encode from .utils import base64_encode
def modify_complex_key(name, amt): def modify_complex_key(name: Union[str, bytes], amt: int) -> bytes:
if not isinstance(name, bytes): q = name if isinstance(name, bytes) else key_as_bytes(name)
name = key_as_bytes(name) return modify_key_bytes(q, amt)
return modify_key_bytes(name, amt)
control_codes: Dict[int, Union[bytes, Tuple[int, ...]]] = { control_codes: Dict[int, Union[bytes, Tuple[int, ...]]] = {
@@ -48,7 +47,7 @@ SHIFTED_KEYS = {
control_alt_codes = { control_alt_codes = {
defines.GLFW_KEY_SPACE: b'\x1b\0', defines.GLFW_KEY_SPACE: b'\x1b\0',
} }
control_alt_shift_codes = {} control_alt_shift_codes: Dict[int, bytes] = {}
ASCII_C0_SHIFTED = { ASCII_C0_SHIFTED = {
# ^@ # ^@
'2': b'\x00', '2': b'\x00',

View File

@@ -5,7 +5,7 @@
from contextlib import suppress from contextlib import suppress
from typing import ( from typing import (
TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Generator, List, NoReturn, TYPE_CHECKING, Any, Callable, Dict, FrozenSet, Generator, List, NoReturn,
Optional, Tuple, Union Optional, Tuple, Type, Union, cast
) )
from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec from kitty.cli import get_defaults_from_seq, parse_args, parse_option_spec
@@ -42,12 +42,26 @@ class UnknownLayout(ValueError):
hide_traceback = True hide_traceback = True
class PayloadGetter:
def __init__(self, cmd: 'RemoteCommand', payload: Dict[str, Any]):
self.payload = payload
self.cmd = cmd
def __call__(self, key: str, opt_name: Optional[str] = None, missing: Any = None):
ans = self.payload.get(key, payload_get)
if ans is not payload_get:
return ans
return self.cmd.get_default(opt_name or key, missing=missing)
no_response = NoResponse() no_response = NoResponse()
payload_get = object()
ResponseType = Optional[Union[bool, str]] ResponseType = Optional[Union[bool, str]]
CmdReturnType = Union[Dict[str, Any], List, Tuple, str, int, float, bool] CmdReturnType = Union[Dict[str, Any], List, Tuple, str, int, float, bool]
CmdGenerator = Generator[CmdReturnType, None, None] CmdGenerator = Generator[CmdReturnType, None, None]
PayloadType = Optional[Union[CmdReturnType, CmdGenerator]] PayloadType = Optional[Union[CmdReturnType, CmdGenerator]]
PayloadGetType = Callable[[str, str], Any] PayloadGetType = PayloadGetter
ArgsType = List[str] ArgsType = List[str]
@@ -106,6 +120,7 @@ class RemoteCommand:
args_count: Optional[int] = None args_count: Optional[int] = None
args_completion: Optional[Dict[str, Tuple[str, Tuple[str, ...]]]] = None args_completion: Optional[Dict[str, Tuple[str, Tuple[str, ...]]]] = None
defaults: Optional[Dict[str, Any]] = None defaults: Optional[Dict[str, Any]] = None
options_class: Type = RCOptions
def __init__(self): def __init__(self):
self.desc = self.desc or self.short_desc self.desc = self.desc or self.short_desc
@@ -124,13 +139,6 @@ class RemoteCommand:
return self.defaults.get(name, missing) return self.defaults.get(name, missing)
return missing return missing
def payload_get(self, payload: Dict[str, Any], key: str, opt_name: Optional[str] = None) -> Any:
payload_get = object()
ans = payload.get(key, payload_get)
if ans is not payload_get:
return ans
return self.get_default(opt_name or key)
def message_to_kitty(self, global_opts: RCOptions, opts: Any, args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: Any, args: ArgsType) -> PayloadType:
raise NotImplementedError() raise NotImplementedError()
@@ -143,7 +151,7 @@ def cli_params_for(command: RemoteCommand) -> Tuple[Callable[[], str], str, str,
def parse_subcommand_cli(command: RemoteCommand, args: ArgsType) -> Tuple[Any, ArgsType]: def parse_subcommand_cli(command: RemoteCommand, args: ArgsType) -> Tuple[Any, ArgsType]:
opts, items = parse_args(args[1:], *cli_params_for(command)) opts, items = parse_args(args[1:], *cli_params_for(command), result_class=command.options_class)
if command.args_count is not None and command.args_count != len(items): if command.args_count is not None and command.args_count != len(items):
if command.args_count == 0: if command.args_count == 0:
raise SystemExit('Unknown extra argument(s) supplied to {}'.format(command.name)) raise SystemExit('Unknown extra argument(s) supplied to {}'.format(command.name))
@@ -163,17 +171,17 @@ def command_for_name(cmd_name: str) -> RemoteCommand:
m = import_module(f'kitty.rc.{cmd_name}') m = import_module(f'kitty.rc.{cmd_name}')
except ImportError: except ImportError:
raise KeyError(f'{cmd_name} is not a known kitty remote control command') raise KeyError(f'{cmd_name} is not a known kitty remote control command')
return getattr(m, cmd_name) return cast(RemoteCommand, getattr(m, cmd_name))
def all_command_names() -> FrozenSet[str]: def all_command_names() -> FrozenSet[str]:
try: try:
from importlib.resources import contents from importlib.resources import contents
except ImportError: except ImportError:
from importlib_resources import contents from importlib_resources import contents # type:ignore
def ok(name: str) -> bool: def ok(name: str) -> bool:
root, _, ext = name.rpartition('.') root, _, ext = name.rpartition('.')
return ext in ('py', 'pyc', 'pyo') and root and root not in ('base', '__init__') return bool(ext in ('py', 'pyc', 'pyo') and root and root not in ('base', '__init__'))
return frozenset({x.rpartition('.')[0] for x in filter(ok, contents('kitty.rc'))}) return frozenset({x.rpartition('.')[0] for x in filter(ok, contents('kitty.rc'))})

View File

@@ -39,7 +39,7 @@ If specified close the tab this command is run in, rather than the active tab.
if not tabs: if not tabs:
raise MatchError(match, 'tabs') raise MatchError(match, 'tabs')
else: else:
tabs = [boss.tab_for_window(window) if window and payload_get('self') else boss.active_tab] tabs = tuple(boss.tab_for_window(window) if window and payload_get('self') else boss.active_tab)
for tab in tabs: for tab in tabs:
if window: if window:
if tab: if tab:

View File

@@ -38,7 +38,7 @@ If specified close the window this command is run in, rather than the active win
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
else: else:
windows = [window if window and payload_get('self') else boss.active_window] windows = tuple(window if window and payload_get('self') else boss.active_window)
for window in windows: for window in windows:
if window: if window:
boss.close_window(window) boss.close_window(window)

View File

@@ -48,7 +48,7 @@ If specified apply marker to the window this command is run in, rather than the
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
else: else:
windows = [window if window and payload_get('self') else boss.active_window] windows = tuple(window if window and payload_get('self') else boss.active_window)
args = payload_get('marker_spec') args = payload_get('marker_spec')
for window in windows: for window in windows:

View File

@@ -43,7 +43,7 @@ If specified detach the tab this command is run in, rather than the active tab.
if not tabs: if not tabs:
raise MatchError(match) raise MatchError(match)
else: else:
tabs = [window.tabref() if payload_get('self') and window and window.tabref() else boss.active_tab] tabs = tuple(window.tabref() if payload_get('self') and window and window.tabref() else boss.active_tab)
match = payload_get('target_tab') match = payload_get('target_tab')
kwargs = {} kwargs = {}
if match: if match:

View File

@@ -45,7 +45,7 @@ If specified detach the window this command is run in, rather than the active wi
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
else: else:
windows = [window if window and payload_get('self') else boss.active_window] windows = tuple(window if window and payload_get('self') else boss.active_window)
match = payload_get('target_tab') match = payload_get('target_tab')
kwargs = {} kwargs = {}
if match: if match:

View File

@@ -42,7 +42,7 @@ using this option means that you will not be notified of failures.
if match: if match:
tabs = tuple(boss.match_tabs(match)) tabs = tuple(boss.match_tabs(match))
else: else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab] tabs = tuple(boss.tab_for_window(window) if window else boss.active_tab)
if not tabs: if not tabs:
raise MatchError(match, 'tabs') raise MatchError(match, 'tabs')
tab = tabs[0] tab = tabs[0]

View File

@@ -41,7 +41,7 @@ the command will exit with a success code.
windows = [window or boss.active_window] windows = [window or boss.active_window]
match = payload_get('match') match = payload_get('match')
if match: if match:
windows = tuple(boss.match_windows(match)) windows = [boss.match_windows(match)]
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
for window in windows: for window in windows:

View File

@@ -42,9 +42,9 @@ configured colors.
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType: def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
ans = {k: getattr(boss.opts, k) for k in boss.opts if isinstance(getattr(boss.opts, k), Color)} ans = {k: getattr(boss.opts, k) for k in boss.opts if isinstance(getattr(boss.opts, k), Color)}
if not payload_get('configured'): if not payload_get('configured'):
windows = (window or boss.active_window,) windows = [window or boss.active_window]
if payload_get('match'): if payload_get('match'):
windows = tuple(boss.match_windows(payload_get('match'))) windows = list(boss.match_windows(payload_get('match')))
if not windows: if not windows:
raise MatchError(payload_get('match')) raise MatchError(payload_get('match'))
ans.update({k: color_from_int(v) for k, v in windows[0].current_colors.items()}) ans.update({k: color_from_int(v) for k, v in windows[0].current_colors.items()})

View File

@@ -55,7 +55,7 @@ If specified get text from the window this command is run in, rather than the ac
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
else: else:
windows = [window if window and payload_get('self') else boss.active_window] windows = tuple(window if window and payload_get('self') else boss.active_window)
window = windows[0] window = windows[0]
if payload_get('extent') == 'selection': if payload_get('extent') == 'selection':
ans = window.text_for_selection() ans = window.text_for_selection()

View File

@@ -44,7 +44,7 @@ class GotoLayout(RemoteCommand):
if not tabs: if not tabs:
raise MatchError(match, 'tabs') raise MatchError(match, 'tabs')
else: else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab] tabs = tuple(boss.tab_for_window(window) if window else boss.active_tab)
for tab in tabs: for tab in tabs:
if tab: if tab:
try: try:

View File

@@ -40,7 +40,7 @@ class Kitten(RemoteCommand):
windows = [window or boss.active_window] windows = [window or boss.active_window]
match = payload_get('match') match = payload_get('match')
if match: if match:
windows = tuple(boss.match_windows(match)) windows = list(boss.match_windows(match))
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
for window in windows: for window in windows:

View File

@@ -39,7 +39,7 @@ class LastUsedLayout(RemoteCommand):
if not tabs: if not tabs:
raise MatchError(match, 'tabs') raise MatchError(match, 'tabs')
else: else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab] tabs = tuple(boss.tab_for_window(window) if window else boss.active_tab)
for tab in tabs: for tab in tabs:
if tab: if tab:
tab.last_used_layout() tab.last_used_layout()

View File

@@ -80,7 +80,7 @@ instead of the active tab
setattr(opts, key, val) setattr(opts, key, val)
match = payload_get('match') match = payload_get('match')
if match: if match:
tabs = tuple(boss.match_tabs(match)) tabs = list(boss.match_tabs(match))
if not tabs: if not tabs:
raise MatchError(match, 'tabs') raise MatchError(match, 'tabs')
else: else:
@@ -88,7 +88,7 @@ instead of the active tab
if payload_get('self') and window and window.tabref(): if payload_get('self') and window and window.tabref():
tabs = [window.tabref()] tabs = [window.tabref()]
tab = tabs[0] tab = tabs[0]
w = do_launch(boss, opts, payload_get('args') or None, target_tab=tab) w = do_launch(boss, opts, payload_get('args') or [], target_tab=tab)
return None if payload_get('no_response') else str(getattr(w, 'id', 0)) return None if payload_get('no_response') else str(getattr(w, 'id', 0))

View File

@@ -32,7 +32,8 @@ class NewWindow(RemoteCommand):
desc = ( desc = (
'Open a new window in the specified tab. If you use the :option:`kitty @ new-window --match` option' 'Open a new window in the specified tab. If you use the :option:`kitty @ new-window --match` option'
' the first matching tab is used. Otherwise the currently active tab is used.' ' the first matching tab is used. Otherwise the currently active tab is used.'
' Prints out the id of the newly opened window (unless :option:`--no-response` is used). Any command line arguments' ' Prints out the id of the newly opened window'
' (unless :option:`--no-response` is used). Any command line arguments'
' are assumed to be the command line used to run in the new window, if none' ' are assumed to be the command line used to run in the new window, if none'
' are provided, the default shell is run. For example:\n' ' are provided, the default shell is run. For example:\n'
':italic:`kitty @ new-window --title Email mutt`' ':italic:`kitty @ new-window --title Email mutt`'
@@ -109,7 +110,7 @@ the id of the new window will not be printed out.
match = payload_get('match') match = payload_get('match')
if match: if match:
tabs = tuple(boss.match_tabs(match)) tabs = list(boss.match_tabs(match))
if not tabs: if not tabs:
raise MatchError(match, 'tabs') raise MatchError(match, 'tabs')
else: else:

View File

@@ -39,7 +39,7 @@ If specified apply marker to the window this command is run in, rather than the
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
else: else:
windows = [window if window and payload_get('self') else boss.active_window] windows = tuple(window if window and payload_get('self') else boss.active_window)
for window in windows: for window in windows:
window.remove_marker() window.remove_marker()

View File

@@ -22,7 +22,10 @@ class ResizeWindow(RemoteCommand):
''' '''
short_desc = 'Resize the specified window' short_desc = 'Resize the specified window'
desc = 'Resize the specified window in the current layout. Note that not all layouts can resize all windows in all directions.' desc = (
'Resize the specified window in the current layout.'
' Note that not all layouts can resize all windows in all directions.'
)
options_spec = MATCH_WINDOW_OPTION + '''\n options_spec = MATCH_WINDOW_OPTION + '''\n
--increment -i --increment -i
type=int type=int
@@ -34,9 +37,10 @@ The number of cells to change the size by, can be negative to decrease the size.
type=choices type=choices
choices=horizontal,vertical,reset choices=horizontal,vertical,reset
default=horizontal default=horizontal
The axis along which to resize. If :italic:`horizontal`, it will make the window wider or narrower by the specified increment. The axis along which to resize. If :italic:`horizontal`,
If :italic:`vertical`, it will make the window taller or shorter by the specified increment. The special value :italic:`reset` will it will make the window wider or narrower by the specified increment.
reset the layout to its default configuration. If :italic:`vertical`, it will make the window taller or shorter by the specified increment.
The special value :italic:`reset` will reset the layout to its default configuration.
--self --self
@@ -56,7 +60,7 @@ If specified resize the window this command is run in, rather than the active wi
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
else: else:
windows = [window if window and payload_get('self') else boss.active_window] windows = tuple(window if window and payload_get('self') else boss.active_window)
resized = False resized = False
if windows and windows[0]: if windows and windows[0]:
resized = boss.resize_layout_window( resized = boss.resize_layout_window(

View File

@@ -3,7 +3,7 @@
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional, Tuple, Union
from .base import ( from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType, MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType,
@@ -35,23 +35,22 @@ class ScrollWindow(RemoteCommand):
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
amt = args[0] amt = args[0]
ans = {'match': opts.match} amount: Tuple[Union[str, int], Optional[str]] = amt, None
if amt in ('start', 'end'): if amt not in ('start', 'end'):
ans['amount'] = amt, None
else:
pages = 'p' in amt pages = 'p' in amt
amt = amt.replace('p', '') amt = amt.replace('p', '')
mult = -1 if amt.endswith('-') else 1 mult = -1 if amt.endswith('-') else 1
amt = int(amt.replace('-', '')) q = int(amt.replace('-', ''))
ans['amount'] = [amt * mult, 'p' if pages else 'l'] amount = q * mult, 'p' if pages else 'l'
return ans
return {'match': opts.match, 'amount': amount}
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType: def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = [window or boss.active_window] windows = [window or boss.active_window]
match = payload_get('match') match = payload_get('match')
amt = payload_get('amount') amt = payload_get('amount')
if match: if match:
windows = tuple(boss.match_windows(match)) windows = list(boss.match_windows(match))
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
for window in windows: for window in windows:

View File

@@ -4,7 +4,7 @@
import os import os
import sys import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Generator, Dict
from kitty.config import parse_send_text_bytes from kitty.config import parse_send_text_bytes
@@ -51,8 +51,7 @@ are sent as is, not interpreted for escapes.
limit = 1024 limit = 1024
ret = {'match': opts.match, 'is_binary': False, 'match_tab': opts.match_tab} ret = {'match': opts.match, 'is_binary': False, 'match_tab': opts.match_tab}
def pipe(): def pipe() -> Generator[Dict, None, None]:
ret['is_binary'] = True
if sys.stdin.isatty(): if sys.stdin.isatty():
import select import select
fd = sys.stdin.fileno() fd = sys.stdin.fileno()
@@ -64,28 +63,29 @@ are sent as is, not interpreted for escapes.
data = os.read(fd, limit) data = os.read(fd, limit)
if not data: if not data:
break # eof break # eof
data = data.decode('utf-8') decoded_data = data.decode('utf-8')
if '\x04' in data: if '\x04' in decoded_data:
data = data[:data.index('\x04')] decoded_data = decoded_data[:decoded_data.index('\x04')]
keep_going = False keep_going = False
ret['text'] = data ret['text'] = decoded_data
yield ret yield ret
else: else:
ret['is_binary'] = True
while True: while True:
data = sys.stdin.read(limit) data = sys.stdin.buffer.read(limit)
if not data: if not data:
break break
ret['text'] = data[:limit] ret['text'] = data[:limit]
yield ret yield ret
def chunks(text): def chunks(text: str) -> Generator[Dict, None, None]:
ret['is_binary'] = False ret['is_binary'] = False
while text: while text:
ret['text'] = text[:limit] ret['text'] = text[:limit]
yield ret yield ret
text = text[limit:] text = text[limit:]
def file_pipe(path): def file_pipe(path: str) -> Generator[Dict, None, None]:
ret['is_binary'] = True ret['is_binary'] = True
with open(path, encoding='utf-8') as f: with open(path, encoding='utf-8') as f:
while True: while True:
@@ -105,7 +105,7 @@ are sent as is, not interpreted for escapes.
text = ' '.join(args) text = ' '.join(args)
sources.append(chunks(text)) sources.append(chunks(text))
def chain(): def chain() -> Generator[Dict, None, None]:
for src in sources: for src in sources:
yield from src yield from src
return chain() return chain()
@@ -114,7 +114,7 @@ are sent as is, not interpreted for escapes.
windows = [boss.active_window] windows = [boss.active_window]
match = payload_get('match') match = payload_get('match')
if match: if match:
windows = tuple(boss.match_windows(match)) windows = list(boss.match_windows(match))
mt = payload_get('match_tab') mt = payload_get('match_tab')
if mt: if mt:
windows = [] windows = []
@@ -123,7 +123,8 @@ are sent as is, not interpreted for escapes.
raise MatchError(payload_get('match_tab'), 'tabs') raise MatchError(payload_get('match_tab'), 'tabs')
for tab in tabs: for tab in tabs:
windows += tuple(tab) windows += tuple(tab)
data = payload_get('text').encode('utf-8') if payload_get('is_binary') else parse_send_text_bytes(payload_get('text')) data = payload_get('text').encode('utf-8') if payload_get('is_binary') else parse_send_text_bytes(
payload_get('text'))
for window in windows: for window in windows:
if window is not None: if window is not None:
window.write_to_child(data) window.write_to_child(data)

View File

@@ -5,12 +5,12 @@
import imghdr import imghdr
import tempfile import tempfile
from base64 import standard_b64decode, standard_b64encode from base64 import standard_b64decode, standard_b64encode
from typing import TYPE_CHECKING, BinaryIO, Optional from typing import IO, TYPE_CHECKING, Dict, Generator, Optional
from uuid import uuid4 from uuid import uuid4
from .base import ( from .base import (
MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, PayloadType,
RCOptions, RemoteCommand, ResponseType, Window, no_response, RCOptions, RemoteCommand, ResponseType, Window,
windows_for_payload windows_for_payload
) )
@@ -59,20 +59,26 @@ How the image should be displayed. The value of configured will use the configur
args_count = 1 args_count = 1
args_completion = {'files': ('PNG Images', ('*.png',))} args_completion = {'files': ('PNG Images', ('*.png',))}
current_img_id: Optional[str] = None current_img_id: Optional[str] = None
current_file_obj: Optional[BinaryIO] = None current_file_obj: Optional[IO[bytes]] = None
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
if not args: if not args:
self.fatal('Must specify path to PNG image') self.fatal('Must specify path to PNG image')
path = args[0] path = args[0]
ret = {'match': opts.match, 'configured': opts.configured, 'layout': opts.layout, 'all': opts.all, 'img_id': str(uuid4())} ret = {
'match': opts.match,
'configured': opts.configured,
'layout': opts.layout,
'all': opts.all,
'img_id': str(uuid4())
}
if path.lower() == 'none': if path.lower() == 'none':
ret['data'] = '-' ret['data'] = '-'
return ret return ret
if imghdr.what(path) != 'png': if imghdr.what(path) != 'png':
self.fatal('{} is not a PNG image'.format(path)) self.fatal('{} is not a PNG image'.format(path))
def file_pipe(path): def file_pipe(path) -> Generator[Dict, None, None]:
with open(path, 'rb') as f: with open(path, 'rb') as f:
while True: while True:
data = f.read(512) data = f.read(512)
@@ -92,8 +98,9 @@ How the image should be displayed. The value of configured will use the configur
set_background_image.current_img_id = img_id set_background_image.current_img_id = img_id
set_background_image.current_file_obj = tempfile.NamedTemporaryFile() set_background_image.current_file_obj = tempfile.NamedTemporaryFile()
if data: if data:
assert set_background_image.current_file_obj is not None
set_background_image.current_file_obj.write(standard_b64decode(data)) set_background_image.current_file_obj.write(standard_b64decode(data))
return no_response return None
windows = windows_for_payload(boss, window, payload_get) windows = windows_for_payload(boss, window, payload_get)
os_windows = tuple({w.os_window_id for w in windows}) os_windows = tuple({w.os_window_id for w in windows})
@@ -101,6 +108,7 @@ How the image should be displayed. The value of configured will use the configur
if data == '-': if data == '-':
path = None path = None
else: else:
assert set_background_image.current_file_obj is not None
f = set_background_image.current_file_obj f = set_background_image.current_file_obj
path = f.name path = f.name
set_background_image.current_file_obj = None set_background_image.current_file_obj = None
@@ -109,7 +117,7 @@ How the image should be displayed. The value of configured will use the configur
try: try:
boss.set_background_image(path, os_windows, payload_get('configured'), layout) boss.set_background_image(path, os_windows, payload_get('configured'), layout)
except ValueError as err: except ValueError as err:
err.hide_traceback = True err.hide_traceback = True # type: ignore
raise raise

View File

@@ -4,7 +4,7 @@
import os import os
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Dict, Optional
from kitty.config import parse_config from kitty.config import parse_config
from kitty.fast_data_types import patch_color_profiles from kitty.fast_data_types import patch_color_profiles
@@ -34,7 +34,8 @@ class SetColors(RemoteCommand):
short_desc = 'Set terminal colors' short_desc = 'Set terminal colors'
desc = ( desc = (
'Set the terminal colors for the specified windows/tabs (defaults to active window). You can either specify the path to a conf file' 'Set the terminal colors for the specified windows/tabs (defaults to active window).'
' You can either specify the path to a conf file'
' (in the same format as kitty.conf) to read the colors from or you can specify individual colors,' ' (in the same format as kitty.conf) to read the colors from or you can specify individual colors,'
' for example: kitty @ set-colors foreground=red background=white' ' for example: kitty @ set-colors foreground=red background=white'
) )
@@ -59,21 +60,28 @@ this option, any color arguments are ignored and --configured and --all are impl
argspec = 'COLOR_OR_FILE ...' argspec = 'COLOR_OR_FILE ...'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
colors, cursor_text_color = {}, False final_colors: Dict[str, int] = {}
cursor_text_color: Optional[int] = None
if not opts.reset: if not opts.reset:
colors: Dict[str, Optional[Color]] = {}
for spec in args: for spec in args:
if '=' in spec: if '=' in spec:
colors.update(parse_config((spec.replace('=', ' '),))) colors.update(parse_config((spec.replace('=', ' '),)))
else: else:
with open(os.path.expanduser(spec), encoding='utf-8', errors='replace') as f: with open(os.path.expanduser(spec), encoding='utf-8', errors='replace') as f:
colors.update(parse_config(f)) colors.update(parse_config(f))
cursor_text_color = colors.pop('cursor_text_color', False) ctc = colors.pop('cursor_text_color')
colors = {k: color_as_int(v) for k, v in colors.items() if isinstance(v, Color)} if isinstance(ctc, Color):
return { cursor_text_color = color_as_int(ctc)
final_colors = {k: color_as_int(v) for k, v in colors.items() if isinstance(v, Color)}
ans = {
'match_window': opts.match, 'match_tab': opts.match_tab, 'match_window': opts.match, 'match_tab': opts.match_tab,
'all': opts.all or opts.reset, 'configured': opts.configured or opts.reset, 'all': opts.all or opts.reset, 'configured': opts.configured or opts.reset,
'colors': colors, 'reset': opts.reset, 'cursor_text_color': cursor_text_color 'colors': final_colors, 'reset': opts.reset
} }
if cursor_text_color is not None:
ans['cursor_text_color'] = cursor_text_color
return ans
def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType: def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType:
windows = windows_for_payload(boss, window, payload_get) windows = windows_for_payload(boss, window, payload_get)

View File

@@ -41,7 +41,7 @@ class SetTabTitle(RemoteCommand):
if not tabs: if not tabs:
raise MatchError(match, 'tabs') raise MatchError(match, 'tabs')
else: else:
tabs = [boss.tab_for_window(window) if window else boss.active_tab] tabs = tuple(boss.tab_for_window(window) if window else boss.active_tab)
for tab in tabs: for tab in tabs:
if tab: if tab:
tab.set_title(payload_get('title')) tab.set_title(payload_get('title'))

View File

@@ -44,7 +44,7 @@ want to allow other programs to change it afterwards, use this option.
windows = [window or boss.active_window] windows = [window or boss.active_window]
match = payload_get('match') match = payload_get('match')
if match: if match:
windows = tuple(boss.match_windows(match)) windows = list(boss.match_windows(match))
if not windows: if not windows:
raise MatchError(match) raise MatchError(match)
for window in windows: for window in windows:

View File

@@ -9,15 +9,15 @@ import sys
import types import types
from contextlib import suppress from contextlib import suppress
from functools import partial from functools import partial
from typing import Any, List, Optional from typing import List
from .cli import emph, parse_args from .cli import emph, parse_args
from .cli_stub import RCOptions from .cli_stub import RCOptions
from .constants import appname, version from .constants import appname, version
from .fast_data_types import read_command_response from .fast_data_types import read_command_response
from .rc.base import ( from .rc.base import (
all_command_names, command_for_name, no_response as no_response_sentinel, PayloadGetter, all_command_names, command_for_name,
parse_subcommand_cli no_response as no_response_sentinel, parse_subcommand_cli
) )
from .utils import TTYIO, parse_address_spec from .utils import TTYIO, parse_address_spec
@@ -33,11 +33,8 @@ def handle_cmd(boss, window, cmd):
c = command_for_name(cmd['cmd']) c = command_for_name(cmd['cmd'])
payload = cmd.get('payload') or {} payload = cmd.get('payload') or {}
def payload_get(key: str, opt_name: Optional[str] = None) -> Any:
return c.payload_get(payload, key, opt_name)
try: try:
ans = c.response_from_kitty(boss, window, payload_get) ans = c.response_from_kitty(boss, window, PayloadGetter(c, payload))
except Exception: except Exception:
if no_response: # don't report errors if --no-response was used if no_response: # don't report errors if --no-response was used
return return

View File

@@ -48,7 +48,7 @@ def cmd_names_matching(prefix):
@lru_cache() @lru_cache()
def options_for_cmd(cmd: str) -> Tuple[Tuple[str, ...], Dict[str, OptionDict]]: def options_for_cmd(cmd: str) -> Tuple[Tuple[str, ...], Dict[str, OptionDict]]:
alias_map = {} alias_map: Dict[str, OptionDict] = {}
try: try:
func = command_for_name(cmd) func = command_for_name(cmd)
except KeyError: except KeyError:

View File

@@ -7,15 +7,15 @@ from binascii import hexlify, unhexlify
from typing import cast, Dict from typing import cast, Dict
def modify_key_bytes(keybytes, amt): def modify_key_bytes(keybytes: bytes, amt: int) -> bytes:
if amt == 0: if amt == 0:
return keybytes return keybytes
ans = bytearray(keybytes) ans = bytearray(keybytes)
amt = str(amt).encode('ascii') samt = str(amt).encode('ascii')
if ans[-1] == ord('~'): if ans[-1] == ord('~'):
return bytes(ans[:-1] + bytearray(b';' + amt + b'~')) return bytes(ans[:-1] + bytearray(b';' + samt + b'~'))
if ans[1] == ord('O'): if ans[1] == ord('O'):
return bytes(ans[:1] + bytearray(b'[1;' + amt) + ans[-1:]) return bytes(ans[:1] + bytearray(b'[1;' + samt) + ans[-1:])
raise ValueError('Unknown key type in key: {!r}'.format(keybytes)) raise ValueError('Unknown key type in key: {!r}'.format(keybytes))
@@ -433,7 +433,7 @@ octal_escape = re.compile(r'\\([0-7]{3})')
escape_escape = re.compile(r'\\[eE]') escape_escape = re.compile(r'\\[eE]')
def key_as_bytes(name): def key_as_bytes(name: str) -> bytes:
ans = string_capabilities[name] ans = string_capabilities[name]
ans = octal_escape.sub(lambda m: chr(int(m.group(1), 8)), ans) ans = octal_escape.sub(lambda m: chr(int(m.group(1), 8)), ans)
ans = escape_escape.sub('\033', ans) ans = escape_escape.sub('\033', ans)

View File

@@ -9,6 +9,7 @@ import weakref
from collections import deque from collections import deque
from enum import IntEnum from enum import IntEnum
from itertools import chain from itertools import chain
from typing import List
from .config import build_ansi_color_table from .config import build_ansi_color_table
from .constants import ScreenGeometry, WindowGeometry, appname, wakeup from .constants import ScreenGeometry, WindowGeometry, appname, wakeup
@@ -483,7 +484,7 @@ class Window:
self.title_stack.append(self.child_title) self.title_stack.append(self.child_title)
# }}} # }}}
def text_for_selection(self): def text_for_selection(self) -> str:
lines = self.screen.text_for_selection() lines = self.screen.text_for_selection()
if self.opts.strip_trailing_spaces == 'always' or ( if self.opts.strip_trailing_spaces == 'always' or (
self.opts.strip_trailing_spaces == 'smart' and not self.screen.is_rectangle_select()): self.opts.strip_trailing_spaces == 'smart' and not self.screen.is_rectangle_select()):
@@ -497,8 +498,8 @@ class Window:
self.screen.reset_callbacks() self.screen.reset_callbacks()
self.screen = None self.screen = None
def as_text(self, as_ansi=False, add_history=False, add_wrap_markers=False, alternate_screen=False): def as_text(self, as_ansi=False, add_history=False, add_wrap_markers=False, alternate_screen=False) -> str:
lines = [] lines: List[str] = []
add_history = add_history and not (self.screen.is_using_alternate_linebuf() ^ alternate_screen) add_history = add_history and not (self.screen.is_using_alternate_linebuf() ^ alternate_screen)
if alternate_screen: if alternate_screen:
f = self.screen.as_text_alternate f = self.screen.as_text_alternate
@@ -506,7 +507,7 @@ class Window:
f = self.screen.as_text_non_visual if add_history else self.screen.as_text f = self.screen.as_text_non_visual if add_history else self.screen.as_text
f(lines.append, as_ansi, add_wrap_markers) f(lines.append, as_ansi, add_wrap_markers)
if add_history: if add_history:
h = [] h: List[str] = []
self.screen.historybuf.pagerhist_as_text(h.append) self.screen.historybuf.pagerhist_as_text(h.append)
if h and (not as_ansi or not add_wrap_markers): if h and (not as_ansi or not add_wrap_markers):
sanitizer = text_sanitizer(as_ansi, add_wrap_markers) sanitizer = text_sanitizer(as_ansi, add_wrap_markers)
@@ -517,7 +518,7 @@ class Window:
h[-1] += '\n' h[-1] += '\n'
if as_ansi: if as_ansi:
h[-1] += '\x1b[m' h[-1] += '\x1b[m'
lines = chain(h, lines) return ''.join(chain(h, lines))
return ''.join(lines) return ''.join(lines)
@property @property

View File

@@ -1,6 +1,6 @@
" Scan the following dirs recursively for tags " Scan the following dirs recursively for tags
let g:project_tags_dirs = ['kitty'] let g:project_tags_dirs = ['kitty', 'kittens']
let g:syntastic_python_flake8_exec = 'flake8' let g:syntastic_python_checkers = ['pylama']
let g:ycm_python_binary_path = 'python3' let g:ycm_python_binary_path = 'python3'
set wildignore+==template.py set wildignore+==template.py
set wildignore+=tags set wildignore+=tags

View File

@@ -13,8 +13,15 @@ blank_line_before_nested_class_or_def = True
combine_as_imports = True combine_as_imports = True
multi_line_output = 5 multi_line_output = 5
[mypy] [pylama]
files = kitty,kittens,glfw,*.py linters=mypy,pycodestyle,pyflakes
[pylama:pycodestyle]
max_line_length = 120
exclude==template.py,linux-package
[pylama:mypy]
files = kitty,kittens,glfw,*.py,docs/conf.py
no_implicit_optional = True no_implicit_optional = True
sqlite_cache = True sqlite_cache = True
cache_fine_grained = True cache_fine_grained = True
@@ -22,4 +29,17 @@ warn_redundant_casts = True
warn_unused_ignores = True warn_unused_ignores = True
warn_return_any = True warn_return_any = True
warn_unreachable = True warn_unreachable = True
warn_no_return = False
check_untyped_defs = True check_untyped_defs = True
[mypy]
files = kitty,kittens,glfw,*.py,docs/conf.py
no_implicit_optional = True
sqlite_cache = True
cache_fine_grained = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_return_any = True
warn_unreachable = True
warn_no_return = False
# check_untyped_defs = True

View File

@@ -741,7 +741,7 @@ def compile_python(base_path):
kw = {} kw = {}
compileall.compile_dir( compileall.compile_dir(
base_path, ddir='', force=True, optimize=optimize, quiet=1, base_path, ddir='', force=True, optimize=optimize, quiet=1,
workers=num_workers, **kw # type: ignore workers=num_workers, **kw
) )