euporie 2.8.1__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.
- euporie/console/_commands.py +143 -0
- euporie/console/_settings.py +58 -0
- euporie/console/app.py +25 -71
- euporie/console/tabs/console.py +267 -147
- euporie/core/__init__.py +1 -9
- euporie/core/__main__.py +31 -5
- euporie/core/_settings.py +104 -0
- euporie/core/app/__init__.py +3 -0
- euporie/core/app/_commands.py +70 -0
- euporie/core/app/_settings.py +427 -0
- euporie/core/{app.py → app/app.py} +214 -572
- 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 +182 -0
- euporie/core/bars/menu.py +258 -0
- euporie/core/{widgets → bars}/search.py +154 -57
- euporie/core/{widgets → bars}/status.py +9 -26
- euporie/core/clipboard.py +19 -80
- euporie/core/comm/base.py +8 -6
- euporie/core/comm/ipywidgets.py +21 -12
- euporie/core/comm/registry.py +2 -1
- euporie/core/commands.py +11 -5
- euporie/core/completion.py +3 -2
- euporie/core/config.py +368 -341
- euporie/core/convert/__init__.py +0 -30
- euporie/core/convert/datum.py +131 -60
- euporie/core/convert/formats/__init__.py +31 -0
- euporie/core/convert/formats/ansi.py +46 -30
- 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 +11 -8
- euporie/core/convert/utils.py +50 -23
- euporie/core/diagnostics.py +2 -2
- euporie/core/filters.py +72 -82
- euporie/core/format.py +13 -2
- euporie/core/ft/ansi.py +1 -1
- euporie/core/ft/html.py +36 -36
- euporie/core/ft/table.py +1 -3
- euporie/core/ft/utils.py +4 -1
- euporie/core/graphics.py +216 -124
- 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} +100 -139
- euporie/core/kernel/manager.py +114 -0
- euporie/core/key_binding/bindings/__init__.py +2 -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 +5 -7
- euporie/core/key_binding/bindings/mouse.py +26 -24
- euporie/core/key_binding/bindings/terminal.py +193 -0
- euporie/core/key_binding/bindings/vi.py +46 -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 +35 -25
- euporie/core/layout/containers.py +280 -74
- euporie/core/layout/decor.py +5 -5
- euporie/core/layout/mouse.py +1 -1
- euporie/core/layout/print.py +16 -3
- euporie/core/layout/scroll.py +26 -28
- euporie/core/log.py +75 -60
- euporie/core/lsp.py +118 -24
- euporie/core/margins.py +60 -31
- euporie/core/path.py +2 -1
- euporie/core/renderer.py +58 -17
- euporie/core/style.py +60 -40
- euporie/core/suggest.py +103 -85
- euporie/core/tabs/__init__.py +34 -0
- euporie/core/tabs/_settings.py +113 -0
- euporie/core/tabs/base.py +11 -435
- euporie/core/tabs/kernel.py +420 -0
- euporie/core/tabs/notebook.py +20 -54
- euporie/core/utils.py +98 -6
- euporie/core/validation.py +1 -1
- euporie/core/widgets/_settings.py +188 -0
- euporie/core/widgets/cell.py +90 -158
- euporie/core/widgets/cell_outputs.py +25 -36
- euporie/core/widgets/decor.py +11 -41
- euporie/core/widgets/dialog.py +55 -44
- euporie/core/widgets/display.py +27 -24
- euporie/core/widgets/file_browser.py +5 -26
- euporie/core/widgets/forms.py +16 -12
- euporie/core/widgets/inputs.py +37 -81
- euporie/core/widgets/layout.py +7 -6
- euporie/core/widgets/logo.py +49 -0
- euporie/core/widgets/menu.py +13 -11
- euporie/core/widgets/pager.py +8 -11
- euporie/core/widgets/palette.py +6 -6
- euporie/hub/app.py +52 -31
- euporie/notebook/_commands.py +24 -0
- euporie/notebook/_settings.py +107 -0
- euporie/notebook/app.py +109 -210
- 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 +2 -2
- euporie/notebook/tabs/edit.py +12 -7
- euporie/notebook/tabs/json.py +3 -3
- euporie/notebook/tabs/log.py +1 -18
- euporie/notebook/tabs/notebook.py +21 -674
- 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 +8 -30
- euporie/preview/tabs/notebook.py +15 -86
- euporie/web/tabs/web.py +4 -6
- euporie/web/widgets/webview.py +5 -12
- {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
- euporie-2.8.5.dist-info/RECORD +172 -0
- {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
- {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
- {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
- euporie/core/launch.py +0 -59
- euporie/core/terminal.py +0 -527
- euporie-2.8.1.dist-info/RECORD +0 -146
- {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.1.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
|
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,
|
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
|
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
|
-
|
135
|
-
|
129
|
+
width,
|
130
|
+
height,
|
136
131
|
self.app.color_palette,
|
137
|
-
self.app.
|
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
|
-
|
160
|
-
|
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.
|
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,
|
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
|
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
|
-
(
|
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
|
-
[
|
210
|
-
|
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[{
|
234
|
+
("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
|
214
235
|
# Place the image without moving cursor
|
215
|
-
(
|
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
|
-
|
250
|
+
visible_width,
|
226
251
|
self.app.color_palette,
|
227
|
-
self.app.
|
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.
|
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((
|
257
|
-
left =
|
258
|
-
top =
|
259
|
-
right = (
|
260
|
-
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
|
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,
|
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
|
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,
|
329
|
+
BoundedWritePosition(0, 0, cols, rows, d_bbox)
|
288
330
|
)
|
289
|
-
cmd = f"\x1b]1337;File=inline=1;width={
|
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
|
-
(
|
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
|
-
[
|
301
|
-
|
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[{
|
356
|
+
("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
|
305
357
|
# Place the image without moving cursor
|
306
|
-
(
|
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
|
-
|
371
|
+
visible_width,
|
317
372
|
self.app.color_palette,
|
318
|
-
self.app.
|
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
|
-
|
341
|
-
|
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.
|
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=
|
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,
|
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
|
-
|
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.
|
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
|
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=
|
481
|
-
r=
|
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 *
|
563
|
+
x=int(px * d_bbox.left / cols),
|
485
564
|
# Vertical pixel offset of the displayed image region
|
486
|
-
y=int(py *
|
565
|
+
y=int(py * d_bbox.top / rows),
|
487
566
|
# Pixel width of the displayed image region
|
488
|
-
w=int(px *
|
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 *
|
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
|
-
(
|
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
|
-
[
|
504
|
-
|
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[{
|
595
|
+
("[ZeroWidthEscape]", f"\x1b[{visible_width}D"),
|
508
596
|
# Place the image without moving cursor
|
509
|
-
(
|
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
|
-
|
612
|
+
visible_width,
|
520
613
|
self.app.color_palette,
|
521
|
-
self.app.
|
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 = ~
|
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 (
|
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
|
760
|
+
and (not _in_mplex or (_in_mplex and force_graphics))
|
669
761
|
):
|
670
762
|
SelectedGraphicControl = ItermGraphicControl
|
671
763
|
elif (
|
672
|
-
(
|
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
|
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 (
|
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
|
-
|
710
|
-
|
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
|
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
|
|
euporie/core/inspection.py
CHANGED
@@ -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
|
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
|
|