euporie 2.8.4__py3-none-any.whl → 2.8.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euporie/console/_commands.py +143 -0
- euporie/console/_settings.py +58 -0
- euporie/console/app.py +25 -71
- euporie/console/tabs/console.py +58 -62
- euporie/core/__init__.py +1 -1
- euporie/core/__main__.py +28 -11
- euporie/core/_settings.py +109 -0
- euporie/core/app/__init__.py +3 -0
- euporie/core/app/_commands.py +95 -0
- euporie/core/app/_settings.py +457 -0
- euporie/core/{app.py → app/app.py} +212 -576
- euporie/core/app/base.py +51 -0
- euporie/core/{current.py → app/current.py} +13 -4
- euporie/core/app/cursor.py +35 -0
- euporie/core/app/dummy.py +12 -0
- euporie/core/app/launch.py +28 -0
- euporie/core/bars/__init__.py +11 -0
- euporie/core/bars/command.py +205 -0
- euporie/core/bars/menu.py +258 -0
- euporie/core/{widgets → bars}/search.py +20 -16
- euporie/core/{widgets → bars}/status.py +6 -23
- euporie/core/clipboard.py +19 -80
- euporie/core/comm/base.py +8 -6
- euporie/core/comm/ipywidgets.py +16 -7
- euporie/core/comm/registry.py +2 -1
- euporie/core/commands.py +10 -20
- euporie/core/completion.py +3 -2
- euporie/core/config.py +368 -341
- euporie/core/convert/__init__.py +0 -30
- euporie/core/convert/datum.py +116 -53
- euporie/core/convert/formats/__init__.py +31 -0
- euporie/core/convert/formats/ansi.py +9 -23
- euporie/core/convert/formats/common.py +11 -23
- euporie/core/convert/formats/html.py +45 -40
- euporie/core/convert/formats/pil.py +1 -1
- euporie/core/convert/formats/png.py +3 -5
- euporie/core/convert/formats/sixel.py +3 -3
- euporie/core/convert/registry.py +4 -6
- euporie/core/convert/utils.py +41 -4
- euporie/core/diagnostics.py +2 -2
- euporie/core/filters.py +98 -40
- euporie/core/format.py +2 -3
- euporie/core/ft/ansi.py +1 -1
- euporie/core/ft/html.py +12 -21
- euporie/core/ft/table.py +1 -3
- euporie/core/ft/utils.py +4 -1
- euporie/core/graphics.py +386 -133
- euporie/core/history.py +2 -2
- euporie/core/inspection.py +3 -2
- euporie/core/io.py +207 -28
- euporie/core/kernel/__init__.py +1 -0
- euporie/core/{kernel.py → kernel/client.py} +45 -108
- euporie/core/kernel/manager.py +114 -0
- euporie/core/key_binding/bindings/__init__.py +1 -8
- euporie/core/key_binding/bindings/basic.py +47 -7
- euporie/core/key_binding/bindings/completion.py +3 -8
- euporie/core/key_binding/bindings/micro.py +1 -6
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/key_binding/bindings/terminal.py +193 -0
- euporie/core/key_binding/key_processor.py +43 -2
- euporie/core/key_binding/registry.py +2 -0
- euporie/core/key_binding/utils.py +22 -2
- euporie/core/keys.py +7156 -93
- euporie/core/layout/cache.py +3 -3
- euporie/core/layout/containers.py +48 -4
- euporie/core/layout/decor.py +2 -2
- euporie/core/layout/mouse.py +1 -1
- euporie/core/layout/print.py +2 -1
- euporie/core/layout/scroll.py +39 -34
- euporie/core/log.py +76 -64
- euporie/core/lsp.py +118 -24
- euporie/core/margins.py +1 -1
- euporie/core/path.py +62 -13
- euporie/core/renderer.py +58 -17
- euporie/core/style.py +57 -39
- euporie/core/suggest.py +103 -85
- euporie/core/tabs/__init__.py +32 -0
- euporie/core/tabs/_settings.py +113 -0
- euporie/core/tabs/base.py +80 -470
- euporie/core/tabs/kernel.py +419 -0
- euporie/core/tabs/notebook.py +24 -101
- euporie/core/utils.py +92 -15
- euporie/core/validation.py +1 -1
- euporie/core/widgets/_settings.py +188 -0
- euporie/core/widgets/cell.py +19 -50
- euporie/core/widgets/cell_outputs.py +25 -36
- euporie/core/widgets/decor.py +11 -41
- euporie/core/widgets/dialog.py +62 -27
- euporie/core/widgets/display.py +12 -15
- euporie/core/widgets/file_browser.py +2 -23
- euporie/core/widgets/forms.py +8 -5
- euporie/core/widgets/inputs.py +13 -70
- euporie/core/widgets/layout.py +2 -1
- euporie/core/widgets/logo.py +49 -0
- euporie/core/widgets/menu.py +10 -8
- euporie/core/widgets/pager.py +6 -10
- euporie/core/widgets/palette.py +6 -6
- euporie/hub/app.py +52 -35
- euporie/notebook/_commands.py +24 -0
- euporie/notebook/_settings.py +107 -0
- euporie/notebook/app.py +49 -171
- euporie/notebook/filters.py +1 -1
- euporie/notebook/tabs/__init__.py +46 -7
- euporie/notebook/tabs/_commands.py +714 -0
- euporie/notebook/tabs/_settings.py +32 -0
- euporie/notebook/tabs/display.py +4 -4
- euporie/notebook/tabs/edit.py +11 -44
- euporie/notebook/tabs/json.py +5 -5
- euporie/notebook/tabs/log.py +1 -18
- euporie/notebook/tabs/notebook.py +11 -660
- euporie/notebook/widgets/_commands.py +11 -0
- euporie/notebook/widgets/_settings.py +19 -0
- euporie/notebook/widgets/side_bar.py +14 -34
- euporie/preview/_settings.py +104 -0
- euporie/preview/app.py +6 -31
- euporie/preview/tabs/notebook.py +6 -72
- euporie/web/__init__.py +1 -0
- euporie/web/tabs/__init__.py +14 -0
- euporie/web/tabs/web.py +11 -6
- euporie/web/widgets/__init__.py +1 -0
- euporie/web/widgets/webview.py +5 -15
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
- euporie-2.8.6.dist-info/RECORD +175 -0
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
- euporie/core/launch.py +0 -64
- euporie/core/terminal.py +0 -522
- euporie-2.8.4.dist-info/RECORD +0 -147
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
euporie/core/graphics.py
CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import logging
|
6
6
|
import weakref
|
7
7
|
from abc import ABCMeta, abstractmethod
|
8
|
-
from math import ceil
|
8
|
+
from math import ceil, floor
|
9
9
|
from typing import TYPE_CHECKING
|
10
10
|
|
11
11
|
from prompt_toolkit.cache import FastDictCache, SimpleCache
|
@@ -20,14 +20,14 @@ from prompt_toolkit.layout.mouse_handlers import MouseHandlers
|
|
20
20
|
from prompt_toolkit.layout.screen import Char, WritePosition
|
21
21
|
from prompt_toolkit.utils import get_cwidth
|
22
22
|
|
23
|
+
from euporie.core.app.current import get_app
|
23
24
|
from euporie.core.convert.datum import Datum
|
24
25
|
from euporie.core.convert.registry import find_route
|
25
|
-
from euporie.core.current import get_app
|
26
26
|
from euporie.core.data_structures import DiInt
|
27
27
|
from euporie.core.filters import has_float, in_mplex
|
28
28
|
from euporie.core.ft.utils import _ZERO_WIDTH_FRAGMENTS
|
29
|
+
from euporie.core.io import passthrough
|
29
30
|
from euporie.core.layout.scroll import BoundedWritePosition
|
30
|
-
from euporie.core.terminal import passthrough
|
31
31
|
|
32
32
|
if TYPE_CHECKING:
|
33
33
|
from typing import Any, Callable, ClassVar
|
@@ -84,7 +84,7 @@ class GraphicControl(UIControl, metaclass=ABCMeta):
|
|
84
84
|
|
85
85
|
@abstractmethod
|
86
86
|
def get_rendered_lines(
|
87
|
-
self,
|
87
|
+
self, visible_width: int, visible_height: int, wrap_lines: bool = False
|
88
88
|
) -> list[StyleAndTextTuples]:
|
89
89
|
"""Render the output data."""
|
90
90
|
return []
|
@@ -100,13 +100,9 @@ class GraphicControl(UIControl, metaclass=ABCMeta):
|
|
100
100
|
`UIContent` for the given output size.
|
101
101
|
|
102
102
|
"""
|
103
|
-
max_cols, aspect = self.datum.cell_size()
|
104
|
-
bbox = self.bbox
|
105
|
-
cols = min(max_cols, width) if max_cols else width
|
106
|
-
rows = ceil(cols * aspect) - bbox.top - bbox.bottom if aspect else height
|
107
103
|
|
108
104
|
def get_content() -> dict[str, Any]:
|
109
|
-
rendered_lines = self.get_rendered_lines(width
|
105
|
+
rendered_lines = self.get_rendered_lines(width, height)
|
110
106
|
self.rendered_lines = rendered_lines[:]
|
111
107
|
line_count = len(rendered_lines)
|
112
108
|
|
@@ -130,10 +126,10 @@ class GraphicControl(UIControl, metaclass=ABCMeta):
|
|
130
126
|
|
131
127
|
# Re-render if the image width changes, or the terminal character size changes
|
132
128
|
key = (
|
133
|
-
|
134
|
-
|
129
|
+
width,
|
130
|
+
height,
|
135
131
|
self.app.color_palette,
|
136
|
-
self.app.
|
132
|
+
self.app.cell_size_px,
|
137
133
|
self.bbox,
|
138
134
|
)
|
139
135
|
return UIContent(
|
@@ -155,19 +151,11 @@ class SixelGraphicControl(GraphicControl):
|
|
155
151
|
def convert_data(self, wp: WritePosition) -> str:
|
156
152
|
"""Convert datum to required format."""
|
157
153
|
bbox = wp.bbox if isinstance(wp, BoundedWritePosition) else DiInt(0, 0, 0, 0)
|
158
|
-
|
159
|
-
|
160
|
-
cmd = str(
|
161
|
-
self.datum.convert(
|
162
|
-
to="sixel",
|
163
|
-
cols=full_width,
|
164
|
-
rows=full_height,
|
165
|
-
)
|
166
|
-
)
|
167
|
-
if any(self.bbox):
|
154
|
+
cmd = str(self.datum.convert(to="sixel", cols=wp.width, rows=wp.height)).strip()
|
155
|
+
if any(bbox):
|
168
156
|
from sixelcrop import sixelcrop
|
169
157
|
|
170
|
-
cell_size_x, cell_size_y = self.app.
|
158
|
+
cell_size_x, cell_size_y = self.app.cell_size_px
|
171
159
|
|
172
160
|
cmd = sixelcrop(
|
173
161
|
data=cmd,
|
@@ -176,42 +164,80 @@ class SixelGraphicControl(GraphicControl):
|
|
176
164
|
# Vertical pixel offset of the displayed image region
|
177
165
|
y=bbox.top * cell_size_y,
|
178
166
|
# Pixel width of the displayed image region
|
179
|
-
w=wp.width * cell_size_x,
|
167
|
+
w=(wp.width - bbox.left - bbox.right) * cell_size_x,
|
180
168
|
# Pixel height of the displayed image region
|
181
|
-
h=wp.height * cell_size_y,
|
169
|
+
h=(wp.height - bbox.top - bbox.bottom) * cell_size_y,
|
182
170
|
)
|
183
171
|
|
184
172
|
return cmd
|
185
173
|
|
186
174
|
def get_rendered_lines(
|
187
|
-
self,
|
175
|
+
self, visible_width: int, visible_height: int, wrap_lines: bool = False
|
188
176
|
) -> list[StyleAndTextTuples]:
|
189
177
|
"""Get rendered lines from the cache, or generate them."""
|
178
|
+
bbox = self.bbox
|
179
|
+
|
180
|
+
d_cols, d_aspect = self.datum.cell_size()
|
181
|
+
d_rows = d_cols * d_aspect
|
182
|
+
|
183
|
+
total_available_width = visible_width + bbox.left + bbox.right
|
184
|
+
total_available_height = visible_height + bbox.top + bbox.bottom
|
185
|
+
|
186
|
+
# Scale down the graphic to fit in the available space
|
187
|
+
if d_rows > total_available_height or d_cols > total_available_width:
|
188
|
+
if d_rows / total_available_height > d_cols / total_available_width:
|
189
|
+
ratio = min(1, total_available_height / d_rows)
|
190
|
+
else:
|
191
|
+
ratio = min(1, total_available_width / d_cols)
|
192
|
+
else:
|
193
|
+
ratio = 1
|
194
|
+
|
195
|
+
# Calculate the size and cropping bbox at which we want to display the graphic
|
196
|
+
cols = floor(d_cols * ratio)
|
197
|
+
rows = ceil(cols * d_aspect)
|
198
|
+
d_bbox = DiInt(
|
199
|
+
top=self.bbox.top,
|
200
|
+
right=max(0, cols - (total_available_width - self.bbox.right)),
|
201
|
+
bottom=max(0, rows - (total_available_height - self.bbox.bottom)),
|
202
|
+
left=self.bbox.left,
|
203
|
+
)
|
190
204
|
|
191
205
|
def render_lines() -> list[StyleAndTextTuples]:
|
192
206
|
"""Render the lines to display in the control."""
|
193
207
|
ft: list[StyleAndTextTuples] = []
|
194
|
-
if
|
195
|
-
cmd = self.convert_data(
|
196
|
-
BoundedWritePosition(0, 0, width, height, self.bbox)
|
197
|
-
)
|
208
|
+
if visible_height >= 0:
|
209
|
+
cmd = self.convert_data(BoundedWritePosition(0, 0, cols, rows, d_bbox))
|
198
210
|
ft.extend(
|
199
211
|
split_lines(
|
200
212
|
to_formatted_text(
|
201
213
|
[
|
202
214
|
# Move cursor down and across by image height and width
|
203
|
-
(
|
215
|
+
(
|
216
|
+
"",
|
217
|
+
"\n".join(
|
218
|
+
(visible_height) * [" " * (visible_width)]
|
219
|
+
),
|
220
|
+
),
|
204
221
|
# Save position, then move back
|
205
222
|
("[ZeroWidthEscape]", "\x1b[s"),
|
206
223
|
# Move cursor up if there is more than one line to display
|
207
224
|
*(
|
208
|
-
[
|
209
|
-
|
225
|
+
[
|
226
|
+
(
|
227
|
+
"[ZeroWidthEscape]",
|
228
|
+
f"\x1b[{visible_height - 1}A",
|
229
|
+
)
|
230
|
+
]
|
231
|
+
if visible_height > 1
|
210
232
|
else []
|
211
233
|
),
|
212
|
-
("[ZeroWidthEscape]", f"\x1b[{
|
234
|
+
("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
|
213
235
|
# Place the image without moving cursor
|
214
|
-
(
|
236
|
+
(
|
237
|
+
"[ZeroWidthEscape]",
|
238
|
+
passthrough(cmd, self.app.config),
|
239
|
+
),
|
240
|
+
# ("[ZeroWidthEscape]", "XXXXX"),
|
215
241
|
# Restore the last known cursor position (at the bottom)
|
216
242
|
("[ZeroWidthEscape]", "\x1b[u"),
|
217
243
|
]
|
@@ -221,9 +247,9 @@ class SixelGraphicControl(GraphicControl):
|
|
221
247
|
return ft
|
222
248
|
|
223
249
|
key = (
|
224
|
-
|
250
|
+
visible_width,
|
225
251
|
self.app.color_palette,
|
226
|
-
self.app.
|
252
|
+
self.app.cell_size_px,
|
227
253
|
self.bbox,
|
228
254
|
)
|
229
255
|
return self._format_cache.get(key, render_lines)
|
@@ -235,28 +261,20 @@ class ItermGraphicControl(GraphicControl):
|
|
235
261
|
def convert_data(self, wp: WritePosition) -> str:
|
236
262
|
"""Convert the graphic's data to base64 data."""
|
237
263
|
datum = self.datum
|
238
|
-
|
239
264
|
bbox = wp.bbox if isinstance(wp, BoundedWritePosition) else DiInt(0, 0, 0, 0)
|
240
|
-
full_width = wp.width + bbox.left + bbox.right
|
241
|
-
full_height = wp.height + bbox.top + bbox.bottom
|
242
|
-
|
243
265
|
# Crop image if necessary
|
244
266
|
if any(bbox):
|
245
267
|
import io
|
246
268
|
|
247
|
-
image = datum.convert(
|
248
|
-
to="pil",
|
249
|
-
cols=full_width,
|
250
|
-
rows=full_height,
|
251
|
-
)
|
269
|
+
image = datum.convert(to="pil", cols=wp.width, rows=wp.height)
|
252
270
|
if image is not None:
|
253
|
-
cell_size_x, cell_size_y = self.app.
|
271
|
+
cell_size_x, cell_size_y = self.app.cell_size_px
|
254
272
|
# Downscale image to fit target region for precise cropping
|
255
|
-
image.thumbnail((
|
256
|
-
left =
|
257
|
-
top =
|
258
|
-
right = (
|
259
|
-
bottom = (
|
273
|
+
image.thumbnail((wp.width * cell_size_x, wp.height * cell_size_y))
|
274
|
+
left = bbox.left * cell_size_x
|
275
|
+
top = bbox.top * cell_size_y
|
276
|
+
right = (wp.width - bbox.right) * cell_size_x
|
277
|
+
bottom = (wp.height - bbox.bottom) * cell_size_y
|
260
278
|
upper, lower = sorted((top, bottom))
|
261
279
|
image = image.crop((left, upper, right, lower))
|
262
280
|
with io.BytesIO() as output:
|
@@ -266,43 +284,81 @@ class ItermGraphicControl(GraphicControl):
|
|
266
284
|
if datum.format.startswith("base64-"):
|
267
285
|
b64data = datum.data
|
268
286
|
else:
|
269
|
-
b64data = datum.convert(
|
270
|
-
to="base64-png",
|
271
|
-
cols=full_width,
|
272
|
-
rows=full_height,
|
273
|
-
)
|
287
|
+
b64data = datum.convert(to="base64-png", cols=wp.width, rows=wp.height)
|
274
288
|
return b64data.replace("\n", "").strip()
|
275
289
|
|
276
290
|
def get_rendered_lines(
|
277
|
-
self,
|
291
|
+
self, visible_width: int, visible_height: int, wrap_lines: bool = False
|
278
292
|
) -> list[StyleAndTextTuples]:
|
279
293
|
"""Get rendered lines from the cache, or generate them."""
|
294
|
+
bbox = self.bbox
|
295
|
+
|
296
|
+
d_cols, d_aspect = self.datum.cell_size()
|
297
|
+
d_rows = d_cols * d_aspect
|
298
|
+
|
299
|
+
total_available_width = visible_width + bbox.left + bbox.right
|
300
|
+
total_available_height = visible_height + bbox.top + bbox.bottom
|
301
|
+
|
302
|
+
# Scale down the graphic to fit in the available space
|
303
|
+
if d_rows > total_available_height or d_cols > total_available_width:
|
304
|
+
if d_rows / total_available_height > d_cols / total_available_width:
|
305
|
+
ratio = min(1, total_available_height / d_rows)
|
306
|
+
else:
|
307
|
+
ratio = min(1, total_available_width / d_cols)
|
308
|
+
else:
|
309
|
+
ratio = 1
|
310
|
+
|
311
|
+
# Calculate the size and cropping bbox at which we want to display the graphic
|
312
|
+
cols = floor(d_cols * ratio)
|
313
|
+
rows = ceil(cols * d_aspect)
|
314
|
+
d_bbox = DiInt(
|
315
|
+
top=self.bbox.top,
|
316
|
+
right=max(0, cols - (total_available_width - self.bbox.right)),
|
317
|
+
bottom=max(0, rows - (total_available_height - self.bbox.bottom)),
|
318
|
+
left=self.bbox.left,
|
319
|
+
)
|
280
320
|
|
281
321
|
def render_lines() -> list[StyleAndTextTuples]:
|
282
322
|
"""Render the lines to display in the control."""
|
283
323
|
ft: list[StyleAndTextTuples] = []
|
284
|
-
if
|
324
|
+
if (
|
325
|
+
rows - d_bbox.top - d_bbox.bottom > 0
|
326
|
+
and cols - d_bbox.left - d_bbox.right > 0
|
327
|
+
):
|
285
328
|
b64data = self.convert_data(
|
286
|
-
BoundedWritePosition(0, 0,
|
329
|
+
BoundedWritePosition(0, 0, cols, rows, d_bbox)
|
287
330
|
)
|
288
|
-
cmd = f"\x1b]1337;File=inline=1;width={
|
331
|
+
cmd = f"\x1b]1337;File=inline=1;width={cols}:{b64data}\a"
|
289
332
|
ft.extend(
|
290
333
|
split_lines(
|
291
334
|
to_formatted_text(
|
292
335
|
[
|
293
336
|
# Move cursor down and across by image height and width
|
294
|
-
(
|
337
|
+
(
|
338
|
+
"",
|
339
|
+
"\n".join(
|
340
|
+
(visible_height) * [" " * (visible_width)]
|
341
|
+
),
|
342
|
+
),
|
295
343
|
# Save position, then move back
|
296
344
|
("[ZeroWidthEscape]", "\x1b[s"),
|
297
345
|
# Move cursor up if there is more than one line to display
|
298
346
|
*(
|
299
|
-
[
|
300
|
-
|
347
|
+
[
|
348
|
+
(
|
349
|
+
"[ZeroWidthEscape]",
|
350
|
+
f"\x1b[{visible_height - 1}A",
|
351
|
+
)
|
352
|
+
]
|
353
|
+
if visible_height > 1
|
301
354
|
else []
|
302
355
|
),
|
303
|
-
("[ZeroWidthEscape]", f"\x1b[{
|
356
|
+
("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
|
304
357
|
# Place the image without moving cursor
|
305
|
-
(
|
358
|
+
(
|
359
|
+
"[ZeroWidthEscape]",
|
360
|
+
passthrough(cmd, self.app.config),
|
361
|
+
),
|
306
362
|
# Restore the last known cursor position (at the bottom)
|
307
363
|
("[ZeroWidthEscape]", "\x1b[u"),
|
308
364
|
]
|
@@ -312,16 +368,16 @@ class ItermGraphicControl(GraphicControl):
|
|
312
368
|
return ft
|
313
369
|
|
314
370
|
key = (
|
315
|
-
|
371
|
+
visible_width,
|
316
372
|
self.app.color_palette,
|
317
|
-
self.app.
|
373
|
+
self.app.cell_size_px,
|
318
374
|
self.bbox,
|
319
375
|
)
|
320
376
|
return self._format_cache.get(key, render_lines)
|
321
377
|
|
322
378
|
|
323
|
-
class
|
324
|
-
"""
|
379
|
+
class BaseKittyGraphicControl(GraphicControl):
|
380
|
+
"""Base graphic control with common methods for both styles of kitty display."""
|
325
381
|
|
326
382
|
_kitty_image_count: ClassVar[int] = 1
|
327
383
|
|
@@ -369,7 +425,7 @@ class KittyGraphicControl(GraphicControl):
|
|
369
425
|
full_width = wp.width + bbox.left + bbox.right
|
370
426
|
full_height = wp.height + bbox.top + bbox.bottom
|
371
427
|
|
372
|
-
datum = self._datum_pad_cache[(self.datum, *self.app.
|
428
|
+
datum = self._datum_pad_cache[(self.datum, *self.app.cell_size_px)]
|
373
429
|
return str(
|
374
430
|
datum.convert(
|
375
431
|
to="base64-png",
|
@@ -389,15 +445,13 @@ class KittyGraphicControl(GraphicControl):
|
|
389
445
|
cmd += "\x1b\\"
|
390
446
|
return cmd
|
391
447
|
|
392
|
-
def load(self, rows: int, cols: int) -> None:
|
448
|
+
def load(self, rows: int, cols: int, bbox: DiInt) -> None:
|
393
449
|
"""Send the graphic to the terminal without displaying it."""
|
394
|
-
# global _kitty_image_count
|
395
|
-
|
396
450
|
data = self.convert_data(
|
397
|
-
BoundedWritePosition(0, 0, width=cols, height=rows, bbox=
|
451
|
+
BoundedWritePosition(0, 0, width=cols, height=rows, bbox=bbox)
|
398
452
|
)
|
399
453
|
self.kitty_image_id = self._kitty_image_count
|
400
|
-
|
454
|
+
self.__class__._kitty_image_count += 1
|
401
455
|
|
402
456
|
while data:
|
403
457
|
chunk, data = data[:4096], data[4096:]
|
@@ -413,25 +467,10 @@ class KittyGraphicControl(GraphicControl):
|
|
413
467
|
C=1, # Do not move the cursor
|
414
468
|
m=1 if data else 0, # Data will be chunked
|
415
469
|
)
|
416
|
-
self.app.output.write_raw(passthrough(cmd))
|
470
|
+
self.app.output.write_raw(passthrough(cmd, self.app.config))
|
417
471
|
self.app.output.flush()
|
418
472
|
self.loaded = True
|
419
473
|
|
420
|
-
def hide(self) -> None:
|
421
|
-
"""Hide the graphic from show without deleting it."""
|
422
|
-
if self.kitty_image_id > 0:
|
423
|
-
self.app.output.write_raw(
|
424
|
-
passthrough(
|
425
|
-
self._kitty_cmd(
|
426
|
-
a="d",
|
427
|
-
d="i",
|
428
|
-
i=self.kitty_image_id,
|
429
|
-
q=1,
|
430
|
-
)
|
431
|
-
)
|
432
|
-
)
|
433
|
-
self.app.output.flush()
|
434
|
-
|
435
474
|
def delete(self) -> None:
|
436
475
|
"""Delete the graphic from the terminal."""
|
437
476
|
if self.kitty_image_id > 0:
|
@@ -442,97 +481,306 @@ class KittyGraphicControl(GraphicControl):
|
|
442
481
|
d="I",
|
443
482
|
i=self.kitty_image_id,
|
444
483
|
q=2,
|
445
|
-
)
|
484
|
+
),
|
485
|
+
self.app.config,
|
446
486
|
)
|
447
487
|
)
|
448
488
|
self.app.output.flush()
|
449
489
|
self.loaded = False
|
450
490
|
|
491
|
+
def reset(self) -> None:
|
492
|
+
"""Hide and delete the kitty graphic from the terminal."""
|
493
|
+
self.hide()
|
494
|
+
self.delete()
|
495
|
+
super().reset()
|
496
|
+
|
497
|
+
def close(self) -> None:
|
498
|
+
"""Remove the displayed object entirely."""
|
499
|
+
super().close()
|
500
|
+
if not self.app.leave_graphics():
|
501
|
+
self.delete()
|
502
|
+
|
503
|
+
|
504
|
+
class KittyGraphicControl(BaseKittyGraphicControl):
|
505
|
+
"""A graphic control which displays images using Kitty's graphics protocol."""
|
506
|
+
|
451
507
|
def get_rendered_lines(
|
452
|
-
self,
|
508
|
+
self, visible_width: int, visible_height: int, wrap_lines: bool = False
|
453
509
|
) -> list[StyleAndTextTuples]:
|
454
510
|
"""Get rendered lines from the cache, or generate them."""
|
455
|
-
|
456
|
-
# images at this point rather than just loading them once
|
457
|
-
if not self.loaded:
|
458
|
-
self.load(cols=width, rows=height)
|
511
|
+
bbox = self.bbox
|
459
512
|
|
460
|
-
cell_size_px = self.app.
|
513
|
+
cell_size_px = self.app.cell_size_px
|
461
514
|
datum = self._datum_pad_cache[(self.datum, *cell_size_px)]
|
462
515
|
px, py = datum.pixel_size()
|
463
|
-
|
516
|
+
# Fall back to a default pixel size
|
464
517
|
px = px or 100
|
465
518
|
py = py or 100
|
466
519
|
|
520
|
+
d_cols, d_aspect = datum.cell_size()
|
521
|
+
d_rows = d_cols * d_aspect
|
522
|
+
|
523
|
+
total_available_width = visible_width + bbox.left + bbox.right
|
524
|
+
total_available_height = visible_height + bbox.top + bbox.bottom
|
525
|
+
|
526
|
+
# Scale down the graphic to fit in the available space
|
527
|
+
if d_rows > total_available_height or d_cols > total_available_width:
|
528
|
+
if d_rows / total_available_height > d_cols / total_available_width:
|
529
|
+
ratio = min(1, total_available_height / d_rows)
|
530
|
+
else:
|
531
|
+
ratio = min(1, total_available_width / d_cols)
|
532
|
+
else:
|
533
|
+
ratio = 1
|
534
|
+
|
535
|
+
# Calculate the size and cropping bbox at which we want to display the graphic
|
536
|
+
cols = floor(d_cols * ratio)
|
537
|
+
rows = ceil(cols * d_aspect)
|
538
|
+
d_bbox = DiInt(
|
539
|
+
top=self.bbox.top,
|
540
|
+
right=max(0, cols - (total_available_width - self.bbox.right)),
|
541
|
+
bottom=max(0, rows - (total_available_height - self.bbox.bottom)),
|
542
|
+
left=self.bbox.left,
|
543
|
+
)
|
544
|
+
if not self.loaded:
|
545
|
+
self.load(cols=cols, rows=rows, bbox=d_bbox)
|
546
|
+
|
467
547
|
def render_lines() -> list[StyleAndTextTuples]:
|
468
548
|
"""Render the lines to display in the control."""
|
469
549
|
ft: list[StyleAndTextTuples] = []
|
470
|
-
if
|
471
|
-
full_width = width + self.bbox.left + self.bbox.right
|
472
|
-
full_height = height + self.bbox.top + self.bbox.bottom
|
550
|
+
if display_rows := rows - d_bbox.top - d_bbox.bottom:
|
473
551
|
cmd = self._kitty_cmd(
|
474
552
|
a="p", # Display a previously transmitted image
|
475
553
|
i=self.kitty_image_id,
|
476
554
|
p=1, # Placement ID
|
477
555
|
m=0, # No batches remaining
|
478
556
|
q=2, # No backchat
|
479
|
-
c=
|
480
|
-
r=
|
557
|
+
c=cols - d_bbox.left - d_bbox.right,
|
558
|
+
r=display_rows,
|
481
559
|
C=1, # 1 = Do move the cursor
|
482
560
|
# Horizontal pixel offset of the displayed image region
|
483
|
-
x=int(px *
|
561
|
+
x=int(px * d_bbox.left / cols),
|
484
562
|
# Vertical pixel offset of the displayed image region
|
485
|
-
y=int(py *
|
563
|
+
y=int(py * d_bbox.top / rows),
|
486
564
|
# Pixel width of the displayed image region
|
487
|
-
w=int(px *
|
565
|
+
w=int(px * (cols - d_bbox.left - d_bbox.right) / cols),
|
488
566
|
# Pixel height of the displayed image region
|
489
|
-
h=int(py *
|
490
|
-
# z=-(2**30) - 1,
|
567
|
+
h=int(py * (rows - d_bbox.top - d_bbox.bottom) / rows),
|
491
568
|
)
|
492
569
|
ft.extend(
|
493
570
|
split_lines(
|
494
571
|
to_formatted_text(
|
495
572
|
[
|
496
573
|
# Move cursor down and acoss by image height and width
|
497
|
-
(
|
574
|
+
(
|
575
|
+
"",
|
576
|
+
"\n".join(
|
577
|
+
(visible_height) * [" " * (visible_width)]
|
578
|
+
),
|
579
|
+
),
|
498
580
|
# Save position, then move back
|
499
581
|
("[ZeroWidthEscape]", "\x1b[s"),
|
500
582
|
# Move cursor up if there is more than one line to display
|
501
583
|
*(
|
502
|
-
[
|
503
|
-
|
584
|
+
[
|
585
|
+
(
|
586
|
+
"[ZeroWidthEscape]",
|
587
|
+
f"\x1b[{visible_height - 1}A",
|
588
|
+
)
|
589
|
+
]
|
590
|
+
if visible_height > 1
|
504
591
|
else []
|
505
592
|
),
|
506
|
-
("[ZeroWidthEscape]", f"\x1b[{
|
593
|
+
("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
|
507
594
|
# Place the image without moving cursor
|
508
|
-
(
|
595
|
+
(
|
596
|
+
"[ZeroWidthEscape]",
|
597
|
+
passthrough(cmd, self.app.config),
|
598
|
+
),
|
509
599
|
# Restore the last known cursor position (at the bottom)
|
510
600
|
("[ZeroWidthEscape]", "\x1b[u"),
|
511
601
|
]
|
512
602
|
)
|
513
603
|
)
|
514
604
|
)
|
605
|
+
else:
|
606
|
+
ft.append([("[ZeroWidthEscape]", self.hide_cmd())])
|
515
607
|
return ft
|
516
608
|
|
517
609
|
key = (
|
518
|
-
|
610
|
+
visible_width,
|
519
611
|
self.app.color_palette,
|
520
|
-
self.app.
|
612
|
+
self.app.cell_size_px,
|
521
613
|
self.bbox,
|
522
614
|
)
|
523
615
|
return self._format_cache.get(key, render_lines)
|
524
616
|
|
525
|
-
def
|
526
|
-
"""
|
527
|
-
|
528
|
-
|
529
|
-
|
617
|
+
def hide_cmd(self) -> str:
|
618
|
+
"""Generate a command to hide the graphic."""
|
619
|
+
return passthrough(
|
620
|
+
self._kitty_cmd(
|
621
|
+
a="d",
|
622
|
+
d="i",
|
623
|
+
i=self.kitty_image_id,
|
624
|
+
q=1,
|
625
|
+
),
|
626
|
+
self.app.config,
|
627
|
+
)
|
530
628
|
|
531
|
-
def
|
532
|
-
"""
|
533
|
-
|
534
|
-
|
535
|
-
self.
|
629
|
+
def hide(self) -> None:
|
630
|
+
"""Hide the graphic from show without deleting it."""
|
631
|
+
if self.kitty_image_id > 0:
|
632
|
+
self.app.output.write_raw(self.hide_cmd())
|
633
|
+
self.app.output.flush()
|
634
|
+
|
635
|
+
|
636
|
+
class KittyUnicodeGraphicControl(BaseKittyGraphicControl):
|
637
|
+
"""A graphic control which displays images using Kitty's Unicode placeholder mechanism."""
|
638
|
+
|
639
|
+
PLACEHOLDER = "\U0010eeee" # U+10EEEE placeholder character
|
640
|
+
# fmt: off
|
641
|
+
DIACRITICS = ( # Diacritics for encoding row/column numbers (0-9)
|
642
|
+
"\u0305", "\u030d", "\u030e", "\u0310", "\u0312", "\u033d", "\u033e", "\u033f",
|
643
|
+
"\u0346", "\u034a", "\u034b", "\u034c", "\u0350", "\u0351", "\u0352", "\u0357",
|
644
|
+
"\u035b", "\u0363", "\u0364", "\u0365", "\u0366", "\u0367", "\u0368", "\u0369",
|
645
|
+
"\u036a", "\u036b", "\u036c", "\u036d", "\u036e", "\u036f", "\u0483", "\u0484",
|
646
|
+
"\u0485", "\u0486", "\u0487", "\u0592", "\u0593", "\u0594", "\u0595", "\u0597",
|
647
|
+
"\u0598", "\u0599", "\u059c", "\u059d", "\u059e", "\u059f", "\u05a0", "\u05a1",
|
648
|
+
"\u05a8", "\u05a9", "\u05ab", "\u05ac", "\u05af", "\u05c4", "\u0610", "\u0611",
|
649
|
+
"\u0612", "\u0613", "\u0614", "\u0615", "\u0616", "\u0617", "\u0657", "\u0658",
|
650
|
+
"\u0659", "\u065a", "\u065b", "\u065d", "\u065e", "\u06d6", "\u06d7", "\u06d8",
|
651
|
+
"\u06d9", "\u06da", "\u06db", "\u06dc", "\u06df", "\u06e0", "\u06e1", "\u06e2",
|
652
|
+
"\u06e4", "\u06e7", "\u06e8", "\u06eb", "\u06ec", "\u0730", "\u0732", "\u0733",
|
653
|
+
"\u0735", "\u0736", "\u073a", "\u073d", "\u073f", "\u0740", "\u0741", "\u0743",
|
654
|
+
"\u0745", "\u0747", "\u0749", "\u074a", "\u07eb", "\u07ec", "\u07ed", "\u07ee",
|
655
|
+
"\u07ef", "\u07f0", "\u07f1", "\u07f3", "\u0816", "\u0817", "\u0818", "\u0819",
|
656
|
+
"\u081b", "\u081c", "\u081d", "\u081e", "\u081f", "\u0820", "\u0821", "\u0822",
|
657
|
+
"\u0823", "\u0825", "\u0826", "\u0827", "\u0829", "\u082a", "\u082b", "\u082c",
|
658
|
+
"\u082d", "\u0951", "\u0953", "\u0954", "\u0f82", "\u0f83", "\u0f86", "\u0f87",
|
659
|
+
"\u135d", "\u135e", "\u135f", "\u17dd", "\u193a", "\u1a17", "\u1a75", "\u1a76",
|
660
|
+
"\u1a77", "\u1a78", "\u1a79", "\u1a7a", "\u1a7b", "\u1a7c", "\u1b6b", "\u1b6d",
|
661
|
+
"\u1b6e", "\u1b6f", "\u1b70", "\u1b71", "\u1b72", "\u1b73", "\u1cd0", "\u1cd1",
|
662
|
+
"\u1cd2", "\u1cda", "\u1cdb", "\u1ce0", "\u1dc0", "\u1dc1", "\u1dc3", "\u1dc4",
|
663
|
+
"\u1dc5", "\u1dc6", "\u1dc7", "\u1dc8", "\u1dc9", "\u1dcb", "\u1dcc", "\u1dd1",
|
664
|
+
"\u1dd2", "\u1dd3", "\u1dd4", "\u1dd5", "\u1dd6", "\u1dd7", "\u1dd8", "\u1dd9",
|
665
|
+
"\u1dda", "\u1ddb", "\u1ddc", "\u1ddd", "\u1dde", "\u1ddf", "\u1de0", "\u1de1",
|
666
|
+
"\u1de2", "\u1de3", "\u1de4", "\u1de5", "\u1de6", "\u1dfe", "\u20d0", "\u20d1",
|
667
|
+
"\u20d4", "\u20d5", "\u20d6", "\u20d7", "\u20db", "\u20dc", "\u20e1", "\u20e7",
|
668
|
+
"\u20e9", "\u20f0", "\u2cef", "\u2cf0", "\u2cf1", "\u2de0", "\u2de1", "\u2de2",
|
669
|
+
"\u2de3", "\u2de4", "\u2de5", "\u2de6", "\u2de7", "\u2de8", "\u2de9", "\u2dea",
|
670
|
+
"\u2deb", "\u2dec", "\u2ded", "\u2dee", "\u2def", "\u2df0", "\u2df1", "\u2df2",
|
671
|
+
"\u2df3", "\u2df4", "\u2df5", "\u2df6", "\u2df7", "\u2df8", "\u2df9", "\u2dfa",
|
672
|
+
"\u2dfb", "\u2dfc", "\u2dfd", "\u2dfe", "\u2dff", "\ua66f", "\ua67c", "\ua67d",
|
673
|
+
"\ua6f0", "\ua6f1", "\ua8e0", "\ua8e1", "\ua8e2", "\ua8e3", "\ua8e4", "\ua8e5",
|
674
|
+
"\ua8e6", "\ua8e7", "\ua8e8", "\ua8e9", "\ua8ea", "\ua8eb", "\ua8ec", "\ua8ed",
|
675
|
+
"\ua8ee", "\ua8ef", "\ua8f0", "\ua8f1", "\uaab0", "\uaab2", "\uaab3", "\uaab7",
|
676
|
+
"\uaab8", "\uaabe", "\uaabf", "\uaac1", "\ufe20", "\ufe21", "\ufe22", "\ufe23",
|
677
|
+
"\ufe24", "\ufe25", "\ufe26",
|
678
|
+
"\U00010a0f", "\U00010a38", "\U0001d185", "\U0001d186", "\U0001d187",
|
679
|
+
"\U0001d188", "\U0001d189", "\U0001d1aa", "\U0001d1ab", "\U0001d1ac",
|
680
|
+
"\U0001d1ad", "\U0001d242", "\U0001d243", "\U0001d244",
|
681
|
+
)
|
682
|
+
# fmt: on
|
683
|
+
|
684
|
+
def __init__(
|
685
|
+
self,
|
686
|
+
datum: Datum,
|
687
|
+
scale: float = 0,
|
688
|
+
bbox: DiInt | None = None,
|
689
|
+
) -> None:
|
690
|
+
"""Create a new kitty graphic instance."""
|
691
|
+
super().__init__(datum, scale, bbox)
|
692
|
+
self.placements: set[tuple[int, int]] = set()
|
693
|
+
|
694
|
+
def get_rendered_lines(
|
695
|
+
self, visible_width: int, visible_height: int, wrap_lines: bool = False
|
696
|
+
) -> list[StyleAndTextTuples]:
|
697
|
+
"""Get rendered lines from the cache, or generate them."""
|
698
|
+
bbox = self.bbox
|
699
|
+
|
700
|
+
cell_size_px = self.app.cell_size_px
|
701
|
+
datum = self._datum_pad_cache[(self.datum, *cell_size_px)]
|
702
|
+
px, py = datum.pixel_size()
|
703
|
+
# Fall back to a default pixel size
|
704
|
+
px = px or 100
|
705
|
+
py = py or 100
|
706
|
+
|
707
|
+
d_cols, d_aspect = datum.cell_size()
|
708
|
+
d_rows = d_cols * d_aspect
|
709
|
+
|
710
|
+
total_available_width = visible_width + bbox.left + bbox.right
|
711
|
+
total_available_height = visible_height + bbox.top + bbox.bottom
|
712
|
+
|
713
|
+
# Scale down the graphic to fit in the available space
|
714
|
+
if d_rows > total_available_height or d_cols > total_available_width:
|
715
|
+
if d_rows / total_available_height > d_cols / total_available_width:
|
716
|
+
ratio = min(1, total_available_height / d_rows)
|
717
|
+
else:
|
718
|
+
ratio = min(1, total_available_width / d_cols)
|
719
|
+
else:
|
720
|
+
ratio = 1
|
721
|
+
|
722
|
+
# Calculate the size and cropping bbox at which we want to display the graphic
|
723
|
+
cols = floor(d_cols * ratio)
|
724
|
+
rows = ceil(cols * d_aspect)
|
725
|
+
if not self.loaded:
|
726
|
+
self.load(cols=cols, rows=rows, bbox=DiInt(0, 0, 0, 0))
|
727
|
+
|
728
|
+
# Add virtual placement for this size if required
|
729
|
+
if (cols, rows) not in self.placements:
|
730
|
+
cmd = self._kitty_cmd(
|
731
|
+
a="p", # Display a previously transmitted image
|
732
|
+
i=self.kitty_image_id,
|
733
|
+
p=1, # Placement ID
|
734
|
+
U=1, # Create a virtual placement
|
735
|
+
c=cols,
|
736
|
+
r=rows,
|
737
|
+
q=2,
|
738
|
+
)
|
739
|
+
self.app.output.write_raw(passthrough(cmd, self.app.config))
|
740
|
+
self.app.output.flush()
|
741
|
+
self.placements.add((cols, rows))
|
742
|
+
|
743
|
+
def render_lines() -> list[StyleAndTextTuples]:
|
744
|
+
"""Render the lines to display in the control."""
|
745
|
+
ft: StyleAndTextTuples = []
|
746
|
+
|
747
|
+
# Generate placeholder grid
|
748
|
+
col_start = bbox.left
|
749
|
+
col_stop = cols - bbox.right
|
750
|
+
placeholder = self.PLACEHOLDER
|
751
|
+
diacritics = self.DIACRITICS
|
752
|
+
for row in range(bbox.top, rows - bbox.bottom):
|
753
|
+
for col in range(col_start, col_stop):
|
754
|
+
ft.extend(
|
755
|
+
[
|
756
|
+
# We set the ptk-color for the last column so the renderer
|
757
|
+
# knows to change the color back after this gets rendered.
|
758
|
+
("fg:#888" if col == col_stop - 1 else "", " "),
|
759
|
+
(
|
760
|
+
"[ZeroWidthEscape]",
|
761
|
+
# We move the cursor back a cell before writing the
|
762
|
+
# kitty unicode char using a ZWE
|
763
|
+
"\b"
|
764
|
+
# Set the kitty graphic and placement we want to render
|
765
|
+
# by manually setting an 8-bit foregroun color.
|
766
|
+
# The placement ID is set to 1 using underline color.
|
767
|
+
f"\x1b[38;5;{self.kitty_image_id}m\x1b[58;1m"
|
768
|
+
# Writing the unicode char moves the cursor forward
|
769
|
+
# again to where the renderer expects it to be
|
770
|
+
f"{placeholder}{diacritics[row]}{diacritics[col]}",
|
771
|
+
),
|
772
|
+
]
|
773
|
+
)
|
774
|
+
ft.append(("", "\n"))
|
775
|
+
return list(split_lines(ft))
|
776
|
+
|
777
|
+
key = (
|
778
|
+
visible_width,
|
779
|
+
self.app.color_palette,
|
780
|
+
self.app.cell_size_px,
|
781
|
+
bbox,
|
782
|
+
)
|
783
|
+
return self._format_cache.get(key, render_lines)
|
536
784
|
|
537
785
|
|
538
786
|
class NotVisible(Exception):
|
@@ -649,14 +897,13 @@ def select_graphic_control(format_: str) -> type[GraphicControl] | None:
|
|
649
897
|
"""Determine which graphic control to use."""
|
650
898
|
SelectedGraphicControl: type[GraphicControl] | None = None
|
651
899
|
app = get_app()
|
652
|
-
term_info = app.term_info
|
653
900
|
preferred_graphics_protocol = app.config.graphics
|
654
901
|
useable_graphics_controls: list[type[GraphicControl]] = []
|
655
902
|
_in_mplex = in_mplex()
|
656
903
|
force_graphics = app.config.force_graphics
|
657
904
|
|
658
905
|
if preferred_graphics_protocol != "none":
|
659
|
-
if (
|
906
|
+
if (app.term_graphics_iterm or force_graphics) and find_route(
|
660
907
|
format_, "base64-png"
|
661
908
|
):
|
662
909
|
useable_graphics_controls.append(ItermGraphicControl)
|
@@ -664,23 +911,29 @@ def select_graphic_control(format_: str) -> type[GraphicControl] | None:
|
|
664
911
|
preferred_graphics_protocol == "iterm"
|
665
912
|
and ItermGraphicControl in useable_graphics_controls
|
666
913
|
# Iterm does not work in mplex without pass-through
|
667
|
-
and (not _in_mplex or (_in_mplex and
|
914
|
+
and (not _in_mplex or (_in_mplex and force_graphics))
|
668
915
|
):
|
669
916
|
SelectedGraphicControl = ItermGraphicControl
|
670
917
|
elif (
|
671
|
-
(
|
918
|
+
(app.term_graphics_kitty or force_graphics)
|
672
919
|
and find_route(format_, "base64-png")
|
673
920
|
# Kitty does not work in mplex without pass-through
|
674
|
-
and (not _in_mplex or (_in_mplex and
|
921
|
+
and (not _in_mplex or (_in_mplex and force_graphics))
|
675
922
|
):
|
676
923
|
useable_graphics_controls.append(KittyGraphicControl)
|
924
|
+
useable_graphics_controls.append(KittyUnicodeGraphicControl)
|
677
925
|
if (
|
678
926
|
preferred_graphics_protocol == "kitty"
|
679
927
|
and KittyGraphicControl in useable_graphics_controls
|
680
928
|
):
|
681
929
|
SelectedGraphicControl = KittyGraphicControl
|
930
|
+
elif (
|
931
|
+
preferred_graphics_protocol == "kitty-unicode"
|
932
|
+
and KittyUnicodeGraphicControl in useable_graphics_controls
|
933
|
+
):
|
934
|
+
SelectedGraphicControl = KittyUnicodeGraphicControl
|
682
935
|
# Tmux now supports sixels (>=3.4)
|
683
|
-
elif (
|
936
|
+
elif (app.term_graphics_sixel or force_graphics) and find_route(
|
684
937
|
format_, "sixel"
|
685
938
|
):
|
686
939
|
useable_graphics_controls.append(SixelGraphicControl)
|