mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-26 18:22:09 +02:00
more typing work
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user