euporie 2.6.2__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.
Files changed (67) hide show
  1. euporie/console/tabs/console.py +52 -38
  2. euporie/core/__init__.py +5 -2
  3. euporie/core/app.py +74 -57
  4. euporie/core/comm/ipywidgets.py +7 -3
  5. euporie/core/config.py +51 -27
  6. euporie/core/convert/__init__.py +2 -0
  7. euporie/core/convert/datum.py +82 -45
  8. euporie/core/convert/formats/ansi.py +1 -2
  9. euporie/core/convert/formats/common.py +7 -11
  10. euporie/core/convert/formats/ft.py +10 -7
  11. euporie/core/convert/formats/png.py +7 -6
  12. euporie/core/convert/formats/sixel.py +1 -1
  13. euporie/core/convert/formats/svg.py +28 -0
  14. euporie/core/convert/mime.py +4 -7
  15. euporie/core/data_structures.py +24 -22
  16. euporie/core/filters.py +16 -2
  17. euporie/core/format.py +30 -4
  18. euporie/core/ft/ansi.py +2 -1
  19. euporie/core/ft/html.py +155 -42
  20. euporie/core/{widgets/graphics.py → graphics.py} +225 -227
  21. euporie/core/io.py +8 -0
  22. euporie/core/key_binding/bindings/__init__.py +8 -2
  23. euporie/core/key_binding/bindings/basic.py +9 -14
  24. euporie/core/key_binding/bindings/micro.py +0 -12
  25. euporie/core/key_binding/bindings/mouse.py +107 -80
  26. euporie/core/key_binding/bindings/page_navigation.py +129 -0
  27. euporie/core/key_binding/key_processor.py +9 -1
  28. euporie/core/layout/__init__.py +1 -0
  29. euporie/core/layout/containers.py +1011 -0
  30. euporie/core/layout/decor.py +381 -0
  31. euporie/core/layout/print.py +130 -0
  32. euporie/core/layout/screen.py +75 -0
  33. euporie/core/{widgets/page.py → layout/scroll.py} +166 -111
  34. euporie/core/log.py +1 -1
  35. euporie/core/margins.py +11 -5
  36. euporie/core/path.py +43 -176
  37. euporie/core/renderer.py +31 -8
  38. euporie/core/style.py +2 -0
  39. euporie/core/tabs/base.py +2 -1
  40. euporie/core/terminal.py +19 -21
  41. euporie/core/widgets/cell.py +2 -4
  42. euporie/core/widgets/cell_outputs.py +2 -2
  43. euporie/core/widgets/decor.py +3 -359
  44. euporie/core/widgets/dialog.py +5 -5
  45. euporie/core/widgets/display.py +32 -12
  46. euporie/core/widgets/file_browser.py +3 -4
  47. euporie/core/widgets/forms.py +36 -14
  48. euporie/core/widgets/inputs.py +171 -99
  49. euporie/core/widgets/layout.py +80 -5
  50. euporie/core/widgets/menu.py +1 -3
  51. euporie/core/widgets/pager.py +3 -3
  52. euporie/core/widgets/palette.py +3 -2
  53. euporie/core/widgets/status_bar.py +2 -6
  54. euporie/core/widgets/tree.py +3 -6
  55. euporie/notebook/app.py +8 -8
  56. euporie/notebook/tabs/notebook.py +2 -2
  57. euporie/notebook/widgets/side_bar.py +1 -1
  58. euporie/preview/tabs/notebook.py +2 -2
  59. euporie/web/tabs/web.py +6 -1
  60. euporie/web/widgets/webview.py +52 -32
  61. {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/METADATA +9 -11
  62. {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/RECORD +67 -60
  63. {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/WHEEL +1 -1
  64. {euporie-2.6.2.data → euporie-2.7.0.data}/data/share/applications/euporie-console.desktop +0 -0
  65. {euporie-2.6.2.data → euporie-2.7.0.data}/data/share/applications/euporie-notebook.desktop +0 -0
  66. {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/entry_points.txt +0 -0
  67. {euporie-2.6.2.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.terminal import tmuxify
35
- from euporie.core.widgets.page import BoundedWritePosition
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
- full_width = width + self.bbox.left + self.bbox.right
158
- full_height = height + self.bbox.top + self.bbox.bottom
159
- cmd = str(
160
- self.datum.convert(
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
- if any(self.bbox):
167
- from sixelcrop import sixelcrop
168
-
169
- cmd = sixelcrop(
170
- data=cmd,
171
- # Horizontal pixel offset of the displayed image region
172
- x=self.bbox.left * cell_size_x,
173
- # Vertical pixel offset of the displayed image region
174
- y=self.bbox.top * cell_size_y,
175
- # Pixel width of the displayed image region
176
- w=width * cell_size_x,
177
- # Pixel height of the displayed image region
178
- h=height * cell_size_y,
179
- )
180
-
181
- if self.app.config.tmux_graphics:
182
- cmd = tmuxify(cmd)
183
-
184
- return list(
185
- split_lines(
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, (cell_size_x, cell_size_y))
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, rows: int, cols: int) -> str:
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
- full_width = cols + self.bbox.left + self.bbox.right
220
- full_height = rows + self.bbox.top + self.bbox.bottom
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(self.bbox):
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
- image = image.crop(
236
- (
237
- self.bbox.left * cell_size_x, # left
238
- self.bbox.top * cell_size_y, # top
239
- (self.bbox.left + cols) * cell_size_x, # right
240
- (self.bbox.top + rows) * cell_size_y, # bottom
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
- b64data = self.convert_data(cols=width, rows=height)
265
- cmd = f"\x1b]1337;File=inline=1;width={width}:{b64data}\a"
266
- return list(
267
- split_lines(
268
- to_formatted_text(
269
- [
270
- # Move cursor down and across by image height and width
271
- ("", "\n".join((height) * [" " * (width)])),
272
- # Save position, then move back
273
- ("[ZeroWidthEscape]", "\x1b[s"),
274
- # Move cursor up if there is more than one line to display
275
- *(
276
- [("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
277
- if height > 1
278
- else []
279
- ),
280
- ("[ZeroWidthEscape]", f"\x1b[{width}D"),
281
- # Place the image without moving cursor
282
- ("[ZeroWidthEscape]", tmuxify(cmd)),
283
- # Restore the last known cursor position (at the bottom)
284
- ("[ZeroWidthEscape]", "\x1b[u"),
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
- def convert_data(self, rows: int, cols: int) -> str:
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
- self.datum.convert(
361
+ datum.convert(
315
362
  to="base64-png",
316
- cols=cols,
317
- rows=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(rows=rows, cols=cols)
337
- self.kitty_image_id = _kitty_image_count
338
- _kitty_image_count += 1
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(tmuxify(cmd))
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
- tmuxify(
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
- tmuxify(
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
- px, py = self.datum.pixel_size()
399
- if not px or not py:
400
- log.warning("Cannot determine image size")
401
- px, py = px or 100, py or 100
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
- full_width = width + self.bbox.left + self.bbox.right
406
- full_height = height + self.bbox.top + self.bbox.bottom
407
- cmd = self._kitty_cmd(
408
- a="p", # Display a previously transmitted image
409
- i=self.kitty_image_id,
410
- p=1, # Placement ID
411
- m=0, # No batches remaining
412
- q=2, # No backchat
413
- c=width,
414
- r=height,
415
- C=1, # 1 = Do move the cursor
416
- # Horizontal pixel offset of the displayed image region
417
- x=int(px * self.bbox.left / full_width),
418
- # Vertical pixel offset of the displayed image region
419
- y=int(py * self.bbox.top / full_height),
420
- # Pixel width of the displayed image region
421
- w=int(px * width / full_width),
422
- # Pixel height of the displayed image region
423
- h=int(py * height / full_height),
424
- # z=-(2**30) - 1,
425
- )
426
- return list(
427
- split_lines(
428
- to_formatted_text(
429
- [
430
- # Move cursor down and acoss by image height and width
431
- ("", "\n".join((height) * [" " * (width)])),
432
- # Save position, then move back
433
- ("[ZeroWidthEscape]", "\x1b[s"),
434
- # Move cursor up if there is more than one line to display
435
- *(
436
- [("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
437
- if height > 1
438
- else []
439
- ),
440
- ("[ZeroWidthEscape]", f"\x1b[{width}D"),
441
- # Place the image without moving cursor
442
- ("[ZeroWidthEscape]", tmuxify(cmd)),
443
- # Restore the last known cursor position (at the bottom)
444
- ("[ZeroWidthEscape]", "\x1b[u"),
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, self.app.term_info.cell_size_px)
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 filter_value or not get_app().leave_graphics():
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
- _in_tmux = in_tmux()
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 tmux without pass-through
647
- and (not _in_tmux or (_in_tmux and app.config.tmux_graphics))
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 tmux without pass-through
654
- and (not _in_tmux or (_in_tmux and app.config.tmux_graphics))
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
- app = self.app
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 app.graphics)
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