From 149ae2866aa75caf2568c503b9991453ea558b43 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Mar 2020 08:10:51 +0530 Subject: [PATCH] more typing work --- docs/conf.py | 6 +- docs/kittens/custom.rst | 4 +- kittens/ask/main.py | 50 ++++++----- kittens/choose/main.py | 49 +++++----- kittens/choose/subseq_matcher.pyi | 6 +- kittens/clipboard/main.py | 23 ++--- kittens/diff/search.py | 20 +++-- kittens/hints/main.py | 119 +++++++++++++----------- kittens/icat/main.py | 113 ++++++++++++++--------- kittens/key_demo/main.py | 16 ++-- kittens/panel/main.py | 31 +++---- kittens/resize_window/main.py | 29 +++--- kittens/runner.py | 27 +++--- kittens/show_error/main.py | 9 +- kittens/ssh/main.py | 4 +- kittens/tui/handler.py | 2 +- kittens/unicode_input/main.py | 144 +++++++++++++++++------------- kitty/boss.py | 5 +- kitty/cli.py | 2 + kitty/config.py | 22 +++++ kitty/rc/set_colors.py | 3 +- setup.cfg | 2 +- 22 files changed, 395 insertions(+), 291 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 030220c46..479b678a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -594,10 +594,10 @@ def write_conf_docs(app, all_kitten_names): def setup(app): os.makedirs('generated/conf', exist_ok=True) from kittens.runner import all_kitten_names - all_kitten_names = all_kitten_names() - write_cli_docs(all_kitten_names) + kn = all_kitten_names() + write_cli_docs(kn) write_remote_control_protocol_docs() - write_conf_docs(app, all_kitten_names) + write_conf_docs(app, kn) app.add_lexer('session', SessionLexer()) app.add_role('link', link_role) app.add_role('iss', partial(num_role, 'issues')) diff --git a/docs/kittens/custom.rst b/docs/kittens/custom.rst index ace34079f..76315968e 100644 --- a/docs/kittens/custom.rst +++ b/docs/kittens/custom.rst @@ -20,7 +20,7 @@ your machine). .. code-block:: python - def main(args): + def main(args: List[str]) -> str: # this is the main entry point of the kitten, it will be executed in # the overlay window when the kitten is launched answer = input('Enter some text: ') @@ -28,7 +28,7 @@ your machine). # handle_result() function return answer - def handle_result(args, answer, target_window_id, boss): + def handle_result(args: List[str], answer: str, target_window_id: int, boss: kitty.boss.Boss) -> None: # get the kitty window into which to paste answer w = boss.window_id_map.get(target_window_id) if w is not None: diff --git a/kittens/ask/main.py b/kittens/ask/main.py index 5133d7b30..666c18e25 100644 --- a/kittens/ask/main.py +++ b/kittens/ask/main.py @@ -4,7 +4,7 @@ import os from contextlib import suppress -from typing import TYPE_CHECKING, Dict, List, Union +from typing import TYPE_CHECKING, List, Optional, Tuple from kitty.cli import parse_args from kitty.cli_stub import AskCLIOptions @@ -15,22 +15,23 @@ from ..tui.operations import alternate_screen, styled if TYPE_CHECKING: import readline + import kitty else: readline = None -def get_history_items(): +def get_history_items() -> List[str]: return list(map(readline.get_history_item, range(1, readline.get_current_history_length() + 1))) -def sort_key(item): +def sort_key(item: str) -> Tuple[int, str]: return len(item), item.lower() class HistoryCompleter: - def __init__(self, name=None): - self.matches = [] + def __init__(self, name: Optional[str] = None): + self.matches: List[str] = [] self.history_path = None if name: ddir = os.path.join(cache_dir(), 'ask') @@ -38,7 +39,7 @@ class HistoryCompleter: os.makedirs(ddir) self.history_path = os.path.join(ddir, name) - def complete(self, text, state): + def complete(self, text: str, state: int) -> Optional[str]: response = None if state == 0: history_values = get_history_items() @@ -53,19 +54,19 @@ class HistoryCompleter: response = None return response - def __enter__(self): + def __enter__(self) -> 'HistoryCompleter': if self.history_path: with suppress(Exception): readline.read_history_file(self.history_path) readline.set_completer(self.complete) return self - def __exit__(self, *a): + def __exit__(self, *a: object) -> None: if self.history_path: readline.write_history_file(self.history_path) -def option_text(): +def option_text() -> str: return '''\ --type -t choices=line @@ -84,7 +85,18 @@ be used for completions and via the browse history readline bindings. ''' -def main(args): +try: + from typing import TypedDict +except ImportError: + TypedDict = dict + + +class Response(TypedDict): + items: List[str] + response: Optional[str] + + +def main(args: List[str]) -> Response: # For some reason importing readline in a key handler in the main kitty process # causes a crash of the python interpreter, probably because of some global # lock @@ -94,7 +106,7 @@ def main(args): from kitty.shell import init_readline msg = 'Ask the user for input' try: - args, items = parse_args(args[1:], option_text, '', msg, 'kitty ask', result_class=AskCLIOptions) + cli_opts, items = parse_args(args[1:], option_text, '', msg, 'kitty ask', result_class=AskCLIOptions) except SystemExit as e: if e.code != 0: print(e.args[0]) @@ -104,23 +116,19 @@ def main(args): init_readline(readline) response = None - with alternate_screen(), HistoryCompleter(args.name): - if args.message: - print(styled(args.message, bold=True)) + with alternate_screen(), HistoryCompleter(cli_opts.name): + if cli_opts.message: + print(styled(cli_opts.message, bold=True)) prompt = '> ' with suppress(KeyboardInterrupt, EOFError): response = input(prompt) - if response is None: - ans: Dict[str, Union[str, List[str]]] = {'items': items} - else: - ans = {'items': items, 'response': response} - return ans + return {'items': items, 'response': response} @result_handler() -def handle_result(args, data, target_window_id, boss): - if 'response' in data: +def handle_result(args: List[str], data: Response, target_window_id: int, boss: 'kitty.boss.Boss') -> None: + if data['response'] is not None: func, *args = data['items'] getattr(boss, func)(data['response'], *args) diff --git a/kittens/choose/main.py b/kittens/choose/main.py index 117f54e90..094cdd314 100644 --- a/kittens/choose/main.py +++ b/kittens/choose/main.py @@ -3,39 +3,40 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal import sys +from typing import Iterable, List, Union +from kitty.key_encoding import KeyEvent +from . import subseq_matcher from ..tui.handler import Handler from ..tui.loop import Loop -from . import subseq_matcher - def match( - input_data, - query, - threads=0, - positions=False, - level1='/', - level2='-_0123456789', - level3='.', - limit=0, - mark_before='', - mark_after='', - delimiter='\n' -): + input_data: Union[str, bytes, Iterable[Union[str, bytes]]], + query: str, + threads: int = 0, + positions: bool = False, + level1: str = '/', + level2: str = '-_0123456789', + level3: str = '.', + limit: int = 0, + mark_before: str = '', + mark_after: str = '', + delimiter: str = '\n' +) -> List[str]: if isinstance(input_data, str): - input_data = input_data.encode('utf-8') + idata = [x.encode('utf-8') for x in input_data.split(delimiter)] if isinstance(input_data, bytes): - input_data = input_data.split(delimiter.encode('utf-8')) + idata = input_data.split(delimiter.encode('utf-8')) else: - input_data = [x.encode('utf-8') if isinstance(x, str) else x for x in input_data] + idata = [x.encode('utf-8') if isinstance(x, str) else x for x in input_data] query = query.lower() level1 = level1.lower() level2 = level2.lower() level3 = level3.lower() data = subseq_matcher.match( - input_data, (level1, level2, level3), query, + idata, (level1, level2, level3), query, positions, limit, threads, mark_before, mark_after, delimiter) if data is None: @@ -45,23 +46,23 @@ def match( class ChooseHandler(Handler): - def initialize(self): + def initialize(self) -> None: pass - def on_text(self, text, in_bracketed_paste=False): + def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: pass - def on_key(self, key_event): + def on_key(self, key_event: KeyEvent) -> None: pass - def on_interrupt(self): + def on_interrupt(self) -> None: self.quit_loop(1) - def on_eot(self): + def on_eot(self) -> None: self.quit_loop(1) -def main(args): +def main(args: List[str]) -> None: loop = Loop() handler = ChooseHandler() loop.loop(handler) diff --git a/kittens/choose/subseq_matcher.pyi b/kittens/choose/subseq_matcher.pyi index 281ea7b30..8ee24f324 100644 --- a/kittens/choose/subseq_matcher.pyi +++ b/kittens/choose/subseq_matcher.pyi @@ -1,9 +1,9 @@ -from typing import List, Tuple +from typing import List, Optional, Tuple def match( - lines: List[str], levels: Tuple[str, str, str], needle: str, + lines: List[bytes], levels: Tuple[str, str, str], needle: str, output_positions: bool, limit: int, num_threads: int, mark_before: str, mark_after: str, delimiter: str -): +) -> Optional[str]: pass diff --git a/kittens/clipboard/main.py b/kittens/clipboard/main.py index 777fd1007..57dc2db66 100644 --- a/kittens/clipboard/main.py +++ b/kittens/clipboard/main.py @@ -4,6 +4,7 @@ import os import sys +from typing import List, NoReturn, Optional from kitty.cli import parse_args from kitty.cli_stub import ClipboardCLIOptions @@ -14,12 +15,12 @@ from ..tui.loop import Loop class Clipboard(Handler): - def __init__(self, data_to_send, args): + def __init__(self, data_to_send: Optional[bytes], args: ClipboardCLIOptions): self.args = args - self.clipboard_contents = None + self.clipboard_contents: Optional[str] = None self.data_to_send = data_to_send - def initialize(self): + def initialize(self) -> None: if self.data_to_send is not None: self.cmd.write_to_clipboard(self.data_to_send, self.args.use_primary) if not self.args.get_clipboard: @@ -33,17 +34,17 @@ class Clipboard(Handler): return self.cmd.request_from_clipboard(self.args.use_primary) - def on_clipboard_response(self, text, from_primary=False): + def on_clipboard_response(self, text: str, from_primary: bool = False) -> None: self.clipboard_contents = text self.quit_loop(0) - def on_capability_response(self, name, val): + def on_capability_response(self, name: str, val: str) -> None: self.quit_loop(0) - def on_interrupt(self): + def on_interrupt(self) -> None: self.quit_loop(1) - def on_eot(self): + def on_eot(self) -> None: self.quit_loop(1) @@ -81,16 +82,16 @@ To set the clipboard text, pipe in the new text on stdin. Use the usage = '' -def main(args): - args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard', result_class=ClipboardCLIOptions) +def main(args: List[str]) -> NoReturn: + cli_opts, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard', result_class=ClipboardCLIOptions) if items: raise SystemExit('Unrecognized extra command line arguments') - data = None + data: Optional[bytes] = None if not sys.stdin.isatty(): data = sys.stdin.buffer.read() sys.stdin = open(os.ctermid(), 'r') loop = Loop() - handler = Clipboard(data, args) + handler = Clipboard(data, cli_opts) loop.loop(handler) if loop.return_code == 0 and handler.clipboard_contents: sys.stdout.write(handler.clipboard_contents) diff --git a/kittens/diff/search.py b/kittens/diff/search.py index 17968b3ba..85432d6c5 100644 --- a/kittens/diff/search.py +++ b/kittens/diff/search.py @@ -3,11 +3,17 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal import re +from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Tuple from kitty.fast_data_types import wcswidth +from kitty.options_stub import DiffOptions from ..tui.operations import styled +if TYPE_CHECKING: + from .render import Line + Line + class BadRegex(ValueError): pass @@ -15,8 +21,8 @@ class BadRegex(ValueError): class Search: - def __init__(self, opts, query, is_regex, is_backward): - self.matches = {} + def __init__(self, opts: DiffOptions, query: str, is_regex: bool, is_backward: bool): + self.matches: Dict[int, List[Tuple[int, str]]] = {} self.count = 0 self.style = styled('|', fg=opts.search_fg, bg=opts.search_bg).split('|', 1)[0] if not is_regex: @@ -26,7 +32,7 @@ class Search: except Exception: raise BadRegex('Not a valid regex: {}'.format(query)) - def __call__(self, diff_lines, margin_size, cols): + def __call__(self, diff_lines: Iterable['Line'], margin_size: int, cols: int) -> bool: self.matches = {} self.count = 0 half_width = cols // 2 @@ -38,7 +44,7 @@ class Search: left, right = text[margin_size:half_width + 1], text[right_offset:] matches = [] - def add(which, offset): + def add(which: str, offset: int) -> None: for m in find(which): before = which[:m.start()] matches.append((wcswidth(before) + offset, m.group())) @@ -50,13 +56,13 @@ class Search: self.matches[i] = matches return bool(self.matches) - def __contains__(self, i): + def __contains__(self, i: int) -> bool: return i in self.matches - def __len__(self): + def __len__(self) -> int: return self.count - def highlight_line(self, write, line_num): + def highlight_line(self, write: Callable[[str], None], line_num: int) -> bool: highlights = self.matches.get(line_num) if not highlights: return False diff --git a/kittens/hints/main.py b/kittens/hints/main.py index 17e75d9af..4520c9657 100644 --- a/kittens/hints/main.py +++ b/kittens/hints/main.py @@ -9,26 +9,36 @@ import sys from functools import lru_cache from gettext import gettext as _ from itertools import repeat -from typing import Callable, Dict, List, Optional, Tuple +from typing import ( + TYPE_CHECKING, Any, Callable, Dict, Generator, Iterable, List, Optional, + Pattern, Sequence, Set, Tuple, Type, cast +) from kitty.cli import parse_args from kitty.cli_stub import HintsCLIOptions from kitty.fast_data_types import set_clipboard_string -from kitty.key_encoding import backspace_key, enter_key, key_defs as K -from kitty.utils import screen_size_function +from kitty.key_encoding import ( + KeyEvent, backspace_key, enter_key, key_defs as K +) +from kitty.utils import ScreenSize, screen_size_function from ..tui.handler import Handler, result_handler from ..tui.loop import Loop from ..tui.operations import faint, styled +if TYPE_CHECKING: + from kitty.config import KittyCommonOpts + from kitty.boss import Boss + @lru_cache() -def kitty_common_opts(): +def kitty_common_opts() -> 'KittyCommonOpts': import json v = os.environ.get('KITTY_COMMON_OPTS') if v: - return json.loads(v) - return {} + return cast(KittyCommonOpts, json.loads(v)) + from kitty.config import common_opts_as_dict + return common_opts_as_dict() DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase @@ -41,14 +51,14 @@ class Mark: __slots__ = ('index', 'start', 'end', 'text', 'groupdict') - def __init__(self, index, start, end, text, groupdict): + def __init__(self, index: int, start: int, end: int, text: str, groupdict: Any): self.index, self.start, self.end = index, start, end self.text = text self.groupdict = groupdict @lru_cache(maxsize=2048) -def encode_hint(num, alphabet): +def encode_hint(num: int, alphabet: str) -> str: res = '' d = len(alphabet) while not res or num > 0: @@ -57,7 +67,7 @@ def encode_hint(num, alphabet): return res -def decode_hint(x, alphabet=DEFAULT_HINT_ALPHABET): +def decode_hint(x: str, alphabet: str = DEFAULT_HINT_ALPHABET) -> int: base = len(alphabet) index_map = {c: i for i, c in enumerate(alphabet)} i = 0 @@ -66,7 +76,7 @@ def decode_hint(x, alphabet=DEFAULT_HINT_ALPHABET): return i -def highlight_mark(m, text, current_input, alphabet): +def highlight_mark(m: Mark, text: str, current_input: str, alphabet: str) -> str: hint = encode_hint(m.index, alphabet) if current_input and not hint.startswith(current_input): return faint(text) @@ -82,7 +92,7 @@ def highlight_mark(m, text, current_input, alphabet): ) -def render(text, current_input, all_marks, ignore_mark_indices, alphabet): +def render(text: str, current_input: str, all_marks: Sequence[Mark], ignore_mark_indices: Set[int], alphabet: str) -> str: for mark in reversed(all_marks): if mark.index in ignore_mark_indices: continue @@ -96,47 +106,47 @@ def render(text, current_input, all_marks, ignore_mark_indices, alphabet): class Hints(Handler): - def __init__(self, text, all_marks, index_map, args): + def __init__(self, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], args: HintsCLIOptions): self.text, self.index_map = text, index_map self.alphabet = args.alphabet or DEFAULT_HINT_ALPHABET self.all_marks = all_marks - self.ignore_mark_indices = set() + self.ignore_mark_indices: Set[int] = set() self.args = args self.window_title = _('Choose URL') if args.type == 'url' else _('Choose text') self.multiple = args.multiple self.match_suffix = self.get_match_suffix(args) - self.chosen = [] + self.chosen: List[Mark] = [] self.reset() @property - def text_matches(self): + def text_matches(self) -> List[str]: return [m.text + self.match_suffix for m in self.chosen] @property - def groupdicts(self): + def groupdicts(self) -> List[Any]: return [m.groupdict for m in self.chosen] - def get_match_suffix(self, args): + def get_match_suffix(self, args: HintsCLIOptions) -> str: if args.add_trailing_space == 'always': return ' ' if args.add_trailing_space == 'never': return '' return ' ' if args.multiple else '' - def reset(self): + def reset(self) -> None: self.current_input = '' - self.current_text = None + self.current_text: Optional[str] = None - def init_terminal_state(self): + def init_terminal_state(self) -> None: self.cmd.set_cursor_visible(False) self.cmd.set_window_title(self.window_title) self.cmd.set_line_wrapping(False) - def initialize(self): + def initialize(self) -> None: self.init_terminal_state() self.draw_screen() - def on_text(self, text, in_bracketed_paste): + def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: changed = False for c in text: if c in self.alphabet: @@ -158,7 +168,7 @@ class Hints(Handler): self.current_text = None self.draw_screen() - def on_key(self, key_event): + def on_key(self, key_event: KeyEvent) -> None: if key_event is backspace_key: self.current_input = self.current_input[:-1] self.current_text = None @@ -181,23 +191,23 @@ class Hints(Handler): elif key_event.key is ESCAPE: self.quit_loop(0 if self.multiple else 1) - def on_interrupt(self): + def on_interrupt(self) -> None: self.quit_loop(1) - def on_eot(self): + def on_eot(self) -> None: self.quit_loop(1) - def on_resize(self, new_size): + def on_resize(self, new_size: ScreenSize) -> None: self.draw_screen() - def draw_screen(self): + def draw_screen(self) -> None: if self.current_text is None: self.current_text = render(self.text, self.current_input, self.all_marks, self.ignore_mark_indices, self.alphabet) self.cmd.clear_screen() self.write(self.current_text) -def regex_finditer(pat, minimum_match_length, text): +def regex_finditer(pat: Pattern, minimum_match_length: int, text: str) -> Generator[Tuple[int, int, Dict], None, None]: has_named_groups = bool(pat.groupindex) for m in pat.finditer(text): s, e = m.span(0 if has_named_groups else pat.groups) @@ -209,16 +219,17 @@ def regex_finditer(pat, minimum_match_length, text): closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"} opening_brackets = ''.join(closing_bracket_map) -postprocessor_map: Dict[str, Callable[[str, int, int], Tuple[int, int]]] = {} +PostprocessorFunc = Callable[[str, int, int], Tuple[int, int]] +postprocessor_map: Dict[str, PostprocessorFunc] = {} -def postprocessor(func): +def postprocessor(func: PostprocessorFunc) -> PostprocessorFunc: postprocessor_map[func.__name__] = func return func @postprocessor -def url(text, s, e): +def url(text: str, s: int, e: int) -> Tuple[int, int]: if s > 4 and text[s - 5:s] == 'link:': # asciidoc URLs url = text[s:e] idx = url.rfind('[') @@ -240,7 +251,7 @@ def url(text, s, e): @postprocessor -def brackets(text, s, e): +def brackets(text: str, s: int, e: int) -> Tuple[int, int]: # Remove matching brackets if s < e <= len(text): before = text[s] @@ -251,7 +262,7 @@ def brackets(text, s, e): @postprocessor -def quotes(text, s, e): +def quotes(text: str, s: int, e: int) -> Tuple[int, int]: # Remove matching quotes if s < e <= len(text): before = text[s] @@ -261,7 +272,7 @@ def quotes(text, s, e): return s, e -def mark(pattern, post_processors, text, args): +def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Generator[Mark, None, None]: pat = re.compile(pattern) for idx, (s, e, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)): for func in post_processors: @@ -270,7 +281,7 @@ def mark(pattern, post_processors, text, args): yield Mark(idx, s, e, mark_text, groupdict) -def run_loop(args, text, all_marks, index_map, extra_cli_args=()): +def run_loop(args: HintsCLIOptions, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], extra_cli_args: Sequence[str] = ()) -> Dict[str, Any]: loop = Loop() handler = Hints(text, all_marks, index_map, args) loop.loop(handler) @@ -283,17 +294,17 @@ def run_loop(args, text, all_marks, index_map, extra_cli_args=()): raise SystemExit(loop.return_code) -def escape(chars): +def escape(chars: str) -> str: return chars.replace('\\', '\\\\').replace('-', r'\-').replace(']', r'\]') -def functions_for(args): +def functions_for(args: HintsCLIOptions) -> Tuple[str, List[PostprocessorFunc]]: post_processors = [] if args.type == 'url': if args.url_prefixes == 'default': - url_prefixes = kitty_common_opts().get('url_prefixes', ('https', 'http', 'file', 'ftp')) + url_prefixes = kitty_common_opts()['url_prefixes'] else: - url_prefixes = args.url_prefixes.split(',') + url_prefixes = tuple(args.url_prefixes.split(',')) from .url_regex import url_delimiters pattern = '(?:{})://[^{}]{{3,}}'.format( '|'.join(url_prefixes), url_delimiters @@ -308,7 +319,7 @@ def functions_for(args): pattern = '[0-9a-f]{7,128}' elif args.type == 'word': chars = args.word_characters - if chars is None: + if not chars: chars = kitty_common_opts()['select_by_word_characters'] pattern = r'(?u)[{}\w]{{{},}}'.format(escape(chars), args.minimum_match_length) post_processors.extend((brackets, quotes)) @@ -331,7 +342,7 @@ def convert_text(text: str, cols: int) -> str: return '\n'.join(lines) -def parse_input(text): +def parse_input(text: str) -> str: try: cols = int(os.environ['OVERLAID_WINDOW_COLS']) except KeyError: @@ -339,14 +350,14 @@ def parse_input(text): return convert_text(text, cols) -def linenum_marks(text, args, Mark, extra_cli_args, *a): +def linenum_marks(text: str, args: HintsCLIOptions, Mark: Type[Mark], extra_cli_args: Sequence[str], *a: Any) -> Generator[Mark, None, None]: regex = args.regex if regex == DEFAULT_REGEX: regex = r'(?P(?:\S*/\S+)|(?:\S+[.][a-zA-Z0-9]{2,7})):(?P\d+)' yield from mark(regex, [brackets, quotes], text, args) -def load_custom_processor(customize_processing): +def load_custom_processor(customize_processing: str) -> Any: if customize_processing.startswith('::import::'): import importlib m = importlib.import_module(customize_processing[len('::import::'):]) @@ -363,7 +374,7 @@ def load_custom_processor(customize_processing): return runpy.run_path(custom_path, run_name='__main__') -def run(args, text, extra_cli_args=()): +def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]: try: text = parse_input(text) pattern, post_processors = functions_for(args) @@ -381,7 +392,7 @@ def run(args, text, extra_cli_args=()): input(_('No {} found, press Enter to quit.').format( 'URLs' if args.type == 'url' else 'matches' )) - return + return None largest_index = all_marks[-1].index offset = max(0, args.hints_offset) @@ -526,13 +537,13 @@ def parse_hints_args(args: List[str]) -> Tuple[HintsCLIOptions, List[str]]: return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten hints', result_class=HintsCLIOptions) -def main(args: List[str]): +def main(args: List[str]) -> Optional[Dict[str, Any]]: text = '' if sys.stdin.isatty(): if '--help' not in args and '-h' not in args: print('You must pass the text to be hinted on STDIN', file=sys.stderr) input(_('Press Enter to quit')) - return + return None else: text = sys.stdin.buffer.read().decode('utf-8') sys.stdin = open(os.ctermid()) @@ -542,14 +553,14 @@ def main(args: List[str]): if e.code != 0: print(e.args[0], file=sys.stderr) input(_('Press Enter to quit')) - return + return None if items and not (opts.customize_processing or opts.type == 'linenum'): print('Extra command line arguments present: {}'.format(' '.join(items)), file=sys.stderr) input(_('Press Enter to quit')) return run(opts, text, items) -def linenum_handle_result(args, data, target_window_id, boss, extra_cli_args, *a): +def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: 'Boss', extra_cli_args: Sequence[str], *a: Any) -> None: for m, g in zip(data['match'], data['groupdicts']): if m: path, line = g['path'], g['line'] @@ -578,14 +589,16 @@ def linenum_handle_result(args, data, target_window_id, boss, extra_cli_args, *a @result_handler(type_of_input='screen') -def handle_result(args, data, target_window_id, boss): +def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: 'Boss') -> None: if data['customize_processing']: m = load_custom_processor(data['customize_processing']) if 'handle_result' in m: - return m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args']) + m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args']) + return None programs = data['programs'] or ('default',) - matches, groupdicts = [], [] + matches: List[str] = [] + groupdicts = [] for m, g in zip(data['match'], data['groupdicts']): if m: matches.append(m) @@ -598,7 +611,7 @@ def handle_result(args, data, target_window_id, boss): text_type = data['type'] @lru_cache() - def joined_text(): + def joined_text() -> str: if is_int is not None: try: return matches[is_int] diff --git a/kittens/icat/main.py b/kittens/icat/main.py index 50513fe09..690df93b1 100755 --- a/kittens/icat/main.py +++ b/kittens/icat/main.py @@ -12,7 +12,10 @@ from base64 import standard_b64encode from functools import lru_cache from math import ceil from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Union +from typing import ( + TYPE_CHECKING, Dict, Generator, List, NamedTuple, Optional, Pattern, Tuple, + Union +) from kitty.cli import parse_args from kitty.cli_stub import IcatCLIOptions @@ -22,7 +25,8 @@ from kitty.utils import ( ) from ..tui.images import ( - ConvertFailed, NoImageMagick, OpenFailed, convert, fsenc, identify, GraphicsCommand + ConvertFailed, GraphicsCommand, NoImageMagick, OpenFailed, convert, fsenc, + identify ) from ..tui.operations import clear_images_on_screen @@ -136,7 +140,7 @@ def write_gr_cmd(cmd: GraphicsCommand, payload: Optional[bytes] = None) -> None: sys.stdout.flush() -def calculate_in_cell_x_offset(width, cell_width, align): +def calculate_in_cell_x_offset(width: int, cell_width: int, align: str) -> int: if align == 'left': return 0 extra_pixels = width % cell_width @@ -147,7 +151,7 @@ def calculate_in_cell_x_offset(width, cell_width, align): return (cell_width - extra_pixels) // 2 -def set_cursor(cmd: GraphicsCommand, width, height, align): +def set_cursor(cmd: GraphicsCommand, width: int, height: int, align: str) -> None: ss = get_screen_size() cw = int(ss.width / ss.cols) num_of_cells_needed = int(ceil(width / cw)) @@ -167,7 +171,7 @@ def set_cursor(cmd: GraphicsCommand, width, height, align): sys.stdout.buffer.write(b' ' * extra_cells) -def set_cursor_for_place(place, cmd: GraphicsCommand, width, height, align): +def set_cursor_for_place(place: 'Place', cmd: GraphicsCommand, width: int, height: int, align: str) -> None: x = place.left + 1 ss = get_screen_size() cw = int(ss.width / ss.cols) @@ -193,7 +197,14 @@ def write_chunked(cmd: GraphicsCommand, data: bytes) -> None: cmd.clear() -def show(outfile, width: int, height: int, zindex: int, fmt: 'GRT_f', transmit_mode: 'GRT_t' = 't', align: str = 'center', place=None): +def show( + outfile: str, + width: int, height: int, zindex: int, + fmt: 'GRT_f', + transmit_mode: 'GRT_t' = 't', + align: str = 'center', + place: Optional['Place'] = None +) -> None: cmd = GraphicsCommand() cmd.a = 'T' cmd.f = fmt @@ -217,7 +228,7 @@ def show(outfile, width: int, height: int, zindex: int, fmt: 'GRT_f', transmit_m write_chunked(cmd, data) -def parse_z_index(val): +def parse_z_index(val: str) -> int: origin = 0 if val.startswith('--'): val = val[1:] @@ -225,11 +236,17 @@ def parse_z_index(val): return origin + int(val) -def process(path, args, is_tempfile): +class ParsedOpts: + + place: Optional['Place'] = None + z_index: int = 0 + + +def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfile: bool) -> bool: m = identify(path) ss = get_screen_size() - available_width = args.place.width * (ss.width / ss.cols) if args.place else ss.width - available_height = args.place.height * (ss.height / ss.rows) if args.place else 10 * m.height + available_width = parsed_opts.place.width * (ss.width // ss.cols) if parsed_opts.place else ss.width + available_height = parsed_opts.place.height * (ss.height // ss.rows) if parsed_opts.place else 10 * m.height needs_scaling = m.width > available_width or m.height > available_height needs_scaling = needs_scaling or args.scale_up file_removed = False @@ -243,13 +260,13 @@ def process(path, args, is_tempfile): fmt = 24 if m.mode == 'rgb' else 32 transmit_mode = 't' outfile, width, height = convert(path, m, available_width, available_height, args.scale_up) - show(outfile, width, height, args.z_index, fmt, transmit_mode, align=args.align, place=args.place) + show(outfile, width, height, parsed_opts.z_index, fmt, transmit_mode, align=args.align, place=parsed_opts.place) if not args.place: print() # ensure cursor is on a new line return file_removed -def scan(d): +def scan(d: str) -> Generator[Tuple[str, str], None, None]: for dirpath, dirnames, filenames in os.walk(d): for f in filenames: mt = mimetypes.guess_type(f)[0] @@ -257,7 +274,7 @@ def scan(d): yield os.path.join(dirpath, f), mt -def detect_support(wait_for: int = 10, silent: bool = False) -> bool: +def detect_support(wait_for: float = 10, silent: bool = False) -> bool: global can_transfer_with_files if not silent: print('Checking for graphics ({}s max. wait)...'.format(wait_for), end='\r') @@ -266,7 +283,7 @@ def detect_support(wait_for: int = 10, silent: bool = False) -> bool: received = b'' responses: Dict[int, bool] = {} - def parse_responses(): + def parse_responses() -> None: for m in re.finditer(b'\033_Gi=([1|2]);(.+?)\033\\\\', received): iid = m.group(1) if iid in (b'1', b'2'): @@ -274,7 +291,7 @@ def detect_support(wait_for: int = 10, silent: bool = False) -> bool: if iid_ not in responses: responses[iid_] = m.group(2) == b'OK' - def more_needed(data): + def more_needed(data: bytes) -> bool: nonlocal received received += data parse_responses() @@ -290,7 +307,7 @@ def detect_support(wait_for: int = 10, silent: bool = False) -> bool: gc.i = 2 write_gr_cmd(gc, standard_b64encode(f.name.encode(fsenc))) with TTYIO() as io: - io.recv(more_needed, timeout=float(wait_for)) + io.recv(more_needed, timeout=wait_for) finally: if not silent: sys.stdout.buffer.write(b'\033[J'), sys.stdout.flush() @@ -325,7 +342,13 @@ help_text = ( usage = 'image-file-or-url-or-directory ...' -def process_single_item(item, args, url_pat=None, maybe_dir=True): +def process_single_item( + item: Union[bytes, str], + args: IcatCLIOptions, + parsed_opts: ParsedOpts, + url_pat: Optional[Pattern] = None, + maybe_dir: bool = True +) -> None: is_tempfile = False file_removed = False try: @@ -343,34 +366,34 @@ def process_single_item(item, args, url_pat=None, maybe_dir=True): raise SystemExit('Failed to download image at URL: {} with error: {}'.format(item, e)) item = tf.name is_tempfile = True - file_removed = process(item, args, is_tempfile) + file_removed = process(item, args, parsed_opts, is_tempfile) elif item.lower().startswith('file://'): from urllib.parse import urlparse from urllib.request import url2pathname - item = urlparse(item) + pitem = urlparse(item) if os.sep == '\\': - item = item.netloc + item.path + item = pitem.netloc + pitem.path else: - item = item.path + item = pitem.path item = url2pathname(item) - file_removed = process(item, args, is_tempfile) + file_removed = process(item, args, parsed_opts, is_tempfile) else: if maybe_dir and os.path.isdir(item): for (x, mt) in scan(item): - process_single_item(x, args, url_pat=None, maybe_dir=False) + process_single_item(x, args, parsed_opts, url_pat=None, maybe_dir=False) else: - file_removed = process(item, args, is_tempfile) + file_removed = process(item, args, parsed_opts, is_tempfile) finally: if is_tempfile and not file_removed: os.remove(item) -def main(args=sys.argv): +def main(args: List[str] = sys.argv) -> None: global can_transfer_with_files - args, items_ = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname), result_class=IcatCLIOptions) + cli_opts, items_ = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname), result_class=IcatCLIOptions) items: List[Union[str, bytes]] = list(items_) - if args.print_window_size: + if cli_opts.print_window_size: screen_size_function.cache_clear() with open(os.ctermid()) as tty: ss = screen_size_function(tty)() @@ -380,7 +403,7 @@ def main(args=sys.argv): if not sys.stdout.isatty(): sys.stdout = open(os.ctermid(), 'w') stdin_data = None - if args.stdin == 'yes' or (not sys.stdin.isatty() and args.stdin == 'detect'): + if cli_opts.stdin == 'yes' or (not sys.stdin.isatty() and cli_opts.stdin == 'detect'): stdin_data = sys.stdin.buffer.read() if stdin_data: items.insert(0, stdin_data) @@ -390,53 +413,55 @@ def main(args=sys.argv): screen_size = get_screen_size_function() signal.signal(signal.SIGWINCH, lambda signum, frame: setattr(screen_size, 'changed', True)) if screen_size().width == 0: - if args.detect_support: + if cli_opts.detect_support: raise SystemExit(1) raise SystemExit( 'Terminal does not support reporting screen sizes via the TIOCGWINSZ ioctl' ) - try: - args.place = parse_place(args.place) - except Exception: - raise SystemExit('Not a valid place specification: {}'.format(args.place)) + parsed_opts = ParsedOpts() + if cli_opts.place: + try: + parsed_opts.place = parse_place(cli_opts.place) + except Exception: + raise SystemExit('Not a valid place specification: {}'.format(cli_opts.place)) try: - args.z_index = parse_z_index(args.z_index) + parsed_opts.z_index = parse_z_index(cli_opts.z_index) except Exception: - raise SystemExit('Not a valid z-index specification: {}'.format(args.z_index)) + raise SystemExit('Not a valid z-index specification: {}'.format(cli_opts.z_index)) - if args.detect_support: - if not detect_support(wait_for=args.detection_timeout, silent=True): + if cli_opts.detect_support: + if not detect_support(wait_for=cli_opts.detection_timeout, silent=True): raise SystemExit(1) print('file' if can_transfer_with_files else 'stream', end='', file=sys.stderr) return - if args.transfer_mode == 'detect': - if not detect_support(wait_for=args.detection_timeout, silent=args.silent): + if cli_opts.transfer_mode == 'detect': + if not detect_support(wait_for=cli_opts.detection_timeout, silent=cli_opts.silent): raise SystemExit('This terminal emulator does not support the graphics protocol, use a terminal emulator such as kitty that does support it') else: - can_transfer_with_files = args.transfer_mode == 'file' + can_transfer_with_files = cli_opts.transfer_mode == 'file' errors = [] - if args.clear: + if cli_opts.clear: sys.stdout.write(clear_images_on_screen(delete_data=True)) if not items: return if not items: raise SystemExit('You must specify at least one file to cat') - if args.place: + if parsed_opts.place: if len(items) > 1 or (isinstance(items[0], str) and os.path.isdir(items[0])): raise SystemExit(f'The --place option can only be used with a single image, not {items}') sys.stdout.buffer.write(b'\0337') # save cursor url_pat = re.compile(r'(?:https?|ftp)://', flags=re.I) for item in items: try: - process_single_item(item, args, url_pat) + process_single_item(item, cli_opts, parsed_opts, url_pat) except NoImageMagick as e: raise SystemExit(str(e)) except ConvertFailed as e: raise SystemExit(str(e)) except OpenFailed as e: errors.append(e) - if args.place: + if parsed_opts.place: sys.stdout.buffer.write(b'\0338') # restore cursor if not errors: return diff --git a/kittens/key_demo/main.py b/kittens/key_demo/main.py index f59667ef6..044da7c7c 100644 --- a/kittens/key_demo/main.py +++ b/kittens/key_demo/main.py @@ -3,9 +3,11 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal import sys +from typing import List from kitty.key_encoding import ( - ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, SUPER, encode_key_event + ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, SUPER, KeyEvent, + encode_key_event ) from ..tui.handler import Handler @@ -14,15 +16,15 @@ from ..tui.loop import Loop class KeysHandler(Handler): - def initialize(self): + def initialize(self) -> None: self.cmd.set_window_title('Kitty extended keyboard protocol demo') self.cmd.set_cursor_visible(False) self.print('Press any keys - Ctrl+C or Ctrl+D will terminate') - def on_text(self, text, in_bracketed_paste=False): + def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: self.print('Text input: ' + text) - def on_key(self, key_event): + def on_key(self, key_event: KeyEvent) -> None: etype = { PRESS: 'PRESS', REPEAT: 'REPEAT', @@ -41,14 +43,14 @@ class KeysHandler(Handler): mods += '+' self.print('Key {}: {}{} [{}]'.format(etype, mods, key_event.key, encode_key_event(key_event))) - def on_interrupt(self): + def on_interrupt(self) -> None: self.quit_loop(0) - def on_eot(self): + def on_eot(self) -> None: self.quit_loop(0) -def main(args): +def main(args: List[str]) -> None: loop = Loop() handler = KeysHandler() loop.loop(handler) diff --git a/kittens/panel/main.py b/kittens/panel/main.py index fe5e5c5e8..b64e91226 100644 --- a/kittens/panel/main.py +++ b/kittens/panel/main.py @@ -6,10 +6,11 @@ import os import shutil import subprocess import sys -from typing import List, Tuple +from typing import Callable, Dict, List, Tuple from kitty.cli import parse_args from kitty.cli_stub import PanelCLIOptions +from kitty.options_stub import Options from kitty.constants import is_macos OPTIONS = r''' @@ -53,7 +54,7 @@ def parse_panel_args(args: List[str]) -> Tuple[PanelCLIOptions, List[str]]: return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions) -def call_xprop(*cmd: str, silent=False): +def call_xprop(*cmd: str, silent: bool = False) -> None: cmd_ = ['xprop'] + list(cmd) try: cp = subprocess.run(cmd_, stdout=subprocess.DEVNULL if silent else None) @@ -64,11 +65,11 @@ def call_xprop(*cmd: str, silent=False): def create_strut( - win_id, - left=0, right=0, top=0, bottom=0, left_start_y=0, left_end_y=0, - right_start_y=0, right_end_y=0, top_start_x=0, top_end_x=0, - bottom_start_x=0, bottom_end_x=0 -): + win_id: int, + left: int = 0, right: int = 0, top: int = 0, bottom: int = 0, left_start_y: int = 0, left_end_y: int = 0, + right_start_y: int = 0, right_end_y: int = 0, top_start_x: int = 0, top_end_x: int = 0, + bottom_start_x: int = 0, bottom_end_x: int = 0 +) -> None: call_xprop( '-id', str(int(win_id)), '-format', '_NET_WM_STRUT_PARTIAL', '32cccccccccccc', @@ -79,26 +80,26 @@ def create_strut( ) -def create_top_strut(win_id, width, height): +def create_top_strut(win_id: int, width: int, height: int) -> None: create_strut(win_id, top=height, top_end_x=width) -def create_bottom_strut(win_id, width, height): +def create_bottom_strut(win_id: int, width: int, height: int) -> None: create_strut(win_id, bottom=height, bottom_end_x=width) -def create_left_strut(win_id, width, height): +def create_left_strut(win_id: int, width: int, height: int) -> None: create_strut(win_id, left=width, left_end_y=height) -def create_right_strut(win_id, width, height): +def create_right_strut(win_id: int, width: int, height: int) -> None: create_strut(win_id, right=width, right_end_y=height) window_width = window_height = 0 -def setup_x11_window(win_id): +def setup_x11_window(win_id: int) -> None: call_xprop( '-id', str(win_id), '-format', '_NET_WM_WINDOW_TYPE', '32a', '-set', '_NET_WM_WINDOW_TYPE', '_NET_WM_WINDOW_TYPE_DOCK' @@ -107,10 +108,10 @@ def setup_x11_window(win_id): func(win_id, window_width, window_height) -def initial_window_size_func(opts, *a): +def initial_window_size_func(opts: Options, cached_values: Dict) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]: from kitty.fast_data_types import glfw_primary_monitor_size, set_smallest_allowed_resize - def initial_window_size(cell_width, cell_height, dpi_x, dpi_y, xscale, yscale): + def initial_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]: global window_width, window_height monitor_width, monitor_height = glfw_primary_monitor_size() if args.edge in {'top', 'bottom'}: @@ -126,7 +127,7 @@ def initial_window_size_func(opts, *a): return initial_window_size -def main(sys_args): +def main(sys_args: List[str]) -> None: global args if is_macos or not os.environ.get('DISPLAY'): raise SystemExit('Currently the panel kitten is supported only on X11 desktops') diff --git a/kittens/resize_window/main.py b/kittens/resize_window/main.py index e9c383e05..22169a0fa 100644 --- a/kittens/resize_window/main.py +++ b/kittens/resize_window/main.py @@ -4,14 +4,15 @@ import sys -from typing import Optional +from typing import Any, Dict, List, Optional from kitty.cli import parse_args from kitty.cli_stub import RCOptions, ResizeCLIOptions -from kitty.rc.base import parse_subcommand_cli, command_for_name from kitty.constants import version -from kitty.key_encoding import CTRL, RELEASE, key_defs as K +from kitty.key_encoding import CTRL, RELEASE, KeyEvent, key_defs as K +from kitty.rc.base import command_for_name, parse_subcommand_cli from kitty.remote_control import encode_send, parse_rc_args +from kitty.utils import ScreenSize from ..tui.handler import Handler from ..tui.loop import Loop @@ -29,10 +30,10 @@ class Resize(Handler): print_on_fail: Optional[str] = None - def __init__(self, opts): + def __init__(self, opts: ResizeCLIOptions): self.opts = opts - def initialize(self): + def initialize(self) -> None: global global_opts global_opts = parse_rc_args(['kitty', '@resize-window'])[0] self.original_size = self.screen_size @@ -40,7 +41,7 @@ class Resize(Handler): self.cmd.set_line_wrapping(False) self.draw_screen() - def do_window_resize(self, is_decrease=False, is_horizontal=True, reset=False, multiplier=1): + def do_window_resize(self, is_decrease: bool = False, is_horizontal: bool = True, reset: bool = False, multiplier: int = 1) -> None: resize_window = command_for_name('resize_window') increment = self.opts.horizontal_increment if is_horizontal else self.opts.vertical_increment increment *= multiplier @@ -53,7 +54,7 @@ class Resize(Handler): send = {'cmd': resize_window.name, 'version': version, 'payload': payload, 'no_response': False} self.write(encode_send(send)) - def on_kitty_cmd_response(self, response): + def on_kitty_cmd_response(self, response: Dict[str, Any]) -> None: if not response.get('ok'): err = response['error'] if response.get('tb'): @@ -65,14 +66,14 @@ class Resize(Handler): if res: self.cmd.bell() - def on_text(self, text, in_bracketed_paste=False): + def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: text = text.upper() if text in 'WNTSR': self.do_window_resize(is_decrease=text in 'NS', is_horizontal=text in 'WN', reset=text == 'R') elif text == 'Q': self.quit_loop(0) - def on_key(self, key_event): + def on_key(self, key_event: KeyEvent) -> None: if key_event.type is RELEASE: return if key_event.key is ESCAPE: @@ -80,10 +81,10 @@ class Resize(Handler): elif key_event.key in (W, N, T, S) and key_event.mods & CTRL: self.do_window_resize(is_decrease=key_event.key in (N, S), is_horizontal=key_event.key in (W, N), multiplier=2) - def on_resize(self, new_size): + def on_resize(self, new_size: ScreenSize) -> None: self.draw_screen() - def draw_screen(self): + def draw_screen(self) -> None: self.cmd.clear_screen() print = self.print print(styled('Resize this window', bold=True, fg='gray', fg_intense=True)) @@ -118,10 +119,10 @@ The base vertical increment. '''.format -def main(args): +def main(args: List[str]) -> None: msg = 'Resize the current window' try: - args, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window', result_class=ResizeCLIOptions) + cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window', result_class=ResizeCLIOptions) except SystemExit as e: if e.code != 0: print(e.args[0], file=sys.stderr) @@ -129,7 +130,7 @@ def main(args): return loop = Loop() - handler = Resize(args) + handler = Resize(cli_opts) loop.loop(handler) if handler.print_on_fail: print(handler.print_on_fail, file=sys.stderr) diff --git a/kittens/runner.py b/kittens/runner.py index 15a040b66..d69bf9a62 100644 --- a/kittens/runner.py +++ b/kittens/runner.py @@ -7,15 +7,16 @@ import importlib import os import sys from functools import lru_cache, partial +from typing import Any, Dict, FrozenSet, List aliases = {'url_hints': 'hints'} -def resolved_kitten(k): +def resolved_kitten(k: str) -> str: return aliases.get(k, k).replace('-', '_') -def path_to_custom_kitten(config_dir, kitten): +def path_to_custom_kitten(config_dir: str, kitten: str) -> str: path = os.path.expanduser(kitten) if not os.path.isabs(path): path = os.path.join(config_dir, path) @@ -23,7 +24,7 @@ def path_to_custom_kitten(config_dir, kitten): return path -def import_kitten_main_module(config_dir, kitten): +def import_kitten_main_module(config_dir: str, kitten: str) -> Dict[str, Any]: if kitten.endswith('.py'): path_modified = False path = path_to_custom_kitten(config_dir, kitten) @@ -45,7 +46,7 @@ def import_kitten_main_module(config_dir, kitten): return {'start': getattr(m, 'main'), 'end': getattr(m, 'handle_result', lambda *a, **k: None)} -def create_kitten_handler(kitten, orig_args): +def create_kitten_handler(kitten: str, orig_args: List[str]) -> Any: from kitty.constants import config_dir kitten = resolved_kitten(kitten) m = import_kitten_main_module(config_dir, kitten) @@ -55,13 +56,13 @@ def create_kitten_handler(kitten, orig_args): return ans -def set_debug(kitten): +def set_debug(kitten: str) -> None: from kittens.tui.loop import debug import builtins setattr(builtins, 'debug', debug) -def launch(args): +def launch(args: List[str]) -> None: config_dir, kitten = args[:2] kitten = resolved_kitten(kitten) del args[:2] @@ -83,7 +84,7 @@ def launch(args): sys.stdout.flush() -def deserialize(output): +def deserialize(output: str) -> Any: import json if output.startswith('OK: '): try: @@ -93,7 +94,7 @@ def deserialize(output): raise ValueError('Failed to parse kitten output: {!r}'.format(output)) -def run_kitten(kitten, run_name='__main__'): +def run_kitten(kitten: str, run_name: str = '__main__') -> None: import runpy original_kitten_name = kitten kitten = resolved_kitten(kitten) @@ -118,7 +119,7 @@ def run_kitten(kitten, run_name='__main__'): @lru_cache(maxsize=2) -def all_kitten_names(): +def all_kitten_names() -> FrozenSet[str]: n = [] import glob base = os.path.dirname(os.path.abspath(__file__)) @@ -129,7 +130,7 @@ def all_kitten_names(): return frozenset(n) -def list_kittens(): +def list_kittens() -> None: print('You must specify the name of a kitten to run') print('Choose from:') print() @@ -137,7 +138,7 @@ def list_kittens(): print(kitten) -def get_kitten_cli_docs(kitten): +def get_kitten_cli_docs(kitten: str) -> Any: setattr(sys, 'cli_docs', {}) run_kitten(kitten, run_name='__doc__') ans = getattr(sys, 'cli_docs') @@ -146,7 +147,7 @@ def get_kitten_cli_docs(kitten): return ans -def get_kitten_conf_docs(kitten): +def get_kitten_conf_docs(kitten: str) -> Any: setattr(sys, 'all_options', None) run_kitten(kitten, run_name='__conf__') ans = getattr(sys, 'all_options') @@ -154,7 +155,7 @@ def get_kitten_conf_docs(kitten): return ans -def main(): +def main() -> None: try: args = sys.argv[1:] launch(args) diff --git a/kittens/show_error/main.py b/kittens/show_error/main.py index 8bb7d011a..880b6be38 100644 --- a/kittens/show_error/main.py +++ b/kittens/show_error/main.py @@ -5,6 +5,7 @@ import os import sys from contextlib import suppress +from typing import List from kitty.cli import parse_args from kitty.cli_stub import ErrorCLIOptions @@ -18,19 +19,19 @@ The title for the error message. '''.format -def real_main(args): +def real_main(args: List[str]) -> None: msg = 'Show an error message' - args, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions) + cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions) error_message = sys.stdin.buffer.read().decode('utf-8') sys.stdin = open(os.ctermid()) - print(styled(args.title, fg_intense=True, fg='red', bold=True)) + print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True)) print() print(error_message) print() input('Press Enter to close.') -def main(args): +def main(args: List[str]) -> None: try: with suppress(KeyboardInterrupt): real_main(args) diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index 8fe2508fa..edb9337cc 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -7,7 +7,7 @@ import re import shlex import subprocess import sys -from typing import List, Set, Tuple +from typing import List, NoReturn, Set, Tuple SHELL_SCRIPT = '''\ #!/bin/sh @@ -113,7 +113,7 @@ def quote(x: str) -> str: return x -def main(args): +def main(args: List[str]) -> NoReturn: ssh_args, server_args, passthrough = parse_ssh_args(args[1:]) if passthrough: cmd = ['ssh'] + ssh_args + server_args diff --git a/kittens/tui/handler.py b/kittens/tui/handler.py index d7d87238a..0fe893cc7 100644 --- a/kittens/tui/handler.py +++ b/kittens/tui/handler.py @@ -141,7 +141,7 @@ class HandleResult: self.no_ui = no_ui self.type_of_input = type_of_input - def __call__(self, args: Sequence[str], data: Dict, target_window_id: int, boss: 'Boss') -> Any: + def __call__(self, args: Sequence[str], data: Any, target_window_id: int, boss: 'Boss') -> Any: return self.impl(args, data, target_window_id, boss) diff --git a/kittens/unicode_input/main.py b/kittens/unicode_input/main.py index 7b19f0448..0681f731a 100644 --- a/kittens/unicode_input/main.py +++ b/kittens/unicode_input/main.py @@ -9,15 +9,20 @@ import sys from contextlib import suppress from functools import lru_cache from gettext import gettext as _ -from typing import List, Optional, Sequence, Tuple, Union +from typing import ( + TYPE_CHECKING, Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Sequence, Tuple, + Union +) from kitty.cli import parse_args from kitty.cli_stub import UnicodeCLIOptions from kitty.config import cached_values_for from kitty.constants import config_dir from kitty.fast_data_types import is_emoji_presentation_base, wcswidth -from kitty.key_encoding import CTRL, RELEASE, SHIFT, enter_key, key_defs as K -from kitty.utils import get_editor +from kitty.key_encoding import ( + CTRL, RELEASE, SHIFT, KeyEvent, enter_key, key_defs as K +) +from kitty.utils import ScreenSize, get_editor from ..tui.handler import Handler, result_handler from ..tui.line_edit import LineEdit @@ -27,6 +32,11 @@ from ..tui.operations import ( sgr, styled ) +if TYPE_CHECKING: + from kitty.boss import Boss + Boss + + HEX, NAME, EMOTICONS, FAVORITES = 'HEX', 'NAME', 'EMOTICONS', 'FAVORITES' UP = K['UP'] DOWN = K['DOWN'] @@ -56,26 +66,25 @@ all_modes = ( ) -def codepoint_ok(code): +def codepoint_ok(code: int) -> bool: return not (code <= 32 or code == 127 or 128 <= code <= 159 or 0xd800 <= code <= 0xdbff or 0xDC00 <= code <= 0xDFFF) @lru_cache(maxsize=256) -def points_for_word(w): +def points_for_word(w: str) -> FrozenSet[int]: from .unicode_names import codepoints_for_word return codepoints_for_word(w.lower()) @lru_cache(maxsize=4096) -def name(cp): +def name(cp: Union[int, str]) -> str: from .unicode_names import name_for_codepoint - if isinstance(cp, str): - cp = ord(cp[0]) - return (name_for_codepoint(cp) or '').capitalize() + c = ord(cp[0]) if isinstance(cp, str) else cp + return (name_for_codepoint(c) or '').capitalize() @lru_cache(maxsize=256) -def codepoints_matching_search(parts): +def codepoints_matching_search(parts: Sequence[str]) -> List[int]: ans = [] if parts and parts[0] and len(parts[0]) > 1: codepoints = points_for_word(parts[0]) @@ -86,13 +95,13 @@ def codepoints_matching_search(parts): if intersection: codepoints = intersection continue - codepoints = {c for c in codepoints if word in name(c).lower()} + codepoints = frozenset(c for c in codepoints if word in name(c).lower()) if codepoints: ans = list(sorted(codepoints)) return ans -def parse_favorites(raw): +def parse_favorites(raw: str) -> Generator[int, None, None]: for line in raw.splitlines(): line = line.strip() if line.startswith('#') or not line: @@ -110,7 +119,7 @@ def parse_favorites(raw): yield code -def serialize_favorites(favorites): +def serialize_favorites(favorites: Iterable[int]) -> str: ans = '''\ # Favorite characters for unicode input # Enter the hex code for each favorite character on a new line. Blank lines are @@ -135,7 +144,7 @@ def load_favorites(refresh: bool = False) -> List[int]: return ans -def encode_hint(num, digits=string.digits + string.ascii_lowercase): +def encode_hint(num: int, digits: str = string.digits + string.ascii_lowercase) -> str: res = '' d = len(digits) while not res or num > 0: @@ -144,53 +153,53 @@ def encode_hint(num, digits=string.digits + string.ascii_lowercase): return res -def decode_hint(x): +def decode_hint(x: str) -> int: return int(x, 36) class Table: - def __init__(self, emoji_variation): + def __init__(self, emoji_variation: str) -> None: self.emoji_variation = emoji_variation - self.layout_dirty = True + self.layout_dirty: bool = True self.last_rows = self.last_cols = -1 - self.codepoints = [] + self.codepoints: List[int] = [] self.current_idx = 0 self.text = '' self.num_cols = 0 self.mode = HEX @property - def current_codepoint(self): + def current_codepoint(self) -> Optional[int]: if self.codepoints: return self.codepoints[self.current_idx] - def set_codepoints(self, codepoints, mode=HEX): + def set_codepoints(self, codepoints: List[int], mode: str = HEX) -> None: self.codepoints = codepoints self.mode = mode self.layout_dirty = True self.current_idx = 0 - def codepoint_at_hint(self, hint): + def codepoint_at_hint(self, hint: str) -> int: return self.codepoints[decode_hint(hint)] - def layout(self, rows, cols): + def layout(self, rows: int, cols: int) -> Optional[str]: if not self.layout_dirty and self.last_cols == cols and self.last_rows == rows: return self.text self.last_cols, self.last_rows = cols, rows self.layout_dirty = False - def safe_chr(codepoint): + def safe_chr(codepoint: int) -> str: ans = chr(codepoint).encode('utf-8', 'replace').decode('utf-8') if self.emoji_variation and is_emoji_presentation_base(codepoint): ans += self.emoji_variation return ans if self.mode is NAME: - def as_parts(i, codepoint): + def as_parts(i: int, codepoint: int) -> Tuple[str, str, str]: return encode_hint(i).ljust(idx_size), safe_chr(codepoint), name(codepoint) - def cell(i, idx, c, desc): + def cell(i: int, idx: str, c: str, desc: str) -> Generator[str, None, None]: is_current = i == self.current_idx text = colored(idx, 'green') + ' ' + sgr('49') + c + ' ' w = wcswidth(c) @@ -207,10 +216,10 @@ class Table: yield styled(text, reverse=True if is_current else None) else: - def as_parts(i, codepoint): + def as_parts(i: int, codepoint: int) -> Tuple[str, str, str]: return encode_hint(i).ljust(idx_size), safe_chr(codepoint), '' - def cell(i, idx, c, desc): + def cell(i: int, idx: str, c: str, desc: str) -> Generator[str, None, None]: yield colored(idx, 'green') + ' ' yield colored(c, 'gray', True) w = wcswidth(c) @@ -249,7 +258,7 @@ class Table: self.text = ''.join(buf) return self.text - def move_current(self, rows=0, cols=0): + def move_current(self, rows: int = 0, cols: int = 0) -> None: if len(self.codepoints) == 0: return if cols: @@ -272,7 +281,7 @@ def is_index(w: str) -> bool: class UnicodeInput(Handler): - def __init__(self, cached_values, emoji_variation='none'): + def __init__(self, cached_values: Dict[str, Any], emoji_variation: str = 'none') -> None: self.cached_values = cached_values self.emoji_variation = '' if emoji_variation == 'text': @@ -281,23 +290,23 @@ class UnicodeInput(Handler): self.emoji_variation = '\ufe0f' self.line_edit = LineEdit() self.recent = list(self.cached_values.get('recent', DEFAULT_SET)) - self.current_char = None + self.current_char: Optional[str] = None self.prompt_template = '{}> ' - self.last_updated_code_point_at = None + self.last_updated_code_point_at: Optional[Tuple[str, Union[Sequence[int], None, str]]] = None self.choice_line = '' self.mode = globals().get(cached_values.get('mode', 'HEX'), 'HEX') self.table = Table(self.emoji_variation) self.update_prompt() @property - def resolved_current_char(self): + def resolved_current_char(self) -> Optional[str]: ans = self.current_char if ans: if self.emoji_variation and is_emoji_presentation_base(ord(ans[0])): ans += self.emoji_variation return ans - def update_codepoints(self): + def update_codepoints(self) -> None: codepoints = None if self.mode is HEX: q: Tuple[str, Optional[Union[str, Sequence[int]]]] = (self.mode, None) @@ -324,9 +333,9 @@ class UnicodeInput(Handler): codepoints = [codepoints[iindex_word]] if q != self.last_updated_code_point_at: self.last_updated_code_point_at = q - self.table.set_codepoints(codepoints, self.mode) + self.table.set_codepoints(codepoints or [], self.mode) - def update_current_char(self): + def update_current_char(self) -> None: self.update_codepoints() self.current_char = None if self.mode is HEX: @@ -350,7 +359,7 @@ class UnicodeInput(Handler): if not codepoint_ok(code): self.current_char = None - def update_prompt(self): + def update_prompt(self) -> None: self.update_current_char() if self.current_char is None: c, color = '??', 'red' @@ -363,15 +372,15 @@ class UnicodeInput(Handler): colored(c, 'green'), hex(ord(c[0]))[2:], faint(styled(name(c) or '', italic=True))) self.prompt = self.prompt_template.format(colored(c, color)) - def init_terminal_state(self): + def init_terminal_state(self) -> None: self.write(set_line_wrapping(False)) self.write(set_window_title(_('Unicode input'))) - def initialize(self): + def initialize(self) -> None: self.init_terminal_state() self.draw_screen() - def draw_title_bar(self): + def draw_title_bar(self) -> None: entries = [] for name, key, mode in all_modes: entry = ' {} ({}) '.format(name, key) @@ -384,12 +393,12 @@ class UnicodeInput(Handler): text += ' ' * extra self.print(styled(text, reverse=True)) - def draw_screen(self): + def draw_screen(self) -> None: self.write(clear_screen()) self.draw_title_bar() y = 1 - def writeln(text=''): + def writeln(text: str = '') -> None: nonlocal y self.print(text) y += 1 @@ -411,17 +420,19 @@ class UnicodeInput(Handler): elif self.mode is FAVORITES: writeln(faint(_('Press F12 to edit the list of favorites'))) self.table_at = y - self.write(self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols)) + q = self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols) + if q: + self.write(q) - def refresh(self): + def refresh(self) -> None: self.update_prompt() self.draw_screen() - def on_text(self, text, in_bracketed_paste): + def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: self.line_edit.on_text(text, in_bracketed_paste) self.refresh() - def on_key(self, key_event): + def on_key(self, key_event: KeyEvent) -> None: if self.mode is HEX and key_event.type is not RELEASE and not key_event.mods: try: val = int(self.line_edit.current_input, 16) @@ -443,21 +454,27 @@ class UnicodeInput(Handler): if self.mode is NAME and key_event.type is not RELEASE and not key_event.mods: if key_event.key is TAB: if key_event.mods == SHIFT: - self.table.move_current(cols=-1), self.refresh() + self.table.move_current(cols=-1) + self.refresh() elif not key_event.mods: - self.table.move_current(cols=1), self.refresh() + self.table.move_current(cols=1) + self.refresh() return elif key_event.key is LEFT and not key_event.mods: - self.table.move_current(cols=-1), self.refresh() + self.table.move_current(cols=-1) + self.refresh() return elif key_event.key is RIGHT and not key_event.mods: - self.table.move_current(cols=1), self.refresh() + self.table.move_current(cols=1) + self.refresh() return elif key_event.key is UP and not key_event.mods: - self.table.move_current(rows=-1), self.refresh() + self.table.move_current(rows=-1) + self.refresh() return elif key_event.key is DOWN and not key_event.mods: - self.table.move_current(rows=1), self.refresh() + self.table.move_current(rows=1) + self.refresh() return if self.line_edit.on_key(key_event): @@ -484,7 +501,7 @@ class UnicodeInput(Handler): elif key_event.mods == CTRL | SHIFT and key_event.key is TAB: self.next_mode(-1) - def edit_favorites(self): + def edit_favorites(self) -> None: if not os.path.exists(favorites_path): with open(favorites_path, 'wb') as f: f.write(serialize_favorites(load_favorites()).encode('utf-8')) @@ -495,7 +512,7 @@ class UnicodeInput(Handler): self.init_terminal_state() self.refresh() - def switch_mode(self, mode): + def switch_mode(self, mode: str) -> None: if mode is not self.mode: self.mode = mode self.cached_values['mode'] = mode @@ -504,18 +521,18 @@ class UnicodeInput(Handler): self.choice_line = '' self.refresh() - def next_mode(self, delta=1): + def next_mode(self, delta: int = 1) -> None: modes = tuple(x[-1] for x in all_modes) idx = (modes.index(self.mode) + delta + len(modes)) % len(modes) self.switch_mode(modes[idx]) - def on_interrupt(self): + def on_interrupt(self) -> None: self.quit_loop(1) - def on_eot(self): + def on_eot(self) -> None: self.quit_loop(1) - def on_resize(self, new_size): + def on_resize(self, new_size: ScreenSize) -> None: self.refresh() @@ -533,22 +550,22 @@ default form specified in the unicode standard for the symbol is used. '''.format -def parse_unicode_input_args(args): +def parse_unicode_input_args(args: List[str]) -> Tuple[UnicodeCLIOptions, List[str]]: return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten unicode_input', result_class=UnicodeCLIOptions) -def main(args): +def main(args: List[str]) -> Optional[str]: try: - args, items = parse_unicode_input_args(args[1:]) + cli_opts, items = parse_unicode_input_args(args[1:]) except SystemExit as e: if e.code != 0: print(e.args[0], file=sys.stderr) input(_('Press Enter to quit')) - return + return None loop = Loop() with cached_values_for('unicode-input') as cached_values: - handler = UnicodeInput(cached_values, args.emoji_variation) + handler = UnicodeInput(cached_values, cli_opts.emoji_variation) loop.loop(handler) if handler.current_char and loop.return_code == 0: with suppress(Exception): @@ -558,10 +575,11 @@ def main(args): return handler.resolved_current_char if loop.return_code != 0: raise SystemExit(loop.return_code) + return None @result_handler() -def handle_result(args, current_char, target_window_id, boss): +def handle_result(args: List[str], current_char: str, target_window_id: int, boss: 'Boss') -> None: w = boss.window_id_map.get(target_window_id) if w is not None: w.paste(current_char) diff --git a/kitty/boss.py b/kitty/boss.py index c86a0b1dd..58cefdd68 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -16,7 +16,8 @@ from .child import cached_process_data, cwd_of_process from .cli import create_opts, parse_args from .conf.utils import to_cmdline from .config import ( - SubSequenceMap, initial_window_size_func, prepare_config_file_for_editing + SubSequenceMap, common_opts_as_dict, initial_window_size_func, + prepare_config_file_for_editing ) from .config_data import MINIMUM_FONT_SIZE from .constants import ( @@ -735,7 +736,7 @@ class Boss: data = input_data if isinstance(data, str): data = data.encode('utf-8') - copts = {k: self.opts[k] for k in ('select_by_word_characters', 'open_url_with', 'url_prefixes')} + copts = common_opts_as_dict(self.opts) overlay_window = tab.new_special_window( SpecialWindow( [kitty_exe(), '+runpy', 'from kittens.runner import main; main()'] + args, diff --git a/kitty/cli.py b/kitty/cli.py index e98b879b8..a69c1d37e 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -402,6 +402,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, e otype = opt['type'] or 'str' if otype in ('str', 'int', 'float'): t = otype + if t == 'str' and defval_for_opt(opt) is None: + t = 'typing.Optional[str]' elif otype == 'list': t = 'typing.Sequence[str]' elif otype in ('choice', 'choices'): diff --git a/kitty/config.py b/kitty/config.py index 7ba5ef87c..2c44b034a 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -25,6 +25,12 @@ from .key_names import get_key_name_lookup, key_name_aliases from .options_stub import Options as OptionsStub from .utils import log_error +try: + from typing import TypedDict +except ImportError: + TypedDict = dict + + KeySpec = Tuple[int, bool, int] KeyMap = Dict[KeySpec, 'KeyAction'] KeySequence = Tuple[KeySpec, ...] @@ -807,3 +813,19 @@ def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumula log_error('Cannot use both macos_titlebar_color and background_opacity') opts.macos_titlebar_color = 0 return opts + + +class KittyCommonOpts(TypedDict): + select_by_word_characters: str + open_url_with: List[str] + url_prefixes: Tuple[str, ...] + + +def common_opts_as_dict(opts: Optional[OptionsStub] = None) -> KittyCommonOpts: + if opts is None: + opts = defaults + return { + 'select_by_word_characters': opts.select_by_word_characters, + 'open_url_with': opts.open_url_with, + 'url_prefixes': opts.url_prefixes, + } diff --git a/kitty/rc/set_colors.py b/kitty/rc/set_colors.py index 89be01130..91902b498 100644 --- a/kitty/rc/set_colors.py +++ b/kitty/rc/set_colors.py @@ -77,10 +77,11 @@ this option, any color arguments are ignored and --configured and --all are impl ans = { 'match_window': opts.match, 'match_tab': opts.match_tab, 'all': opts.all or opts.reset, 'configured': opts.configured or opts.reset, - 'colors': final_colors, 'reset': opts.reset + 'colors': final_colors, 'reset': opts.reset, 'dummy': 0 } if cursor_text_color is not None: ans['cursor_text_color'] = cursor_text_color + del ans['dummy'] return ans def response_from_kitty(self, boss: 'Boss', window: 'Window', payload_get: PayloadGetType) -> ResponseType: diff --git a/setup.cfg b/setup.cfg index c12bc451e..0668a3408 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ warn_unused_configs = True check_untyped_defs = True # disallow_untyped_defs = True -[mypy-kitty.rc.*,kitty.conf.*,kitty.fonts.*,kittens.tui.*,kitty.launch,kitty.child,kitty.cli,kitty.config,kitty.choose_entry,kitty.main] +[mypy-kitty.rc.*,kitty.conf.*,kitty.fonts.*,kittens.*,kitty.launch,kitty.child,kitty.cli,kitty.config,kitty.choose_entry,kitty.main] disallow_untyped_defs = True [mypy-conf]