Migrate type checker to ty

Much faster than mypy. Matches usage of ruff from same developer.
This commit is contained in:
Kovid Goyal
2026-07-04 09:16:05 +05:30
parent bb452f2066
commit a65b4c70a7
45 changed files with 218 additions and 238 deletions

View File

@@ -23,15 +23,15 @@ jobs:
cc: [gcc, clang]
include:
- python: a
pyver: "3.11"
pyver: "3.12"
sanitize: 0
- python: b
pyver: "3.12"
pyver: "3.13"
sanitize: 1
- python: c
pyver: "3.13"
pyver: "3.14"
sanitize: 1
@@ -106,7 +106,7 @@ jobs:
${{ runner.os }}-golang-static-
- name: Install build-only deps
run: python -m pip install -r docs/requirements.txt ruff mypy types-requests types-docutils types-Pygments
run: python -m pip install -r docs/requirements.txt ruff ty types-requests types-docutils types-Pygments
- name: Run ruff
run: ruff check .
@@ -120,8 +120,8 @@ jobs:
- name: Build kitty
run: python setup.py build --debug
- name: Run mypy
run: which python && python -m mypy --version && ./test.py mypy
- name: Run type checking
run: which python && ./test.py type-check
- name: Run go vet
run: go version && go vet -tags testing ./...

View File

@@ -1 +1 @@
to_vm_excludes '/dependencies /build /dist /kitty/launcher/kitty* /.build-cache /.cache /.mypy_cache /.ruff_cache /tags __pycache__ /*_commands.json *.so *.pyd *.pyc *_generated.go'
to_vm_excludes '/dependencies /build /dist /kitty/launcher/kitty* /.build-cache /.cache /.ruff_cache /tags __pycache__ /*_commands.json *.so *.pyd *.pyc *_generated.go'

View File

@@ -17,7 +17,7 @@ from functools import lru_cache, partial
from typing import Any, Callable, Dict, Iterable, Iterator, List, Tuple
from docutils import nodes
from docutils.parsers.rst.roles import set_classes
from docutils.parsers.rst.roles import normalize_options
from pygments.lexer import RegexLexer
from pygments.lexer import bygroups as untyped_bygroups
from pygments.token import Comment, Error, Keyword, Literal, Name, Number, String, Whitespace
@@ -221,7 +221,7 @@ def commit_role(
prb = inliner.problematic(rawtext, rawtext, msg)
return [prb], [msg]
url = f'https://github.com/kovidgoyal/kitty/commit/{commit_id}'
set_classes(options)
normalize_options(options)
short_id = subprocess.check_output(
f'git rev-list --max-count=1 --abbrev-commit {commit_id}'.split()).decode('utf-8').strip()
node = nodes.reference(rawtext, f'commit: {short_id}', refuri=url, **options)
@@ -498,7 +498,7 @@ def link_role(
return [prb], [msg]
text, url = m.group(1, 2)
url = url.replace(' ', '')
set_classes(options)
normalize_options(options)
node = nodes.reference(rawtext, text, refuri=url, **options)
return [node], []
@@ -672,7 +672,7 @@ def monkeypatch_man_writer() -> None:
th += "\n"
sh_tmpl: str = (".SH Name\n"
"%(ktitle)s \\- %(subtitle)s\n")
return th + sh_tmpl % di # type: ignore
return th + sh_tmpl % di
setattr(ManualPageTranslator, 'header', header)

View File

@@ -6,6 +6,7 @@ import os
import re
import subprocess
import sys
import types
from kitty.conf.generate import write_output
@@ -45,7 +46,7 @@ def main(args: list[str]=sys.argv) -> None:
all_colors = []
special_colors = []
for opt in definition.iter_all_options():
if callable(opt.parser_func):
if isinstance(opt.parser_func, types.FunctionType):
match opt.parser_func.__name__:
case 'to_color':
all_colors.append(opt.name)

View File

@@ -605,7 +605,7 @@ def load_ref_map() -> dict[str, dict[str, str]]:
raw = f.read()
raw = raw.split('{', 1)[1].split('}', 1)[0]
data = json.loads(bytes(bytearray(json.loads(f'[{raw}]'))))
return data # type: ignore
return data
def generate_constants() -> str:

View File

@@ -51,8 +51,8 @@ class Broadcast(Handler):
self.cmd.clear_to_end_of_screen()
self.line_edit.write(self.write, screen_cols=self.screen_size.cols)
def on_resize(self, screen_size: ScreenSize) -> None:
super().on_resize(screen_size)
def on_resize(self, new_size: ScreenSize) -> None:
super().on_resize(new_size)
self.commit_line()
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:

View File

@@ -297,7 +297,7 @@ def linenum_handle_result(args: list[str], data: dict[str, Any], target_window_i
def is_copy_action(s: str) -> bool:
return s in ('-', '@', '*') or s.startswith('@')
programs = list(filter(is_copy_action, data['programs'] or ()))
programs = list(filter(is_copy_action, data['programs'] or []))
# keep for backward compatibility, previously option `--program` does not need to be specified to perform copy actions
if is_copy_action(cmd[0]):
programs.append(cmd.pop(0))
@@ -412,7 +412,8 @@ def handle_result(args: list[str], data: dict[str, Any], target_window_id: int,
w = boss.window_id_map.get(target_window_id)
boss.call_remote_control(self_window=w, args=tuple(launch_args + ([m] if isinstance(m, str) else m)))
else:
boss.open_url(m, program, cwd=cwd)
if isinstance(m, str):
boss.open_url(m, program, cwd=cwd)
if __name__ == '__main__':

View File

@@ -102,7 +102,7 @@ class KittenUI:
raise ValueError('Remote control is not enabled, remember to use allow_remote_control=True')
prefix = [kitten_exe(), '@']
r = -1
pass_fds = list(kw.get('pass_fds') or ())
pass_fds = list(kw.get('pass_fds') or [])
try:
if self.rc_fd > -1:
pass_fds.append(self.rc_fd)
@@ -200,12 +200,13 @@ class Handler:
self.debug.fobj = self
self.initialize()
def __exit__(self, etype: type, value: Exception, tb: TracebackType) -> None:
def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, tb: TracebackType | None) -> bool | None:
del self.debug.fobj
with suppress(Exception):
self.finalize()
if self._image_manager is not None:
self._image_manager.__exit__(etype, value, tb)
self._image_manager.__exit__(exc_type, exc_val, tb)
return None
def initialize(self) -> None:
pass
@@ -213,8 +214,8 @@ class Handler:
def finalize(self) -> None:
pass
def on_resize(self, screen_size: ScreenSize) -> None:
self.screen_size = screen_size
def on_resize(self, new_size: ScreenSize) -> None:
self.screen_size = new_size
def quit_loop(self, return_code: int | None = None) -> None:
self._tui_loop.quit(return_code)

View File

@@ -10,7 +10,7 @@ from collections.abc import Callable, Iterator, Sequence
from contextlib import suppress
from enum import IntEnum
from itertools import count
from typing import Any, ClassVar, DefaultDict, Deque, Generic, Optional, TypeVar, Union, cast
from typing import Any, ClassVar, DefaultDict, Deque, Generic, Optional, TypeVar, Union, cast, final
from kitty.conf.utils import positive_float, positive_int
from kitty.fast_data_types import create_canvas
@@ -343,6 +343,7 @@ class Alias(Generic[T]):
self.name = Alias.currently_processing
@final
class GraphicsCommand:
a = action = Alias('t')
q = quiet = Alias(0)
@@ -467,12 +468,13 @@ class ImageManager:
gc.t = 'f'
self.handler.cmd.gr_command(gc, standard_b64encode(f.name.encode(fsenc)))
def __exit__(self, *a: Any) -> None:
def __exit__(self, *a: Any) -> bool | None:
import shutil
shutil.rmtree(self.tdir, ignore_errors=True)
self.handler.cmd.clear_images_on_screen(delete_data=True)
self.delete_all_sent_images()
del self.handler
return None
def delete_all_sent_images(self) -> None:
gc = GraphicsCommand()

View File

@@ -84,7 +84,7 @@ class PathCompleter:
import readline
from .dircolors import Dircolors
if 'libedit' in readline.__doc__:
if 'libedit' in getattr(readline, '__doc__', '') or '':
readline.parse_and_bind("bind -e")
readline.parse_and_bind("bind '\t' rl_complete")
else:

View File

@@ -2,7 +2,7 @@
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import inspect
from typing import NamedTuple
from typing import NamedTuple, cast
from .boss import Boss
from .tabs import Tab
@@ -50,7 +50,7 @@ def get_all_actions() -> dict[ActionGroup, list[Action]]:
short_help = first
long_help = '\n'.join(lines).strip()
assert spec.group in groups
return Action(getattr(x, '__name__'), spec.group, short_help, long_help)
return Action(getattr(x, '__name__'), cast(ActionGroup, spec.group), short_help, long_help)
seen = set()
for cls in (Window, Tab, Boss):

View File

@@ -22,6 +22,7 @@ from typing import (
Literal,
Optional,
Union,
cast,
)
from weakref import WeakValueDictionary
@@ -899,7 +900,8 @@ class Boss:
for x in payload:
c.response_from_kitty(self, self_window, PayloadGetter(c, x if isinstance(x, dict) else {}))
return None
return c.response_from_kitty(self, self_window, PayloadGetter(c, payload if isinstance(payload, dict) else {}))
pd = cast(dict[str, Any], payload if isinstance(payload, dict) else {})
return c.response_from_kitty(self, self_window, PayloadGetter(c, pd))
except Exception as e:
if silent:
log_error(f'Failed to run remote_control mapping: {aa} with error: {e}')
@@ -3169,7 +3171,7 @@ class Boss:
if theme_colors.has_applied_theme:
theme_colors.refresh()
if theme_colors.has_applied_theme: # in case the theme file was deleted
assert theme_colors.applied_theme # to make mypy happy
assert theme_colors.applied_theme # to make type check happy
theme_colors.apply_theme(theme_colors.applied_theme, notify_on_bg_change=False)
for w in self.all_windows:
if w.screen.color_profile.default_bg != bg_colors_before.get(w.id):
@@ -3302,7 +3304,8 @@ class Boss:
except (Exception, SystemExit) as err:
self.show_error('Failed to set colors', str(err))
return
c.response_from_kitty(self, self.window_for_dispatch or self.active_window, PayloadGetter(c, payload if isinstance(payload, dict) else {}))
pd = cast(dict[str, Any], payload if isinstance(payload, dict) else {})
c.response_from_kitty(self, self.window_for_dispatch or self.active_window, PayloadGetter(c, pd))
def _move_window_to(
self,

View File

@@ -4,6 +4,7 @@
import os
import re
import sys
import types
from collections.abc import Callable, Iterator, Sequence
from re import Match
from typing import Any, NoReturn, TypeVar, cast
@@ -134,6 +135,7 @@ role_map: dict[str, Callable[[str], str]] = {}
def role(func: Callable[[str], str]) -> Callable[[str], str]:
assert isinstance(func, types.FunctionType)
role_map[func.__name__] = func
return func

View File

@@ -102,7 +102,7 @@ class Clipboard:
def set_mime(self, data: Mapping[str, DataType]) -> None:
if self.enabled and isinstance(data, dict):
self.data = data
self.data = dict(data)
set_clipboard_data_types(self.clipboard_type, tuple(self.data))
def get_text(self) -> str:
@@ -147,7 +147,7 @@ class Clipboard:
def __call__(self, mime: str) -> Callable[[], bytes]:
data = self.data.get(mime, b'')
if isinstance(data, str):
data = data.encode('utf-8') # type: ignore
data = data.encode('utf-8')
if isinstance(data, bytes):
def chunker() -> bytes:
nonlocal data

View File

@@ -5,7 +5,7 @@ import os
from collections.abc import Iterable, Sequence
from contextlib import suppress
from enum import Enum
from typing import Any, Literal, Optional, TypedDict
from typing import Any, Literal, Optional, TypedDict, cast
from .config import parse_config
from .constants import config_dir
@@ -75,13 +75,14 @@ class ThemeColors:
def refresh(self) -> bool:
found = False
d: BackgroundImageOptions
with suppress(FileNotFoundError):
for x in os.scandir(config_dir):
if x.name == ThemeFile.dark.value:
mtime = x.stat().st_mtime_ns
if mtime > self.dark_mtime:
with open(x.path) as f:
d: BackgroundImageOptions = {}
d = {}
self.dark_spec, self.dark_tbc = self.parse_colors(f, d)
self.dark_background_image_options = d
self.dark_mtime = mtime
@@ -222,9 +223,10 @@ def parse_colors(
conf = parse_config(spec)
transparent_background_colors = conf.pop('transparent_background_colors', ())
if background_image_options is not None:
bio: dict[str, Any] = cast(dict[str, Any], background_image_options)
for key in BackgroundImageOptions.__optional_keys__:
if key in conf:
background_image_options.__setitem__(key, conf[key])
bio[key] = conf[key]
colors.update(conf)
for k in nullable_colors:
q = colors.pop(k, False)

View File

@@ -6,6 +6,7 @@ import inspect
import os
import re
import textwrap
import types
from collections.abc import Callable, Iterator
from typing import Any, get_type_hints
@@ -56,6 +57,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]:
def option_type_data(option: Option | MultiOption) -> tuple[Callable[[Any], Any], str]:
func = option.parser_func
if func.__module__ == 'builtins':
assert isinstance(func, types.FunctionType)
return func, func.__name__
th = get_type_hints(func)
rettype = th['return']
@@ -86,6 +88,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]:
if isinstance(option, MultiOption):
mval: dict[str, dict[str, Any]] = {'macos': {}, 'linux': {}, '': {}}
func, typ = option_type_data(option)
assert isinstance(func, types.FunctionType)
for val in option:
if val.add_to_default:
gr = mval[val.only]
@@ -114,6 +117,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]:
func = str
elif defn.has_color_table and option.is_color_table_color:
func, typ = option_type_data(option)
assert isinstance(func, types.FunctionType)
t(f' ans[{option.name!r}] = {func.__name__}(val)')
tc_imports.add((func.__module__, func.__name__))
cnum = int(option.name[5:])
@@ -128,6 +132,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]:
params = dict(inspect.signature(func).parameters)
except Exception:
params = {}
assert isinstance(func, types.FunctionType)
if 'dict_with_parse_results' in params:
t(f' {func.__name__}(val, ans)')
else:
@@ -163,6 +168,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]:
a(f' {option_name}: {typ} = ' '{}')
for parser, aliases in defn.deprecations.items():
assert isinstance(parser, types.FunctionType)
for alias in aliases:
parser_function_declaration(alias)
tc_imports.add((parser.__module__, parser.__name__))
@@ -183,6 +189,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]:
for aname, action in defn.actions.items():
option_names.add(aname)
action_parsers[aname] = func = action.parser_func
assert isinstance(func, types.FunctionType)
th = get_type_hints(func)
rettype = th['return']
typ = option_type_as_str(rettype)
@@ -487,6 +494,7 @@ def go_type_data(parser_func: ParserFuncType, ctype: str, is_multiple: bool = Fa
_, rsep, fsep = ctype.split('_', 2)
return 'map[string]string', f'config.ParseStrDict(val, `{rsep}`, `{fsep}`)'
return f'*{ctype}', f'Parse{ctype}(val)'
assert isinstance(parser_func, types.FunctionType)
p = parser_func.__name__
if p == 'int':
return 'int64', 'strconv.ParseInt(val, 10, 64)'

View File

@@ -558,7 +558,7 @@ def uniq(vals: Iterable[T]) -> list[T]:
def save_type_stub(text: str, fpath: str) -> None:
fpath += 'i'
preamble = '# Update this file by running: ./test.py mypy\n\n'
preamble = '# Update this file by running: ./test.py type-check\n\n'
try:
with open(fpath) as fs:
existing = fs.read()

View File

@@ -82,7 +82,7 @@ def finalize_keys(opts: Options, accumulate_bad_lines: list[BadLine] | None = No
defns: list[KeyDefinition] = []
for d in opts.map:
if d is None: # clear_all_shortcuts
defns = [] # type: ignore
defns = []
else:
try:
defns.append(d.resolve_and_copy(opts.kitty_mod))
@@ -122,7 +122,7 @@ def finalize_mouse_mappings(opts: Options, accumulate_bad_lines: list[BadLine] |
defns: list[MouseMapping] = []
for d in opts.mouse_map:
if d is None: # clear_all_mouse_actions
defns = [] # type: ignore
defns = []
else:
try:
defns.append(d.resolve_and_copy(opts.kitty_mod))

View File

@@ -233,27 +233,13 @@ def running_in_kitty(set_val: bool | None = None) -> bool:
def list_kitty_resources(package: str = 'kitty') -> Iterator[str]:
try:
if sys.version_info[:2] < (3, 10):
raise ImportError("importlib.resources.files() doesn't work with frozen builds on python 3.9")
from importlib.resources import files
except ImportError:
from importlib.resources import contents
return iter(contents(package))
else:
return (path.name for path in files(package).iterdir())
from importlib.resources import files
return (path.name for path in files(package).iterdir())
def read_kitty_resource(name: str, package_name: str = 'kitty') -> bytes:
try:
if sys.version_info[:2] < (3, 10):
raise ImportError("importlib.resources.files() doesn't work with frozen builds on python 3.9")
from importlib.resources import files
except ImportError:
from importlib.resources import read_binary
return read_binary(package_name, name)
else:
return (files(package_name) / name).read_bytes()
from importlib.resources import files
return (files(package_name) / name).read_bytes()
def website_url(doc_name: str = '', website: str = website_base_url) -> str:

View File

@@ -11,7 +11,7 @@ from collections.abc import Callable, Iterator, Sequence
from contextlib import suppress
from functools import partial
from pprint import pformat
from typing import IO, TypeVar
from typing import IO, TYPE_CHECKING, Protocol, cast
from kittens.tui.operations import colored, styled
@@ -24,12 +24,15 @@ from .options.types import Options as KittyOpts
from .options.types import defaults, secret_options
from .options.utils import KeyboardMode, KeyDefinition
from .rgb import color_as_sharp, color_from_int
from .types import MouseEvent, Shortcut, mod_to_names
from .types import Shortcut, mod_to_names
from .utils import shlex_split
AnyEvent = TypeVar('AnyEvent', MouseEvent, Shortcut)
Print = Callable[..., None]
ShortcutMap = dict[Shortcut, str]
if TYPE_CHECKING:
from .fast_data_types import FontFace
else:
FontFace = object
def green(x: str) -> str:
@@ -54,9 +57,13 @@ def print_mapping_changes(defns: dict[str, str], changes: set[str], text: str, p
for k in sorted(changes):
print_event(k, defns[k], print)
class AnyEvent(Protocol):
def human_repr(self, kitty_mod: int = 0) -> str: ...
def compare_maps(
final: dict[AnyEvent, str], final_kitty_mod: int, initial: dict[AnyEvent, str], initial_kitty_mod: int, print: Print, mode_name: str = ''
def compare_maps[T: AnyEvent](
final: dict[T, str], final_kitty_mod: int, initial: dict[T, str],
initial_kitty_mod: int, print: Print, mode_name: str = ''
) -> None:
ei = {k.human_repr(initial_kitty_mod): v for k, v in initial.items()}
ef = {k.human_repr(final_kitty_mod): v for k, v in final.items()}
@@ -308,7 +315,8 @@ def debug_config(opts: KittyOpts | None = None, global_shortcuts: dict[str, Sing
p(green('Fonts:'))
for k, font in current_fonts().items():
if hasattr(font, 'identify_for_debug'):
flines = font.identify_for_debug().splitlines()
face: FontFace = cast(FontFace, font)
flines = face.identify_for_debug().splitlines()
p(yellow(f'{k.rjust(8)}:'), flines[0])
for fl in flines[1:]:
p(' ' * 9, fl)

View File

@@ -1087,7 +1087,7 @@ class FileTransmission:
q = self.active_receives.get(receive_id)
if q is None:
return
ar = q # for mypy
ar = q # for type check
while ar.signature_pending_chunks:
if self.write_ftc_to_child(ar.signature_pending_chunks[0], use_pending=False):
ar.signature_pending_chunks.popleft()
@@ -1244,11 +1244,11 @@ class TestFileTransmission(FileTransmission):
self.test_responses.append(payload.asdict())
return True
def start_receive(self, aid: str) -> None:
self.handle_send_confirmation(self.allow, aid)
def start_receive(self, ar_id: str) -> None:
self.handle_send_confirmation(self.allow, ar_id)
def start_send(self, aid: str) -> None:
self.handle_receive_confirmation(self.allow, aid)
def start_send(self, asd_id: str) -> None:
self.handle_receive_confirmation(self.allow, asd_id)
def callback_after(self, callback: Callable[[int | None], None], timeout: float = 0) -> int | None:
callback(None)

View File

@@ -1,6 +1,6 @@
from collections.abc import Sequence
from enum import Enum, IntEnum, auto
from typing import TYPE_CHECKING, Literal, NamedTuple, TypedDict, TypeVar, Union
from typing import TYPE_CHECKING, Literal, NamedTuple, TypedDict, Union
from kitty.fast_data_types import ParsedFontFeature
from kitty.types import run_once
@@ -205,7 +205,6 @@ class FontSpec(NamedTuple):
Descriptor = Union[FontConfigPattern, CoreTextFont]
DescriptorVar = TypeVar('DescriptorVar', FontConfigPattern, CoreTextFont, Descriptor)
class Score(NamedTuple):
@@ -215,7 +214,7 @@ class Score(NamedTuple):
width_score: int
class Scorer:
class Scorer[T]:
def __init__(self, bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> None:
self.bold = bold
@@ -223,7 +222,7 @@ class Scorer:
self.monospaced = monospaced
self.prefer_variable = prefer_variable
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> list[DescriptorVar]:
def sorted_candidates(self, candidates: Sequence[T], dump: bool = False) -> list[T]:
raise NotImplementedError()
def __repr__(self) -> str:

View File

@@ -1,15 +1,16 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2024, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union
import copy
from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union, cast
from kitty.constants import is_macos
from kitty.fast_data_types import ParsedFontFeature
from kitty.fonts import Descriptor, DescriptorVar, DesignAxis, FontSpec, NamedStyle, Scorer, VariableAxis, VariableData, family_name_to_key
from kitty.fonts import Descriptor, DesignAxis, FontSpec, NamedStyle, Scorer, VariableAxis, VariableData, family_name_to_key
from kitty.options.types import Options
if TYPE_CHECKING:
from kitty.fast_data_types import CTFace
from kitty.fast_data_types import CoreTextFont, CTFace, FontConfigPattern
from kitty.fast_data_types import Face as FT_Face
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map']
@@ -139,9 +140,9 @@ def get_variable_data_for_face(d: Face) -> VariableData:
return ans
def find_best_match_in_candidates(
candidates: list[DescriptorVar], scorer: Scorer, is_medium_face: bool, ignore_face: DescriptorVar | None = None
) -> DescriptorVar | None:
def find_best_match_in_candidates[T](
candidates: list[T], scorer: Scorer[T], is_medium_face: bool, ignore_face: T | None = None
) -> T | None:
if candidates:
for x in scorer.sorted_candidates(candidates):
if ignore_face is None or x != ignore_face:
@@ -154,8 +155,8 @@ def pprint(*a: Any, **kw: Any) -> None:
pprint(*a, **kw)
def find_medium_variant(font: DescriptorVar) -> DescriptorVar:
font = font.copy()
def find_medium_variant[T: (CoreTextFont | FontConfigPattern)](font: T) -> T:
font = copy.copy(font)
vd = get_variable_data_for_descriptor(font)
for i, ns in enumerate(vd['named_styles']):
if ns['name'] == 'Regular':
@@ -452,7 +453,7 @@ def _get_named_style(axis_map: dict[str, float], vd: VariableData) -> NamedStyle
def get_named_style(face_or_descriptor: Face | Descriptor) -> NamedStyle | None:
if isinstance(face_or_descriptor, dict):
d: Descriptor = face_or_descriptor
d = cast(Descriptor, face_or_descriptor)
vd = get_variable_data_for_descriptor(d)
if d['descriptor_type'] == 'fontconfig':
ns = d.get('named_style', -1)
@@ -466,7 +467,7 @@ def get_named_style(face_or_descriptor: Face | Descriptor) -> NamedStyle | None:
else:
axis_map = d.get('axis_map', {}).copy()
else:
face: Face = face_or_descriptor
face = cast(Face, face_or_descriptor)
vd = get_variable_data_for_face(face)
q = face.get_variation()
if q is None:
@@ -479,7 +480,7 @@ def get_axis_map(face_or_descriptor: Face | Descriptor) -> dict[str, float]:
base_axis_map = {}
axis_map: dict[str, float] = {}
if isinstance(face_or_descriptor, dict):
d: Descriptor = face_or_descriptor
d = cast(Descriptor, face_or_descriptor)
vd = get_variable_data_for_descriptor(d)
if d['descriptor_type'] == 'fontconfig':
ns = d.get('named_style', -1)
@@ -494,7 +495,7 @@ def get_axis_map(face_or_descriptor: Face | Descriptor) -> dict[str, float]:
else:
axis_map = d.get('axis_map', {}).copy()
else:
face: Face = face_or_descriptor
face = cast(Face, face_or_descriptor)
q = face.get_variation()
if q is not None:
axis_map = q

View File

@@ -12,7 +12,7 @@ from kitty.fast_data_types import CTFace, coretext_all_fonts
from kitty.typing_compat import CoreTextFont
from kitty.utils import log_error
from . import Descriptor, DescriptorVar, ListedFont, Score, Scorer, VariableData, family_name_to_key
from . import Descriptor, ListedFont, Score, Scorer, VariableData, family_name_to_key
attr_map = {(False, False): 'font_family',
(True, False): 'bold_font',
@@ -113,7 +113,7 @@ def weight_range_for_family(family: str) -> WeightRange:
return WeightRange(mini, maxi, medium, bold)
class CTScorer(Scorer):
class CTScorer(Scorer[CoreTextFont]):
weight_range: WeightRange | None = None
def score(self, candidate: Descriptor) -> Score:
@@ -140,7 +140,7 @@ class CTScorer(Scorer):
is_regular_width = not candidate['expanded'] and not candidate['condensed']
return Score(variable_score, bold_score + italic_score, monospace_match, 0 if is_regular_width else 1)
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> list[DescriptorVar]:
def sorted_candidates(self, candidates: Sequence[CoreTextFont], dump: bool = False) -> list[CoreTextFont]:
self.weight_range = None
families = {x['family'] for x in candidates}
if len(families) == 1:

View File

@@ -22,7 +22,7 @@ from kitty.fast_data_types import (
from kitty.fast_data_types import fc_match as fc_match_impl
from kitty.typing_compat import FontConfigPattern
from . import Descriptor, DescriptorVar, ListedFont, Score, Scorer, VariableData, family_name_to_key
from . import Descriptor, ListedFont, Score, Scorer, VariableData, family_name_to_key
FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map']
FontMap = dict[FontCollectionMapType, dict[str, list[FontConfigPattern]]]
@@ -130,7 +130,7 @@ def weight_range_for_family(family: str) -> WeightRange:
class FCScorer(Scorer):
class FCScorer(Scorer[FontConfigPattern]):
weight_range: WeightRange | None = None
@@ -148,7 +148,7 @@ class FCScorer(Scorer):
width_score = abs(candidate['width'] - FC_WIDTH_NORMAL)
return Score(variable_score, bold_score / 1000 + italic_score / 110, monospace_match, width_score)
def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> list[DescriptorVar]:
def sorted_candidates(self, candidates: Sequence[FontConfigPattern], dump: bool = False) -> list[FontConfigPattern]:
self.weight_range = None
families = {x['family'] for x in candidates}
if len(families) == 1:

View File

@@ -100,7 +100,7 @@ def guess_type(path: str, allow_filesystem_access: bool = False) -> str | None:
mt = mt or is_special_file(path)
if not mt:
if is_dir:
mt = 'inode/directory' # type: ignore
mt = 'inode/directory'
elif is_exe:
mt = 'inode/executable'
return mt

View File

@@ -5,7 +5,7 @@ from collections.abc import Callable, Generator, Iterable, Iterator, Sequence
from enum import Enum
from functools import partial
from itertools import repeat
from typing import Any, ClassVar, NamedTuple
from typing import Any, ClassVar, NamedTuple, cast
from kitty.borders import BorderColor
from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE, Region, get_options, set_active_window, viewport_for_window
@@ -92,7 +92,8 @@ def calculate_cells_map(
number_of_windows: int, number_of_cells: int
) -> list[int]:
if isinstance(bias, dict):
bias = convert_bias_map(bias, number_of_windows, number_of_cells)
b: dict[int, float] = cast(dict[int, float], bias)
bias = convert_bias_map(b, number_of_windows, number_of_cells)
cells_per_window = number_of_cells // number_of_windows
if bias is not None and number_of_windows > 1 and number_of_windows == len(bias) and cells_per_window > 5:
cells_map = [int(b * number_of_cells) for b in bias]

View File

@@ -93,10 +93,10 @@ class Grid(Layout):
b[bias_idx] = increment
return tuple(self.variable_layout(layout_func, num_windows, b)) == before_layout
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
num_windows = all_windows.num_groups
ncols, nrows, special_rows, special_col = calc_grid_size(num_windows)
row_num, col_num = self.position_for_window_idx(idx, num_windows, ncols, nrows, special_rows, special_col)
row_num, col_num = self.position_for_window_idx(window_id, num_windows, ncols, nrows, special_rows, special_col)
if is_horizontal:
b = self.biased_cols
@@ -151,13 +151,13 @@ class Grid(Layout):
pos += rows
on_col_done(col_windows)
def do_layout(self, all_windows: WindowList) -> None:
n = all_windows.num_groups
def do_layout(self, windows: WindowList) -> None:
n = windows.num_groups
if n == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
self.layout_single_window_group(next(windows.iter_all_layoutable_groups()))
return
ncols, nrows, special_rows, special_col = calc_grid_size(n)
groups = tuple(all_windows.iter_all_layoutable_groups())
groups = tuple(windows.iter_all_layoutable_groups())
win_col_map: list[list[WindowGroup]] = []
def on_col_done(col_windows: list[int]) -> None:
@@ -193,17 +193,17 @@ class Grid(Layout):
n, nrows, ncols, special_rows, special_col, on_col_done):
position_window_in_grid_cell(window_idx, xl, yl)
def minimal_borders(self, all_windows: WindowList) -> Iterator[BorderLine]:
n = all_windows.num_groups
def minimal_borders(self, windows: WindowList) -> Iterator[BorderLine]:
n = windows.num_groups
if not lgd.draw_minimal_borders or n < 2:
return
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
needs_borders_map = windows.compute_needs_borders_map(lgd.draw_active_borders)
ncols, nrows, special_rows, special_col = calc_grid_size(n)
is_first_row: set[int] = set()
is_last_row: set[int] = set()
is_first_column: set[int] = set()
is_last_column: set[int] = set()
groups = tuple(all_windows.iter_all_layoutable_groups())
groups = tuple(windows.iter_all_layoutable_groups())
bw = groups[0].effective_border()
if not bw:
return
@@ -227,7 +227,7 @@ class Grid(Layout):
all_groups_in_order.append(wg)
layout_data_map[wg.id] = xl, yl
is_last_column = {groups[x].id for x in prev_col_windows}
active_group = all_windows.active_group
active_group = windows.active_group
def ends(yl: LayoutData) -> tuple[int, int]:
return yl.content_pos - yl.space_before, yl.content_pos + yl.content_size + yl.space_after
@@ -258,17 +258,17 @@ class Grid(Layout):
wid = wg.active_window_id
yield from borders_for_window(wg.id, color, wid)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
n = all_windows.num_groups
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
n = windows.num_groups
if n < 4:
return neighbors_for_tall_window(1, window, all_windows)
return neighbors_for_tall_window(1, window, windows)
wg = all_windows.group_for_window(window)
wg = windows.group_for_window(window)
assert wg is not None
ncols, nrows, special_rows, special_col = calc_grid_size(n)
blank_row: list[int | None] = [None for i in range(ncols)]
matrix = tuple(blank_row[:] for j in range(max(nrows, special_rows)))
wi = all_windows.iter_all_layoutable_groups()
wi = windows.iter_all_layoutable_groups()
pos_map: dict[int, tuple[int, int]] = {}
col_counts: list[int] = []
for col in range(ncols):

View File

@@ -576,7 +576,7 @@ class SplitsLayoutOpts(LayoutOpts):
class Splits(Layout):
name = 'splits'
needs_all_windows = True
layout_opts = SplitsLayoutOpts({})
layout_opts: SplitsLayoutOpts = SplitsLayoutOpts({})
no_minimal_window_borders = True
drag_overlay_mode = DragOverlayMode.free
@@ -608,8 +608,8 @@ class Splits(Layout):
if isinstance(q, Pair):
self.pairs_root = q
def do_layout(self, all_windows: WindowList) -> None:
groups = tuple(all_windows.iter_all_layoutable_groups())
def do_layout(self, windows: WindowList) -> None:
groups = tuple(windows.iter_all_layoutable_groups())
root = self.pairs_root
all_present_group_ids = {g.id for g in groups}
already_placed_group_ids = frozenset(root.all_window_ids())
@@ -700,19 +700,19 @@ class Splits(Layout):
return self.equalize_biases()
return False
def minimal_borders(self, all_windows: WindowList) -> Iterator[BorderLine]:
groups = tuple(all_windows.iter_all_layoutable_groups())
def minimal_borders(self, windows: WindowList) -> Iterator[BorderLine]:
groups = tuple(windows.iter_all_layoutable_groups())
window_count = len(groups)
if not lgd.draw_minimal_borders or window_count < 2:
return
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
ag = all_windows.active_group
needs_borders_map = windows.compute_needs_borders_map(lgd.draw_active_borders)
ag = windows.active_group
active_group_id = -1 if ag is None else ag.id
border_color_map = {}
for grp_id, needs_borders in needs_borders_map.items():
if needs_borders:
wid = g.active_window_id if (g := all_windows.group_for_id(grp_id)) else 0
wid = g.active_window_id if (g := windows.group_for_id(grp_id)) else 0
if wid:
color = BorderColor.active if grp_id is active_group_id else BorderColor.bell
border_color_map[wid] = color
@@ -723,13 +723,13 @@ class Splits(Layout):
for bb in which:
yield bb._replace(color=border_color_map.get(abs(bb.window_id), BorderColor.inactive))
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
wg = all_windows.group_for_window(window)
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
wg = windows.group_for_window(window)
assert wg is not None
pair = self.pairs_root.pair_for_window(wg.id)
ans: NeighborsMap = {}
if pair is not None:
pair.neighbors_for_window(wg.id, ans, self, all_windows)
pair.neighbors_for_window(wg.id, ans, self, windows)
return ans
def move_window(self, all_windows: WindowList, delta: int = 1) -> bool:
@@ -886,9 +886,9 @@ class Splits(Layout):
return None
def drag_resize_window(self, all_windows: WindowList, pair_id: int, increment: float, is_horizontal: bool = True) -> bool:
def drag_resize_window(self, all_windows: WindowList, window_id: int, increment: float, is_horizontal: bool = True) -> bool:
for pair in self.pairs_root.self_and_descendants():
if id(pair) == pair_id:
if id(pair) == window_id:
new_bias = max(0, min(pair.bias + increment, 1))
if new_bias != pair.bias:
pair.bias = new_bias

View File

@@ -14,15 +14,15 @@ class Stack(Layout):
needs_window_borders = False
only_active_window_visible = True
def do_layout(self, all_windows: WindowList) -> None:
active_group = all_windows.active_group
for group in all_windows.iter_all_layoutable_groups():
def do_layout(self, windows: WindowList) -> None:
active_group = windows.active_group
for group in windows.iter_all_layoutable_groups():
self.layout_single_window_group(group, add_blank_rects=group is active_group)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
wg = all_windows.group_for_window(window)
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
wg = windows.group_for_window(window)
assert wg is not None
groups = tuple(all_windows.iter_all_layoutable_groups())
groups = tuple(windows.iter_all_layoutable_groups())
idx = groups.index(wg)
before = [] if wg is groups[0] else [groups[idx-1].id]
after = [] if wg is groups[-1] else [groups[idx+1].id]

View File

@@ -138,7 +138,7 @@ class Tall(Layout):
main_is_horizontal = True
no_minimal_window_borders = True
drag_overlay_mode = DragOverlayMode.axis_y
layout_opts = TallLayoutOpts({})
layout_opts: TallLayoutOpts = TallLayoutOpts({})
main_axis_layout = Layout.xlayout
perp_axis_layout = Layout.ylayout
@@ -167,12 +167,12 @@ class Tall(Layout):
after_layout = tuple(self.variable_layout(all_windows, self.biased_map))
return before_layout == after_layout
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
num_windows = all_windows.num_groups
if self.main_is_horizontal == is_horizontal:
before_main_bias = self.main_bias
ncols = self.num_full_size_windows + 1
biased_col = idx if idx < self.num_full_size_windows else (ncols - 1)
biased_col = window_id if window_id < self.num_full_size_windows else (ncols - 1)
self.main_bias = [
safe_increment_bias(self.main_bias[i], increment * (1 if i == biased_col else -1)) for i in range(ncols)
]
@@ -180,13 +180,13 @@ class Tall(Layout):
return self.main_bias != before_main_bias
num_of_short_windows = num_windows - self.num_full_size_windows
if idx < self.num_full_size_windows or num_of_short_windows < 2:
if window_id < self.num_full_size_windows or num_of_short_windows < 2:
return False
idx -= self.num_full_size_windows
window_id -= self.num_full_size_windows
before_layout = tuple(self.variable_layout(all_windows, self.biased_map))
before = self.biased_map.get(idx, 0.)
before = self.biased_map.get(window_id, 0.)
candidate = self.biased_map.copy()
candidate[idx] = after = before + increment
candidate[window_id] = after = before + increment
if before_layout == tuple(self.variable_layout(all_windows, candidate)):
return False
self.biased_map = candidate
@@ -251,12 +251,12 @@ class Tall(Layout):
xl, yl = yl, xl
yield wg, xl, yl, False
def do_layout(self, all_windows: WindowList) -> None:
num = all_windows.num_groups
def do_layout(self, windows: WindowList) -> None:
num = windows.num_groups
if num == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
self.layout_single_window_group(next(windows.iter_all_layoutable_groups()))
return
layouts = (self.simple_layout if num <= self.num_full_size_windows + 1 else self.full_layout)(all_windows)
layouts = (self.simple_layout if num <= self.num_full_size_windows + 1 else self.full_layout)(windows)
for wg, xl, yl, is_full_size in layouts:
self.set_window_group_geometry(wg, xl, yl)
@@ -303,25 +303,25 @@ class Tall(Layout):
return False
return None
def minimal_borders(self, all_windows: WindowList) -> Iterator[BorderLine]:
num = all_windows.num_groups
def minimal_borders(self, windows: WindowList) -> Iterator[BorderLine]:
num = windows.num_groups
if num < 2 or not lgd.draw_minimal_borders:
return
try:
bw = next(all_windows.iter_all_layoutable_groups()).effective_border()
bw = next(windows.iter_all_layoutable_groups()).effective_border()
except StopIteration:
bw = 0
if not bw:
return
if num <= self.num_full_size_windows + 1:
layout = (x[:3] for x in self.simple_layout(all_windows))
yield from borders(layout, self.main_is_horizontal, all_windows)
layout = (x[:3] for x in self.simple_layout(windows))
yield from borders(layout, self.main_is_horizontal, windows)
return
main_layouts: list[tuple[WindowGroup, LayoutData, LayoutData]] = []
perp_borders: list[BorderLine] = []
layouts = (self.simple_layout if num <= self.num_full_size_windows else self.full_layout)(all_windows)
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
active_group = all_windows.active_group
layouts = (self.simple_layout if num <= self.num_full_size_windows else self.full_layout)(windows)
needs_borders_map = windows.compute_needs_borders_map(lgd.draw_active_borders)
active_group = windows.active_group
mirrored = self.layout_opts.mirrored
for wg, xl, yl, is_full_size in layouts:
if is_full_size:
@@ -356,7 +356,7 @@ class Tall(Layout):
mirrored = self.layout_opts.mirrored
yield from borders(
main_layouts, self.main_is_horizontal, all_windows,
main_layouts, self.main_is_horizontal, windows,
start_offset=int(not mirrored), end_offset=int(mirrored)
)
yield from perp_borders[1:-1]

View File

@@ -80,7 +80,7 @@ class Vertical(Layout):
self.biased_map: dict[int, float] = {}
return True
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
if self.main_is_horizontal != is_horizontal:
return False
num_windows = all_windows.num_groups
@@ -88,8 +88,8 @@ class Vertical(Layout):
return False
before_layout = list(self.variable_layout(all_windows, self.biased_map))
candidate = self.biased_map.copy()
before = candidate.get(idx, 0)
candidate[idx] = before + increment
before = candidate.get(window_id, 0)
candidate[window_id] = before + increment
if before_layout == list(self.variable_layout(all_windows, candidate)):
return False
self.biased_map = candidate
@@ -109,24 +109,24 @@ class Vertical(Layout):
xl, yl = yl, xl
yield wg, xl, yl
def do_layout(self, all_windows: WindowList) -> None:
window_count = all_windows.num_groups
def do_layout(self, windows: WindowList) -> None:
window_count = windows.num_groups
if window_count == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
self.layout_single_window_group(next(windows.iter_all_layoutable_groups()))
return
for wg, xl, yl in self.generate_layout_data(all_windows):
for wg, xl, yl in self.generate_layout_data(windows):
self.set_window_group_geometry(wg, xl, yl)
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
window_count = all_windows.num_groups
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
window_count = windows.num_groups
if window_count < 2 or not lgd.draw_minimal_borders:
return
yield from borders(self.generate_layout_data(all_windows), self.main_is_horizontal, all_windows)
yield from borders(self.generate_layout_data(windows), self.main_is_horizontal, windows)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
wg = all_windows.group_for_window(window)
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
wg = windows.group_for_window(window)
assert wg is not None
groups = tuple(all_windows.iter_all_layoutable_groups())
groups = tuple(windows.iter_all_layoutable_groups())
idx = groups.index(wg)
lg = len(groups)
if lg > 1:

View File

@@ -606,7 +606,6 @@ def kitty_main(called_from_panel: bool = False) -> None:
def main(called_from_panel: bool = False) -> None:
global redirected_for_quick_access
try:
if is_macos and launched_by_launch_services and not called_from_panel:
with suppress(OSError):

View File

@@ -6,31 +6,17 @@
import os
import sys
from collections.abc import Callable, Sequence
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import context, get_all_start_methods, get_context, spawn, util
from typing import TYPE_CHECKING, Any, Union
from typing import Any
from .constants import kitty_exe
orig_spawn_passfds = util.spawnv_passfds
orig_executable = spawn.get_executable()
if TYPE_CHECKING:
from collections.abc import Buffer
from typing import SupportsIndex, SupportsInt
if sys.version_info[:2] >= (3, 14):
ArgsType = Sequence[Union[str, Buffer, SupportsInt, SupportsIndex]]
else:
from _typeshed import ReadableBuffer, SupportsTrunc
ArgsType = Sequence[Union[str, ReadableBuffer, SupportsInt, SupportsIndex, SupportsTrunc]]
else:
ArgsType = Sequence[str]
def spawnv_passfds(path: bytes, args: ArgsType, passfds: Sequence[int]) -> int:
def spawnv_passfds(path: bytes, args: list[str], passfds: Sequence[int]) -> int:
if '-c' in args:
idx = args.index('-c')
patched_args = [spawn.get_executable(), '+runpy'] + list(args)[idx + 1:]
@@ -45,7 +31,7 @@ def spawnv_passfds(path: bytes, args: ArgsType, passfds: Sequence[int]) -> int:
def monkey_patch_multiprocessing() -> None:
# Use kitty to run the worker process used by multiprocessing
spawn.set_executable(kitty_exe())
util.spawnv_passfds = spawnv_passfds
util.spawnv_passfds = spawnv_passfds # type: ignore
def unmonkey_patch_multiprocessing() -> None:

View File

@@ -456,8 +456,9 @@ class NotificationCommand:
from .search_query_parser import search
def get_matches(location: str, query: str, candidates: set['NotificationCommand']) -> set['NotificationCommand']:
return {x for x in candidates if x.matches_rule_item(location, query)}
universal_set: set['NotificationCommand'] = {self}
try:
return self in search(rule, ('title', 'body', 'app', 'type'), {self}, get_matches)
return self in search(rule, ('title', 'body', 'app', 'type'), universal_set, get_matches)
except Exception as e:
self.log(f'Ignoring invalid filter_notification rule: {rule} with error: {e}')
return False
@@ -992,7 +993,7 @@ class NotificationManager:
self.send_closed_response(channel_id, cmd.identifier, untracked=True)
return desktop_notification_id
def expire_notification(self, desktop_notification_id: int, command_id: int, timer_id: int) -> None:
def expire_notification(self, desktop_notification_id: int, command_id: int, timer_id: int | None) -> None:
if n := self.in_progress_notification_commands.get(desktop_notification_id):
if id(n) == command_id:
self.desktop_integration.close_notification(desktop_notification_id)

View File

@@ -681,7 +681,7 @@ class Options:
tab_separator: str = ''
tab_switch_strategy: choices_for_tab_switch_strategy = 'previous'
tab_title_max_length: int = 0
tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{tab.last_focused_progress_percent}{title}'
tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{secure_input_symbol}{fmt.fg.tab}{tab.last_focused_progress_percent}{title}'
term: str = 'xterm-kitty'
terminfo_type: choices_for_terminfo_type = 'path'
text_composition_strategy: str = 'platform'

View File

@@ -2,7 +2,7 @@
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal
from .base import MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window
@@ -60,13 +60,14 @@ using this option means that you will not be notified of failures.
return {'match': opts.match, 'amount': amount, 'self': True}
def response_from_kitty(self, boss: Boss, window: Window | None, payload_get: PayloadGetType) -> ResponseType:
amt = payload_get('amount')
vamt: tuple[int | float | Literal['start', 'end'], str] = payload_get('amount')
for window in self.windows_for_match_payload(boss, window, payload_get):
if window:
if amt[0] in ('start', 'end'):
(window.scroll_home if amt[0] == 'start' else window.scroll_end)()
if vamt[0] in ('start', 'end'):
(window.scroll_home if vamt[0] == 'start' else window.scroll_end)()
else:
amt, unit = amt
amt, unit = vamt
assert isinstance(amt, (int, float))
match unit:
case 'u':
window.screen.reverse_scroll(int(abs(amt)), True)
@@ -78,6 +79,7 @@ using this option means that you will not be notified of failures.
if not isinstance(amt, int) and not amt.is_integer():
amt = round(window.screen.lines * amt)
unit = 'line'
assert isinstance(amt, int)
direction = 'up' if amt < 0 else 'down'
func = getattr(window, f'scroll_{unit}_{direction}')
for i in range(int(abs(amt))):

View File

@@ -3,7 +3,7 @@
from collections.abc import Iterable
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, LiteralString
from .base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window
@@ -36,7 +36,7 @@ def patch_configured_edges(opts: 'Options', s: dict[str, float | None]) -> None:
def parse_spacing_settings(args: Iterable[str]) -> dict[str, float | None]:
mapper: dict[str, list[str]] = {}
mapper: dict[str, list[LiteralString]] = {}
for q in ('margin', 'padding'):
mapper[q] = f'{q}-left {q}-top {q}-right {q}-bottom'.split()
mapper[f'{q}-h'] = mapper[f'{q}-horizontal'] = f'{q}-left {q}-right'.split()

View File

@@ -42,14 +42,15 @@ again. If you want to allow other programs to change it afterwards, use this opt
return ans
def response_from_kitty(self, boss: Boss, window: Window | None, payload_get: PayloadGetType) -> ResponseType:
title = payload_get('title')
title: str | None = payload_get('title')
btitle: memoryview | None = None
if payload_get('temporary') and title is not None:
title = memoryview(title.encode('utf-8'))
btitle = memoryview(title.encode('utf-8'))
for window in self.windows_for_match_payload(boss, window, payload_get):
if window:
if payload_get('temporary'):
window.override_title = None
window.title_changed(title)
window.title_changed(btitle)
else:
window.set_title(title)
return None

View File

@@ -5,7 +5,7 @@ from collections.abc import Callable, Iterator, Sequence
from enum import Enum
from functools import lru_cache
from gettext import gettext as _
from typing import NamedTuple, TypeVar
from typing import NamedTuple, TypeVar, cast
from .types import run_once
@@ -121,13 +121,13 @@ class Token(NamedTuple):
@run_once
def lex_scanner() -> Callable[[str], tuple[list[Token], str]]:
return getattr(re, 'Scanner')([ # type: ignore
return cast(Callable[[str], tuple[list[Token], str]], getattr(re, 'Scanner')([
(r'[()]', lambda x, t: Token(TokenType.OPCODE, t)),
(r'@.+?:[^")\s]+', lambda x, t: Token(TokenType.WORD, str(t))),
(r'[^"()\s]+', lambda x, t: Token(TokenType.WORD, str(t))),
(r'".*?((?<!\\)")', lambda x, t: Token(TokenType.QUOTED_WORD, t[1:-1])),
(r'\s+', None)
], flags=re.DOTALL).scan
], flags=re.DOTALL).scan)
@run_once

View File

@@ -123,9 +123,9 @@ class Session:
def add_window(self, cmd: None | str | list[str], expand: Callable[[str], str] = lambda x: x) -> None:
from .launch import parse_launch_args
needs_expandvars = False
if isinstance(cmd, str) and cmd:
if isinstance(cmd, str):
needs_expandvars = True
cmd = list(shlex_split(cmd))
cmd = list(shlex_split(cmd)) if cmd else []
serialize_data: dict[str, Any] = {'id': 0, 'cmd_at_shell_startup': ()}
if cmd and cmd[0].startswith(unserialize_launch_flag):
serialize_data = json.loads(cmd[0][len(unserialize_launch_flag):])

View File

@@ -4,7 +4,7 @@
from collections.abc import Callable, Iterator, Mapping, Sequence
from enum import Enum
from functools import update_wrapper
from typing import TYPE_CHECKING, Any, Generic, NamedTuple, TypedDict, TypeVar, Union
from typing import TYPE_CHECKING, Any, Generic, Literal, NamedTuple, TypedDict, TypeVar, Union
if TYPE_CHECKING:
from kitty.fast_data_types import SingleKey
@@ -213,11 +213,7 @@ def modmap() -> dict[str, int]:
'caps_lock': GLFW_MOD_CAPS_LOCK, 'num_lock': GLFW_MOD_NUM_LOCK}
if TYPE_CHECKING:
from typing import Literal
ActionGroup = Literal['cp', 'sc', 'win', 'tab', 'fs', 'mouse', 'mk', 'lay', 'misc', 'debug', 'session']
else:
ActionGroup = str
ActionGroup = Literal['cp', 'sc', 'win', 'tab', 'fs', 'mouse', 'mk', 'lay', 'misc', 'debug', 'session']
class ActionSpec(NamedTuple):

View File

@@ -88,11 +88,6 @@ def filter_tests_by_module(suite: unittest.TestSuite, *names: str) -> unittest.T
return filter_tests(suite, q)
@lru_cache
def python_for_type_check() -> str:
return shutil.which('python') or shutil.which('python3') or 'python'
def type_check() -> NoReturn:
from kitty.cli_stub import generate_stub # type:ignore
@@ -100,8 +95,7 @@ def type_check() -> NoReturn:
from kittens.tui.operations_stub import generate_stub # type: ignore
generate_stub()
py = python_for_type_check()
os.execlp(py, py, '-m', 'mypy', '--pretty')
os.execlp('ty', 'ty', 'check')
def run_cli(suite: unittest.TestSuite, verbosity: int = 4) -> bool:
@@ -309,11 +303,9 @@ def env_for_python_tests(report_env: bool = False) -> Iterator[None]:
path = os.pathsep.join(x for x in paths if not x.startswith(current_home))
launcher_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty', 'launcher')
path = f'{launcher_dir}{os.pathsep}{path}'
python_for_type_check()
print('Running under CI:', BaseTest.is_ci)
if report_env:
print('Using PATH in test environment:', path)
print('Python:', python_for_type_check())
from kitty.fast_data_types import has_avx2, has_sse4_2
print(f'Intrinsics: {has_avx2=} {has_sse4_2=}')
# we need fonts installed in the user home directory as well, so initialize

View File

@@ -1,5 +1,5 @@
[project]
requires-python = ">=3.11"
requires-python = ">=3.12"
license = "GPL-3.0-only"
[build-system]
@@ -10,30 +10,15 @@ requires = [
"packaging == 23.1",
]
[tool.mypy]
files = 'kitty,kittens,glfw,*.py,docs/conf.py,gen'
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_unused_configs = true
check_untyped_defs = true
disallow_untyped_defs = true
disallow_untyped_decorators = true
disallow_untyped_calls = true
disallow_incomplete_defs = true
strict = true
strict_bytes = true
no_implicit_reexport = true
[tool.pylsp-mypy]
enabled = true
dmypy = true
exclude = ['kitty_tests/*']
report_progress = true
[tool.ty.src]
include = [
"kitty",
"kittens",
"glfw",
"*.py",
"docs/conf.py",
"gen"
]
[tool.ruff]
line-length = 160

View File

@@ -1369,8 +1369,11 @@ def read_bool_options(path: str = 'kitty/cli.py') -> Tuple[str, ...]:
def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 'source') -> str:
werror = '' if args.ignore_compiler_warnings else '-pedantic-errors -Werror'
cflags = f'-Wall {werror} -fpie {c_std}'.strip().split()
cflags = ['-Wall']
if not args.ignore_compiler_warnings:
cflags.extend(('-pedantic-errors', '-Werror'))
if c_std:
cflags.append(c_std)
cppflags = [define(f'WRAPPED_KITTENS=" {wrapped_kittens()} "')]
ldflags = shlex.split(os.environ.get('LDFLAGS', ''))
xxhash = xxhash_flags()