euporie 2.8.4__py3-none-any.whl → 2.8.6__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 (131) 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 +58 -62
  5. euporie/core/__init__.py +1 -1
  6. euporie/core/__main__.py +28 -11
  7. euporie/core/_settings.py +109 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +95 -0
  10. euporie/core/app/_settings.py +457 -0
  11. euporie/core/{app.py → app/app.py} +212 -576
  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 +205 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +20 -16
  21. euporie/core/{widgets → bars}/status.py +6 -23
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +16 -7
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +10 -20
  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 +116 -53
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +9 -23
  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 +4 -6
  39. euporie/core/convert/utils.py +41 -4
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +98 -40
  42. euporie/core/format.py +2 -3
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +12 -21
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +386 -133
  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} +45 -108
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +1 -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 +1 -6
  58. euporie/core/key_binding/bindings/mouse.py +2 -2
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/key_processor.py +43 -2
  61. euporie/core/key_binding/registry.py +2 -0
  62. euporie/core/key_binding/utils.py +22 -2
  63. euporie/core/keys.py +7156 -93
  64. euporie/core/layout/cache.py +3 -3
  65. euporie/core/layout/containers.py +48 -4
  66. euporie/core/layout/decor.py +2 -2
  67. euporie/core/layout/mouse.py +1 -1
  68. euporie/core/layout/print.py +2 -1
  69. euporie/core/layout/scroll.py +39 -34
  70. euporie/core/log.py +76 -64
  71. euporie/core/lsp.py +118 -24
  72. euporie/core/margins.py +1 -1
  73. euporie/core/path.py +62 -13
  74. euporie/core/renderer.py +58 -17
  75. euporie/core/style.py +57 -39
  76. euporie/core/suggest.py +103 -85
  77. euporie/core/tabs/__init__.py +32 -0
  78. euporie/core/tabs/_settings.py +113 -0
  79. euporie/core/tabs/base.py +80 -470
  80. euporie/core/tabs/kernel.py +419 -0
  81. euporie/core/tabs/notebook.py +24 -101
  82. euporie/core/utils.py +92 -15
  83. euporie/core/validation.py +1 -1
  84. euporie/core/widgets/_settings.py +188 -0
  85. euporie/core/widgets/cell.py +19 -50
  86. euporie/core/widgets/cell_outputs.py +25 -36
  87. euporie/core/widgets/decor.py +11 -41
  88. euporie/core/widgets/dialog.py +62 -27
  89. euporie/core/widgets/display.py +12 -15
  90. euporie/core/widgets/file_browser.py +2 -23
  91. euporie/core/widgets/forms.py +8 -5
  92. euporie/core/widgets/inputs.py +13 -70
  93. euporie/core/widgets/layout.py +2 -1
  94. euporie/core/widgets/logo.py +49 -0
  95. euporie/core/widgets/menu.py +10 -8
  96. euporie/core/widgets/pager.py +6 -10
  97. euporie/core/widgets/palette.py +6 -6
  98. euporie/hub/app.py +52 -35
  99. euporie/notebook/_commands.py +24 -0
  100. euporie/notebook/_settings.py +107 -0
  101. euporie/notebook/app.py +49 -171
  102. euporie/notebook/filters.py +1 -1
  103. euporie/notebook/tabs/__init__.py +46 -7
  104. euporie/notebook/tabs/_commands.py +714 -0
  105. euporie/notebook/tabs/_settings.py +32 -0
  106. euporie/notebook/tabs/display.py +4 -4
  107. euporie/notebook/tabs/edit.py +11 -44
  108. euporie/notebook/tabs/json.py +5 -5
  109. euporie/notebook/tabs/log.py +1 -18
  110. euporie/notebook/tabs/notebook.py +11 -660
  111. euporie/notebook/widgets/_commands.py +11 -0
  112. euporie/notebook/widgets/_settings.py +19 -0
  113. euporie/notebook/widgets/side_bar.py +14 -34
  114. euporie/preview/_settings.py +104 -0
  115. euporie/preview/app.py +6 -31
  116. euporie/preview/tabs/notebook.py +6 -72
  117. euporie/web/__init__.py +1 -0
  118. euporie/web/tabs/__init__.py +14 -0
  119. euporie/web/tabs/web.py +11 -6
  120. euporie/web/widgets/__init__.py +1 -0
  121. euporie/web/widgets/webview.py +5 -15
  122. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
  123. euporie-2.8.6.dist-info/RECORD +175 -0
  124. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
  125. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
  126. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
  127. euporie/core/launch.py +0 -64
  128. euporie/core/terminal.py +0 -522
  129. euporie-2.8.4.dist-info/RECORD +0 -147
  130. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
  131. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -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
- ]
@@ -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, Coroutine
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
 
@@ -166,23 +170,31 @@ class Datum(Generic[T], metaclass=_MetaDatum):
166
170
  return data.encode()
167
171
  elif isinstance(data, list):
168
172
  return to_plain_text(data).encode()
169
- elif isinstance(data, PilImage):
170
- return data.tobytes()
171
173
  elif isinstance(data, bytes):
172
174
  return data
173
175
  else:
174
- return b"Error"
176
+ from PIL.Image import Image as PilImage
177
+
178
+ if isinstance(data, PilImage):
179
+ return data.tobytes()
180
+ else:
181
+ return b"Error"
175
182
 
176
183
  @staticmethod
177
184
  def get_hash(data: Any) -> str:
178
185
  """Calculate a hash of data."""
179
- if isinstance(data, str):
186
+ if isinstance(data, bytes):
187
+ hash_data = data
188
+ elif isinstance(data, str):
180
189
  hash_data = data.encode()
181
- elif isinstance(data, PilImage):
182
- hash_data = data.tobytes()
183
190
  else:
184
- hash_data = data
185
- return hashlib.sha1(hash_data).hexdigest() # noqa S324
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()
186
198
 
187
199
  @property
188
200
  def hash(self) -> str:
@@ -229,20 +241,30 @@ class Datum(Generic[T], metaclass=_MetaDatum):
229
241
  if not bg and hasattr(app := get_app(), "color_palette"):
230
242
  bg = self.bg or app.color_palette.bg.base_hex
231
243
 
232
- if (key := (to, cols, rows, fg, bg, extend)) in self._conversions:
233
- return self._conversions[key]
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()
234
250
 
235
251
  routes = _CONVERTOR_ROUTE_CACHE[(self.format, to)]
236
- # log.debug("Converting from '%s' to '%s' using route: %s", self, to, routes)
252
+ log.debug(
253
+ "Converting %s->'%s'@%s using routes: %s",
254
+ self,
255
+ to,
256
+ (cols, rows),
257
+ routes,
258
+ )
237
259
  output: T | None = None
238
260
  if routes:
239
261
  datum = self
240
262
  output = None
241
263
  for route in routes:
242
264
  for stage_a, stage_b in zip(route, route[1:]):
243
- key = (stage_b, cols, rows, fg, bg, extend)
244
- if key in self._conversions:
245
- output = self._conversions[key]
265
+ key_stage = (stage_b, cols, rows, fg, bg, extend)
266
+ if key_stage in self._conversions:
267
+ output = self._conversions[key_stage]
246
268
  else:
247
269
  # Find converter with lowest weight
248
270
  for converter in sorted(
@@ -257,14 +279,18 @@ class Datum(Generic[T], metaclass=_MetaDatum):
257
279
  output = await converter.func(
258
280
  datum, cols, rows, fg, bg, extend
259
281
  )
260
- self._conversions[key] = output
282
+ self._conversions[key_stage] = output
261
283
  except Exception:
262
- log.debug("Conversion step %s failed", converter)
284
+ log.debug(
285
+ "Conversion step %s failed",
286
+ converter,
287
+ exc_info=True,
288
+ )
263
289
  continue
264
290
  else:
265
291
  break
266
292
  else:
267
- log.exception("An error occurred during format conversion")
293
+ log.warning("An error occurred during format conversion")
268
294
  output = None
269
295
  if output is None:
270
296
  log.error(
@@ -298,6 +324,9 @@ class Datum(Generic[T], metaclass=_MetaDatum):
298
324
  if output is None:
299
325
  output = ERROR_OUTPUTS.get(to, "(Conversion Error)")
300
326
 
327
+ event.set()
328
+ del self._queue[key_conv]
329
+
301
330
  return output
302
331
 
303
332
  def _to_sync(self, coro: Coroutine) -> Any:
@@ -331,36 +360,68 @@ class Datum(Generic[T], metaclass=_MetaDatum):
331
360
  try:
332
361
  return self._pixel_size
333
362
  except AttributeError:
334
- px, py = self.px, self.py
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:
335
371
  # Do not bother trying if the format is ANSI
336
- if self.format != "ansi" and (px is None or py is None):
337
- # Try using imagesize to get the size of the output
338
- if (
339
- self.format not in {"png", "svg", "jpeg", "gif", "tiff"}
340
- and _CONVERTOR_ROUTE_CACHE[(self.format, "png")]
341
- ):
342
- data = await self.convert_async(to="png")
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)
343
414
  else:
344
- data = self.data
345
- if isinstance(data, str):
346
- data = data.encode()
347
- try:
348
- px_calc, py_calc = imagesize.get(io.BytesIO(data))
349
- except ValueError:
350
- 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)
351
419
  else:
352
- if px is None and px_calc > 0:
353
- if py is not None and py_calc > 0:
354
- px = px_calc * py / py_calc
355
- else:
356
- px = px_calc
357
- if py is None and py_calc > 0:
358
- if px is not None and px_calc > 0:
359
- py = py_calc * px / px_calc
360
- else:
361
- py = py_calc
362
- self._pixel_size = (px, py)
363
- return self._pixel_size
420
+ py = py_calc
421
+ break
422
+
423
+ self._pixel_size = (px, py)
424
+ return self._pixel_size
364
425
 
365
426
  def pixel_size(self) -> Any:
366
427
  """Get data dimensions synchronously."""
@@ -378,8 +439,8 @@ class Datum(Generic[T], metaclass=_MetaDatum):
378
439
  px, py = await self.pixel_size_async()
379
440
  if px is not None and py is not None:
380
441
  app = get_app()
381
- if hasattr(app, "term_info"):
382
- cell_px, cell_py = app.term_info.cell_size_px
442
+ if hasattr(app, "cell_size_px"):
443
+ cell_px, cell_py = app.cell_size_px
383
444
  else:
384
445
  cell_px, cell_py = 10, 20
385
446
  cols = max(1, int(px // cell_px))
@@ -389,7 +450,9 @@ class Datum(Generic[T], metaclass=_MetaDatum):
389
450
 
390
451
  def cell_size(self) -> Any:
391
452
  """Get cell width and aspect synchronously."""
392
- return self._to_sync(self.cell_size_async())
453
+ if self._cell_size is None:
454
+ return self._to_sync(self.cell_size_async())
455
+ return self._cell_size
393
456
 
394
457
  # def crop(self, bbox: DiInt) -> T:
395
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:
@@ -274,27 +273,14 @@ async def pil_to_ansi_py_timg(
274
273
  """Convert a PIL image to ANSI text using :py:mod:`timg`."""
275
274
  import timg
276
275
 
277
- data = datum.data
278
- px, py = get_app().term_info.cell_size_px
279
-
280
- # Calculate rows based on image aspect ratio
281
- w, h = data.size
282
- if rows is None and cols is not None:
283
- w, h = data.size
284
- rows = ceil(cols / w * h)
285
- elif cols is None and rows is not None:
286
- w, h = data.size
287
- cols = ceil(rows / h * w)
288
- elif rows is None and cols is None:
289
- cols = ceil(w / px)
290
- rows = ceil(h / py)
291
- assert rows is not None
292
- assert cols is not None
293
-
276
+ # Scale image to fit available space
277
+ cols, rows = await scale_to_fit(datum, cols, rows)
294
278
  # `timg` assumes a 2x1 terminal cell aspect ratio, so we correct for this while
295
- # resizing the image
296
- data = data.resize((cols, ceil(rows * 2 * (px / py) / 0.5)))
297
-
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
298
284
  if bg:
299
285
  data = set_background(data, bg)
300
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.convert.utils import call_subproc
10
- from euporie.core.current import get_app
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] = ["convert", "-density", "300"]
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, "term_info"):
47
- px, _ = app.term_info.cell_size_px
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
- if bg:
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.chafa import Canvas, CanvasConfig, PixelMode, PixelType
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
- if hasattr(app := get_app(), "term_info"):
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 markdown_it import MarkdownIt
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
- class MarkdownParser(MarkdownIt):
26
- """Subclas the markdown parser to allow ``file:`` URIs."""
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
- def validateLink(self, url: str) -> bool:
29
- """Allow all link URIs."""
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
- markdown_parser = (
34
- (
35
- MarkdownParser(
36
- options_update={
37
- "highlight": lambda text, language, lang_args: highlight(
38
- text,
39
- detect_lexer(text, language=language),
40
- HtmlFormatter(
41
- nowrap=True,
42
- noclasses=True,
43
- style=(
44
- app.syntax_theme
45
- if hasattr((app := get_app()), "syntax_theme")
46
- else "default"
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
- .enable("linkify")
53
- .enable("table")
54
- .enable("strikethrough")
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
- assert markdown_parser is not None
78
+ parser = markdown_parser()
74
79
  data = datum.data
75
80
  markup = data.decode() if isinstance(data, bytes) else data
76
- return markdown_parser.render(markup)
81
+ return parser.render(markup)
@@ -72,7 +72,7 @@ def crop(data: PilImage, bbox: DiInt) -> PilImage:
72
72
  rows=full_height,
73
73
  )
74
74
  if image is not None:
75
- cell_size_x, cell_size_y = self.app.term_info.cell_size_px
75
+ cell_size_x, cell_size_y = self.app.cell_size_px
76
76
  # Downscale image to fit target region for precise cropping
77
77
  image.thumbnail((full_width * cell_size_x, full_height * cell_size_y))
78
78
  image = image.crop(
@@ -13,7 +13,6 @@ from euporie.core.filters import command_exists, have_modules
13
13
  if TYPE_CHECKING:
14
14
  from euporie.core.convert.datum import Datum
15
15
 
16
-
17
16
  register(
18
17
  from_="base64-png",
19
18
  to="png",
@@ -126,9 +125,8 @@ async def latex_to_png_py_mpl(
126
125
  from matplotlib.backends import backend_agg
127
126
 
128
127
  # mpl mathtext doesn't support display math, force inline
129
- data = datum.data.strip().replace("$$", "$")
130
- if not data.startswith("$"):
131
- data = f"${data}$"
128
+ data = datum.data.replace("$", "").strip()
129
+ data = f"${data}$"
132
130
  buffer = BytesIO()
133
131
  prop = font_manager.FontProperties(size=12)
134
132
  parser = mathtext.MathTextParser("path")
@@ -143,7 +141,7 @@ async def latex_to_png_py_mpl(
143
141
  register(
144
142
  from_=("svg", "jpeg", "pdf", "gif"),
145
143
  to="png",
146
- filter_=command_exists("convert", "mogrify"),
144
+ filter_=command_exists("magick"),
147
145
  )(partial(imagemagick_convert, "PNG"))
148
146
 
149
147
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from functools import partial
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from euporie.core.app.current import get_app
8
9
  from euporie.core.convert.formats.common import (
9
10
  chafa_convert_cmd,
10
11
  chafa_convert_py,
@@ -12,7 +13,6 @@ from euporie.core.convert.formats.common import (
12
13
  )
13
14
  from euporie.core.convert.registry import register
14
15
  from euporie.core.convert.utils import call_subproc
15
- from euporie.core.current import get_app
16
16
  from euporie.core.filters import command_exists, have_modules
17
17
 
18
18
  if TYPE_CHECKING:
@@ -53,7 +53,7 @@ async def png_to_sixel_img2sixel(
53
53
  if bg:
54
54
  cmd += [f"--bgcolor={bg}"]
55
55
  if cols is not None:
56
- px, _ = get_app().term_info.cell_size_px
56
+ px, _ = get_app().cell_size_px
57
57
  cmd += [f"--width={int(cols * px)}"]
58
58
  return (await call_subproc(datum.data, cmd)).decode()
59
59
 
@@ -61,7 +61,7 @@ async def png_to_sixel_img2sixel(
61
61
  register(
62
62
  from_=("png", "jpeg", "svg", "pdf"),
63
63
  to="sixel",
64
- filter_=command_exists("convert", "mogrify"),
64
+ filter_=command_exists("magick"),
65
65
  )(partial(imagemagick_convert, "sixel"))
66
66
 
67
67