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.
- 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 -92
- 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 +26 -37
- 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 +9 -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.0.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
- euporie-2.8.5.dist-info/RECORD +172 -0
- {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
- {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
- {euporie-2.8.0.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.0.dist-info/RECORD +0 -146
- {euporie-2.8.0.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.0.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
euporie/core/convert/__init__.py
CHANGED
@@ -1,31 +1 @@
|
|
1
1
|
"""Sub-module concerned with the conversion of data formats."""
|
2
|
-
|
3
|
-
from euporie.core.convert.formats import (
|
4
|
-
ansi,
|
5
|
-
base64,
|
6
|
-
ft,
|
7
|
-
html,
|
8
|
-
jpeg,
|
9
|
-
markdown,
|
10
|
-
pdf,
|
11
|
-
pil,
|
12
|
-
png,
|
13
|
-
rich,
|
14
|
-
sixel,
|
15
|
-
svg,
|
16
|
-
)
|
17
|
-
|
18
|
-
__all__ = [
|
19
|
-
"ansi",
|
20
|
-
"base64",
|
21
|
-
"jpeg",
|
22
|
-
"html",
|
23
|
-
"markdown",
|
24
|
-
"ft",
|
25
|
-
"pdf",
|
26
|
-
"pil",
|
27
|
-
"png",
|
28
|
-
"rich",
|
29
|
-
"sixel",
|
30
|
-
"svg",
|
31
|
-
]
|
euporie/core/convert/datum.py
CHANGED
@@ -3,31 +3,31 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
import hashlib
|
7
6
|
import inspect
|
8
7
|
import io
|
9
8
|
import logging
|
10
9
|
import threading
|
10
|
+
from hashlib import md5
|
11
11
|
from typing import TYPE_CHECKING, Generic, TypeVar
|
12
12
|
from weakref import ReferenceType, WeakValueDictionary, finalize, ref
|
13
13
|
|
14
|
-
import imagesize
|
15
|
-
from PIL.Image import Image as PilImage
|
16
14
|
from prompt_toolkit.data_structures import Size
|
17
15
|
from prompt_toolkit.layout.containers import WindowAlign
|
18
16
|
|
17
|
+
from euporie.core.app.current import get_app
|
19
18
|
from euporie.core.convert.registry import (
|
20
19
|
_CONVERTOR_ROUTE_CACHE,
|
21
20
|
_FILTER_CACHE,
|
22
21
|
converters,
|
23
22
|
)
|
24
|
-
from euporie.core.current import get_app
|
25
23
|
from euporie.core.ft.utils import to_plain_text
|
26
24
|
|
27
25
|
if TYPE_CHECKING:
|
26
|
+
from collections.abc import Coroutine
|
28
27
|
from pathlib import Path
|
29
|
-
from typing import Any, ClassVar
|
28
|
+
from typing import Any, ClassVar
|
30
29
|
|
30
|
+
from PIL.Image import Image as PilImage
|
31
31
|
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
32
32
|
from rich.console import ConsoleRenderable
|
33
33
|
|
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
|
|
35
35
|
from euporie.core.style import ColorPaletteColor
|
36
36
|
|
37
37
|
|
38
|
-
T = TypeVar("T", bytes, str, "StyleAndTextTuples", PilImage, "ConsoleRenderable")
|
38
|
+
T = TypeVar("T", bytes, str, "StyleAndTextTuples", "PilImage", "ConsoleRenderable")
|
39
39
|
|
40
40
|
|
41
41
|
log = logging.getLogger(__name__)
|
@@ -139,6 +139,10 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
139
139
|
self._conversions: dict[
|
140
140
|
tuple[str, int | None, int | None, str | None, str | None, bool], T | None
|
141
141
|
] = {}
|
142
|
+
self._queue: dict[
|
143
|
+
tuple[str, int | None, int | None, str | None, str | None, bool],
|
144
|
+
asyncio.Event,
|
145
|
+
] = {}
|
142
146
|
self._finalizer = finalize(self, self._cleanup_datum_sizes, self.hash)
|
143
147
|
self._finalizer.atexit = False
|
144
148
|
|
@@ -153,7 +157,10 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
153
157
|
for key, (datum_ref, _size) in list(size_instances.items()):
|
154
158
|
datum = datum_ref()
|
155
159
|
if not datum or datum.hash == data_hash:
|
156
|
-
|
160
|
+
try:
|
161
|
+
del size_instances[key]
|
162
|
+
except KeyError:
|
163
|
+
pass
|
157
164
|
del datum
|
158
165
|
|
159
166
|
def to_bytes(self) -> bytes:
|
@@ -163,23 +170,31 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
163
170
|
return data.encode()
|
164
171
|
elif isinstance(data, list):
|
165
172
|
return to_plain_text(data).encode()
|
166
|
-
elif isinstance(data, PilImage):
|
167
|
-
return data.tobytes()
|
168
173
|
elif isinstance(data, bytes):
|
169
174
|
return data
|
170
175
|
else:
|
171
|
-
|
176
|
+
from PIL.Image import Image as PilImage
|
177
|
+
|
178
|
+
if isinstance(data, PilImage):
|
179
|
+
return data.tobytes()
|
180
|
+
else:
|
181
|
+
return b"Error"
|
172
182
|
|
173
183
|
@staticmethod
|
174
184
|
def get_hash(data: Any) -> str:
|
175
185
|
"""Calculate a hash of data."""
|
176
|
-
if isinstance(data,
|
186
|
+
if isinstance(data, bytes):
|
187
|
+
hash_data = data
|
188
|
+
elif isinstance(data, str):
|
177
189
|
hash_data = data.encode()
|
178
|
-
elif isinstance(data, PilImage):
|
179
|
-
hash_data = data.tobytes()
|
180
190
|
else:
|
181
|
-
|
182
|
-
|
191
|
+
from PIL.Image import Image as PilImage
|
192
|
+
|
193
|
+
if isinstance(data, PilImage):
|
194
|
+
hash_data = data.tobytes()
|
195
|
+
else:
|
196
|
+
hash_data = data
|
197
|
+
return md5(hash_data, usedforsecurity=False).hexdigest()
|
183
198
|
|
184
199
|
@property
|
185
200
|
def hash(self) -> str:
|
@@ -226,37 +241,56 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
226
241
|
if not bg and hasattr(app := get_app(), "color_palette"):
|
227
242
|
bg = self.bg or app.color_palette.bg.base_hex
|
228
243
|
|
229
|
-
if (
|
230
|
-
|
244
|
+
if (key_conv := (to, cols, rows, fg, bg, extend)) in self._queue:
|
245
|
+
await self._queue[key_conv].wait()
|
246
|
+
if key_conv in self._conversions:
|
247
|
+
return self._conversions[key_conv]
|
248
|
+
|
249
|
+
self._queue[key_conv] = event = asyncio.Event()
|
231
250
|
|
232
251
|
routes = _CONVERTOR_ROUTE_CACHE[(self.format, to)]
|
233
|
-
|
234
|
-
|
235
|
-
|
252
|
+
log.debug(
|
253
|
+
"Converting %s->'%s'@%s using routes: %s",
|
254
|
+
self,
|
255
|
+
to,
|
256
|
+
(cols, rows),
|
257
|
+
routes,
|
258
|
+
)
|
236
259
|
output: T | None = None
|
237
260
|
if routes:
|
238
261
|
datum = self
|
239
262
|
output = None
|
240
263
|
for route in routes:
|
241
264
|
for stage_a, stage_b in zip(route, route[1:]):
|
242
|
-
|
243
|
-
if
|
244
|
-
output = self._conversions[
|
265
|
+
key_stage = (stage_b, cols, rows, fg, bg, extend)
|
266
|
+
if key_stage in self._conversions:
|
267
|
+
output = self._conversions[key_stage]
|
245
268
|
else:
|
246
269
|
# Find converter with lowest weight
|
247
|
-
|
270
|
+
for converter in sorted(
|
248
271
|
[
|
249
272
|
conv
|
250
273
|
for conv in converters[stage_b][stage_a]
|
251
274
|
if _FILTER_CACHE.get((conv,), conv.filter_)
|
252
275
|
],
|
253
276
|
key=lambda x: x.weight,
|
254
|
-
)
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
277
|
+
):
|
278
|
+
try:
|
279
|
+
output = await converter.func(
|
280
|
+
datum, cols, rows, fg, bg, extend
|
281
|
+
)
|
282
|
+
self._conversions[key_stage] = output
|
283
|
+
except Exception:
|
284
|
+
log.debug(
|
285
|
+
"Conversion step %s failed",
|
286
|
+
converter,
|
287
|
+
exc_info=True,
|
288
|
+
)
|
289
|
+
continue
|
290
|
+
else:
|
291
|
+
break
|
292
|
+
else:
|
293
|
+
log.warning("An error occurred during format conversion")
|
260
294
|
output = None
|
261
295
|
if output is None:
|
262
296
|
log.error(
|
@@ -290,6 +324,9 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
290
324
|
if output is None:
|
291
325
|
output = ERROR_OUTPUTS.get(to, "(Conversion Error)")
|
292
326
|
|
327
|
+
event.set()
|
328
|
+
del self._queue[key_conv]
|
329
|
+
|
293
330
|
return output
|
294
331
|
|
295
332
|
def _to_sync(self, coro: Coroutine) -> Any:
|
@@ -323,36 +360,68 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
323
360
|
try:
|
324
361
|
return self._pixel_size
|
325
362
|
except AttributeError:
|
326
|
-
|
363
|
+
pass
|
364
|
+
|
365
|
+
px, py = self.px, self.py
|
366
|
+
self_data = self.data
|
367
|
+
format = self.format
|
368
|
+
data: bytes
|
369
|
+
|
370
|
+
while px is None or py is None:
|
327
371
|
# Do not bother trying if the format is ANSI
|
328
|
-
if
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
372
|
+
if format == "ansi":
|
373
|
+
break
|
374
|
+
|
375
|
+
from PIL.Image import Image as PilImage
|
376
|
+
|
377
|
+
if isinstance(self_data, PilImage):
|
378
|
+
px, py = self_data.size
|
379
|
+
break
|
380
|
+
|
381
|
+
# Decode base64 data
|
382
|
+
if format.startswith("base64-"):
|
383
|
+
data = await self.convert_async(to=format[7:])
|
384
|
+
|
385
|
+
# Encode string data
|
386
|
+
if isinstance(self_data, str):
|
387
|
+
data = self_data.encode()
|
388
|
+
|
389
|
+
if isinstance(self_data, bytes):
|
390
|
+
data = self_data
|
391
|
+
|
392
|
+
# Try using imagesize to get the size of the output
|
393
|
+
try:
|
394
|
+
import imagesize
|
395
|
+
|
396
|
+
px_calc, py_calc = imagesize.get(io.BytesIO(data))
|
397
|
+
except ValueError:
|
398
|
+
px_calc = py_calc = -1
|
399
|
+
|
400
|
+
if (
|
401
|
+
format != "png"
|
402
|
+
and px_calc <= 0
|
403
|
+
and py_calc <= 0
|
404
|
+
and _CONVERTOR_ROUTE_CACHE[(format, "png")]
|
405
|
+
):
|
406
|
+
# Try converting to PNG on failure
|
407
|
+
self_data = await self.convert_async(to="png")
|
408
|
+
format = "png"
|
409
|
+
continue
|
410
|
+
|
411
|
+
if px is None and px_calc > 0:
|
412
|
+
if py is not None and py_calc > 0:
|
413
|
+
px = int(px_calc * py / py_calc)
|
335
414
|
else:
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
px_calc, py_calc = imagesize.get(io.BytesIO(data))
|
341
|
-
except ValueError:
|
342
|
-
pass
|
415
|
+
px = px_calc
|
416
|
+
if py is None and py_calc > 0:
|
417
|
+
if px is not None and px_calc > 0:
|
418
|
+
py = int(py_calc * px / px_calc)
|
343
419
|
else:
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
if py is None and py_calc > 0:
|
350
|
-
if px is not None and px_calc > 0:
|
351
|
-
py = py_calc * px / px_calc
|
352
|
-
else:
|
353
|
-
py = py_calc
|
354
|
-
self._pixel_size = (px, py)
|
355
|
-
return self._pixel_size
|
420
|
+
py = py_calc
|
421
|
+
break
|
422
|
+
|
423
|
+
self._pixel_size = (px, py)
|
424
|
+
return self._pixel_size
|
356
425
|
|
357
426
|
def pixel_size(self) -> Any:
|
358
427
|
"""Get data dimensions synchronously."""
|
@@ -370,8 +439,8 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
370
439
|
px, py = await self.pixel_size_async()
|
371
440
|
if px is not None and py is not None:
|
372
441
|
app = get_app()
|
373
|
-
if hasattr(app, "
|
374
|
-
cell_px, cell_py = app.
|
442
|
+
if hasattr(app, "cell_size_px"):
|
443
|
+
cell_px, cell_py = app.cell_size_px
|
375
444
|
else:
|
376
445
|
cell_px, cell_py = 10, 20
|
377
446
|
cols = max(1, int(px // cell_px))
|
@@ -381,7 +450,9 @@ class Datum(Generic[T], metaclass=_MetaDatum):
|
|
381
450
|
|
382
451
|
def cell_size(self) -> Any:
|
383
452
|
"""Get cell width and aspect synchronously."""
|
384
|
-
|
453
|
+
if self._cell_size is None:
|
454
|
+
return self._to_sync(self.cell_size_async())
|
455
|
+
return self._cell_size
|
385
456
|
|
386
457
|
# def crop(self, bbox: DiInt) -> T:
|
387
458
|
# """Crop displayable data."""
|
@@ -3,4 +3,35 @@
|
|
3
3
|
They are grouped into sub-modules based on output format.
|
4
4
|
"""
|
5
5
|
|
6
|
+
from . import (
|
7
|
+
ansi,
|
8
|
+
base64,
|
9
|
+
ft,
|
10
|
+
html,
|
11
|
+
jpeg,
|
12
|
+
markdown,
|
13
|
+
pdf,
|
14
|
+
pil,
|
15
|
+
png,
|
16
|
+
rich,
|
17
|
+
sixel,
|
18
|
+
svg,
|
19
|
+
)
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
"BASE64_FORMATS",
|
23
|
+
"ansi",
|
24
|
+
"base64",
|
25
|
+
"ft",
|
26
|
+
"html",
|
27
|
+
"jpeg",
|
28
|
+
"markdown",
|
29
|
+
"pdf",
|
30
|
+
"pil",
|
31
|
+
"png",
|
32
|
+
"rich",
|
33
|
+
"sixel",
|
34
|
+
"svg",
|
35
|
+
]
|
36
|
+
|
6
37
|
BASE64_FORMATS = {"png", "jpeg", "pdf", "gif"}
|
@@ -4,14 +4,13 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import logging
|
6
6
|
from functools import partial
|
7
|
-
from math import ceil
|
8
7
|
from typing import TYPE_CHECKING
|
9
8
|
|
9
|
+
from euporie.core.app.current import get_app
|
10
10
|
from euporie.core.convert.formats.common import chafa_convert_cmd, chafa_convert_py
|
11
11
|
from euporie.core.convert.formats.pil import set_background
|
12
12
|
from euporie.core.convert.registry import register
|
13
|
-
from euporie.core.convert.utils import call_subproc
|
14
|
-
from euporie.core.current import get_app
|
13
|
+
from euporie.core.convert.utils import call_subproc, scale_to_fit
|
15
14
|
from euporie.core.filters import command_exists, have_modules
|
16
15
|
|
17
16
|
if TYPE_CHECKING:
|
@@ -169,9 +168,10 @@ async def html_to_ansi_py_htmlparser(
|
|
169
168
|
@register(
|
170
169
|
from_="latex",
|
171
170
|
to="ansi",
|
172
|
-
filter_=
|
171
|
+
filter_=command_exists("utftex"),
|
172
|
+
weight=0,
|
173
173
|
)
|
174
|
-
async def
|
174
|
+
async def latex_to_ansi_utftex(
|
175
175
|
datum: Datum,
|
176
176
|
cols: int | None = None,
|
177
177
|
rows: int | None = None,
|
@@ -179,16 +179,15 @@ async def latex_to_ansi_py_flatlatex(
|
|
179
179
|
bg: str | None = None,
|
180
180
|
extend: bool = True,
|
181
181
|
) -> str:
|
182
|
-
"""
|
183
|
-
|
184
|
-
|
185
|
-
return flatlatex.converter().convert(datum.data.strip().strip("$").strip())
|
182
|
+
"""Render LaTeX maths as unicode."""
|
183
|
+
return (await call_subproc(datum.data, ["utftex"])).decode()
|
186
184
|
|
187
185
|
|
188
186
|
@register(
|
189
187
|
from_="latex",
|
190
188
|
to="ansi",
|
191
189
|
filter_=have_modules("pylatexenc"),
|
190
|
+
weight=0,
|
192
191
|
)
|
193
192
|
async def latex_to_ansi_py_pylatexenc(
|
194
193
|
datum: Datum,
|
@@ -204,6 +203,36 @@ async def latex_to_ansi_py_pylatexenc(
|
|
204
203
|
return LatexNodes2Text().latex_to_text(datum.data.strip().strip("$").strip())
|
205
204
|
|
206
205
|
|
206
|
+
@register(
|
207
|
+
from_="latex",
|
208
|
+
to="ansi",
|
209
|
+
filter_=have_modules("flatlatex.latexfuntypes"),
|
210
|
+
weight=0,
|
211
|
+
)
|
212
|
+
async def latex_to_ansi_py_flatlatex(
|
213
|
+
datum: Datum,
|
214
|
+
cols: int | None = None,
|
215
|
+
rows: int | None = None,
|
216
|
+
fg: str | None = None,
|
217
|
+
bg: str | None = None,
|
218
|
+
extend: bool = True,
|
219
|
+
) -> str:
|
220
|
+
"""Convert LaTeX to ANSI using :py:mod:`flatlatex`."""
|
221
|
+
import flatlatex
|
222
|
+
from flatlatex.latexfuntypes import latexfun
|
223
|
+
|
224
|
+
converter = flatlatex.converter()
|
225
|
+
for style in (
|
226
|
+
r"\textstyle",
|
227
|
+
r"\displaystyle",
|
228
|
+
r"\scriptstyle",
|
229
|
+
r"\scriptscriptstyle",
|
230
|
+
):
|
231
|
+
converter._converter__cmds[style] = latexfun(lambda x: "", 0)
|
232
|
+
|
233
|
+
return converter.convert(datum.data.strip().strip("$").strip())
|
234
|
+
|
235
|
+
|
207
236
|
@register(
|
208
237
|
from_="latex",
|
209
238
|
to="ansi",
|
@@ -244,27 +273,14 @@ async def pil_to_ansi_py_timg(
|
|
244
273
|
"""Convert a PIL image to ANSI text using :py:mod:`timg`."""
|
245
274
|
import timg
|
246
275
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
elif cols is None and rows is not None:
|
256
|
-
w, h = data.size
|
257
|
-
cols = ceil(rows / h * w)
|
258
|
-
elif rows is None and cols is None:
|
259
|
-
cols = ceil(w / px)
|
260
|
-
rows = ceil(h / py)
|
261
|
-
assert rows is not None
|
262
|
-
assert cols is not None
|
263
|
-
|
264
|
-
# `timg` assumes a 2x1 terminal cell aspect ratio, so we correct for while
|
265
|
-
# resizing the image
|
266
|
-
data = data.resize((cols, ceil(rows * 2 * (px / py) / 0.5)))
|
267
|
-
|
276
|
+
# Scale image to fit available space
|
277
|
+
cols, rows = await scale_to_fit(datum, cols, rows)
|
278
|
+
# `timg` assumes a 2x1 terminal cell aspect ratio, so we correct for this while
|
279
|
+
px, py = get_app().cell_size_px
|
280
|
+
rows = int(rows * 2 * (px / py) / 0.5)
|
281
|
+
# Resize the image
|
282
|
+
data = datum.data.resize((cols, rows))
|
283
|
+
# Set background if necessary
|
268
284
|
if bg:
|
269
285
|
data = set_background(data, bg)
|
270
286
|
return timg.Ansi24HblockMethod(data).to_string()
|
@@ -6,8 +6,8 @@ import base64
|
|
6
6
|
import logging
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
|
-
from euporie.core.
|
10
|
-
from euporie.core.
|
9
|
+
from euporie.core.app.current import get_app
|
10
|
+
from euporie.core.convert.utils import call_subproc, scale_to_fit
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
13
|
from typing import Any, Literal
|
@@ -41,14 +41,15 @@ async def imagemagick_convert(
|
|
41
41
|
extend: bool = True,
|
42
42
|
) -> str | bytes:
|
43
43
|
"""Convert image data to PNG bytes using ``imagemagick``."""
|
44
|
-
cmd: list[Any] = ["
|
44
|
+
cmd: list[Any] = ["magick"]
|
45
|
+
if bg:
|
46
|
+
cmd += ["-background", bg]
|
47
|
+
cmd.extend(["-[0]", "-density", "300"])
|
45
48
|
app = get_app()
|
46
|
-
if cols is not None and hasattr(app, "
|
47
|
-
px, _ = app.
|
49
|
+
if cols is not None and hasattr(app, "cell_size_px"):
|
50
|
+
px, _ = app.cell_size_px
|
48
51
|
cmd += ["-geometry", f"{int(cols * px)}"]
|
49
|
-
|
50
|
-
cmd += ["-background", bg, "-flatten"]
|
51
|
-
cmd += ["-[0]", f"{output_format}:-"]
|
52
|
+
cmd += [f"{output_format}:-"]
|
52
53
|
result: bytes | str = await call_subproc(datum.data, cmd)
|
53
54
|
|
54
55
|
if output_format in {"sixel", "svg"} and isinstance(result, bytes):
|
@@ -90,7 +91,7 @@ async def chafa_convert_py(
|
|
90
91
|
extend: bool = True,
|
91
92
|
) -> str | bytes:
|
92
93
|
"""Convert image data to ANSI text using ::`chafa.py`."""
|
93
|
-
from chafa
|
94
|
+
from chafa import Canvas, CanvasConfig, PixelMode, PixelType
|
94
95
|
from PIL import Image
|
95
96
|
|
96
97
|
pil_mode_to_pixel_type = {
|
@@ -111,25 +112,12 @@ async def chafa_convert_py(
|
|
111
112
|
# Always convert the image, as unconverted images sometime result in an off-by-one
|
112
113
|
# line width errors resulting in diagonal image striping for some reason
|
113
114
|
data = data.convert("RGBA", palette=Image.Palette.ADAPTIVE, colors=16)
|
114
|
-
|
115
115
|
# Init canvas config
|
116
116
|
config = CanvasConfig()
|
117
117
|
# Set output mode
|
118
118
|
config.pixel_mode = str_to_pixel_mode[output_format]
|
119
119
|
# Configure the canvas geometry based on our cell size
|
120
|
-
|
121
|
-
px, py = app.term_info.cell_size_px
|
122
|
-
else:
|
123
|
-
px, py = 10, 20
|
124
|
-
config.cell_width, config.cell_height = px, py
|
125
|
-
# Set canvas height and width
|
126
|
-
if cols:
|
127
|
-
config.width = cols
|
128
|
-
if rows:
|
129
|
-
config.height = max(1, rows)
|
130
|
-
# If we don't have specified, use the image's aspect
|
131
|
-
else:
|
132
|
-
config.height = max(1, int(cols / data.size[0] * data.size[1] * px / py))
|
120
|
+
config.width, config.height = await scale_to_fit(datum, cols, rows)
|
133
121
|
|
134
122
|
# Set the foreground color
|
135
123
|
if fg and (color := fg.lstrip("#")):
|
@@ -3,61 +3,66 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import logging
|
6
|
+
from functools import cache
|
6
7
|
from typing import TYPE_CHECKING
|
7
8
|
|
8
|
-
from
|
9
|
-
from mdit_py_plugins.amsmath import amsmath_plugin
|
10
|
-
from mdit_py_plugins.dollarmath.index import dollarmath_plugin
|
11
|
-
from mdit_py_plugins.texmath.index import texmath_plugin
|
12
|
-
from pygments import highlight
|
13
|
-
from pygments.formatters import HtmlFormatter
|
14
|
-
|
9
|
+
from euporie.core.app.current import get_app
|
15
10
|
from euporie.core.convert.registry import register
|
16
|
-
from euporie.core.current import get_app
|
17
11
|
from euporie.core.lexers import detect_lexer
|
18
12
|
|
19
13
|
if TYPE_CHECKING:
|
14
|
+
from markdown_it import MarkdownIt
|
15
|
+
|
20
16
|
from euporie.core.convert.datum import Datum
|
21
17
|
|
22
18
|
log = logging.getLogger(__name__)
|
23
19
|
|
24
20
|
|
25
|
-
|
26
|
-
|
21
|
+
@cache
|
22
|
+
def markdown_parser() -> MarkdownIt:
|
23
|
+
"""Lazy-load a markdown parser."""
|
24
|
+
from markdown_it import MarkdownIt
|
25
|
+
from mdit_py_plugins.amsmath import amsmath_plugin
|
26
|
+
from mdit_py_plugins.dollarmath.index import dollarmath_plugin
|
27
|
+
from mdit_py_plugins.texmath.index import texmath_plugin
|
28
|
+
from pygments import highlight
|
29
|
+
from pygments.formatters import HtmlFormatter
|
27
30
|
|
28
|
-
|
29
|
-
"""
|
30
|
-
return True
|
31
|
+
class MarkdownParser(MarkdownIt):
|
32
|
+
"""Subclas the markdown parser to allow ``file:`` URIs."""
|
31
33
|
|
34
|
+
def validateLink(self, url: str) -> bool:
|
35
|
+
"""Allow all link URIs."""
|
36
|
+
return True
|
32
37
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
38
|
+
return (
|
39
|
+
(
|
40
|
+
MarkdownParser(
|
41
|
+
options_update={
|
42
|
+
"highlight": lambda text, language, lang_args: highlight(
|
43
|
+
text,
|
44
|
+
detect_lexer(text, language=language),
|
45
|
+
HtmlFormatter(
|
46
|
+
nowrap=True,
|
47
|
+
noclasses=True,
|
48
|
+
style=(
|
49
|
+
app.syntax_theme
|
50
|
+
if hasattr((app := get_app()), "syntax_theme")
|
51
|
+
else "default"
|
52
|
+
),
|
47
53
|
),
|
48
|
-
)
|
49
|
-
|
50
|
-
|
54
|
+
)
|
55
|
+
}
|
56
|
+
)
|
57
|
+
.enable("linkify")
|
58
|
+
.enable("table")
|
59
|
+
.enable("strikethrough")
|
51
60
|
)
|
52
|
-
.
|
53
|
-
.
|
54
|
-
.
|
61
|
+
.use(texmath_plugin)
|
62
|
+
.use(dollarmath_plugin)
|
63
|
+
.use(amsmath_plugin)
|
64
|
+
# .use(tasklists_plugin)
|
55
65
|
)
|
56
|
-
.use(texmath_plugin)
|
57
|
-
.use(dollarmath_plugin)
|
58
|
-
.use(amsmath_plugin)
|
59
|
-
# .use(tasklists_plugin)
|
60
|
-
)
|
61
66
|
|
62
67
|
|
63
68
|
@register(from_="markdown", to="html")
|
@@ -70,7 +75,7 @@ async def markdown_to_html_markdown_it(
|
|
70
75
|
extend: bool = True,
|
71
76
|
) -> str:
|
72
77
|
"""Convert markdown to HTML using :py:mod:`markdownit_py`."""
|
73
|
-
|
78
|
+
parser = markdown_parser()
|
74
79
|
data = datum.data
|
75
80
|
markup = data.decode() if isinstance(data, bytes) else data
|
76
|
-
return
|
81
|
+
return parser.render(markup)
|