More typing work

This commit is contained in:
Kovid Goyal
2020-03-11 09:35:59 +05:30
parent bfbb3c7068
commit ce94a9b2df
10 changed files with 199 additions and 130 deletions

View File

@@ -7,7 +7,7 @@ import re
import shlex import shlex
import subprocess import subprocess
import sys import sys
from typing import List, Tuple from typing import List, Set, Tuple
SHELL_SCRIPT = '''\ SHELL_SCRIPT = '''\
#!/bin/sh #!/bin/sh
@@ -43,10 +43,12 @@ exec -a "-$shell_name" "$0"
''' '''
def get_ssh_cli(): def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
other_ssh_args: List[str] = [] other_ssh_args: List[str] = []
boolean_ssh_args: List[str] = [] boolean_ssh_args: List[str] = []
raw = subprocess.Popen(['ssh'], stderr=subprocess.PIPE).stderr.read().decode('utf-8') stderr = subprocess.Popen(['ssh'], stderr=subprocess.PIPE).stderr
assert stderr is not None
raw = stderr.read().decode('utf-8')
for m in re.finditer(r'\[(.+?)\]', raw): for m in re.finditer(r'\[(.+?)\]', raw):
q = m.group(1) q = m.group(1)
if len(q) < 2 or q[0] != '-': if len(q) < 2 or q[0] != '-':

View File

@@ -3,19 +3,36 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING, Callable, Optional, Type from typing import (
TYPE_CHECKING, Any, Callable, ContextManager, Dict, Optional, Sequence, Type,
Union
)
if TYPE_CHECKING: if TYPE_CHECKING:
from kitty.utils import ScreenSize from kitty.utils import ScreenSize
ScreenSize from .loop import TermManager, Loop, Debug, MouseEvent
from .images import ImageManager
from kitty.config import KeyAction
from kitty.boss import Boss
from kitty.key_encoding import KeyEvent
from types import TracebackType
ScreenSize, TermManager, Loop, Debug, KeyAction, KeyEvent, MouseEvent, TracebackType, Boss, ImageManager
import asyncio
class Handler: class Handler:
image_manager_class: Optional[Type['ImageManagerBase']] = None image_manager_class: Optional[Type['ImageManager']] = None
def _initialize(self, screen_size: 'ScreenSize', term_manager, schedule_write, tui_loop, debug, image_manager=None): def _initialize(
self,
screen_size: 'ScreenSize',
term_manager: 'TermManager',
schedule_write: Callable[[bytes], None],
tui_loop: 'Loop',
debug: 'Debug',
image_manager: Optional['ImageManager'] = None
) -> None:
from .operations import commander from .operations import commander
self.screen_size = screen_size self.screen_size = screen_size
self._term_manager = term_manager self._term_manager = term_manager
@@ -23,13 +40,18 @@ class Handler:
self._schedule_write = schedule_write self._schedule_write = schedule_write
self.debug = debug self.debug = debug
self.cmd = commander(self) self.cmd = commander(self)
self.image_manager = image_manager self._image_manager = image_manager
@property @property
def asyncio_loop(self): def image_manager(self) -> 'ImageManager':
assert self._image_manager is not None
return self._image_manager
@property
def asyncio_loop(self) -> 'asyncio.AbstractEventLoop':
return self._tui_loop.asycio_loop return self._tui_loop.asycio_loop
def add_shortcut(self, action, key, mods=None, is_text=False): def add_shortcut(self, action: 'KeyAction', key: str, mods: Optional[int] = None, is_text: Optional[bool] = False) -> None:
if not hasattr(self, '_text_shortcuts'): if not hasattr(self, '_text_shortcuts'):
self._text_shortcuts, self._key_shortcuts = {}, {} self._text_shortcuts, self._key_shortcuts = {}, {}
if is_text: if is_text:
@@ -37,104 +59,95 @@ class Handler:
else: else:
self._key_shortcuts[(key, mods or 0)] = action self._key_shortcuts[(key, mods or 0)] = action
def shortcut_action(self, key_event_or_text): def shortcut_action(self, key_event_or_text: Union[str, 'KeyEvent']) -> Optional['KeyAction']:
if isinstance(key_event_or_text, str): if isinstance(key_event_or_text, str):
return self._text_shortcuts.get(key_event_or_text) return self._text_shortcuts.get(key_event_or_text)
return self._key_shortcuts.get((key_event_or_text.key, key_event_or_text.mods)) return self._key_shortcuts.get((key_event_or_text.key, key_event_or_text.mods))
def __enter__(self): def __enter__(self) -> None:
if self.image_manager is not None: if self._image_manager is not None:
self.image_manager.__enter__() self._image_manager.__enter__()
self.debug.fobj = self self.debug.fobj = self
self.initialize() self.initialize()
def __exit__(self, etype, value, tb): def __exit__(self, etype: type, value: Exception, tb: 'TracebackType') -> None:
del self.debug.fobj del self.debug.fobj
self.finalize() self.finalize()
if self.image_manager is not None: if self._image_manager is not None:
self.image_manager.__exit__(etype, value, tb) self._image_manager.__exit__(etype, value, tb)
def initialize(self): def initialize(self) -> None:
pass pass
def finalize(self): def finalize(self) -> None:
pass pass
def on_resize(self, screen_size): def on_resize(self, screen_size: 'ScreenSize') -> None:
self.screen_size = screen_size self.screen_size = screen_size
def quit_loop(self, return_code=None): def quit_loop(self, return_code: Optional[int] = None) -> None:
self._tui_loop.quit(return_code) self._tui_loop.quit(return_code)
def on_term(self): def on_term(self) -> None:
self._tui_loop.quit(1) self._tui_loop.quit(1)
def on_text(self, text, in_bracketed_paste=False): def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
pass pass
def on_key(self, key_event): def on_key(self, key_event: 'KeyEvent') -> None:
pass pass
def on_mouse(self, mouse_event): def on_mouse(self, mouse_event: 'MouseEvent') -> None:
pass pass
def on_interrupt(self): def on_interrupt(self) -> None:
pass pass
def on_eot(self): def on_eot(self) -> None:
pass pass
def on_kitty_cmd_response(self, response): def on_kitty_cmd_response(self, response: Dict) -> None:
pass pass
def on_clipboard_response(self, text, from_primary=False): def on_clipboard_response(self, text: str, from_primary: bool = False) -> None:
pass pass
def on_capability_response(self, name, val): def on_capability_response(self, name: str, val: str) -> None:
pass pass
def write(self, data): def write(self, data: Union[bytes, str]) -> None:
if isinstance(data, str): if isinstance(data, str):
data = data.encode('utf-8') data = data.encode('utf-8')
self._schedule_write(data) self._schedule_write(data)
def print(self, *args, sep=' ', end='\r\n'): def flush(self) -> None:
pass
def print(self, *args: object, sep: str = ' ', end: str = '\r\n') -> None:
data = sep.join(map(str, args)) + end data = sep.join(map(str, args)) + end
self.write(data) self.write(data)
def suspend(self): def suspend(self) -> ContextManager['TermManager']:
return self._term_manager.suspend() return self._term_manager.suspend()
class ImageManagerBase:
def __init__(self, handler: Handler):
pass
def __enter__(self):
pass
def __exit__(self, etype, value, tb):
pass
class HandleResult: class HandleResult:
type_of_input: Optional[str] = None type_of_input: Optional[str] = None
no_ui: bool = False no_ui: bool = False
def __init__(self, impl, type_of_input: Optional[str], no_ui: bool): def __init__(self, impl: Callable, type_of_input: Optional[str], no_ui: bool):
self.impl = impl self.impl = impl
self.no_ui = no_ui self.no_ui = no_ui
self.type_of_input = type_of_input self.type_of_input = type_of_input
def __call__(self, args, data, target_window_id, boss): def __call__(self, args: Sequence[str], data: Dict, target_window_id: int, boss: 'Boss') -> Any:
return self.impl(args, data, target_window_id, boss) return self.impl(args, data, target_window_id, boss)
def result_handler(type_of_input: Optional[str] = None, no_ui=False) -> Callable[[Callable], HandleResult]: def result_handler(type_of_input: Optional[str] = None, no_ui: bool = False) -> Callable[[Callable], HandleResult]:
def wrapper(impl): def wrapper(impl: Callable) -> HandleResult:
return HandleResult(impl, type_of_input, no_ui) return HandleResult(impl, type_of_input, no_ui)
return wrapper return wrapper

View File

@@ -16,7 +16,6 @@ from typing import (
from kitty.utils import ScreenSize, fit_image from kitty.utils import ScreenSize, fit_image
from .handler import ImageManagerBase
from .operations import cursor from .operations import cursor
try: try:
@@ -192,7 +191,7 @@ class Placement(TypedDict):
y: int y: int
class ImageManager(ImageManagerBase): class ImageManager:
def __init__(self, handler: 'Handler'): def __init__(self, handler: 'Handler'):
self.image_id_counter = count() self.image_id_counter = count()

View File

@@ -2,8 +2,10 @@
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Callable, Tuple
from kitty.fast_data_types import truncate_point_for_length, wcswidth from kitty.fast_data_types import truncate_point_for_length, wcswidth
from kitty.key_encoding import RELEASE, key_defs as K from kitty.key_encoding import RELEASE, KeyEvent, key_defs as K
HOME = K['HOME'] HOME = K['HOME']
END = K['END'] END = K['END']
@@ -15,21 +17,21 @@ RIGHT = K['RIGHT']
class LineEdit: class LineEdit:
def __init__(self): def __init__(self) -> None:
self.clear() self.clear()
def clear(self): def clear(self) -> None:
self.current_input = '' self.current_input = ''
self.cursor_pos = 0 self.cursor_pos = 0
self.pending_bell = False self.pending_bell = False
def split_at_cursor(self, delta=0): def split_at_cursor(self, delta: int = 0) -> Tuple[str, str]:
pos = max(0, self.cursor_pos + delta) pos = max(0, self.cursor_pos + delta)
x = truncate_point_for_length(self.current_input, pos) if pos else 0 x = truncate_point_for_length(self.current_input, pos) if pos else 0
before, after = self.current_input[:x], self.current_input[x:] before, after = self.current_input[:x], self.current_input[x:]
return before, after return before, after
def write(self, write, prompt=''): def write(self, write: Callable[[str], None], prompt: str = '') -> None:
if self.pending_bell: if self.pending_bell:
write('\a') write('\a')
self.pending_bell = False self.pending_bell = False
@@ -37,7 +39,7 @@ class LineEdit:
write(self.current_input) write(self.current_input)
write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt))) write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt)))
def add_text(self, text): def add_text(self, text: str) -> None:
if self.current_input: if self.current_input:
x = truncate_point_for_length(self.current_input, self.cursor_pos) if self.cursor_pos else 0 x = truncate_point_for_length(self.current_input, self.cursor_pos) if self.cursor_pos else 0
self.current_input = self.current_input[:x] + text + self.current_input[x:] self.current_input = self.current_input[:x] + text + self.current_input[x:]
@@ -45,10 +47,10 @@ class LineEdit:
self.current_input = text self.current_input = text
self.cursor_pos += wcswidth(text) self.cursor_pos += wcswidth(text)
def on_text(self, text, in_bracketed_paste): def on_text(self, text: str, in_bracketed_paste: bool) -> None:
self.add_text(text) self.add_text(text)
def backspace(self, num=1): def backspace(self, num: int = 1) -> bool:
before, after = self.split_at_cursor() before, after = self.split_at_cursor()
nbefore = before[:-num] nbefore = before[:-num]
if nbefore != before: if nbefore != before:
@@ -58,7 +60,7 @@ class LineEdit:
self.pending_bell = True self.pending_bell = True
return False return False
def delete(self, num=1): def delete(self, num: int = 1) -> bool:
before, after = self.split_at_cursor() before, after = self.split_at_cursor()
nafter = after[num:] nafter = after[num:]
if nafter != after: if nafter != after:
@@ -68,7 +70,7 @@ class LineEdit:
self.pending_bell = True self.pending_bell = True
return False return False
def _left(self): def _left(self) -> None:
if not self.current_input: if not self.current_input:
self.cursor_pos = 0 self.cursor_pos = 0
return return
@@ -76,7 +78,7 @@ class LineEdit:
before, after = self.split_at_cursor(-1) before, after = self.split_at_cursor(-1)
self.cursor_pos = wcswidth(before) self.cursor_pos = wcswidth(before)
def _right(self): def _right(self) -> None:
if not self.current_input: if not self.current_input:
self.cursor_pos = 0 self.cursor_pos = 0
return return
@@ -87,33 +89,37 @@ class LineEdit:
before, after = self.split_at_cursor(1) before, after = self.split_at_cursor(1)
self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos) self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos)
def _move_loop(self, func, num): def _move_loop(self, func: Callable[[], None], num: int) -> bool:
before = self.cursor_pos before = self.cursor_pos
while func() and num > 0: changed = False
while num > 0:
func()
changed = self.cursor_pos != before
if not changed:
break
num -= 1 num -= 1
changed = self.cursor_pos != before
if not changed: if not changed:
self.pending_bell = True self.pending_bell = True
return changed return changed
def left(self, num=1): def left(self, num: int = 1) -> bool:
return self._move_loop(self._left, num) return self._move_loop(self._left, num)
def right(self, num=1): def right(self, num: int = 1) -> bool:
return self._move_loop(self._right, num) return self._move_loop(self._right, num)
def home(self): def home(self) -> bool:
if self.cursor_pos: if self.cursor_pos:
self.cursor_pos = 0 self.cursor_pos = 0
return True return True
return False return False
def end(self): def end(self) -> bool:
orig = self.cursor_pos orig = self.cursor_pos
self.cursor_pos = wcswidth(self.current_input) self.cursor_pos = wcswidth(self.current_input)
return self.cursor_pos != orig return self.cursor_pos != orig
def on_key(self, key_event): def on_key(self, key_event: KeyEvent) -> bool:
if key_event.type is RELEASE: if key_event.type is RELEASE:
return False return False
elif key_event.key is HOME: elif key_event.key is HOME:

View File

@@ -10,69 +10,90 @@ import re
import selectors import selectors
import signal import signal
import sys import sys
from collections import namedtuple
from contextlib import contextmanager from contextlib import contextmanager
from functools import partial from functools import partial
from typing import List from typing import (
TYPE_CHECKING, Any, Callable, Dict, Generator, List, NamedTuple, Optional,
Protocol
)
from kitty.constants import is_macos from kitty.constants import is_macos
from kitty.fast_data_types import ( from kitty.fast_data_types import (
close_tty, normal_tty, open_tty, parse_input_from_terminal, raw_tty close_tty, normal_tty, open_tty, parse_input_from_terminal, raw_tty
) )
from kitty.key_encoding import ( from kitty.key_encoding import (
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, backspace_key, ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, backspace_key, decode_key_event,
decode_key_event, enter_key, key_defs as K enter_key, key_defs as K
) )
from kitty.utils import screen_size_function, write_all from kitty.utils import screen_size_function, write_all
from .handler import Handler from .handler import Handler
from .operations import init_state, reset_state from .operations import init_state, reset_state
if TYPE_CHECKING:
from kitty.key_encoding import KeyEvent
from .images import ImageManager
KeyEvent, ImageManager
C, D = K['C'], K['D'] C, D = K['C'], K['D']
def debug(*a, **kw): class BinaryWrite(Protocol):
from base64 import standard_b64encode
buf = io.StringIO() def write(self, data: bytes) -> None:
kw['file'] = buf pass
print(*a, **kw)
stext = buf.getvalue() def flush(self) -> None:
text = b'\x1bP@kitty-print|' + standard_b64encode(stext.encode('utf-8')) + b'\x1b\\' pass
fobj = getattr(debug, 'fobj', sys.stdout.buffer)
fobj.write(text)
if hasattr(fobj, 'flush'): class Debug:
fobj: Optional[BinaryWrite] = None
def __call__(self, *a: Any, **kw: Any) -> None:
from base64 import standard_b64encode
buf = io.StringIO()
kw['file'] = buf
print(*a, **kw)
stext = buf.getvalue()
text = b'\x1bP@kitty-print|' + standard_b64encode(stext.encode('utf-8')) + b'\x1b\\'
fobj = self.fobj or sys.stdout.buffer
fobj.write(text)
fobj.flush() fobj.flush()
debug = Debug()
class TermManager: class TermManager:
def __init__(self): def __init__(self) -> None:
self.extra_finalize = None self.extra_finalize: Optional[str] = None
def set_state_for_loop(self, set_raw=True): def set_state_for_loop(self, set_raw: bool = True) -> None:
if set_raw: if set_raw:
raw_tty(self.tty_fd, self.original_termios) raw_tty(self.tty_fd, self.original_termios)
write_all(self.tty_fd, init_state()) write_all(self.tty_fd, init_state())
def reset_state_to_original(self): def reset_state_to_original(self) -> None:
normal_tty(self.tty_fd, self.original_termios) normal_tty(self.tty_fd, self.original_termios)
if self.extra_finalize: if self.extra_finalize:
write_all(self.tty_fd, self.extra_finalize) write_all(self.tty_fd, self.extra_finalize)
write_all(self.tty_fd, reset_state()) write_all(self.tty_fd, reset_state())
@contextmanager @contextmanager
def suspend(self): def suspend(self) -> Generator['TermManager', None, None]:
self.reset_state_to_original() self.reset_state_to_original()
yield self yield self
self.set_state_for_loop() self.set_state_for_loop()
def __enter__(self): def __enter__(self) -> 'TermManager':
self.tty_fd, self.original_termios = open_tty() self.tty_fd, self.original_termios = open_tty()
self.set_state_for_loop(set_raw=False) self.set_state_for_loop(set_raw=False)
return self return self
def __exit__(self, *a): def __exit__(self, *a: object) -> None:
self.reset_state_to_original() self.reset_state_to_original()
close_tty(self.tty_fd, self.original_termios) close_tty(self.tty_fd, self.original_termios)
del self.tty_fd, self.original_termios del self.tty_fd, self.original_termios
@@ -80,7 +101,6 @@ class TermManager:
LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 1, 2, 4, 8, 16 LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 1, 2, 4, 8, 16
DRAG = REPEAT DRAG = REPEAT
MouseEvent = namedtuple('MouseEvent', 'x y type buttons mods')
bmap = {0: LEFT, 1: MIDDLE, 2: RIGHT} bmap = {0: LEFT, 1: MIDDLE, 2: RIGHT}
MOTION_INDICATOR = 1 << 5 MOTION_INDICATOR = 1 << 5
EXTRA_BUTTON_INDICATOR = 1 << 6 EXTRA_BUTTON_INDICATOR = 1 << 6
@@ -89,10 +109,18 @@ ALT_INDICATOR = 1 << 3
CTRL_INDICATOR = 1 << 4 CTRL_INDICATOR = 1 << 4
def decode_sgr_mouse(text): class MouseEvent(NamedTuple):
cb, x, y = text.split(';') x: int
m, y = y[-1], y[:-1] y: int
cb, x, y = map(int, (cb, x, y)) type: int
buttons: int
mods: int
def decode_sgr_mouse(text: str) -> MouseEvent:
cb_, x_, y_ = text.split(';')
m, y_ = y_[-1], y_[:-1]
cb, x, y = map(int, (cb_, x_, y_))
typ = RELEASE if m == 'm' else (DRAG if cb & MOTION_INDICATOR else PRESS) typ = RELEASE if m == 'm' else (DRAG if cb & MOTION_INDICATOR else PRESS)
buttons = 0 buttons = 0
cb3 = cb & 3 cb3 = cb & 3
@@ -113,10 +141,10 @@ def decode_sgr_mouse(text):
class UnhandledException(Handler): class UnhandledException(Handler):
def __init__(self, tb): def __init__(self, tb: str) -> None:
self.tb = tb self.tb = tb
def initialize(self): def initialize(self) -> None:
self.cmd.clear_screen() self.cmd.clear_screen()
self.cmd.set_scrolling_region() self.cmd.set_scrolling_region()
self.cmd.set_cursor_visible(True) self.cmd.set_cursor_visible(True)
@@ -125,37 +153,45 @@ class UnhandledException(Handler):
self.write('\r\n') self.write('\r\n')
self.write('Press the Enter key to quit') self.write('Press the Enter key to quit')
def on_key(self, key_event): def on_key(self, key_event: 'KeyEvent') -> None:
if key_event is enter_key: if key_event is enter_key:
self.quit_loop(1) self.quit_loop(1)
def on_interrupt(self): def on_interrupt(self) -> None:
self.quit_loop(1) self.quit_loop(1)
on_eot = on_term = on_interrupt on_eot = on_term = on_interrupt
class SignalManager: class SignalManager:
def __init__(self, loop, on_winch, on_interrupt, on_term): def __init__(
self,
loop: asyncio.AbstractEventLoop,
on_winch: Callable,
on_interrupt: Callable,
on_term: Callable
) -> None:
self.asycio_loop = loop self.asycio_loop = loop
self.on_winch, self.on_interrupt, self.on_term = on_winch, on_interrupt, on_term self.on_winch, self.on_interrupt, self.on_term = on_winch, on_interrupt, on_term
def __enter__(self): def __enter__(self) -> None:
tuple(map(lambda x: self.asycio_loop.add_signal_handler(*x), ( tuple(map(lambda x: self.asycio_loop.add_signal_handler(*x), (
(signal.SIGWINCH, self.on_winch), (signal.SIGWINCH, self.on_winch),
(signal.SIGINT, self.on_interrupt), (signal.SIGINT, self.on_interrupt),
(signal.SIGTERM, self.on_term) (signal.SIGTERM, self.on_term)
))) )))
def __exit__(self, *a): def __exit__(self, *a: Any) -> None:
tuple(map(self.asycio_loop.remove_signal_handler, ( tuple(map(self.asycio_loop.remove_signal_handler, (
signal.SIGWINCH, signal.SIGINT, signal.SIGTERM))) signal.SIGWINCH, signal.SIGINT, signal.SIGTERM)))
class Loop: class Loop:
def __init__(self, def __init__(
sanitize_bracketed_paste='[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'): self,
sanitize_bracketed_paste: str = '[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'
):
if is_macos: if is_macos:
# On macOS PTY devices are not supported by the KqueueSelector and # On macOS PTY devices are not supported by the KqueueSelector and
# the PollSelector is broken, causes 100% CPU usage # the PollSelector is broken, causes 100% CPU usage
@@ -177,7 +213,7 @@ class Loop:
if self.sanitize_bracketed_paste: if self.sanitize_bracketed_paste:
self.sanitize_ibp_pat = re.compile(sanitize_bracketed_paste) self.sanitize_ibp_pat = re.compile(sanitize_bracketed_paste)
def _read_ready(self, handler, fd): def _read_ready(self, handler: Handler, fd: int) -> None:
try: try:
bdata = os.read(fd, io.DEFAULT_BUFFER_SIZE) bdata = os.read(fd, io.DEFAULT_BUFFER_SIZE)
except BlockingIOError: except BlockingIOError:
@@ -198,7 +234,7 @@ class Loop:
del self.handler del self.handler
# terminal input callbacks {{{ # terminal input callbacks {{{
def _on_text(self, text): def _on_text(self, text: str) -> None:
if self.in_bracketed_paste and self.sanitize_bracketed_paste: if self.in_bracketed_paste and self.sanitize_bracketed_paste:
text = self.sanitize_ibp_pat.sub('', text) text = self.sanitize_ibp_pat.sub('', text)
@@ -217,8 +253,7 @@ class Loop:
elif chunk: elif chunk:
self.handler.on_text(chunk, self.in_bracketed_paste) self.handler.on_text(chunk, self.in_bracketed_paste)
def _on_dcs(self, dcs): def _on_dcs(self, dcs: str) -> None:
debug(dcs)
if dcs.startswith('@kitty-cmd'): if dcs.startswith('@kitty-cmd'):
import json import json
self.handler.on_kitty_cmd_response(json.loads(dcs[len('@kitty-cmd'):])) self.handler.on_kitty_cmd_response(json.loads(dcs[len('@kitty-cmd'):]))
@@ -233,7 +268,7 @@ class Loop:
continue continue
self.handler.on_capability_response(name, val) self.handler.on_capability_response(name, val)
def _on_csi(self, csi): def _on_csi(self, csi: str) -> None:
q = csi[-1] q = csi[-1]
if q in 'mM': if q in 'mM':
if csi.startswith('<'): if csi.startswith('<'):
@@ -250,10 +285,10 @@ class Loop:
elif csi == '201~': elif csi == '201~':
self.in_bracketed_paste = False self.in_bracketed_paste = False
def _on_pm(self, pm): def _on_pm(self, pm: str) -> None:
pass pass
def _on_osc(self, osc): def _on_osc(self, osc: str) -> None:
m = re.match(r'(\d+);', osc) m = re.match(r'(\d+);', osc)
if m is not None: if m is not None:
code = int(m.group(1)) code = int(m.group(1))
@@ -264,7 +299,7 @@ class Loop:
from base64 import standard_b64decode from base64 import standard_b64decode
self.handler.on_clipboard_response(standard_b64decode(rest).decode('utf-8'), from_primary) self.handler.on_clipboard_response(standard_b64decode(rest).decode('utf-8'), from_primary)
def _on_apc(self, apc): def _on_apc(self, apc: str) -> None:
if apc.startswith('K'): if apc.startswith('K'):
try: try:
k = decode_key_event(apc) k = decode_key_event(apc)
@@ -284,7 +319,7 @@ class Loop:
self.handler.image_manager.handle_response(apc) self.handler.image_manager.handle_response(apc)
# }}} # }}}
def _write_ready(self, handler, fd): def _write_ready(self, handler: Handler, fd: int) -> None:
if len(self.write_buf) > self.iov_limit: if len(self.write_buf) > self.iov_limit:
self.write_buf[self.iov_limit - 1] = b''.join(self.write_buf[self.iov_limit - 1:]) self.write_buf[self.iov_limit - 1] = b''.join(self.write_buf[self.iov_limit - 1:])
del self.write_buf[self.iov_limit:] del self.write_buf[self.iov_limit:]
@@ -312,24 +347,24 @@ class Loop:
break break
del self.write_buf[:consumed] del self.write_buf[:consumed]
def quit(self, return_code=None): def quit(self, return_code: Optional[int] = None) -> None:
if return_code is not None: if return_code is not None:
self.return_code = return_code self.return_code = return_code
self.asycio_loop.stop() self.asycio_loop.stop()
def loop_impl(self, handler, term_manager, image_manager=None): def loop_impl(self, handler: Handler, term_manager: TermManager, image_manager: Optional['ImageManager'] = None) -> Optional[str]:
self.write_buf = [] self.write_buf = []
tty_fd = term_manager.tty_fd tty_fd = term_manager.tty_fd
tb = None tb = None
self.waiting_for_writes = True self.waiting_for_writes = True
def schedule_write(data): def schedule_write(data: bytes) -> None:
self.write_buf.append(data) self.write_buf.append(data)
if not self.waiting_for_writes: if not self.waiting_for_writes:
self.asycio_loop.add_writer(tty_fd, self._write_ready, handler, tty_fd) self.asycio_loop.add_writer(tty_fd, self._write_ready, handler, tty_fd)
self.waiting_for_writes = True self.waiting_for_writes = True
def handle_exception(loop, context): def handle_exception(loop: asyncio.AbstractEventLoop, context: Dict[str, Any]) -> None:
nonlocal tb nonlocal tb
loop.stop() loop.stop()
tb = context['message'] tb = context['message']
@@ -351,10 +386,10 @@ class Loop:
self.asycio_loop.remove_writer(tty_fd) self.asycio_loop.remove_writer(tty_fd)
return tb return tb
def loop(self, handler): def loop(self, handler: Handler) -> None:
tb = None tb: Optional[str] = None
def _on_sigwinch(): def _on_sigwinch() -> None:
self._get_screen_size.changed = True self._get_screen_size.changed = True
handler.screen_size = self._get_screen_size() handler.screen_size = self._get_screen_size()
handler.on_resize(handler.screen_size) handler.on_resize(handler.screen_size)
@@ -376,5 +411,5 @@ class Loop:
self.return_code = 1 self.return_code = 1
self._report_error_loop(tb, term_manager) self._report_error_loop(tb, term_manager)
def _report_error_loop(self, tb, term_manager): def _report_error_loop(self, tb: str, term_manager: TermManager) -> None:
self.loop_impl(UnhandledException(tb), term_manager) self.loop_impl(UnhandledException(tb), term_manager)

View File

@@ -346,6 +346,9 @@ def request_from_clipboard(use_primary: bool = False) -> str:
return '\x1b]52;{};?\x07'.format('p' if use_primary else 'c') return '\x1b]52;{};?\x07'.format('p' if use_primary else 'c')
# Boilerplate to make operations availble via Handler.cmd {{{
def writer(handler: 'Handler', func: Callable) -> Callable: def writer(handler: 'Handler', func: Callable) -> Callable:
@wraps(func) @wraps(func)
def f(*a: Any, **kw: Any) -> None: def f(*a: Any, **kw: Any) -> None:
@@ -373,6 +376,8 @@ def as_type_stub() -> str:
'from typing import * # noqa', 'from typing import * # noqa',
'from kitty.utils import ScreenSize', 'from kitty.utils import ScreenSize',
'from kittens.tui.images import GraphicsCommand', 'from kittens.tui.images import GraphicsCommand',
'from kitty.rgb import Color',
'import kitty.rgb',
] ]
methods = [] methods = []
for name, func in all_cmds.items(): for name, func in all_cmds.items():
@@ -383,3 +388,4 @@ def as_type_stub() -> str:
ans += ['', '', 'class CMD:'] + methods ans += ['', '', 'class CMD:'] + methods
return '\n'.join(ans) + '\n\n\n' return '\n'.join(ans) + '\n\n\n'
# }}}

9
kitty/key_encoding.py generated
View File

@@ -3,7 +3,7 @@
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import string import string
from collections import namedtuple from typing import NamedTuple
from . import fast_data_types as defines from . import fast_data_types as defines
from .key_names import key_name_aliases from .key_names import key_name_aliases
@@ -451,9 +451,14 @@ def update_encoding():
subprocess.check_call(['yapf', '-i', __file__]) subprocess.check_call(['yapf', '-i', __file__])
class KeyEvent(NamedTuple):
type: int
mods: int
key: str
PRESS, REPEAT, RELEASE = 1, 2, 4 PRESS, REPEAT, RELEASE = 1, 2, 4
SHIFT, ALT, CTRL, SUPER = 1, 2, 4, 8 SHIFT, ALT, CTRL, SUPER = 1, 2, 4, 8
KeyEvent = namedtuple('KeyEvent', 'type mods key')
type_map = {'p': PRESS, 't': REPEAT, 'r': RELEASE} type_map = {'p': PRESS, 't': REPEAT, 'r': RELEASE}
rtype_map = {v: k for k, v in type_map.items()} rtype_map = {v: k for k, v in type_map.items()}
mod_map = {c: i for i, c in enumerate('ABCDEFGHIJKLMNOP')} mod_map = {c: i for i, c in enumerate('ABCDEFGHIJKLMNOP')}

View File

@@ -14,7 +14,7 @@ from contextlib import suppress
from functools import lru_cache from functools import lru_cache
from time import monotonic from time import monotonic
from typing import ( from typing import (
Any, Dict, Generator, List, NamedTuple, Optional, Tuple, cast Any, Dict, Generator, List, NamedTuple, Optional, Tuple, Union, cast
) )
from .constants import ( from .constants import (
@@ -382,7 +382,7 @@ def parse_address_spec(spec):
return family, address, socket_path return family, address, socket_path
def write_all(fd, data): def write_all(fd: int, data: Union[str, bytes]) -> None:
if isinstance(data, str): if isinstance(data, str):
data = data.encode('utf-8') data = data.encode('utf-8')
while data: while data:

View File

@@ -27,7 +27,7 @@ warn_unused_configs = True
check_untyped_defs = True check_untyped_defs = True
# disallow_untyped_defs = True # disallow_untyped_defs = True
[mypy-kitty.rc.*,kitty.conf.*,kitty.fonts.*,kitty.launch,kitty.child,kitty.cli,kitty.config] [mypy-kitty.rc.*,kitty.conf.*,kitty.fonts.*,kittens.tui.*,kitty.launch,kitty.child,kitty.cli,kitty.config,kitty.choose_entry]
disallow_untyped_defs = True disallow_untyped_defs = True
[mypy-conf] [mypy-conf]

View File

@@ -196,11 +196,14 @@ def get_sanitize_args(cc, ccver):
return sanitize_args return sanitize_args
def test_compile(cc, *cflags, src=None): def test_compile(cc: str, *cflags: str, src: Optional[str] = None) -> bool:
src = src or 'int main(void) { return 0; }' src = src or 'int main(void) { return 0; }'
p = subprocess.Popen([cc] + list(cflags) + ['-x', 'c', '-o', os.devnull, '-'], stdin=subprocess.PIPE) p = subprocess.Popen([cc] + list(cflags) + ['-x', 'c', '-o', os.devnull, '-'], stdin=subprocess.PIPE)
stdin = p.stdin
assert stdin is not None
try: try:
p.stdin.write(src.encode('utf-8')), p.stdin.close() stdin.write(src.encode('utf-8'))
stdin.close()
except BrokenPipeError: except BrokenPipeError:
return False return False
return p.wait() == 0 return p.wait() == 0