euporie 2.6.1__py3-none-any.whl → 2.7.0__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/tabs/console.py +51 -43
- euporie/core/__init__.py +5 -2
- euporie/core/app.py +74 -57
- euporie/core/comm/ipywidgets.py +7 -3
- euporie/core/config.py +51 -27
- euporie/core/convert/__init__.py +2 -0
- euporie/core/convert/datum.py +82 -45
- euporie/core/convert/formats/ansi.py +1 -2
- euporie/core/convert/formats/common.py +7 -11
- euporie/core/convert/formats/ft.py +10 -7
- euporie/core/convert/formats/png.py +7 -6
- euporie/core/convert/formats/sixel.py +1 -1
- euporie/core/convert/formats/svg.py +28 -0
- euporie/core/convert/mime.py +4 -7
- euporie/core/data_structures.py +24 -22
- euporie/core/filters.py +16 -2
- euporie/core/format.py +30 -4
- euporie/core/ft/ansi.py +2 -1
- euporie/core/ft/html.py +155 -42
- euporie/core/{widgets/graphics.py → graphics.py} +225 -227
- euporie/core/io.py +8 -0
- euporie/core/key_binding/bindings/__init__.py +8 -2
- euporie/core/key_binding/bindings/basic.py +9 -14
- euporie/core/key_binding/bindings/micro.py +0 -12
- euporie/core/key_binding/bindings/mouse.py +107 -80
- euporie/core/key_binding/bindings/page_navigation.py +129 -0
- euporie/core/key_binding/key_processor.py +9 -1
- euporie/core/layout/__init__.py +1 -0
- euporie/core/layout/containers.py +1011 -0
- euporie/core/layout/decor.py +381 -0
- euporie/core/layout/print.py +130 -0
- euporie/core/layout/screen.py +75 -0
- euporie/core/{widgets/page.py → layout/scroll.py} +166 -111
- euporie/core/log.py +1 -1
- euporie/core/margins.py +11 -5
- euporie/core/path.py +43 -176
- euporie/core/renderer.py +31 -8
- euporie/core/style.py +2 -0
- euporie/core/tabs/base.py +2 -1
- euporie/core/terminal.py +19 -21
- euporie/core/widgets/cell.py +2 -4
- euporie/core/widgets/cell_outputs.py +2 -2
- euporie/core/widgets/decor.py +3 -359
- euporie/core/widgets/dialog.py +5 -5
- euporie/core/widgets/display.py +32 -12
- euporie/core/widgets/file_browser.py +3 -4
- euporie/core/widgets/forms.py +36 -14
- euporie/core/widgets/inputs.py +171 -99
- euporie/core/widgets/layout.py +80 -5
- euporie/core/widgets/menu.py +1 -3
- euporie/core/widgets/pager.py +3 -3
- euporie/core/widgets/palette.py +3 -2
- euporie/core/widgets/status_bar.py +2 -6
- euporie/core/widgets/tree.py +3 -6
- euporie/notebook/app.py +8 -8
- euporie/notebook/tabs/notebook.py +2 -2
- euporie/notebook/widgets/side_bar.py +1 -1
- euporie/preview/tabs/notebook.py +2 -2
- euporie/web/tabs/web.py +6 -1
- euporie/web/widgets/webview.py +52 -32
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/METADATA +9 -11
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/RECORD +67 -60
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/WHEEL +1 -1
- {euporie-2.6.1.data → euporie-2.7.0.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.6.1.data → euporie-2.7.0.data}/data/share/applications/euporie-notebook.desktop +0 -0
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/entry_points.txt +0 -0
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -25,17 +25,13 @@ from euporie.core.convert.datum import Datum
|
|
25
25
|
from euporie.core.convert.registry import find_route
|
26
26
|
from euporie.core.current import get_app
|
27
27
|
from euporie.core.data_structures import DiInt
|
28
|
-
from euporie.core.filters import
|
29
|
-
has_dialog,
|
30
|
-
has_menus,
|
31
|
-
in_tmux,
|
32
|
-
)
|
28
|
+
from euporie.core.filters import has_dialog, has_menus, in_mplex
|
33
29
|
from euporie.core.ft.utils import _ZERO_WIDTH_FRAGMENTS
|
34
|
-
from euporie.core.
|
35
|
-
from euporie.core.
|
30
|
+
from euporie.core.layout.scroll import BoundedWritePosition
|
31
|
+
from euporie.core.terminal import passthrough
|
36
32
|
|
37
33
|
if TYPE_CHECKING:
|
38
|
-
from typing import Any, Callable
|
34
|
+
from typing import Any, Callable, ClassVar
|
39
35
|
|
40
36
|
from prompt_toolkit.filters import FilterOrBool
|
41
37
|
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
@@ -82,6 +78,11 @@ class GraphicControl(UIControl, metaclass=ABCMeta):
|
|
82
78
|
self.rendered_lines = self.get_rendered_lines(width, max_available_height)
|
83
79
|
return len(self.rendered_lines)
|
84
80
|
|
81
|
+
@abstractmethod
|
82
|
+
def convert_data(self, wp: WritePosition) -> str:
|
83
|
+
"""Convert datum to required format."""
|
84
|
+
return ""
|
85
|
+
|
85
86
|
@abstractmethod
|
86
87
|
def get_rendered_lines(
|
87
88
|
self, width: int, height: int, wrap_lines: bool = False
|
@@ -146,81 +147,91 @@ class GraphicControl(UIControl, metaclass=ABCMeta):
|
|
146
147
|
class SixelGraphicControl(GraphicControl):
|
147
148
|
"""A graphic control which displays images as sixels."""
|
148
149
|
|
150
|
+
def convert_data(self, wp: WritePosition) -> str:
|
151
|
+
"""Convert datum to required format."""
|
152
|
+
bbox = wp.bbox if isinstance(wp, BoundedWritePosition) else DiInt(0, 0, 0, 0)
|
153
|
+
full_width = wp.width + bbox.left + bbox.right
|
154
|
+
full_height = wp.height + bbox.top + bbox.bottom
|
155
|
+
cmd = str(
|
156
|
+
self.datum.convert(
|
157
|
+
to="sixel",
|
158
|
+
cols=full_width,
|
159
|
+
rows=full_height,
|
160
|
+
)
|
161
|
+
)
|
162
|
+
if any(self.bbox):
|
163
|
+
from sixelcrop import sixelcrop
|
164
|
+
|
165
|
+
cell_size_x, cell_size_y = self.app.term_info.cell_size_px
|
166
|
+
|
167
|
+
cmd = sixelcrop(
|
168
|
+
data=cmd,
|
169
|
+
# Horizontal pixel offset of the displayed image region
|
170
|
+
x=bbox.left * cell_size_x,
|
171
|
+
# Vertical pixel offset of the displayed image region
|
172
|
+
y=bbox.top * cell_size_y,
|
173
|
+
# Pixel width of the displayed image region
|
174
|
+
w=wp.width * cell_size_x,
|
175
|
+
# Pixel height of the displayed image region
|
176
|
+
h=wp.height * cell_size_y,
|
177
|
+
)
|
178
|
+
|
179
|
+
return cmd
|
180
|
+
|
149
181
|
def get_rendered_lines(
|
150
182
|
self, width: int, height: int, wrap_lines: bool = False
|
151
183
|
) -> list[StyleAndTextTuples]:
|
152
184
|
"""Get rendered lines from the cache, or generate them."""
|
153
|
-
cell_size_x, cell_size_y = self.app.term_info.cell_size_px
|
154
185
|
|
155
186
|
def render_lines() -> list[StyleAndTextTuples]:
|
156
187
|
"""Render the lines to display in the control."""
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
to="sixel",
|
162
|
-
cols=full_width,
|
163
|
-
rows=full_height,
|
188
|
+
ft: list[StyleAndTextTuples] = []
|
189
|
+
if height:
|
190
|
+
cmd = self.convert_data(
|
191
|
+
BoundedWritePosition(0, 0, width, height, self.bbox)
|
164
192
|
)
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
to_formatted_text(
|
187
|
-
[
|
188
|
-
# Move cursor down and across by image height and width
|
189
|
-
("", "\n".join((height) * [" " * (width)])),
|
190
|
-
# Save position, then move back
|
191
|
-
("[ZeroWidthEscape]", "\x1b[s"),
|
192
|
-
# Move cursor up if there is more than one line to display
|
193
|
-
*(
|
194
|
-
[("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
|
195
|
-
if height > 1
|
196
|
-
else []
|
197
|
-
),
|
198
|
-
("[ZeroWidthEscape]", f"\x1b[{width}D"),
|
199
|
-
# Place the image without moving cursor
|
200
|
-
("[ZeroWidthEscape]", cmd),
|
201
|
-
# Restore the last known cursor position (at the bottom)
|
202
|
-
("[ZeroWidthEscape]", "\x1b[u"),
|
203
|
-
]
|
193
|
+
ft.extend(
|
194
|
+
split_lines(
|
195
|
+
to_formatted_text(
|
196
|
+
[
|
197
|
+
# Move cursor down and across by image height and width
|
198
|
+
("", "\n".join((height) * [" " * (width)])),
|
199
|
+
# Save position, then move back
|
200
|
+
("[ZeroWidthEscape]", "\x1b[s"),
|
201
|
+
# Move cursor up if there is more than one line to display
|
202
|
+
*(
|
203
|
+
[("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
|
204
|
+
if height > 1
|
205
|
+
else []
|
206
|
+
),
|
207
|
+
("[ZeroWidthEscape]", f"\x1b[{width}D"),
|
208
|
+
# Place the image without moving cursor
|
209
|
+
("[ZeroWidthEscape]", passthrough(cmd)),
|
210
|
+
# Restore the last known cursor position (at the bottom)
|
211
|
+
("[ZeroWidthEscape]", "\x1b[u"),
|
212
|
+
]
|
213
|
+
)
|
204
214
|
)
|
205
215
|
)
|
206
|
-
|
216
|
+
return ft
|
207
217
|
|
208
|
-
key = (width, self.bbox,
|
218
|
+
key = (width, self.bbox, self.app.term_info.cell_size_px)
|
209
219
|
return self._format_cache.get(key, render_lines)
|
210
220
|
|
211
221
|
|
212
222
|
class ItermGraphicControl(GraphicControl):
|
213
223
|
"""A graphic control which displays images using iTerm's graphics protocol."""
|
214
224
|
|
215
|
-
def convert_data(self,
|
225
|
+
def convert_data(self, wp: WritePosition) -> str:
|
216
226
|
"""Convert the graphic's data to base64 data."""
|
217
227
|
datum = self.datum
|
218
228
|
|
219
|
-
|
220
|
-
|
229
|
+
bbox = wp.bbox if isinstance(wp, BoundedWritePosition) else DiInt(0, 0, 0, 0)
|
230
|
+
full_width = wp.width + bbox.left + bbox.right
|
231
|
+
full_height = wp.height + bbox.top + bbox.bottom
|
221
232
|
|
222
233
|
# Crop image if necessary
|
223
|
-
if any(
|
234
|
+
if any(bbox):
|
224
235
|
import io
|
225
236
|
|
226
237
|
image = datum.convert(
|
@@ -232,14 +243,12 @@ class ItermGraphicControl(GraphicControl):
|
|
232
243
|
cell_size_x, cell_size_y = self.app.term_info.cell_size_px
|
233
244
|
# Downscale image to fit target region for precise cropping
|
234
245
|
image.thumbnail((full_width * cell_size_x, full_height * cell_size_y))
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
)
|
242
|
-
)
|
246
|
+
left = self.bbox.left * cell_size_x
|
247
|
+
top = self.bbox.top * cell_size_y
|
248
|
+
right = (self.bbox.left + wp.width) * cell_size_x
|
249
|
+
bottom = (self.bbox.top + wp.height) * cell_size_y
|
250
|
+
upper, lower = sorted((top, bottom))
|
251
|
+
image = image.crop((left, upper, right, lower))
|
243
252
|
with io.BytesIO() as output:
|
244
253
|
image.save(output, format="PNG")
|
245
254
|
datum = Datum(data=output.getvalue(), format="png")
|
@@ -261,42 +270,46 @@ class ItermGraphicControl(GraphicControl):
|
|
261
270
|
|
262
271
|
def render_lines() -> list[StyleAndTextTuples]:
|
263
272
|
"""Render the lines to display in the control."""
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
273
|
+
ft: list[StyleAndTextTuples] = []
|
274
|
+
if height:
|
275
|
+
b64data = self.convert_data(
|
276
|
+
BoundedWritePosition(0, 0, width, height, self.bbox)
|
277
|
+
)
|
278
|
+
cmd = f"\x1b]1337;File=inline=1;width={width}:{b64data}\a"
|
279
|
+
ft.extend(
|
280
|
+
split_lines(
|
281
|
+
to_formatted_text(
|
282
|
+
[
|
283
|
+
# Move cursor down and across by image height and width
|
284
|
+
("", "\n".join((height) * [" " * (width)])),
|
285
|
+
# Save position, then move back
|
286
|
+
("[ZeroWidthEscape]", "\x1b[s"),
|
287
|
+
# Move cursor up if there is more than one line to display
|
288
|
+
*(
|
289
|
+
[("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
|
290
|
+
if height > 1
|
291
|
+
else []
|
292
|
+
),
|
293
|
+
("[ZeroWidthEscape]", f"\x1b[{width}D"),
|
294
|
+
# Place the image without moving cursor
|
295
|
+
("[ZeroWidthEscape]", passthrough(cmd)),
|
296
|
+
# Restore the last known cursor position (at the bottom)
|
297
|
+
("[ZeroWidthEscape]", "\x1b[u"),
|
298
|
+
]
|
299
|
+
)
|
286
300
|
)
|
287
301
|
)
|
288
|
-
|
302
|
+
return ft
|
289
303
|
|
290
304
|
key = (width, self.bbox, self.app.term_info.cell_size_px)
|
291
305
|
return self._format_cache.get(key, render_lines)
|
292
306
|
|
293
307
|
|
294
|
-
_kitty_image_count = 1
|
295
|
-
|
296
|
-
|
297
308
|
class KittyGraphicControl(GraphicControl):
|
298
309
|
"""A graphic control which displays images using Kitty's graphics protocol."""
|
299
310
|
|
311
|
+
_kitty_image_count: ClassVar[int] = 1
|
312
|
+
|
300
313
|
def __init__(
|
301
314
|
self,
|
302
315
|
datum: Datum,
|
@@ -307,14 +320,48 @@ class KittyGraphicControl(GraphicControl):
|
|
307
320
|
super().__init__(datum=datum, scale=scale, bbox=bbox)
|
308
321
|
self.kitty_image_id = 0
|
309
322
|
self.loaded = False
|
323
|
+
self._datum_pad_cache: FastDictCache[
|
324
|
+
tuple[Datum, int, int], Datum
|
325
|
+
] = FastDictCache(get_value=self._pad_datum, size=1)
|
326
|
+
|
327
|
+
def _pad_datum(self, datum: Datum, cell_size_x: int, cell_size_y: int) -> Datum:
|
328
|
+
from PIL import ImageOps
|
329
|
+
|
330
|
+
px, py = datum.pixel_size()
|
310
331
|
|
311
|
-
|
332
|
+
if px and py:
|
333
|
+
target_width = int((px + cell_size_x - 1) // cell_size_x * cell_size_x)
|
334
|
+
target_height = int((py + cell_size_y - 1) // cell_size_y * cell_size_y)
|
335
|
+
|
336
|
+
image = ImageOps.pad(
|
337
|
+
datum.convert("pil").convert("RGBA"),
|
338
|
+
(target_width, target_height),
|
339
|
+
centering=(0, 0),
|
340
|
+
)
|
341
|
+
datum = Datum(
|
342
|
+
image,
|
343
|
+
format="pil",
|
344
|
+
fg=datum.fg,
|
345
|
+
bg=datum.bg,
|
346
|
+
px=target_width,
|
347
|
+
py=target_height,
|
348
|
+
path=datum.path,
|
349
|
+
align=datum.align,
|
350
|
+
)
|
351
|
+
return datum
|
352
|
+
|
353
|
+
def convert_data(self, wp: WritePosition) -> str:
|
312
354
|
"""Convert the graphic's data to base64 data for kitty graphics protocol."""
|
355
|
+
bbox = wp.bbox if isinstance(wp, BoundedWritePosition) else DiInt(0, 0, 0, 0)
|
356
|
+
full_width = wp.width + bbox.left + bbox.right
|
357
|
+
full_height = wp.height + bbox.top + bbox.bottom
|
358
|
+
|
359
|
+
datum = self._datum_pad_cache[(self.datum, *self.app.term_info.cell_size_px)]
|
313
360
|
return str(
|
314
|
-
|
361
|
+
datum.convert(
|
315
362
|
to="base64-png",
|
316
|
-
cols=
|
317
|
-
rows=
|
363
|
+
cols=full_width,
|
364
|
+
rows=full_height,
|
318
365
|
)
|
319
366
|
).replace("\n", "")
|
320
367
|
|
@@ -331,11 +378,13 @@ class KittyGraphicControl(GraphicControl):
|
|
331
378
|
|
332
379
|
def load(self, rows: int, cols: int) -> None:
|
333
380
|
"""Send the graphic to the terminal without displaying it."""
|
334
|
-
global _kitty_image_count
|
381
|
+
# global _kitty_image_count
|
335
382
|
|
336
|
-
data = self.convert_data(
|
337
|
-
|
338
|
-
|
383
|
+
data = self.convert_data(
|
384
|
+
BoundedWritePosition(0, 0, width=cols, height=rows, bbox=self.bbox)
|
385
|
+
)
|
386
|
+
self.kitty_image_id = self._kitty_image_count
|
387
|
+
KittyGraphicControl._kitty_image_count += 1
|
339
388
|
|
340
389
|
while data:
|
341
390
|
chunk, data = data[:4096], data[4096:]
|
@@ -351,7 +400,7 @@ class KittyGraphicControl(GraphicControl):
|
|
351
400
|
C=1, # Do not move the cursor
|
352
401
|
m=1 if data else 0, # Data will be chunked
|
353
402
|
)
|
354
|
-
self.app.output.write_raw(
|
403
|
+
self.app.output.write_raw(passthrough(cmd))
|
355
404
|
self.app.output.flush()
|
356
405
|
self.loaded = True
|
357
406
|
|
@@ -359,7 +408,7 @@ class KittyGraphicControl(GraphicControl):
|
|
359
408
|
"""Hide the graphic from show without deleting it."""
|
360
409
|
if self.kitty_image_id > 0:
|
361
410
|
self.app.output.write_raw(
|
362
|
-
|
411
|
+
passthrough(
|
363
412
|
self._kitty_cmd(
|
364
413
|
a="d",
|
365
414
|
d="i",
|
@@ -374,7 +423,7 @@ class KittyGraphicControl(GraphicControl):
|
|
374
423
|
"""Delete the graphic from the terminal."""
|
375
424
|
if self.kitty_image_id > 0:
|
376
425
|
self.app.output.write_raw(
|
377
|
-
|
426
|
+
passthrough(
|
378
427
|
self._kitty_cmd(
|
379
428
|
a="D",
|
380
429
|
d="I",
|
@@ -395,59 +444,64 @@ class KittyGraphicControl(GraphicControl):
|
|
395
444
|
if not self.loaded:
|
396
445
|
self.load(cols=width, rows=height)
|
397
446
|
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
447
|
+
cell_size_px = self.app.term_info.cell_size_px
|
448
|
+
datum = self._datum_pad_cache[(self.datum, *cell_size_px)]
|
449
|
+
px, py = datum.pixel_size()
|
450
|
+
|
451
|
+
px = px or 100
|
452
|
+
py = py or 100
|
402
453
|
|
403
454
|
def render_lines() -> list[StyleAndTextTuples]:
|
404
455
|
"""Render the lines to display in the control."""
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
456
|
+
ft: list[StyleAndTextTuples] = []
|
457
|
+
if height:
|
458
|
+
full_width = width + self.bbox.left + self.bbox.right
|
459
|
+
full_height = height + self.bbox.top + self.bbox.bottom
|
460
|
+
cmd = self._kitty_cmd(
|
461
|
+
a="p", # Display a previously transmitted image
|
462
|
+
i=self.kitty_image_id,
|
463
|
+
p=1, # Placement ID
|
464
|
+
m=0, # No batches remaining
|
465
|
+
q=2, # No backchat
|
466
|
+
c=width,
|
467
|
+
r=height,
|
468
|
+
C=1, # 1 = Do move the cursor
|
469
|
+
# Horizontal pixel offset of the displayed image region
|
470
|
+
x=int(px * self.bbox.left / full_width),
|
471
|
+
# Vertical pixel offset of the displayed image region
|
472
|
+
y=int(py * self.bbox.top / full_height),
|
473
|
+
# Pixel width of the displayed image region
|
474
|
+
w=int(px * width / full_width),
|
475
|
+
# Pixel height of the displayed image region
|
476
|
+
h=int(py * height / full_height),
|
477
|
+
# z=-(2**30) - 1,
|
478
|
+
)
|
479
|
+
ft.extend(
|
480
|
+
split_lines(
|
481
|
+
to_formatted_text(
|
482
|
+
[
|
483
|
+
# Move cursor down and acoss by image height and width
|
484
|
+
("", "\n".join((height) * [" " * (width)])),
|
485
|
+
# Save position, then move back
|
486
|
+
("[ZeroWidthEscape]", "\x1b[s"),
|
487
|
+
# Move cursor up if there is more than one line to display
|
488
|
+
*(
|
489
|
+
[("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
|
490
|
+
if height > 1
|
491
|
+
else []
|
492
|
+
),
|
493
|
+
("[ZeroWidthEscape]", f"\x1b[{width}D"),
|
494
|
+
# Place the image without moving cursor
|
495
|
+
("[ZeroWidthEscape]", passthrough(cmd)),
|
496
|
+
# Restore the last known cursor position (at the bottom)
|
497
|
+
("[ZeroWidthEscape]", "\x1b[u"),
|
498
|
+
]
|
499
|
+
)
|
446
500
|
)
|
447
501
|
)
|
448
|
-
|
502
|
+
return ft
|
449
503
|
|
450
|
-
key = (width, height, self.bbox,
|
504
|
+
key = (width, height, self.bbox, cell_size_px)
|
451
505
|
return self._format_cache.get(key, render_lines)
|
452
506
|
|
453
507
|
def reset(self) -> None:
|
@@ -467,68 +521,6 @@ class NotVisible(Exception):
|
|
467
521
|
"""Exception to signal that a graphic is not currently visible."""
|
468
522
|
|
469
523
|
|
470
|
-
def get_position_func_overlay(
|
471
|
-
target_window: Window,
|
472
|
-
) -> Callable[[Screen], BoundedWritePosition]:
|
473
|
-
"""Generate function to positioning floats over existing windows."""
|
474
|
-
|
475
|
-
def get_position(screen: Screen) -> BoundedWritePosition:
|
476
|
-
target_wp = screen.visible_windows_to_write_positions.get(target_window)
|
477
|
-
if target_wp is None:
|
478
|
-
raise NotVisible
|
479
|
-
|
480
|
-
render_info = target_window.render_info
|
481
|
-
if render_info is None:
|
482
|
-
raise NotVisible
|
483
|
-
|
484
|
-
xpos = target_wp.xpos
|
485
|
-
ypos = target_wp.ypos
|
486
|
-
|
487
|
-
content_height = render_info.ui_content.line_count
|
488
|
-
content_width = target_wp.width # TODO - get the actual content width
|
489
|
-
|
490
|
-
# Calculate the cropping box in case the window is scrolled
|
491
|
-
bbox = DiInt(
|
492
|
-
top=render_info.vertical_scroll,
|
493
|
-
right=max(
|
494
|
-
0,
|
495
|
-
content_height
|
496
|
-
- target_wp.width
|
497
|
-
- getattr(render_info, "horizontal_scroll", 0),
|
498
|
-
),
|
499
|
-
bottom=max(
|
500
|
-
0,
|
501
|
-
content_height - target_wp.height - render_info.vertical_scroll,
|
502
|
-
),
|
503
|
-
left=getattr(render_info, "horizontal_scroll", 0),
|
504
|
-
)
|
505
|
-
|
506
|
-
# If the target is within a scrolling container, we might need to adjust
|
507
|
-
# the position of the cropped region so the float covers only the visible
|
508
|
-
# part of the target window
|
509
|
-
if isinstance(target_wp, BoundedWritePosition):
|
510
|
-
bbox = bbox._replace(
|
511
|
-
top=bbox.top + target_wp.bbox.top,
|
512
|
-
right=bbox.right + target_wp.bbox.right,
|
513
|
-
bottom=bbox.bottom + target_wp.bbox.bottom,
|
514
|
-
left=bbox.left + target_wp.bbox.left,
|
515
|
-
)
|
516
|
-
xpos += bbox.left
|
517
|
-
ypos += bbox.top
|
518
|
-
content_height -= bbox.top + bbox.bottom
|
519
|
-
content_width -= bbox.left + bbox.right
|
520
|
-
|
521
|
-
return BoundedWritePosition(
|
522
|
-
xpos=xpos,
|
523
|
-
ypos=ypos,
|
524
|
-
width=max(0, content_width),
|
525
|
-
height=max(0, content_height),
|
526
|
-
bbox=bbox,
|
527
|
-
)
|
528
|
-
|
529
|
-
return get_position
|
530
|
-
|
531
|
-
|
532
524
|
class GraphicWindow(Window):
|
533
525
|
"""A window responsible for displaying terminal graphics content.
|
534
526
|
|
@@ -564,6 +556,7 @@ class GraphicWindow(Window):
|
|
564
556
|
self.content = content
|
565
557
|
self.get_position = get_position
|
566
558
|
self.filter = ~has_completions & ~has_dialog & ~has_menus & to_filter(filter)
|
559
|
+
self._pre_rendered = False
|
567
560
|
|
568
561
|
def write_to_screen(
|
569
562
|
self,
|
@@ -575,6 +568,10 @@ class GraphicWindow(Window):
|
|
575
568
|
z_index: int | None,
|
576
569
|
) -> None:
|
577
570
|
"""Draw the graphic window's contents to the screen if required."""
|
571
|
+
# Pre-convert datum for this write position so result is cached
|
572
|
+
if not self._pre_rendered:
|
573
|
+
self.content.convert_data(write_position)
|
574
|
+
self._pre_rendered = True
|
578
575
|
filter_value = self.filter()
|
579
576
|
if filter_value:
|
580
577
|
try:
|
@@ -591,6 +588,8 @@ class GraphicWindow(Window):
|
|
591
588
|
and new_write_position.width
|
592
589
|
and new_write_position.height
|
593
590
|
):
|
591
|
+
# Do not pass the bbox on to the window when writing
|
592
|
+
new_write_position.bbox = DiInt(0, 0, 0, 0)
|
594
593
|
super().write_to_screen(
|
595
594
|
screen,
|
596
595
|
MouseHandlers(), # Do not let the float add mouse events
|
@@ -603,7 +602,7 @@ class GraphicWindow(Window):
|
|
603
602
|
return
|
604
603
|
|
605
604
|
# Otherwise hide the content (required for kitty graphics)
|
606
|
-
if not
|
605
|
+
if not get_app().leave_graphics():
|
607
606
|
self.content.hide()
|
608
607
|
|
609
608
|
def _fill_bg(
|
@@ -635,7 +634,7 @@ def select_graphic_control(format_: str) -> type[GraphicControl] | None:
|
|
635
634
|
term_info = app.term_info
|
636
635
|
preferred_graphics_protocol = app.config.graphics
|
637
636
|
useable_graphics_controls: list[type[GraphicControl]] = []
|
638
|
-
|
637
|
+
_in_mplex = in_mplex()
|
639
638
|
|
640
639
|
if preferred_graphics_protocol != "none":
|
641
640
|
if term_info.iterm_graphics_status.value and find_route(format_, "base64-png"):
|
@@ -643,15 +642,15 @@ def select_graphic_control(format_: str) -> type[GraphicControl] | None:
|
|
643
642
|
if (
|
644
643
|
preferred_graphics_protocol == "iterm"
|
645
644
|
and ItermGraphicControl in useable_graphics_controls
|
646
|
-
# Iterm does not work in
|
647
|
-
and (not
|
645
|
+
# Iterm does not work in mplex without pass-through
|
646
|
+
and (not _in_mplex or (_in_mplex and app.config.mplex_graphics))
|
648
647
|
):
|
649
648
|
SelectedGraphicControl = ItermGraphicControl
|
650
649
|
elif (
|
651
650
|
term_info.kitty_graphics_status.value
|
652
651
|
and find_route(format_, "base64-png")
|
653
|
-
# Kitty does not work in
|
654
|
-
and (not
|
652
|
+
# Kitty does not work in mplex without pass-through
|
653
|
+
and (not _in_mplex or (_in_mplex and app.config.mplex_graphics))
|
655
654
|
):
|
656
655
|
useable_graphics_controls.append(KittyGraphicControl)
|
657
656
|
if (
|
@@ -800,7 +799,7 @@ class GraphicProcessor:
|
|
800
799
|
|
801
800
|
GraphicControl = select_graphic_control(format_=datum.format)
|
802
801
|
if GraphicControl is None:
|
803
|
-
log.debug("Terminal graphics not supported or format not graphical")
|
802
|
+
# log.debug("Terminal graphics not supported or format not graphical")
|
804
803
|
return None
|
805
804
|
|
806
805
|
bg_color = datum.bg
|
@@ -809,15 +808,14 @@ class GraphicProcessor:
|
|
809
808
|
content=(graphic_control := GraphicControl(datum)),
|
810
809
|
get_position=self._get_position(key, rows, cols),
|
811
810
|
style=f"bg:{bg_color}" if bg_color else "",
|
812
|
-
|
811
|
+
align=datum.align,
|
812
|
+
)
|
813
813
|
)
|
814
814
|
# Register graphic with application
|
815
|
-
|
816
|
-
if graphic_float:
|
817
|
-
app.graphics.add(graphic_float)
|
815
|
+
(app_graphics := self.app.graphics).add(graphic_float)
|
818
816
|
# Hide the graphic from app if the float is deleted
|
819
817
|
weak_float_ref = weakref.ref(graphic_float)
|
820
|
-
graphic_window.filter &= Condition(lambda: weak_float_ref() in
|
818
|
+
graphic_window.filter &= Condition(lambda: weak_float_ref() in app_graphics)
|
821
819
|
# Hide the graphic from terminal if the float is deleted
|
822
820
|
weakref.finalize(graphic_float, graphic_control.close)
|
823
821
|
|
euporie/core/io.py
CHANGED
@@ -84,6 +84,14 @@ class Vt100_Output(PtkVt100_Output):
|
|
84
84
|
super().disable_mouse_support()
|
85
85
|
self.write_raw("\x1b[?1016l")
|
86
86
|
|
87
|
+
def enable_private_sixel_colors(self) -> None:
|
88
|
+
"""Enable private color registers for sixel graphics."""
|
89
|
+
self.write_raw("\x1b[1070h")
|
90
|
+
|
91
|
+
def disable_private_sixel_colors(self) -> None:
|
92
|
+
"""Disable private color registers for sixel graphics."""
|
93
|
+
self.write_raw("\x1b[1070l")
|
94
|
+
|
87
95
|
def enable_extended_keys(self) -> None:
|
88
96
|
"""Request extended keys."""
|
89
97
|
# xterm
|