diff --git a/kittens/icat/main.py b/kittens/icat/main.py index 02d750bfa..50513fe09 100755 --- a/kittens/icat/main.py +++ b/kittens/icat/main.py @@ -12,7 +12,7 @@ from base64 import standard_b64encode from functools import lru_cache from math import ceil from tempfile import NamedTemporaryFile -from typing import Dict, List, NamedTuple, Optional, Union +from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Union from kitty.cli import parse_args from kitty.cli_stub import IcatCLIOptions @@ -22,9 +22,12 @@ from kitty.utils import ( ) from ..tui.images import ( - ConvertFailed, NoImageMagick, OpenFailed, convert, fsenc, identify + ConvertFailed, NoImageMagick, OpenFailed, convert, fsenc, identify, GraphicsCommand ) -from ..tui.operations import clear_images_on_screen, serialize_gr_command +from ..tui.operations import clear_images_on_screen + +if TYPE_CHECKING: + from ..tui.images import GRT_f, GRT_t # noqa OPTIONS = '''\ --align @@ -128,8 +131,8 @@ def options_spec() -> str: return OPTIONS.format(appname='{}-icat'.format(appname)) -def write_gr_cmd(cmd, payload=None): - sys.stdout.buffer.write(serialize_gr_command(cmd, payload)) +def write_gr_cmd(cmd: GraphicsCommand, payload: Optional[bytes] = None) -> None: + sys.stdout.buffer.write(cmd.serialize(payload or b'')) sys.stdout.flush() @@ -144,7 +147,7 @@ def calculate_in_cell_x_offset(width, cell_width, align): return (cell_width - extra_pixels) // 2 -def set_cursor(cmd, width, height, align): +def set_cursor(cmd: GraphicsCommand, width, height, align): ss = get_screen_size() cw = int(ss.width / ss.cols) num_of_cells_needed = int(ceil(width / cw)) @@ -152,9 +155,9 @@ def set_cursor(cmd, width, height, align): w, h = fit_image(width, height, ss.width, height) ch = int(ss.height / ss.rows) num_of_rows_needed = int(ceil(height / ch)) - cmd['c'], cmd['r'] = ss.cols, num_of_rows_needed + cmd.c, cmd.r = ss.cols, num_of_rows_needed else: - cmd['X'] = calculate_in_cell_x_offset(width, cw, align) + cmd.X = calculate_in_cell_x_offset(width, cw, align) extra_cells = 0 if align == 'center': extra_cells = (ss.cols - num_of_cells_needed) // 2 @@ -164,12 +167,12 @@ def set_cursor(cmd, width, height, align): sys.stdout.buffer.write(b' ' * extra_cells) -def set_cursor_for_place(place, cmd, width, height, align): +def set_cursor_for_place(place, cmd: GraphicsCommand, width, height, align): x = place.left + 1 ss = get_screen_size() cw = int(ss.width / ss.cols) num_of_cells_needed = int(ceil(width / cw)) - cmd['X'] = calculate_in_cell_x_offset(width, cw, align) + cmd.X = calculate_in_cell_x_offset(width, cw, align) extra_cells = 0 if align == 'center': extra_cells = (place.width - num_of_cells_needed) // 2 @@ -178,27 +181,31 @@ def set_cursor_for_place(place, cmd, width, height, align): sys.stdout.buffer.write('\033[{};{}H'.format(place.top + 1, x + extra_cells).encode('ascii')) -def write_chunked(cmd, data): - if cmd['f'] != 100: +def write_chunked(cmd: GraphicsCommand, data: bytes) -> None: + if cmd.f != 100: data = zlib.compress(data) - cmd['o'] = 'z' + cmd.o = 'z' data = standard_b64encode(data) while data: chunk, data = data[:4096], data[4096:] - m = 1 if data else 0 - cmd['m'] = m + cmd.m = 1 if data else 0 write_gr_cmd(cmd, chunk) cmd.clear() -def show(outfile, width, height, zindex, fmt, transmit_mode='t', align='center', place=None): - cmd = {'a': 'T', 'f': fmt, 's': width, 'v': height, 'z': zindex} +def show(outfile, width: int, height: int, zindex: int, fmt: 'GRT_f', transmit_mode: 'GRT_t' = 't', align: str = 'center', place=None): + cmd = GraphicsCommand() + cmd.a = 'T' + cmd.f = fmt + cmd.s = width + cmd.v = height + cmd.z = zindex if place: set_cursor_for_place(place, cmd, width, height, align) else: set_cursor(cmd, width, height, align) if can_transfer_with_files: - cmd['t'] = transmit_mode + cmd.t = transmit_mode write_gr_cmd(cmd, standard_b64encode(os.path.abspath(outfile).encode(fsenc))) else: with open(outfile, 'rb') as f: @@ -206,7 +213,7 @@ def show(outfile, width, height, zindex, fmt, transmit_mode='t', align='center', if transmit_mode == 't': os.unlink(outfile) if fmt == 100: - cmd['S'] = len(data) + cmd.S = len(data) write_chunked(cmd, data) @@ -228,8 +235,8 @@ def process(path, args, is_tempfile): file_removed = False if m.fmt == 'png' and not needs_scaling: outfile = path - transmit_mode = 't' if is_tempfile else 'f' - fmt = 100 + transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f' + fmt: 'GRT_f' = 100 width, height = m.width, m.height file_removed = transmit_mode == 't' else: @@ -250,7 +257,7 @@ def scan(d): yield os.path.join(dirpath, f), mt -def detect_support(wait_for=10, silent=False): +def detect_support(wait_for: int = 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') @@ -275,8 +282,13 @@ def detect_support(wait_for=10, silent=False): with NamedTemporaryFile() as f: f.write(b'abcd'), f.flush() - write_gr_cmd(dict(a='q', s=1, v=1, i=1), standard_b64encode(b'abcd')) - write_gr_cmd(dict(a='q', s=1, v=1, i=2, t='f'), standard_b64encode(f.name.encode(fsenc))) + gc = GraphicsCommand() + gc.a = 'q' + gc.s = gc.v = gc.i = 1 + write_gr_cmd(gc, standard_b64encode(b'abcd')) + gc.t = 'f' + 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)) finally: diff --git a/kittens/tui/handler.py b/kittens/tui/handler.py index 1b3d66537..a05dd5560 100644 --- a/kittens/tui/handler.py +++ b/kittens/tui/handler.py @@ -3,16 +3,21 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal -from typing import Callable, Optional, Type +from typing import TYPE_CHECKING, Callable, Optional, Type from .operations import commander +if TYPE_CHECKING: + from kitty.utils import ScreenSize + ScreenSize + + class Handler: image_manager_class: Optional[Type['ImageManagerBase']] = None - def _initialize(self, screen_size, term_manager, schedule_write, tui_loop, debug, image_manager=None): + def _initialize(self, screen_size: 'ScreenSize', term_manager, schedule_write, tui_loop, debug, image_manager=None): self.screen_size = screen_size self._term_manager = term_manager self._tui_loop = tui_loop diff --git a/kittens/tui/images.py b/kittens/tui/images.py index 51b3978f8..061acfd70 100644 --- a/kittens/tui/images.py +++ b/kittens/tui/images.py @@ -9,9 +9,12 @@ from base64 import standard_b64encode from collections import defaultdict, deque from contextlib import suppress from itertools import count -from typing import Any, DefaultDict, Deque, Dict, Tuple +from typing import ( + TYPE_CHECKING, Any, DefaultDict, Deque, Dict, List, Optional, Sequence, + Tuple, Union +) -from kitty.utils import fit_image +from kitty.utils import ScreenSize, fit_image from .handler import ImageManagerBase from .operations import cursor @@ -23,16 +26,33 @@ except Exception: fsenc = 'utf-8' +try: + from typing import TypedDict, Literal + GRT_a = Literal['t', 'T', 'q', 'p', 'd'] + GRT_f = Literal[24, 32, 100] + GRT_t = Literal['d', 'f', 't', 's'] + GRT_o = Literal['z'] + GRT_m = Literal[0, 1] + GRT_d = Literal['a', 'A', 'c', 'C', 'i', 'I', 'p', 'P', 'q', 'Q', 'x', 'X', 'y', 'Y', 'z', 'Z'] +except ImportError: + TypedDict = dict + + +if TYPE_CHECKING: + import subprocess + from .handler import Handler + + class ImageData: - def __init__(self, fmt, width, height, mode): + def __init__(self, fmt: str, width: int, height: int, mode: str): self.width, self.height, self.fmt, self.mode = width, height, fmt, mode - self.transmit_fmt = str(24 if self.mode == 'rgb' else 32) + self.transmit_fmt: 'GRT_f' = (24 if self.mode == 'rgb' else 32) class OpenFailed(ValueError): - def __init__(self, path, message): + def __init__(self, path: str, message: str): ValueError.__init__( self, 'Failed to open image: {} with error: {}'.format(path, message) ) @@ -41,7 +61,7 @@ class OpenFailed(ValueError): class ConvertFailed(ValueError): - def __init__(self, path, message): + def __init__(self, path: str, message: str): ValueError.__init__( self, 'Failed to convert image: {} with error: {}'.format(path, message) ) @@ -52,7 +72,7 @@ class NoImageMagick(Exception): pass -def run_imagemagick(path, cmd, keep_stdout=True): +def run_imagemagick(path: str, cmd: Sequence[str], keep_stdout: bool = True) -> 'subprocess.CompletedProcess[bytes]': import subprocess try: p = subprocess.run(cmd, stdout=subprocess.PIPE if keep_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE) @@ -63,14 +83,19 @@ def run_imagemagick(path, cmd, keep_stdout=True): return p -def identify(path): +def identify(path: str) -> ImageData: p = run_imagemagick(path, ['identify', '-format', '%m %w %h %A', '--', path]) parts: Tuple[str, ...] = tuple(filter(None, p.stdout.decode('utf-8').split())) mode = 'rgb' if parts[3].lower() == 'false' else 'rgba' return ImageData(parts[0].lower(), int(parts[1]), int(parts[2]), mode) -def convert(path, m, available_width, available_height, scale_up, tdir=None): +def convert( + path: str, m: ImageData, + available_width: int, available_height: int, + scale_up: bool, + tdir: Optional[str] = None +) -> Tuple[str, int, int]: from tempfile import NamedTemporaryFile width, height = m.width, m.height cmd = ['convert', '-background', 'none', '--', path] @@ -102,58 +127,121 @@ def convert(path, m, available_width, available_height, scale_up, tdir=None): return outfile.name, width, height -def can_display_images(): +def can_display_images() -> bool: import shutil - ans = getattr(can_display_images, 'ans', None) + ans: Optional[bool] = getattr(can_display_images, 'ans', None) if ans is None: ans = shutil.which('convert') is not None setattr(can_display_images, 'ans', ans) return ans +ImageKey = Tuple[str, int, int] +SentImageKey = Tuple[int, int, int] + + +class GraphicsCommand: + a: 'GRT_a' = 't' # action + f: 'GRT_f' = 32 # image data format + t: 'GRT_t' = 'd' # transmission medium + s: int = 0 # sent image width + v: int = 0 # sent image height + S: int = 0 # size of data to read from file + O: int = 0 # offset of data to read from file + i: int = 0 # image id + o: Optional['GRT_o'] = None # type of compression + m: 'GRT_m' = 0 # 0 or 1 whether there is more chunked data + x: int = 0 # left edge of image area to display + y: int = 0 # top edge of image area to display + w: int = 0 # image width to display + h: int = 0 # image height to display + X: int = 0 # X-offset within cell + Y: int = 0 # Y-offset within cell + c: int = 0 # number of cols to display image over + r: int = 0 # number of rows to display image over + z: int = 0 # z-index + d: 'GRT_d' = 'a' # what to delete + + def serialize(self, payload: bytes = b'') -> bytes: + items = [] + for k in GraphicsCommand.__annotations__: + val: Union[str, None, int] = getattr(self, k) + defval: Union[str, None, int] = getattr(GraphicsCommand, k) + if val != defval and val is not None: + items.append('{}={}'.format(k, val)) + + ans: List[bytes] = [] + w = ans.append + w(b'\033_G') + w(','.join(items).encode('ascii')) + if payload: + w(b';') + w(payload) + w(b'\033\\') + return b''.join(ans) + + def clear(self) -> None: + for k in GraphicsCommand.__annotations__: + defval: Union[str, None, int] = getattr(GraphicsCommand, k) + setattr(self, k, defval) + + +class Placement(TypedDict): + cmd: GraphicsCommand + x: int + y: int + + class ImageManager(ImageManagerBase): - def __init__(self, handler): + def __init__(self, handler: 'Handler'): self.image_id_counter = count() self.handler = handler - self.filesystem_ok = None - self.image_data = {} - self.failed_images = {} - self.converted_images = {} - self.sent_images = {} - self.image_id_to_image_data = {} - self.image_id_to_converted_data = {} - self.transmission_status = {} - self.placements_in_flight: DefaultDict[int, Deque[Dict[str, Any]]] = defaultdict(deque) + self.filesystem_ok: Optional[bool] = None + self.image_data: Dict[str, ImageData] = {} + self.failed_images: Dict[str, Exception] = {} + self.converted_images: Dict[ImageKey, ImageKey] = {} + self.sent_images: Dict[ImageKey, int] = {} + self.image_id_to_image_data: Dict[int, ImageData] = {} + self.image_id_to_converted_data: Dict[int, ImageKey] = {} + self.transmission_status: Dict[int, Union[str, int]] = {} + self.placements_in_flight: DefaultDict[int, Deque[Placement]] = defaultdict(deque) @property - def next_image_id(self): + def next_image_id(self) -> int: return next(self.image_id_counter) + 2 @property - def screen_size(self): + def screen_size(self) -> ScreenSize: return self.handler.screen_size - def __enter__(self): + def __enter__(self) -> None: import tempfile self.tdir = tempfile.mkdtemp(prefix='kitten-images-') with tempfile.NamedTemporaryFile(dir=self.tdir, delete=False) as f: f.write(b'abcd') - self.handler.cmd.gr_command(dict(a='q', s=1, v=1, i=1, t='f'), standard_b64encode(f.name.encode(fsenc))) + gc = GraphicsCommand() + gc.a = 'q' + gc.s = gc.v = gc.i = 1 + gc.t = 'f' + self.handler.cmd.gr_command(gc, standard_b64encode(f.name.encode(fsenc))) - def __exit__(self, *a): + def __exit__(self, *a: Any) -> None: import shutil shutil.rmtree(self.tdir, ignore_errors=True) self.handler.cmd.clear_images_on_screen(delete_data=True) self.delete_all_sent_images() del self.handler - def delete_all_sent_images(self): + def delete_all_sent_images(self) -> None: + gc = GraphicsCommand() + gc.a = 'd' for img_id in self.transmission_status: - self.handler.cmd.gr_command({'a': 'd', 'i': img_id}) + gc.i = img_id + self.handler.cmd.gr_command(gc) self.transmission_status.clear() - def handle_response(self, apc): + def handle_response(self, apc: str) -> None: cdata, payload = apc[1:].partition(';')[::2] control = {} for x in cdata.split(','): @@ -180,7 +268,7 @@ class ImageManager(ImageManagerBase): if not in_flight: self.placements_in_flight.pop(image_id, None) - def resend_image(self, image_id, pl): + def resend_image(self, image_id: int, pl: Placement) -> None: image_data = self.image_id_to_image_data[image_id] skey = self.image_id_to_converted_data[image_id] self.transmit_image(image_data, image_id, *skey) @@ -188,7 +276,7 @@ class ImageManager(ImageManagerBase): self.handler.cmd.set_cursor_position(pl['x'], pl['y']) self.handler.cmd.gr_command(pl['cmd']) - def send_image(self, path, max_cols=None, max_rows=None, scale_up=False): + def send_image(self, path: str, max_cols: Optional[int] = None, max_rows: Optional[int] = None, scale_up: bool = False) -> SentImageKey: path = os.path.abspath(path) if path in self.failed_images: raise self.failed_images[path] @@ -226,41 +314,50 @@ class ImageManager(ImageManagerBase): self.image_id_to_image_data[image_id] = m return image_id, skey[1], skey[2] - def hide_image(self, image_id): - self.handler.cmd.gr_command({'a': 'd', 'i': image_id}) + def hide_image(self, image_id: int) -> None: + gc = GraphicsCommand() + gc.a = 'd' + gc.i = image_id + self.handler.cmd.gr_command(gc) - def show_image(self, image_id, x, y, src_rect=None): - cmd = {'a': 'p', 'i': image_id} + def show_image(self, image_id: int, x: int, y: int, src_rect: Optional[Tuple[int, int, int, int]] = None) -> None: + gc = GraphicsCommand() + gc.a = 'p' + gc.i = image_id if src_rect is not None: - cmd['x'], cmd['y'], cmd['w'], cmd['h'] = map(int, src_rect) - self.placements_in_flight[image_id].append({'cmd': cmd, 'x': x, 'y': y}) + gc.x, gc.y, gc.w, gc.h = map(int, src_rect) + self.placements_in_flight[image_id].append({'cmd': gc, 'x': x, 'y': y}) with cursor(self.handler.write): self.handler.cmd.set_cursor_position(x, y) - self.handler.cmd.gr_command(cmd) + self.handler.cmd.gr_command(gc) - def convert_image(self, path, available_width, available_height, image_data, scale_up=False): + def convert_image(self, path: str, available_width: int, available_height: int, image_data: ImageData, scale_up: bool = False) -> ImageKey: rgba_path, width, height = convert(path, image_data, available_width, available_height, scale_up, tdir=self.tdir) return rgba_path, width, height - def transmit_image(self, image_data, image_id, rgba_path, width, height): + def transmit_image(self, image_data: ImageData, image_id: int, rgba_path: str, width: int, height: int) -> int: self.transmission_status[image_id] = 0 - cmd = {'a': 't', 'f': image_data.transmit_fmt, 's': width, 'v': height, 'i': image_id} + gc = GraphicsCommand() + gc.a = 't' + gc.f = image_data.transmit_fmt + gc.s = width + gc.v = height + gc.i = image_id if self.filesystem_ok: - cmd['t'] = 'f' + gc.t = 'f' self.handler.cmd.gr_command( - cmd, standard_b64encode(rgba_path.encode(fsenc))) + gc, standard_b64encode(rgba_path.encode(fsenc))) else: import zlib with open(rgba_path, 'rb') as f: data = f.read() - cmd['S'] = len(data) + gc.S = len(data) data = zlib.compress(data) - cmd['o'] = 'z' + gc.o = 'z' data = standard_b64encode(data) while data: chunk, data = data[:4096], data[4096:] - m = 1 if data else 0 - cmd['m'] = m - self.handler.cmd.gr_command(cmd, chunk) - cmd.clear() + gc.m = 1 if data else 0 + self.handler.cmd.gr_command(gc, chunk) + gc.clear() return image_id diff --git a/kittens/tui/operations.py b/kittens/tui/operations.py index 46b834301..dd3a10119 100644 --- a/kittens/tui/operations.py +++ b/kittens/tui/operations.py @@ -5,10 +5,15 @@ import sys from contextlib import contextmanager from functools import wraps -from typing import List, Optional, Union +from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union from kitty.rgb import Color, color_as_sharp, to_color +if TYPE_CHECKING: + from kitty.utils import ScreenSize + from .images import GraphicsCommand + ScreenSize, GraphicsCommand + S7C1T = '\033 F' SAVE_CURSOR = '\0337' RESTORE_CURSOR = '\0338' @@ -37,9 +42,9 @@ MODES = dict( ) -def set_mode(which: str, private=True) -> str: - num, private = MODES[which] - return '\033[{}{}h'.format(private, num) +def set_mode(which: str, private: bool = True) -> str: + num, private_ = MODES[which] + return '\033[{}{}h'.format(private_, num) def reset_mode(which: str) -> str: @@ -75,18 +80,18 @@ def set_cursor_visible(yes_or_no: bool) -> str: return set_mode('DECTCEM') if yes_or_no else reset_mode('DECTCEM') -def set_cursor_position(x, y) -> str: # (0, 0) is top left +def set_cursor_position(x: int, y: int) -> str: # (0, 0) is top left return '\033[{};{}H'.format(y + 1, x + 1) -def set_cursor_shape(shape='block', blink=True) -> str: +def set_cursor_shape(shape: str = 'block', blink: bool = True) -> str: val = {'block': 1, 'underline': 3, 'bar': 5}.get(shape, 1) if not blink: val += 1 return '\033[{} q'.format(val) -def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str: +def set_scrolling_region(screen_size: Optional['ScreenSize'] = None, top: Optional[int] = None, bottom: Optional[int] = None) -> str: if screen_size is None: return '\033[r' if top is None: @@ -100,7 +105,7 @@ def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str: return '\033[{};{}r'.format(top + 1, bottom + 1) -def scroll_screen(amt=1) -> str: +def scroll_screen(amt: int = 1) -> str: return '\033[' + str(abs(amt)) + ('T' if amt < 0 else 'S') @@ -111,7 +116,10 @@ UNDERLINE_STYLES = {name: i + 1 for i, name in enumerate( 'straight double curly'.split())} -def color_code(color, intense=False, base=30): +ColorSpec = Union[int, str, Tuple[int, int, int]] + + +def color_code(color: ColorSpec, intense: bool = False, base: int = 30) -> str: if isinstance(color, str): e = str((base + 60 if intense else base) + STANDARD_COLORS[color]) elif isinstance(color, int): @@ -121,20 +129,37 @@ def color_code(color, intense=False, base=30): return e -def sgr(*parts) -> str: +def sgr(*parts: str) -> str: return '\033[{}m'.format(';'.join(parts)) -def colored(text, color, intense=False, reset_to=None, reset_to_intense=False) -> str: +def colored( + text: str, + color: ColorSpec, + intense: bool = False, + reset_to: Optional[ColorSpec] = None, + reset_to_intense: bool = False +) -> str: e = color_code(color, intense) return '\033[{}m{}\033[{}m'.format(e, text, 39 if reset_to is None else color_code(reset_to, reset_to_intense)) -def faint(text) -> str: +def faint(text: str) -> str: return colored(text, 'black', True) -def styled(text: str, fg=None, bg=None, fg_intense=False, bg_intense=False, italic=None, bold=None, underline=None, underline_color=None, reverse=None) -> str: +def styled( + text: str, + fg: Optional[ColorSpec] = None, + bg: Optional[ColorSpec] = None, + fg_intense: bool = False, + bg_intense: bool = False, + italic: Optional[bool] = None, + bold: Optional[bool] = None, + underline: Optional[str] = None, + underline_color: Optional[ColorSpec] = None, + reverse: Optional[bool] = None +) -> str: start, end = [], [] if fg is not None: start.append(color_code(fg, fg_intense)) @@ -167,24 +192,28 @@ def styled(text: str, fg=None, bg=None, fg_intense=False, bg_intense=False, ital return '\033[{}m{}\033[{}m'.format(';'.join(start), text, ';'.join(end)) -def serialize_gr_command(cmd, payload=None) -> bytes: - cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items()) - ans: List[bytes] = [] - w = ans.append - w(b'\033_G'), w(cmd.encode('ascii')) - if payload: - w(b';') - w(payload) - w(b'\033\\') - return b''.join(ans) +def serialize_gr_command(cmd: Dict[str, Union[int, str]], payload: Optional[bytes] = None) -> bytes: + from .images import GraphicsCommand + gc = GraphicsCommand() + for k, v in cmd.items(): + setattr(gc, k, v) + return gc.serialize(payload or b'') -def gr_command(cmd, payload=None) -> str: - return serialize_gr_command(cmd, payload).decode('ascii') +def gr_command(cmd: Union[Dict, 'GraphicsCommand'], payload: Optional[bytes] = None) -> str: + if isinstance(cmd, dict): + raw = serialize_gr_command(cmd, payload) + else: + raw = cmd.serialize(payload or b'') + return raw.decode('ascii') -def clear_images_on_screen(delete_data=False) -> str: - return serialize_gr_command({'a': 'd', 'd': 'A' if delete_data else 'a'}).decode('ascii') +def clear_images_on_screen(delete_data: bool = False) -> str: + from .images import GraphicsCommand + gc = GraphicsCommand() + gc.a = 'd' + gc.d = 'A' if delete_data else 'a' + return gc.serialize().decode('ascii') def init_state(alternate_screen=True): diff --git a/kitty/choose_entry.py b/kitty/choose_entry.py index 8115f0e1f..05b511eac 100644 --- a/kitty/choose_entry.py +++ b/kitty/choose_entry.py @@ -4,8 +4,15 @@ import re +from typing import TYPE_CHECKING, List, Generator, Any, Type -def mark(text, args, Mark, extra_cli_args, *a): +if TYPE_CHECKING: + from kitty.cli_stub import HintsCLIOptions + from kittens.hints.main import Mark as MarkClass + HintsCLIOptions, MarkClass + + +def mark(text: str, args: 'HintsCLIOptions', Mark: Type['MarkClass'], extra_cli_args: List[str], *a: Any) -> Generator['MarkClass', None, None]: for idx, m in enumerate(re.finditer(args.regex, text)): start, end = m.span() mark_text = text[start:end].replace('\n', '').replace('\0', '')