Add type information to the Graphics Command infrastructure

This commit is contained in:
Kovid Goyal 2020-03-10 20:14:04 +05:30
parent fc0adfd965
commit 01142cdc8c
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 253 additions and 103 deletions

View File

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

View File

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

View File

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

View File

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

View File

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