more typing work

This commit is contained in:
Kovid Goyal
2020-03-12 08:10:51 +05:30
parent 21707171bb
commit 149ae2866a
22 changed files with 395 additions and 291 deletions

View File

@@ -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)

View File

@@ -3,39 +3,40 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@@ -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

View File

@@ -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)

View File

@@ -3,11 +3,17 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
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

View File

@@ -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<path>(?:\S*/\S+)|(?:\S+[.][a-zA-Z0-9]{2,7})):(?P<line>\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]

View File

@@ -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

View File

@@ -3,9 +3,11 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@@ -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')

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)