euporie 2.8.0__py3-none-any.whl → 2.8.5__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 (129) hide show
  1. euporie/console/_commands.py +143 -0
  2. euporie/console/_settings.py +58 -0
  3. euporie/console/app.py +25 -71
  4. euporie/console/tabs/console.py +267 -147
  5. euporie/core/__init__.py +1 -9
  6. euporie/core/__main__.py +31 -5
  7. euporie/core/_settings.py +104 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +70 -0
  10. euporie/core/app/_settings.py +427 -0
  11. euporie/core/{app.py → app/app.py} +214 -572
  12. euporie/core/app/base.py +51 -0
  13. euporie/core/{current.py → app/current.py} +13 -4
  14. euporie/core/app/cursor.py +35 -0
  15. euporie/core/app/dummy.py +12 -0
  16. euporie/core/app/launch.py +28 -0
  17. euporie/core/bars/__init__.py +11 -0
  18. euporie/core/bars/command.py +182 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +154 -57
  21. euporie/core/{widgets → bars}/status.py +9 -26
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +21 -12
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +11 -5
  27. euporie/core/completion.py +3 -2
  28. euporie/core/config.py +368 -341
  29. euporie/core/convert/__init__.py +0 -30
  30. euporie/core/convert/datum.py +131 -60
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +46 -30
  33. euporie/core/convert/formats/common.py +11 -23
  34. euporie/core/convert/formats/html.py +45 -40
  35. euporie/core/convert/formats/pil.py +1 -1
  36. euporie/core/convert/formats/png.py +3 -5
  37. euporie/core/convert/formats/sixel.py +3 -3
  38. euporie/core/convert/registry.py +11 -8
  39. euporie/core/convert/utils.py +50 -23
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +72 -82
  42. euporie/core/format.py +13 -2
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +36 -36
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +216 -124
  48. euporie/core/history.py +2 -2
  49. euporie/core/inspection.py +3 -2
  50. euporie/core/io.py +207 -28
  51. euporie/core/kernel/__init__.py +1 -0
  52. euporie/core/{kernel.py → kernel/client.py} +100 -139
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +2 -8
  55. euporie/core/key_binding/bindings/basic.py +47 -7
  56. euporie/core/key_binding/bindings/completion.py +3 -8
  57. euporie/core/key_binding/bindings/micro.py +5 -7
  58. euporie/core/key_binding/bindings/mouse.py +26 -24
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/bindings/vi.py +46 -0
  61. euporie/core/key_binding/key_processor.py +43 -2
  62. euporie/core/key_binding/registry.py +2 -0
  63. euporie/core/key_binding/utils.py +22 -2
  64. euporie/core/keys.py +7156 -92
  65. euporie/core/layout/cache.py +35 -25
  66. euporie/core/layout/containers.py +280 -74
  67. euporie/core/layout/decor.py +5 -5
  68. euporie/core/layout/mouse.py +1 -1
  69. euporie/core/layout/print.py +16 -3
  70. euporie/core/layout/scroll.py +26 -28
  71. euporie/core/log.py +75 -60
  72. euporie/core/lsp.py +118 -24
  73. euporie/core/margins.py +60 -31
  74. euporie/core/path.py +2 -1
  75. euporie/core/renderer.py +58 -17
  76. euporie/core/style.py +60 -40
  77. euporie/core/suggest.py +103 -85
  78. euporie/core/tabs/__init__.py +34 -0
  79. euporie/core/tabs/_settings.py +113 -0
  80. euporie/core/tabs/base.py +11 -435
  81. euporie/core/tabs/kernel.py +420 -0
  82. euporie/core/tabs/notebook.py +20 -54
  83. euporie/core/utils.py +98 -6
  84. euporie/core/validation.py +1 -1
  85. euporie/core/widgets/_settings.py +188 -0
  86. euporie/core/widgets/cell.py +90 -158
  87. euporie/core/widgets/cell_outputs.py +26 -37
  88. euporie/core/widgets/decor.py +11 -41
  89. euporie/core/widgets/dialog.py +55 -44
  90. euporie/core/widgets/display.py +27 -24
  91. euporie/core/widgets/file_browser.py +5 -26
  92. euporie/core/widgets/forms.py +16 -12
  93. euporie/core/widgets/inputs.py +37 -81
  94. euporie/core/widgets/layout.py +7 -6
  95. euporie/core/widgets/logo.py +49 -0
  96. euporie/core/widgets/menu.py +13 -11
  97. euporie/core/widgets/pager.py +9 -11
  98. euporie/core/widgets/palette.py +6 -6
  99. euporie/hub/app.py +52 -31
  100. euporie/notebook/_commands.py +24 -0
  101. euporie/notebook/_settings.py +107 -0
  102. euporie/notebook/app.py +109 -210
  103. euporie/notebook/filters.py +1 -1
  104. euporie/notebook/tabs/__init__.py +46 -7
  105. euporie/notebook/tabs/_commands.py +714 -0
  106. euporie/notebook/tabs/_settings.py +32 -0
  107. euporie/notebook/tabs/display.py +2 -2
  108. euporie/notebook/tabs/edit.py +12 -7
  109. euporie/notebook/tabs/json.py +3 -3
  110. euporie/notebook/tabs/log.py +1 -18
  111. euporie/notebook/tabs/notebook.py +21 -674
  112. euporie/notebook/widgets/_commands.py +11 -0
  113. euporie/notebook/widgets/_settings.py +19 -0
  114. euporie/notebook/widgets/side_bar.py +14 -34
  115. euporie/preview/_settings.py +104 -0
  116. euporie/preview/app.py +8 -30
  117. euporie/preview/tabs/notebook.py +15 -86
  118. euporie/web/tabs/web.py +4 -6
  119. euporie/web/widgets/webview.py +5 -12
  120. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
  121. euporie-2.8.5.dist-info/RECORD +172 -0
  122. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
  123. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
  124. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
  125. euporie/core/launch.py +0 -59
  126. euporie/core/terminal.py +0 -527
  127. euporie-2.8.0.dist-info/RECORD +0 -146
  128. {euporie-2.8.0.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
  129. {euporie-2.8.0.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
euporie/core/graphics.py CHANGED
@@ -5,12 +5,11 @@ 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
12
12
  from prompt_toolkit.data_structures import Point
13
- from prompt_toolkit.filters.app import has_completions
14
13
  from prompt_toolkit.filters.base import Condition
15
14
  from prompt_toolkit.filters.utils import to_filter
16
15
  from prompt_toolkit.formatted_text.base import to_formatted_text
@@ -21,14 +20,14 @@ from prompt_toolkit.layout.mouse_handlers import MouseHandlers
21
20
  from prompt_toolkit.layout.screen import Char, WritePosition
22
21
  from prompt_toolkit.utils import get_cwidth
23
22
 
23
+ from euporie.core.app.current import get_app
24
24
  from euporie.core.convert.datum import Datum
25
25
  from euporie.core.convert.registry import find_route
26
- from euporie.core.current import get_app
27
26
  from euporie.core.data_structures import DiInt
28
- from euporie.core.filters import has_dialog, has_menus, in_mplex
27
+ from euporie.core.filters import has_float, in_mplex
29
28
  from euporie.core.ft.utils import _ZERO_WIDTH_FRAGMENTS
29
+ from euporie.core.io import passthrough
30
30
  from euporie.core.layout.scroll import BoundedWritePosition
31
- from euporie.core.terminal import passthrough
32
31
 
33
32
  if TYPE_CHECKING:
34
33
  from typing import Any, Callable, ClassVar
@@ -85,7 +84,7 @@ class GraphicControl(UIControl, metaclass=ABCMeta):
85
84
 
86
85
  @abstractmethod
87
86
  def get_rendered_lines(
88
- self, width: int, height: int, wrap_lines: bool = False
87
+ self, visible_width: int, visible_height: int, wrap_lines: bool = False
89
88
  ) -> list[StyleAndTextTuples]:
90
89
  """Render the output data."""
91
90
  return []
@@ -101,13 +100,9 @@ class GraphicControl(UIControl, metaclass=ABCMeta):
101
100
  `UIContent` for the given output size.
102
101
 
103
102
  """
104
- max_cols, aspect = self.datum.cell_size()
105
- bbox = self.bbox
106
- cols = min(max_cols, width) if max_cols else width
107
- rows = ceil(cols * aspect) - bbox.top - bbox.bottom if aspect else height
108
103
 
109
104
  def get_content() -> dict[str, Any]:
110
- rendered_lines = self.get_rendered_lines(width=cols, height=rows)
105
+ rendered_lines = self.get_rendered_lines(width, height)
111
106
  self.rendered_lines = rendered_lines[:]
112
107
  line_count = len(rendered_lines)
113
108
 
@@ -131,10 +126,10 @@ class GraphicControl(UIControl, metaclass=ABCMeta):
131
126
 
132
127
  # Re-render if the image width changes, or the terminal character size changes
133
128
  key = (
134
- cols,
135
- rows,
129
+ width,
130
+ height,
136
131
  self.app.color_palette,
137
- self.app.term_info.cell_size_px,
132
+ self.app.cell_size_px,
138
133
  self.bbox,
139
134
  )
140
135
  return UIContent(
@@ -156,19 +151,11 @@ class SixelGraphicControl(GraphicControl):
156
151
  def convert_data(self, wp: WritePosition) -> str:
157
152
  """Convert datum to required format."""
158
153
  bbox = wp.bbox if isinstance(wp, BoundedWritePosition) else DiInt(0, 0, 0, 0)
159
- full_width = wp.width + bbox.left + bbox.right
160
- full_height = wp.height + bbox.top + bbox.bottom
161
- cmd = str(
162
- self.datum.convert(
163
- to="sixel",
164
- cols=full_width,
165
- rows=full_height,
166
- )
167
- )
168
- if any(self.bbox):
154
+ cmd = str(self.datum.convert(to="sixel", cols=wp.width, rows=wp.height)).strip()
155
+ if any(bbox):
169
156
  from sixelcrop import sixelcrop
170
157
 
171
- cell_size_x, cell_size_y = self.app.term_info.cell_size_px
158
+ cell_size_x, cell_size_y = self.app.cell_size_px
172
159
 
173
160
  cmd = sixelcrop(
174
161
  data=cmd,
@@ -177,42 +164,80 @@ class SixelGraphicControl(GraphicControl):
177
164
  # Vertical pixel offset of the displayed image region
178
165
  y=bbox.top * cell_size_y,
179
166
  # Pixel width of the displayed image region
180
- w=wp.width * cell_size_x,
167
+ w=(wp.width - bbox.left - bbox.right) * cell_size_x,
181
168
  # Pixel height of the displayed image region
182
- h=wp.height * cell_size_y,
169
+ h=(wp.height - bbox.top - bbox.bottom) * cell_size_y,
183
170
  )
184
171
 
185
172
  return cmd
186
173
 
187
174
  def get_rendered_lines(
188
- self, width: int, height: int, wrap_lines: bool = False
175
+ self, visible_width: int, visible_height: int, wrap_lines: bool = False
189
176
  ) -> list[StyleAndTextTuples]:
190
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
+ )
191
204
 
192
205
  def render_lines() -> list[StyleAndTextTuples]:
193
206
  """Render the lines to display in the control."""
194
207
  ft: list[StyleAndTextTuples] = []
195
- if height:
196
- cmd = self.convert_data(
197
- BoundedWritePosition(0, 0, width, height, self.bbox)
198
- )
208
+ if visible_height >= 0:
209
+ cmd = self.convert_data(BoundedWritePosition(0, 0, cols, rows, d_bbox))
199
210
  ft.extend(
200
211
  split_lines(
201
212
  to_formatted_text(
202
213
  [
203
214
  # Move cursor down and across by image height and width
204
- ("", "\n".join((height) * [" " * (width)])),
215
+ (
216
+ "",
217
+ "\n".join(
218
+ (visible_height) * [" " * (visible_width)]
219
+ ),
220
+ ),
205
221
  # Save position, then move back
206
222
  ("[ZeroWidthEscape]", "\x1b[s"),
207
223
  # Move cursor up if there is more than one line to display
208
224
  *(
209
- [("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
210
- if height > 1
225
+ [
226
+ (
227
+ "[ZeroWidthEscape]",
228
+ f"\x1b[{visible_height - 1}A",
229
+ )
230
+ ]
231
+ if visible_height > 1
211
232
  else []
212
233
  ),
213
- ("[ZeroWidthEscape]", f"\x1b[{width}D"),
234
+ ("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
214
235
  # Place the image without moving cursor
215
- ("[ZeroWidthEscape]", passthrough(cmd)),
236
+ (
237
+ "[ZeroWidthEscape]",
238
+ passthrough(cmd, self.app.config),
239
+ ),
240
+ # ("[ZeroWidthEscape]", "XXXXX"),
216
241
  # Restore the last known cursor position (at the bottom)
217
242
  ("[ZeroWidthEscape]", "\x1b[u"),
218
243
  ]
@@ -222,9 +247,9 @@ class SixelGraphicControl(GraphicControl):
222
247
  return ft
223
248
 
224
249
  key = (
225
- width,
250
+ visible_width,
226
251
  self.app.color_palette,
227
- self.app.term_info.cell_size_px,
252
+ self.app.cell_size_px,
228
253
  self.bbox,
229
254
  )
230
255
  return self._format_cache.get(key, render_lines)
@@ -236,28 +261,20 @@ class ItermGraphicControl(GraphicControl):
236
261
  def convert_data(self, wp: WritePosition) -> str:
237
262
  """Convert the graphic's data to base64 data."""
238
263
  datum = self.datum
239
-
240
264
  bbox = wp.bbox if isinstance(wp, BoundedWritePosition) else DiInt(0, 0, 0, 0)
241
- full_width = wp.width + bbox.left + bbox.right
242
- full_height = wp.height + bbox.top + bbox.bottom
243
-
244
265
  # Crop image if necessary
245
266
  if any(bbox):
246
267
  import io
247
268
 
248
- image = datum.convert(
249
- to="pil",
250
- cols=full_width,
251
- rows=full_height,
252
- )
269
+ image = datum.convert(to="pil", cols=wp.width, rows=wp.height)
253
270
  if image is not None:
254
- cell_size_x, cell_size_y = self.app.term_info.cell_size_px
271
+ cell_size_x, cell_size_y = self.app.cell_size_px
255
272
  # Downscale image to fit target region for precise cropping
256
- image.thumbnail((full_width * cell_size_x, full_height * cell_size_y))
257
- left = self.bbox.left * cell_size_x
258
- top = self.bbox.top * cell_size_y
259
- right = (self.bbox.left + wp.width) * cell_size_x
260
- bottom = (self.bbox.top + wp.height) * cell_size_y
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
261
278
  upper, lower = sorted((top, bottom))
262
279
  image = image.crop((left, upper, right, lower))
263
280
  with io.BytesIO() as output:
@@ -267,43 +284,81 @@ class ItermGraphicControl(GraphicControl):
267
284
  if datum.format.startswith("base64-"):
268
285
  b64data = datum.data
269
286
  else:
270
- b64data = datum.convert(
271
- to="base64-png",
272
- cols=full_width,
273
- rows=full_height,
274
- )
287
+ b64data = datum.convert(to="base64-png", cols=wp.width, rows=wp.height)
275
288
  return b64data.replace("\n", "").strip()
276
289
 
277
290
  def get_rendered_lines(
278
- self, width: int, height: int, wrap_lines: bool = False
291
+ self, visible_width: int, visible_height: int, wrap_lines: bool = False
279
292
  ) -> list[StyleAndTextTuples]:
280
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
+ )
281
320
 
282
321
  def render_lines() -> list[StyleAndTextTuples]:
283
322
  """Render the lines to display in the control."""
284
323
  ft: list[StyleAndTextTuples] = []
285
- if height:
324
+ if (
325
+ rows - d_bbox.top - d_bbox.bottom > 0
326
+ and cols - d_bbox.left - d_bbox.right > 0
327
+ ):
286
328
  b64data = self.convert_data(
287
- BoundedWritePosition(0, 0, width, height, self.bbox)
329
+ BoundedWritePosition(0, 0, cols, rows, d_bbox)
288
330
  )
289
- cmd = f"\x1b]1337;File=inline=1;width={width}:{b64data}\a"
331
+ cmd = f"\x1b]1337;File=inline=1;width={cols}:{b64data}\a"
290
332
  ft.extend(
291
333
  split_lines(
292
334
  to_formatted_text(
293
335
  [
294
336
  # Move cursor down and across by image height and width
295
- ("", "\n".join((height) * [" " * (width)])),
337
+ (
338
+ "",
339
+ "\n".join(
340
+ (visible_height) * [" " * (visible_width)]
341
+ ),
342
+ ),
296
343
  # Save position, then move back
297
344
  ("[ZeroWidthEscape]", "\x1b[s"),
298
345
  # Move cursor up if there is more than one line to display
299
346
  *(
300
- [("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
301
- if height > 1
347
+ [
348
+ (
349
+ "[ZeroWidthEscape]",
350
+ f"\x1b[{visible_height - 1}A",
351
+ )
352
+ ]
353
+ if visible_height > 1
302
354
  else []
303
355
  ),
304
- ("[ZeroWidthEscape]", f"\x1b[{width}D"),
356
+ ("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
305
357
  # Place the image without moving cursor
306
- ("[ZeroWidthEscape]", passthrough(cmd)),
358
+ (
359
+ "[ZeroWidthEscape]",
360
+ passthrough(cmd, self.app.config),
361
+ ),
307
362
  # Restore the last known cursor position (at the bottom)
308
363
  ("[ZeroWidthEscape]", "\x1b[u"),
309
364
  ]
@@ -313,9 +368,9 @@ class ItermGraphicControl(GraphicControl):
313
368
  return ft
314
369
 
315
370
  key = (
316
- width,
371
+ visible_width,
317
372
  self.app.color_palette,
318
- self.app.term_info.cell_size_px,
373
+ self.app.cell_size_px,
319
374
  self.bbox,
320
375
  )
321
376
  return self._format_cache.get(key, render_lines)
@@ -336,9 +391,9 @@ class KittyGraphicControl(GraphicControl):
336
391
  super().__init__(datum=datum, scale=scale, bbox=bbox)
337
392
  self.kitty_image_id = 0
338
393
  self.loaded = False
339
- self._datum_pad_cache: FastDictCache[
340
- tuple[Datum, int, int], Datum
341
- ] = FastDictCache(get_value=self._pad_datum, size=1)
394
+ self._datum_pad_cache: FastDictCache[tuple[Datum, int, int], Datum] = (
395
+ FastDictCache(get_value=self._pad_datum, size=1)
396
+ )
342
397
 
343
398
  def _pad_datum(self, datum: Datum, cell_size_x: int, cell_size_y: int) -> Datum:
344
399
  from PIL import ImageOps
@@ -370,7 +425,7 @@ class KittyGraphicControl(GraphicControl):
370
425
  full_width = wp.width + bbox.left + bbox.right
371
426
  full_height = wp.height + bbox.top + bbox.bottom
372
427
 
373
- datum = self._datum_pad_cache[(self.datum, *self.app.term_info.cell_size_px)]
428
+ datum = self._datum_pad_cache[(self.datum, *self.app.cell_size_px)]
374
429
  return str(
375
430
  datum.convert(
376
431
  to="base64-png",
@@ -390,12 +445,10 @@ class KittyGraphicControl(GraphicControl):
390
445
  cmd += "\x1b\\"
391
446
  return cmd
392
447
 
393
- def load(self, rows: int, cols: int) -> None:
448
+ def load(self, rows: int, cols: int, bbox: DiInt) -> None:
394
449
  """Send the graphic to the terminal without displaying it."""
395
- # global _kitty_image_count
396
-
397
450
  data = self.convert_data(
398
- BoundedWritePosition(0, 0, width=cols, height=rows, bbox=self.bbox)
451
+ BoundedWritePosition(0, 0, width=cols, height=rows, bbox=bbox)
399
452
  )
400
453
  self.kitty_image_id = self._kitty_image_count
401
454
  KittyGraphicControl._kitty_image_count += 1
@@ -414,23 +467,26 @@ class KittyGraphicControl(GraphicControl):
414
467
  C=1, # Do not move the cursor
415
468
  m=1 if data else 0, # Data will be chunked
416
469
  )
417
- self.app.output.write_raw(passthrough(cmd))
470
+ self.app.output.write_raw(passthrough(cmd, self.app.config))
418
471
  self.app.output.flush()
419
472
  self.loaded = True
420
473
 
474
+ def hide_cmd(self) -> str:
475
+ """Generate a command to hide the graphic."""
476
+ return passthrough(
477
+ self._kitty_cmd(
478
+ a="d",
479
+ d="i",
480
+ i=self.kitty_image_id,
481
+ q=1,
482
+ ),
483
+ self.app.config,
484
+ )
485
+
421
486
  def hide(self) -> None:
422
487
  """Hide the graphic from show without deleting it."""
423
488
  if self.kitty_image_id > 0:
424
- self.app.output.write_raw(
425
- passthrough(
426
- self._kitty_cmd(
427
- a="d",
428
- d="i",
429
- i=self.kitty_image_id,
430
- q=1,
431
- )
432
- )
433
- )
489
+ self.app.output.write_raw(self.hide_cmd())
434
490
  self.app.output.flush()
435
491
 
436
492
  def delete(self) -> None:
@@ -443,82 +499,119 @@ class KittyGraphicControl(GraphicControl):
443
499
  d="I",
444
500
  i=self.kitty_image_id,
445
501
  q=2,
446
- )
502
+ ),
503
+ self.app.config,
447
504
  )
448
505
  )
449
506
  self.app.output.flush()
450
507
  self.loaded = False
451
508
 
452
509
  def get_rendered_lines(
453
- self, width: int, height: int, wrap_lines: bool = False
510
+ self, visible_width: int, visible_height: int, wrap_lines: bool = False
454
511
  ) -> list[StyleAndTextTuples]:
455
512
  """Get rendered lines from the cache, or generate them."""
456
- # TODO - wezterm does not scale kitty graphics, so we might want to resize
457
- # images at this point rather than just loading them once
458
- if not self.loaded:
459
- self.load(cols=width, rows=height)
513
+ bbox = self.bbox
460
514
 
461
- cell_size_px = self.app.term_info.cell_size_px
515
+ cell_size_px = self.app.cell_size_px
462
516
  datum = self._datum_pad_cache[(self.datum, *cell_size_px)]
463
517
  px, py = datum.pixel_size()
464
-
518
+ # Fall back to a default pixel size
465
519
  px = px or 100
466
520
  py = py or 100
467
521
 
522
+ d_cols, d_aspect = datum.cell_size()
523
+ d_rows = d_cols * d_aspect
524
+
525
+ total_available_width = visible_width + bbox.left + bbox.right
526
+ total_available_height = visible_height + bbox.top + bbox.bottom
527
+
528
+ # Scale down the graphic to fit in the available space
529
+ if d_rows > total_available_height or d_cols > total_available_width:
530
+ if d_rows / total_available_height > d_cols / total_available_width:
531
+ ratio = min(1, total_available_height / d_rows)
532
+ else:
533
+ ratio = min(1, total_available_width / d_cols)
534
+ else:
535
+ ratio = 1
536
+
537
+ # Calculate the size and cropping bbox at which we want to display the graphic
538
+ cols = floor(d_cols * ratio)
539
+ rows = ceil(cols * d_aspect)
540
+ d_bbox = DiInt(
541
+ top=self.bbox.top,
542
+ right=max(0, cols - (total_available_width - self.bbox.right)),
543
+ bottom=max(0, rows - (total_available_height - self.bbox.bottom)),
544
+ left=self.bbox.left,
545
+ )
546
+ if not self.loaded:
547
+ self.load(cols=cols, rows=rows, bbox=d_bbox)
548
+
468
549
  def render_lines() -> list[StyleAndTextTuples]:
469
550
  """Render the lines to display in the control."""
470
551
  ft: list[StyleAndTextTuples] = []
471
- if height:
472
- full_width = width + self.bbox.left + self.bbox.right
473
- full_height = height + self.bbox.top + self.bbox.bottom
552
+ if display_rows := rows - d_bbox.top - d_bbox.bottom:
474
553
  cmd = self._kitty_cmd(
475
554
  a="p", # Display a previously transmitted image
476
555
  i=self.kitty_image_id,
477
556
  p=1, # Placement ID
478
557
  m=0, # No batches remaining
479
558
  q=2, # No backchat
480
- c=width,
481
- r=height,
559
+ c=cols - d_bbox.left - d_bbox.right,
560
+ r=display_rows,
482
561
  C=1, # 1 = Do move the cursor
483
562
  # Horizontal pixel offset of the displayed image region
484
- x=int(px * self.bbox.left / full_width),
563
+ x=int(px * d_bbox.left / cols),
485
564
  # Vertical pixel offset of the displayed image region
486
- y=int(py * self.bbox.top / full_height),
565
+ y=int(py * d_bbox.top / rows),
487
566
  # Pixel width of the displayed image region
488
- w=int(px * width / full_width),
567
+ w=int(px * (cols - d_bbox.left - d_bbox.right) / cols),
489
568
  # Pixel height of the displayed image region
490
- h=int(py * height / full_height),
491
- # z=-(2**30) - 1,
569
+ h=int(py * (rows - d_bbox.top - d_bbox.bottom) / rows),
492
570
  )
493
571
  ft.extend(
494
572
  split_lines(
495
573
  to_formatted_text(
496
574
  [
497
575
  # Move cursor down and acoss by image height and width
498
- ("", "\n".join((height) * [" " * (width)])),
576
+ (
577
+ "",
578
+ "\n".join(
579
+ (visible_height) * [" " * (visible_width)]
580
+ ),
581
+ ),
499
582
  # Save position, then move back
500
583
  ("[ZeroWidthEscape]", "\x1b[s"),
501
584
  # Move cursor up if there is more than one line to display
502
585
  *(
503
- [("[ZeroWidthEscape]", f"\x1b[{height-1}A")]
504
- if height > 1
586
+ [
587
+ (
588
+ "[ZeroWidthEscape]",
589
+ f"\x1b[{visible_height - 1}A",
590
+ )
591
+ ]
592
+ if visible_height > 1
505
593
  else []
506
594
  ),
507
- ("[ZeroWidthEscape]", f"\x1b[{width}D"),
595
+ ("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
508
596
  # Place the image without moving cursor
509
- ("[ZeroWidthEscape]", passthrough(cmd)),
597
+ (
598
+ "[ZeroWidthEscape]",
599
+ passthrough(cmd, self.app.config),
600
+ ),
510
601
  # Restore the last known cursor position (at the bottom)
511
602
  ("[ZeroWidthEscape]", "\x1b[u"),
512
603
  ]
513
604
  )
514
605
  )
515
606
  )
607
+ else:
608
+ ft.append([("[ZeroWidthEscape]", self.hide_cmd())])
516
609
  return ft
517
610
 
518
611
  key = (
519
- width,
612
+ visible_width,
520
613
  self.app.color_palette,
521
- self.app.term_info.cell_size_px,
614
+ self.app.cell_size_px,
522
615
  self.bbox,
523
616
  )
524
617
  return self._format_cache.get(key, render_lines)
@@ -574,7 +667,7 @@ class GraphicWindow(Window):
574
667
  super().__init__(*args, **kwargs)
575
668
  self.content = content
576
669
  self.get_position = get_position
577
- self.filter = ~has_completions & ~has_dialog & ~has_menus & to_filter(filter)
670
+ self.filter = ~has_float & to_filter(filter)
578
671
  self._pre_rendered = False
579
672
 
580
673
  def write_to_screen(
@@ -650,14 +743,13 @@ def select_graphic_control(format_: str) -> type[GraphicControl] | None:
650
743
  """Determine which graphic control to use."""
651
744
  SelectedGraphicControl: type[GraphicControl] | None = None
652
745
  app = get_app()
653
- term_info = app.term_info
654
746
  preferred_graphics_protocol = app.config.graphics
655
747
  useable_graphics_controls: list[type[GraphicControl]] = []
656
748
  _in_mplex = in_mplex()
657
749
  force_graphics = app.config.force_graphics
658
750
 
659
751
  if preferred_graphics_protocol != "none":
660
- if (term_info.iterm_graphics_status.value or force_graphics) and find_route(
752
+ if (app.term_graphics_iterm or force_graphics) and find_route(
661
753
  format_, "base64-png"
662
754
  ):
663
755
  useable_graphics_controls.append(ItermGraphicControl)
@@ -665,14 +757,14 @@ def select_graphic_control(format_: str) -> type[GraphicControl] | None:
665
757
  preferred_graphics_protocol == "iterm"
666
758
  and ItermGraphicControl in useable_graphics_controls
667
759
  # Iterm does not work in mplex without pass-through
668
- and (not _in_mplex or (_in_mplex and app.config.mplex_graphics))
760
+ and (not _in_mplex or (_in_mplex and force_graphics))
669
761
  ):
670
762
  SelectedGraphicControl = ItermGraphicControl
671
763
  elif (
672
- (term_info.kitty_graphics_status.value or force_graphics)
764
+ (app.term_graphics_kitty or force_graphics)
673
765
  and find_route(format_, "base64-png")
674
766
  # Kitty does not work in mplex without pass-through
675
- and (not _in_mplex or (_in_mplex and app.config.mplex_graphics))
767
+ and (not _in_mplex or (_in_mplex and force_graphics))
676
768
  ):
677
769
  useable_graphics_controls.append(KittyGraphicControl)
678
770
  if (
@@ -681,7 +773,7 @@ def select_graphic_control(format_: str) -> type[GraphicControl] | None:
681
773
  ):
682
774
  SelectedGraphicControl = KittyGraphicControl
683
775
  # Tmux now supports sixels (>=3.4)
684
- elif (term_info.sixel_graphics_status.value or force_graphics) and find_route(
776
+ elif (app.term_graphics_sixel or force_graphics) and find_route(
685
777
  format_, "sixel"
686
778
  ):
687
779
  useable_graphics_controls.append(SixelGraphicControl)
@@ -705,9 +797,9 @@ class GraphicProcessor:
705
797
  self.control = control
706
798
 
707
799
  self.positions: dict[str, Point] = {}
708
- self._position_cache: FastDictCache[
709
- tuple[UIContent], dict[str, Point]
710
- ] = FastDictCache(self._load_positions, size=1_000)
800
+ self._position_cache: FastDictCache[tuple[UIContent], dict[str, Point]] = (
801
+ FastDictCache(self._load_positions, size=1_000)
802
+ )
711
803
  self._float_cache: FastDictCache[tuple[str], Float | None] = FastDictCache(
712
804
  self.get_graphic_float, size=1_000
713
805
  )
euporie/core/history.py CHANGED
@@ -8,9 +8,9 @@ from typing import TYPE_CHECKING
8
8
  from prompt_toolkit.history import History
9
9
 
10
10
  if TYPE_CHECKING:
11
- from typing import AsyncGenerator, Iterable
11
+ from collections.abc import AsyncGenerator, Iterable
12
12
 
13
- from euporie.core.kernel import Kernel
13
+ from euporie.core.kernel.client import Kernel
14
14
 
15
15
  log = logging.getLogger(__name__)
16
16
 
@@ -7,12 +7,13 @@ from abc import ABCMeta, abstractmethod
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Sequence
10
11
  from pathlib import Path
11
- from typing import Any, Callable, Sequence
12
+ from typing import Any, Callable
12
13
 
13
14
  from prompt_toolkit.document import Document
14
15
 
15
- from euporie.core.kernel import Kernel
16
+ from euporie.core.kernel.client import Kernel
16
17
  from euporie.core.lsp import LspClient
17
18
 
18
19