euporie 2.6.1__py3-none-any.whl → 2.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euporie/console/tabs/console.py +51 -43
- euporie/core/__init__.py +5 -2
- euporie/core/app.py +74 -57
- euporie/core/comm/ipywidgets.py +7 -3
- euporie/core/config.py +51 -27
- euporie/core/convert/__init__.py +2 -0
- euporie/core/convert/datum.py +82 -45
- euporie/core/convert/formats/ansi.py +1 -2
- euporie/core/convert/formats/common.py +7 -11
- euporie/core/convert/formats/ft.py +10 -7
- euporie/core/convert/formats/png.py +7 -6
- euporie/core/convert/formats/sixel.py +1 -1
- euporie/core/convert/formats/svg.py +28 -0
- euporie/core/convert/mime.py +4 -7
- euporie/core/data_structures.py +24 -22
- euporie/core/filters.py +16 -2
- euporie/core/format.py +30 -4
- euporie/core/ft/ansi.py +2 -1
- euporie/core/ft/html.py +155 -42
- euporie/core/{widgets/graphics.py → graphics.py} +225 -227
- euporie/core/io.py +8 -0
- euporie/core/key_binding/bindings/__init__.py +8 -2
- euporie/core/key_binding/bindings/basic.py +9 -14
- euporie/core/key_binding/bindings/micro.py +0 -12
- euporie/core/key_binding/bindings/mouse.py +107 -80
- euporie/core/key_binding/bindings/page_navigation.py +129 -0
- euporie/core/key_binding/key_processor.py +9 -1
- euporie/core/layout/__init__.py +1 -0
- euporie/core/layout/containers.py +1011 -0
- euporie/core/layout/decor.py +381 -0
- euporie/core/layout/print.py +130 -0
- euporie/core/layout/screen.py +75 -0
- euporie/core/{widgets/page.py → layout/scroll.py} +166 -111
- euporie/core/log.py +1 -1
- euporie/core/margins.py +11 -5
- euporie/core/path.py +43 -176
- euporie/core/renderer.py +31 -8
- euporie/core/style.py +2 -0
- euporie/core/tabs/base.py +2 -1
- euporie/core/terminal.py +19 -21
- euporie/core/widgets/cell.py +2 -4
- euporie/core/widgets/cell_outputs.py +2 -2
- euporie/core/widgets/decor.py +3 -359
- euporie/core/widgets/dialog.py +5 -5
- euporie/core/widgets/display.py +32 -12
- euporie/core/widgets/file_browser.py +3 -4
- euporie/core/widgets/forms.py +36 -14
- euporie/core/widgets/inputs.py +171 -99
- euporie/core/widgets/layout.py +80 -5
- euporie/core/widgets/menu.py +1 -3
- euporie/core/widgets/pager.py +3 -3
- euporie/core/widgets/palette.py +3 -2
- euporie/core/widgets/status_bar.py +2 -6
- euporie/core/widgets/tree.py +3 -6
- euporie/notebook/app.py +8 -8
- euporie/notebook/tabs/notebook.py +2 -2
- euporie/notebook/widgets/side_bar.py +1 -1
- euporie/preview/tabs/notebook.py +2 -2
- euporie/web/tabs/web.py +6 -1
- euporie/web/widgets/webview.py +52 -32
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/METADATA +9 -11
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/RECORD +67 -60
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/WHEEL +1 -1
- {euporie-2.6.1.data → euporie-2.7.0.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.6.1.data → euporie-2.7.0.data}/data/share/applications/euporie-notebook.desktop +0 -0
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/entry_points.txt +0 -0
- {euporie-2.6.1.dist-info → euporie-2.7.0.dist-info}/licenses/LICENSE +0 -0
euporie/core/convert/datum.py
CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
|
4
4
|
import asyncio
|
5
5
|
import hashlib
|
6
|
+
import inspect
|
6
7
|
import io
|
7
8
|
import logging
|
8
9
|
import threading
|
@@ -13,6 +14,7 @@ import imagesize
|
|
13
14
|
from PIL.Image import Image as PilImage
|
14
15
|
from prompt_toolkit.cache import SimpleCache
|
15
16
|
from prompt_toolkit.data_structures import Size
|
17
|
+
from prompt_toolkit.layout.containers import WindowAlign
|
16
18
|
|
17
19
|
from euporie.core.convert.registry import (
|
18
20
|
_CONVERTOR_ROUTE_CACHE,
|
@@ -26,18 +28,20 @@ if TYPE_CHECKING:
|
|
26
28
|
from pathlib import Path
|
27
29
|
from typing import Any, ClassVar, Coroutine
|
28
30
|
|
31
|
+
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
29
32
|
from rich.console import ConsoleRenderable
|
30
33
|
|
34
|
+
from euporie.core.data_structures import DiInt
|
31
35
|
from euporie.core.style import ColorPaletteColor
|
32
36
|
|
33
37
|
|
34
|
-
T = TypeVar("T", bytes, str, PilImage, "ConsoleRenderable")
|
38
|
+
T = TypeVar("T", bytes, str, "StyleAndTextTuples", PilImage, "ConsoleRenderable")
|
35
39
|
|
36
40
|
|
37
41
|
log = logging.getLogger(__name__)
|
38
42
|
|
39
43
|
|
40
|
-
ERROR_OUTPUTS = {
|
44
|
+
ERROR_OUTPUTS: dict[str, Any] = {
|
41
45
|
"ansi": "(Format Conversion Error)",
|
42
46
|
"ft": [("fg:white bg:darkred", "(Format Conversion Error)")],
|
43
47
|
}
|
@@ -77,27 +81,42 @@ def get_loop() -> asyncio.AbstractEventLoop:
|
|
77
81
|
return _LOOP[0]
|
78
82
|
|
79
83
|
|
80
|
-
class
|
84
|
+
class _MetaDatum(type):
|
85
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
86
|
+
super().__init__(*args, **kwargs)
|
87
|
+
self._instances: WeakValueDictionary[
|
88
|
+
tuple[Any, ...], Datum
|
89
|
+
] = WeakValueDictionary()
|
90
|
+
|
91
|
+
def __call__(self, data: T, *args: Any, **kwargs: Any) -> Datum[T]:
|
92
|
+
data_hash = Datum.get_hash(data)
|
93
|
+
key: tuple[Any, ...] = (
|
94
|
+
data_hash,
|
95
|
+
*args,
|
96
|
+
# Get defaults for non-passed kwargs
|
97
|
+
*(
|
98
|
+
kwargs.get(param.name, param.default)
|
99
|
+
for param in inspect.signature(Datum.__init__).parameters.values()
|
100
|
+
if param.default is not inspect._empty
|
101
|
+
and param.name not in {"path", "source"}
|
102
|
+
),
|
103
|
+
)
|
104
|
+
if key in self._instances:
|
105
|
+
return self._instances[key]
|
106
|
+
instance = super().__call__(data, *args, **kwargs)
|
107
|
+
self._instances[key] = instance
|
108
|
+
return instance
|
109
|
+
|
110
|
+
|
111
|
+
class Datum(Generic[T], metaclass=_MetaDatum):
|
81
112
|
"""Class for storing and converting display data."""
|
82
113
|
|
83
114
|
_pixel_size: tuple[int | None, int | None]
|
84
115
|
_hash: str
|
85
116
|
_root: ReferenceType[Datum]
|
86
117
|
|
87
|
-
_instances: WeakValueDictionary[str, Datum] = WeakValueDictionary()
|
88
118
|
_sizes: ClassVar[dict[str, tuple[ReferenceType[Datum], Size]]] = {}
|
89
119
|
|
90
|
-
def __new__(cls, data: T, *args: Any, **kwargs: Any) -> Datum:
|
91
|
-
"""Create a single instance based on unique data."""
|
92
|
-
data_hash = cls.get_hash(data)
|
93
|
-
|
94
|
-
if instance := cls._instances.get(data_hash):
|
95
|
-
return instance
|
96
|
-
|
97
|
-
instance = super().__new__(cls)
|
98
|
-
cls._instances[data_hash] = instance
|
99
|
-
return instance
|
100
|
-
|
101
120
|
def __init__(
|
102
121
|
self,
|
103
122
|
data: T,
|
@@ -108,24 +127,37 @@ class Datum(Generic[T]):
|
|
108
127
|
bg: ColorPaletteColor | str | None = None,
|
109
128
|
path: Path | None = None,
|
110
129
|
source: Datum | None = None,
|
130
|
+
align: WindowAlign = WindowAlign.LEFT,
|
111
131
|
) -> None:
|
112
132
|
"""Create a new instance of display data."""
|
113
133
|
# self.self = self
|
114
134
|
self.data: T = data
|
115
135
|
self.format = format
|
116
136
|
self.px, self.py = px, py
|
117
|
-
self.
|
118
|
-
self.
|
137
|
+
self._fg = str(fg) if fg else None
|
138
|
+
self._bg = str(bg) if bg else None
|
119
139
|
self.path = path
|
120
140
|
self.source: ReferenceType[Datum] = ref(source) if source else ref(self)
|
121
|
-
|
141
|
+
self.align = align
|
122
142
|
self._cell_size: tuple[int, float] | None = None
|
123
|
-
|
124
|
-
self._conversions: dict[tuple[str, int | None, int | None, bool], T] = {}
|
125
|
-
|
143
|
+
self._conversions: dict[tuple[str, int | None, int | None, bool], T | None] = {}
|
126
144
|
self._finalizer = finalize(self, self._cleanup_datum_sizes, self.hash)
|
127
145
|
self._finalizer.atexit = False
|
128
146
|
|
147
|
+
@property
|
148
|
+
def fg(self) -> str:
|
149
|
+
"""The foreground color."""
|
150
|
+
if not (fg := self._fg) and hasattr(app := get_app(), "color_palette"):
|
151
|
+
return app.color_palette.fg.base_hex
|
152
|
+
return fg or "#ffffff"
|
153
|
+
|
154
|
+
@property
|
155
|
+
def bg(self) -> str:
|
156
|
+
"""The background color."""
|
157
|
+
if not (bg := self._bg) and hasattr(app := get_app(), "color_palette"):
|
158
|
+
return app.color_palette.bg.base_hex
|
159
|
+
return bg or "#000000"
|
160
|
+
|
129
161
|
def __repr__(self) -> str:
|
130
162
|
"""Return a string representation of object."""
|
131
163
|
return f"{self.__class__.__name__}(format={self.format!r})"
|
@@ -196,45 +228,44 @@ class Datum(Generic[T]):
|
|
196
228
|
cols: int | None = None,
|
197
229
|
rows: int | None = None,
|
198
230
|
extend: bool = True,
|
231
|
+
bbox: DiInt | None = None,
|
199
232
|
) -> Any:
|
200
233
|
"""Perform conversion asynchronously, caching the result."""
|
201
234
|
if to == self.format:
|
202
235
|
# TODO - crop
|
203
236
|
return self.data
|
204
237
|
|
205
|
-
if to in self._conversions:
|
238
|
+
if (to, cols, rows, extend) in self._conversions:
|
206
239
|
return self._conversions[to, cols, rows, extend]
|
207
240
|
|
208
241
|
routes = _CONVERTOR_ROUTE_CACHE[(self.format, to)]
|
209
242
|
# log.debug(
|
210
243
|
# "Converting from '%s' to '%s' using route: %s", self, to, routes
|
211
244
|
# )
|
212
|
-
|
213
|
-
|
214
|
-
# f"Cannot convert from `self.format` to `to`"
|
215
|
-
# )
|
216
|
-
log.warning("Cannot convert from `%s` to `%s`", self.format, to)
|
217
|
-
output = None
|
218
|
-
else:
|
245
|
+
output: T | None = None
|
246
|
+
if routes:
|
219
247
|
datum = self
|
220
248
|
output = None
|
221
249
|
for route in routes:
|
222
250
|
for stage_a, stage_b in zip(route, route[1:]):
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
251
|
+
if (stage_b, cols, rows, extend) in self._conversions:
|
252
|
+
output = self._conversions[stage_b, cols, rows, extend]
|
253
|
+
else:
|
254
|
+
# Find converter with lowest weight
|
255
|
+
func = sorted(
|
256
|
+
[
|
257
|
+
conv
|
258
|
+
for conv in converters[stage_b][stage_a]
|
259
|
+
if _FILTER_CACHE.get((conv,), conv.filter_)
|
260
|
+
],
|
261
|
+
key=lambda x: x.weight,
|
262
|
+
)[0].func
|
263
|
+
try:
|
264
|
+
output = await func(datum, cols, rows, extend)
|
265
|
+
self._conversions[stage_b, cols, rows, extend] = output
|
266
|
+
except Exception:
|
267
|
+
log.exception("An error occurred during format conversion")
|
268
|
+
output = None
|
238
269
|
if output is None:
|
239
270
|
log.error(
|
240
271
|
"Failed to convert `%s`"
|
@@ -261,6 +292,9 @@ class Datum(Generic[T]):
|
|
261
292
|
# If this route succeeded, stop trying routes
|
262
293
|
break
|
263
294
|
|
295
|
+
# Crop or pad output
|
296
|
+
# if bbox and any(bbox):
|
297
|
+
|
264
298
|
if output is None:
|
265
299
|
output = ERROR_OUTPUTS.get(to, "(Conversion Error)")
|
266
300
|
|
@@ -299,7 +333,10 @@ class Datum(Generic[T]):
|
|
299
333
|
# Do not bother trying if the format is ANSI
|
300
334
|
if self.format != "ansi" and (px is None or py is None):
|
301
335
|
# Try using imagesize to get the size of the output
|
302
|
-
if
|
336
|
+
if (
|
337
|
+
self.format not in {"png", "svg", "jpeg", "gif", "tiff"}
|
338
|
+
and _CONVERTOR_ROUTE_CACHE[(self.format, "png")]
|
339
|
+
):
|
303
340
|
data = await self.convert_async(to="png")
|
304
341
|
else:
|
305
342
|
data = self.data
|
@@ -246,10 +246,9 @@ async def pil_to_ansi_py_timg(
|
|
246
246
|
# resizing the image
|
247
247
|
data = data.resize((cols, ceil(rows * 2 * (px / py) / 0.5)))
|
248
248
|
|
249
|
-
bg = str(datum.bg
|
249
|
+
bg = str(datum.bg or get_app().color_palette.bg.base_hex)
|
250
250
|
if bg:
|
251
251
|
data = set_background(data, bg)
|
252
|
-
data = set_background(data, bg)
|
253
252
|
return timg.Ansi24HblockMethod(data).to_string()
|
254
253
|
|
255
254
|
|
@@ -46,7 +46,7 @@ async def imagemagick_convert(
|
|
46
46
|
if not bg and hasattr(app, "color_palette"):
|
47
47
|
bg = app.color_palette.bg.base_hex
|
48
48
|
if bg:
|
49
|
-
cmd += ["-background", str(bg)]
|
49
|
+
cmd += ["-background", str(bg), "-flatten"]
|
50
50
|
cmd += ["-[0]", f"{output_format}:-"]
|
51
51
|
result: bytes | str = await call_subproc(datum.data, cmd)
|
52
52
|
|
@@ -86,6 +86,7 @@ async def chafa_convert_py(
|
|
86
86
|
) -> str | bytes:
|
87
87
|
"""Convert image data to ANSI text using ::`chafa.py`."""
|
88
88
|
from chafa.chafa import Canvas, CanvasConfig, PixelMode, PixelType
|
89
|
+
from PIL import Image
|
89
90
|
|
90
91
|
pil_mode_to_pixel_type = {
|
91
92
|
"RGBa": PixelType.CHAFA_PIXEL_RGBA8_PREMULTIPLIED,
|
@@ -102,10 +103,9 @@ async def chafa_convert_py(
|
|
102
103
|
|
103
104
|
# Convert PIL image to format that chafa can use
|
104
105
|
data = datum.data
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
data = data.convert("RGBA", palette=Image.Palette.ADAPTIVE, colors=16)
|
106
|
+
# Always convert the image, as unconverted images sometime result in an off-by-one
|
107
|
+
# line width errors resulting in diagonal image striping for some reason
|
108
|
+
data = data.convert("RGBA", palette=Image.Palette.ADAPTIVE, colors=16)
|
109
109
|
|
110
110
|
# Init canvas config
|
111
111
|
config = CanvasConfig()
|
@@ -127,9 +127,7 @@ async def chafa_convert_py(
|
|
127
127
|
config.height = max(1, int(cols / data.size[0] * data.size[1] * px / py))
|
128
128
|
|
129
129
|
# Set the foreground color
|
130
|
-
if
|
131
|
-
fg = app.color_palette.fg.base_hex
|
132
|
-
if fg and (color := fg.lstrip("#")):
|
130
|
+
if (fg := datum.fg) and (color := fg.lstrip("#")):
|
133
131
|
config.fg_color = (
|
134
132
|
int(color[0:2], 16),
|
135
133
|
int(color[2:4], 16),
|
@@ -137,9 +135,7 @@ async def chafa_convert_py(
|
|
137
135
|
)
|
138
136
|
|
139
137
|
# Set the background color
|
140
|
-
if
|
141
|
-
bg = app.color_palette.bg.base_hex
|
142
|
-
if bg and (color := bg.lstrip("#")):
|
138
|
+
if (bg := datum.bg) and (color := bg.lstrip("#")):
|
143
139
|
config.bg_color = (
|
144
140
|
int(color[0:2], 16),
|
145
141
|
int(color[2:4], 16),
|
@@ -55,11 +55,11 @@ async def html_to_ft(
|
|
55
55
|
return await html._render(cols, rows)
|
56
56
|
|
57
57
|
|
58
|
-
|
59
|
-
"
|
60
|
-
"
|
61
|
-
"
|
62
|
-
"
|
58
|
+
_WHITELISTED_LEXERS = {
|
59
|
+
"python",
|
60
|
+
"markdown",
|
61
|
+
"javascript",
|
62
|
+
"json",
|
63
63
|
}
|
64
64
|
|
65
65
|
|
@@ -72,6 +72,7 @@ async def ansi_to_ft(
|
|
72
72
|
cols: int | None = None,
|
73
73
|
rows: int | None = None,
|
74
74
|
extend: bool = True,
|
75
|
+
lex: bool = False,
|
75
76
|
) -> StyleAndTextTuples:
|
76
77
|
"""Convert ANSI text to formatted text, lexing & formatting automatically."""
|
77
78
|
data = datum.data
|
@@ -84,8 +85,10 @@ async def ansi_to_ft(
|
|
84
85
|
markup = markup.expandtabs()
|
85
86
|
# Use lexer whitelist
|
86
87
|
if (
|
87
|
-
|
88
|
-
|
88
|
+
lex
|
89
|
+
and (lexer := detect_lexer(markup, path=datum.path)) is not None
|
90
|
+
and lexer.name in _WHITELISTED_LEXERS
|
91
|
+
):
|
89
92
|
from prompt_toolkit.lexers.pygments import _token_cache
|
90
93
|
|
91
94
|
log.debug('Lexing output using "%s" lexer', lexer.name)
|
@@ -73,7 +73,7 @@ async def latex_to_png_dvipng(
|
|
73
73
|
"-T",
|
74
74
|
"tight",
|
75
75
|
"-D",
|
76
|
-
"
|
76
|
+
"175",
|
77
77
|
"-z",
|
78
78
|
"9",
|
79
79
|
"-bg",
|
@@ -122,14 +122,15 @@ async def latex_to_png_py_mpl(
|
|
122
122
|
from matplotlib.backends import backend_agg
|
123
123
|
|
124
124
|
# mpl mathtext doesn't support display math, force inline
|
125
|
-
data = datum.data.replace("$$", "$")
|
126
|
-
|
125
|
+
data = datum.data.strip().replace("$$", "$")
|
126
|
+
if not data.startswith("$"):
|
127
|
+
data = f"${data}$"
|
127
128
|
buffer = BytesIO()
|
128
129
|
prop = font_manager.FontProperties(size=12)
|
129
130
|
parser = mathtext.MathTextParser("path")
|
130
131
|
width, height, depth, _, _ = parser.parse(data, dpi=72, prop=prop)
|
131
|
-
fig = figure.Figure(figsize=(width
|
132
|
-
fig.text(0, depth / height, data, fontproperties=prop, color=datum.fg)
|
132
|
+
fig = figure.Figure(figsize=(width / 72, height / 72))
|
133
|
+
fig.text(0, depth / height, data, fontproperties=prop, color=datum.fg, usetex=False)
|
133
134
|
backend_agg.FigureCanvasAgg(fig)
|
134
135
|
fig.savefig(buffer, dpi=120, format="png", transparent=True)
|
135
136
|
return buffer.getvalue()
|
@@ -138,7 +139,7 @@ async def latex_to_png_py_mpl(
|
|
138
139
|
register(
|
139
140
|
from_=("svg", "jpeg", "pdf", "gif"),
|
140
141
|
to="png",
|
141
|
-
filter_=commands_exist("convert", "
|
142
|
+
filter_=commands_exist("convert", "mogrify"),
|
142
143
|
)(partial(imagemagick_convert, "PNG"))
|
143
144
|
|
144
145
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
"""Contain function which convert data to SVG format."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
from euporie.core.convert.registry import register
|
8
|
+
from euporie.core.convert.utils import have_modules
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from euporie.core.convert.datum import Datum
|
12
|
+
|
13
|
+
|
14
|
+
@register(from_="latex", to="svg", filter_=have_modules("ziamath"))
|
15
|
+
async def latex_to_svg_py_ziamath(
|
16
|
+
datum: Datum,
|
17
|
+
cols: int | None = None,
|
18
|
+
rows: int | None = None,
|
19
|
+
extend: bool = True,
|
20
|
+
) -> str:
|
21
|
+
"""Convert LaTeX to SVG using :py:mod:`ziamath`."""
|
22
|
+
import ziamath as zm
|
23
|
+
|
24
|
+
data = datum.data.strip()
|
25
|
+
if not data.startswith("$"):
|
26
|
+
data = f"$${data}$$"
|
27
|
+
latex = zm.Text(data, color=datum.fg, size=12)
|
28
|
+
return latex.svg()
|
euporie/core/convert/mime.py
CHANGED
@@ -10,9 +10,6 @@ from typing import TYPE_CHECKING
|
|
10
10
|
from upath import UPath
|
11
11
|
from upath.implementations.http import HTTPPath
|
12
12
|
|
13
|
-
# from euporie.core.cache import cache
|
14
|
-
from euporie.core.path import DataPath
|
15
|
-
|
16
13
|
if TYPE_CHECKING:
|
17
14
|
from pathlib import Path
|
18
15
|
|
@@ -50,11 +47,11 @@ def get_mime(path: Path | str) -> str | None:
|
|
50
47
|
mime = None
|
51
48
|
|
52
49
|
# Read from path of data URI
|
53
|
-
if isinstance(path,
|
54
|
-
mime =
|
50
|
+
if path.exists() and isinstance(stat := path.stat(), dict):
|
51
|
+
mime = stat.get("mimetype")
|
55
52
|
|
56
|
-
# If we have a web-address,
|
57
|
-
# Check http-headers and
|
53
|
+
# If we have a web-address, ensure we have a url
|
54
|
+
# Check http-headers and ensure we have a url
|
58
55
|
if not mime and isinstance(path, HTTPPath) and path._url is not None:
|
59
56
|
from fsspec.asyn import sync
|
60
57
|
|
euporie/core/data_structures.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
"""Contain commonly used data structures."""
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
3
5
|
from functools import lru_cache
|
4
6
|
from typing import NamedTuple
|
5
7
|
|
@@ -7,13 +9,13 @@ from typing import NamedTuple
|
|
7
9
|
class DiBool(NamedTuple):
|
8
10
|
"""A tuple of four bools with directions."""
|
9
11
|
|
10
|
-
top:
|
11
|
-
right:
|
12
|
-
bottom:
|
13
|
-
left:
|
12
|
+
top: bool = False
|
13
|
+
right: bool = False
|
14
|
+
bottom: bool = False
|
15
|
+
left: bool = False
|
14
16
|
|
15
17
|
@classmethod
|
16
|
-
def from_value(cls, value:
|
18
|
+
def from_value(cls, value: bool) -> DiBool:
|
17
19
|
"""Construct an instance from a single value."""
|
18
20
|
return cls(top=value, right=value, bottom=value, left=value)
|
19
21
|
|
@@ -21,13 +23,13 @@ class DiBool(NamedTuple):
|
|
21
23
|
class DiInt(NamedTuple):
|
22
24
|
"""A tuple of four integers with directions."""
|
23
25
|
|
24
|
-
top:
|
25
|
-
right:
|
26
|
-
bottom:
|
27
|
-
left:
|
26
|
+
top: int = 0
|
27
|
+
right: int = 0
|
28
|
+
bottom: int = 0
|
29
|
+
left: int = 0
|
28
30
|
|
29
31
|
@classmethod
|
30
|
-
def from_value(cls, value:
|
32
|
+
def from_value(cls, value: int) -> DiInt:
|
31
33
|
"""Construct an instance from a single value."""
|
32
34
|
return cls(top=value, right=value, bottom=value, left=value)
|
33
35
|
|
@@ -35,13 +37,13 @@ class DiInt(NamedTuple):
|
|
35
37
|
class DiStr(NamedTuple):
|
36
38
|
"""A tuple of four strings with directions."""
|
37
39
|
|
38
|
-
top:
|
39
|
-
right:
|
40
|
-
bottom:
|
41
|
-
left:
|
40
|
+
top: str = ""
|
41
|
+
right: str = ""
|
42
|
+
bottom: str = ""
|
43
|
+
left: str = ""
|
42
44
|
|
43
45
|
@classmethod
|
44
|
-
def from_value(cls, value:
|
46
|
+
def from_value(cls, value: str) -> DiStr:
|
45
47
|
"""Construct an instance from a single value."""
|
46
48
|
return cls(top=value, right=value, bottom=value, left=value)
|
47
49
|
|
@@ -49,22 +51,22 @@ class DiStr(NamedTuple):
|
|
49
51
|
class WeightedInt(NamedTuple):
|
50
52
|
"""Ainterger with an associated weight."""
|
51
53
|
|
52
|
-
weight:
|
53
|
-
value:
|
54
|
+
weight: int
|
55
|
+
value: int
|
54
56
|
|
55
57
|
|
56
58
|
class WeightedDiInt(NamedTuple):
|
57
59
|
"""A tuple of four weighted integers."""
|
58
60
|
|
59
|
-
top:
|
60
|
-
right:
|
61
|
-
bottom:
|
62
|
-
left:
|
61
|
+
top: WeightedInt
|
62
|
+
right: WeightedInt
|
63
|
+
bottom: WeightedInt
|
64
|
+
left: WeightedInt
|
63
65
|
|
64
66
|
# We cannot use :py:func:`functools.cached_property` here as it does not work with
|
65
67
|
# :py:Class:`NamedTuple`s.
|
66
68
|
@property # type: ignore
|
67
69
|
@lru_cache(maxsize=1) # noqa: B019
|
68
|
-
def unweighted(self) ->
|
70
|
+
def unweighted(self) -> DiInt:
|
69
71
|
"""Get the padding without weights."""
|
70
72
|
return DiInt(*(x.value for x in self))
|
euporie/core/filters.py
CHANGED
@@ -24,6 +24,18 @@ if TYPE_CHECKING:
|
|
24
24
|
from prompt_toolkit.layout.containers import Window
|
25
25
|
|
26
26
|
|
27
|
+
@Condition
|
28
|
+
@lru_cache
|
29
|
+
def have_ruff() -> bool:
|
30
|
+
"""Determine if ruff is available."""
|
31
|
+
try:
|
32
|
+
import ruff # noqa F401
|
33
|
+
except ModuleNotFoundError:
|
34
|
+
return False
|
35
|
+
else:
|
36
|
+
return True
|
37
|
+
|
38
|
+
|
27
39
|
@Condition
|
28
40
|
@lru_cache
|
29
41
|
def have_black() -> bool:
|
@@ -61,10 +73,12 @@ def have_ssort() -> bool:
|
|
61
73
|
|
62
74
|
|
63
75
|
# Determine if we have at least one formatter
|
64
|
-
have_formatter = have_black | have_isort | have_ssort
|
76
|
+
have_formatter = have_black | have_isort | have_ssort | have_ruff
|
65
77
|
|
66
|
-
# Determine if euporie is running inside
|
78
|
+
# Determine if euporie is running inside a multiplexer.
|
79
|
+
in_screen = to_filter(os.environ.get("TERM", "").startswith("screen"))
|
67
80
|
in_tmux = to_filter(os.environ.get("TMUX") is not None)
|
81
|
+
in_mplex = in_tmux | in_screen
|
68
82
|
|
69
83
|
|
70
84
|
@Condition
|
euporie/core/format.py
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import contextlib
|
5
6
|
import logging
|
6
7
|
from typing import TYPE_CHECKING
|
7
8
|
|
9
|
+
from euporie.core.filters import have_black, have_isort, have_ruff, have_ssort
|
8
10
|
from euporie.core.log import stdout_to_log
|
9
11
|
|
10
12
|
if TYPE_CHECKING:
|
@@ -13,6 +15,27 @@ if TYPE_CHECKING:
|
|
13
15
|
log = logging.getLogger(__name__)
|
14
16
|
|
15
17
|
|
18
|
+
def format_ruff(text: str) -> str:
|
19
|
+
"""Format a code string using :py:mod:`ruff`."""
|
20
|
+
from ruff.__main__ import find_ruff_bin
|
21
|
+
|
22
|
+
try:
|
23
|
+
ruff_path = find_ruff_bin()
|
24
|
+
except FileNotFoundError:
|
25
|
+
pass
|
26
|
+
else:
|
27
|
+
import subprocess
|
28
|
+
|
29
|
+
with contextlib.suppress(subprocess.CalledProcessError):
|
30
|
+
text = subprocess.check_output(
|
31
|
+
[ruff_path, "format", "-"],
|
32
|
+
input=text,
|
33
|
+
text=True,
|
34
|
+
stderr=subprocess.DEVNULL,
|
35
|
+
)
|
36
|
+
return text
|
37
|
+
|
38
|
+
|
16
39
|
def format_black(text: str) -> str:
|
17
40
|
"""Format a code string using :py:mod:`black`."""
|
18
41
|
try:
|
@@ -22,7 +45,7 @@ def format_black(text: str) -> str:
|
|
22
45
|
else:
|
23
46
|
try:
|
24
47
|
text = black.format_str(text, mode=black.Mode()).rstrip()
|
25
|
-
except black.parsing.InvalidInput:
|
48
|
+
except (black.parsing.InvalidInput, KeyError):
|
26
49
|
log.warning("Error formatting code with black: invalid input")
|
27
50
|
return text
|
28
51
|
|
@@ -60,10 +83,13 @@ def format_ssort(text: str) -> str:
|
|
60
83
|
|
61
84
|
def format_code(text: str, config: Config) -> str:
|
62
85
|
"""Format a code string using :py:mod:``."""
|
63
|
-
|
86
|
+
formatters = set(config.formatters)
|
87
|
+
if have_ssort() and "ssort" in formatters:
|
64
88
|
text = format_ssort(text)
|
65
|
-
if
|
89
|
+
if have_isort() and "isort" in formatters:
|
66
90
|
text = format_isort(text)
|
67
|
-
if
|
91
|
+
if have_black() and "black" in formatters:
|
68
92
|
text = format_black(text)
|
93
|
+
if have_ruff() and "ruff" in formatters:
|
94
|
+
text = format_ruff(text)
|
69
95
|
return text.strip()
|
euporie/core/ft/ansi.py
CHANGED
@@ -73,7 +73,8 @@ class ANSI(PTANSI):
|
|
73
73
|
# Check for backspace
|
74
74
|
elif char == "\x08":
|
75
75
|
# TODO - remove last character from last non-ZeroWidthEscape fragment
|
76
|
-
formatted_text
|
76
|
+
if formatted_text:
|
77
|
+
formatted_text.pop()
|
77
78
|
continue
|
78
79
|
|
79
80
|
elif char in ("\x1b", "\x9b"):
|