euporie 2.6.2__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 +52 -38
- 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.2.dist-info → euporie-2.7.0.dist-info}/METADATA +9 -11
- {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/RECORD +67 -60
- {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/WHEEL +1 -1
- {euporie-2.6.2.data → euporie-2.7.0.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.6.2.data → euporie-2.7.0.data}/data/share/applications/euporie-notebook.desktop +0 -0
- {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/entry_points.txt +0 -0
- {euporie-2.6.2.dist-info → euporie-2.7.0.dist-info}/licenses/LICENSE +0 -0
euporie/core/ft/html.py
CHANGED
@@ -24,6 +24,7 @@ from prompt_toolkit.filters.base import Condition
|
|
24
24
|
from prompt_toolkit.filters.utils import _always as always
|
25
25
|
from prompt_toolkit.filters.utils import _never as never
|
26
26
|
from prompt_toolkit.formatted_text.utils import split_lines
|
27
|
+
from prompt_toolkit.layout.containers import WindowAlign
|
27
28
|
from prompt_toolkit.layout.dimension import Dimension
|
28
29
|
from prompt_toolkit.utils import Event
|
29
30
|
from upath import UPath
|
@@ -56,6 +57,7 @@ from euporie.core.data_structures import DiBool, DiInt, DiStr
|
|
56
57
|
from euporie.core.ft.table import Cell, Table, compute_padding
|
57
58
|
from euporie.core.ft.utils import (
|
58
59
|
FormattedTextAlign,
|
60
|
+
FormattedTextVerticalAlign,
|
59
61
|
add_border,
|
60
62
|
align,
|
61
63
|
apply_reverse_overwrites,
|
@@ -70,6 +72,7 @@ from euporie.core.ft.utils import (
|
|
70
72
|
paste,
|
71
73
|
strip,
|
72
74
|
truncate,
|
75
|
+
valign,
|
73
76
|
)
|
74
77
|
|
75
78
|
|
@@ -94,6 +97,7 @@ if TYPE_CHECKING:
|
|
94
97
|
from pathlib import Path
|
95
98
|
from typing import Any, Callable, Generator, Iterator
|
96
99
|
|
100
|
+
from fsspec.spec import AbstractFileSystem
|
97
101
|
from prompt_toolkit.filters.base import Filter
|
98
102
|
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
99
103
|
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
|
@@ -874,6 +878,13 @@ class Theme(Mapping):
|
|
874
878
|
self.available_width = available_width
|
875
879
|
self.available_height = available_height
|
876
880
|
|
881
|
+
def reset(self) -> None:
|
882
|
+
"""Reset all cached properties."""
|
883
|
+
# Iterate ove a copy of keys as dict changes size during loop
|
884
|
+
for attr in list(self.__dict__.keys()):
|
885
|
+
if isinstance(Theme.__dict__.get(attr), cached_property):
|
886
|
+
delattr(self, attr)
|
887
|
+
|
877
888
|
@cached_property
|
878
889
|
def theme(self) -> dict[str, str]:
|
879
890
|
"""Return the combined computed theme."""
|
@@ -1741,6 +1752,12 @@ class Theme(Mapping):
|
|
1741
1752
|
if "line-through" in theme["text_decoration"]:
|
1742
1753
|
style = f"{style} strike"
|
1743
1754
|
|
1755
|
+
if "blink" in theme["text_decoration"]:
|
1756
|
+
style = f"{style} blink"
|
1757
|
+
|
1758
|
+
if self.hidden:
|
1759
|
+
style = f"{style} hidden nounderline"
|
1760
|
+
|
1744
1761
|
return style
|
1745
1762
|
|
1746
1763
|
async def text_transform(self, value: str) -> str:
|
@@ -1928,17 +1945,19 @@ class Theme(Mapping):
|
|
1928
1945
|
)
|
1929
1946
|
|
1930
1947
|
@cached_property
|
1931
|
-
def grid_template(self) ->
|
1948
|
+
def grid_template(self) -> list[list[str]]:
|
1932
1949
|
"""Calculate the size of the grid tracks."""
|
1933
1950
|
|
1934
1951
|
def _multiply_repeats(m: re.Match) -> str:
|
1935
1952
|
result = m.groupdict()
|
1936
1953
|
return " ".join(int(result["count"]) * [result["value"]])
|
1937
1954
|
|
1955
|
+
template = []
|
1938
1956
|
for css_item in ("grid_template_columns", "grid_template_rows"):
|
1939
1957
|
value = self.theme.get(css_item, "")
|
1940
1958
|
value = _GRID_TEMPLATE_REPEAT_RE.sub(_multiply_repeats, value)
|
1941
|
-
|
1959
|
+
template.append([m.group() for m in _GRID_TEMPLATE_RE.finditer(value)])
|
1960
|
+
return template
|
1942
1961
|
|
1943
1962
|
@cached_property
|
1944
1963
|
def grid_column_start(self) -> int | None:
|
@@ -1995,6 +2014,11 @@ class Theme(Mapping):
|
|
1995
2014
|
(order > 0, order),
|
1996
2015
|
)
|
1997
2016
|
|
2017
|
+
@cached_property
|
2018
|
+
def latex(self) -> bool:
|
2019
|
+
"""If the element should be rendered as LaTeX."""
|
2020
|
+
return self.theme.get("_format") == "latex"
|
2021
|
+
|
1998
2022
|
# Mapping methods - these are passed on to the underlying theme dictionary
|
1999
2023
|
|
2000
2024
|
def __getitem__(self, key: str) -> Any:
|
@@ -2075,6 +2099,10 @@ _BROWSER_CSS: CssSelectors = {
|
|
2075
2099
|
"display": "inline",
|
2076
2100
|
"font_weight": "bold",
|
2077
2101
|
},
|
2102
|
+
((CssSelector(item="blink"),),): {
|
2103
|
+
"display": "inline",
|
2104
|
+
"text_decoration": "blink",
|
2105
|
+
},
|
2078
2106
|
(
|
2079
2107
|
(CssSelector(item="cite"),),
|
2080
2108
|
(CssSelector(item="dfn"),),
|
@@ -2739,7 +2767,13 @@ _BROWSER_CSS: CssSelectors = {
|
|
2739
2767
|
CssSelector(item="::root", attr="[_initial_format=markdown]"),
|
2740
2768
|
CssSelector(item=".math"),
|
2741
2769
|
),
|
2742
|
-
): {
|
2770
|
+
): {
|
2771
|
+
"_format": "latex",
|
2772
|
+
"text_transform": "latex",
|
2773
|
+
"display": "inline-block",
|
2774
|
+
"vertical_align": "top",
|
2775
|
+
# "margin_right": "1em",
|
2776
|
+
},
|
2743
2777
|
(
|
2744
2778
|
(
|
2745
2779
|
CssSelector(item="::root", attr="[_initial_format=markdown]"),
|
@@ -2747,6 +2781,7 @@ _BROWSER_CSS: CssSelectors = {
|
|
2747
2781
|
),
|
2748
2782
|
): {
|
2749
2783
|
"text_align": "center",
|
2784
|
+
"display": "block",
|
2750
2785
|
},
|
2751
2786
|
(
|
2752
2787
|
(
|
@@ -2823,6 +2858,16 @@ class Node:
|
|
2823
2858
|
|
2824
2859
|
self.theme = Theme(self, parent_theme=parent.theme if parent else None)
|
2825
2860
|
|
2861
|
+
def reset(self) -> None:
|
2862
|
+
"""Reset the node and all its children."""
|
2863
|
+
for attr in list(self.__dict__.keys()):
|
2864
|
+
if isinstance(Node.__dict__.get(attr), cached_property):
|
2865
|
+
delattr(self, attr)
|
2866
|
+
self.theme.reset()
|
2867
|
+
for child in self.contents:
|
2868
|
+
child.reset()
|
2869
|
+
self.marker = None
|
2870
|
+
|
2826
2871
|
def _outer_html(self, d: int = 0, attrs: bool = True) -> str:
|
2827
2872
|
dd = " " * d
|
2828
2873
|
s = ""
|
@@ -3425,7 +3470,9 @@ class HTML:
|
|
3425
3470
|
browser_css: CssSelectors | None = None,
|
3426
3471
|
mouse_handler: Callable[[Node, MouseEvent], NotImplementedOrNone] | None = None,
|
3427
3472
|
paste_fixed: bool = True,
|
3473
|
+
defer_assets: bool = False,
|
3428
3474
|
on_update: Callable[[HTML], None] | None = None,
|
3475
|
+
on_change: Callable[[HTML], None] | None = None,
|
3429
3476
|
_initial_format: str = "",
|
3430
3477
|
) -> None:
|
3431
3478
|
"""Initialize the markdown formatter.
|
@@ -3444,7 +3491,9 @@ class HTML:
|
|
3444
3491
|
browser_css: The browser CSS to use
|
3445
3492
|
mouse_handler: A mouse handler function to use when links are clicked
|
3446
3493
|
paste_fixed: Whether fixed elements should be pasted over the output
|
3494
|
+
defer_assets: Whether to render the page before remote assets are loaded
|
3447
3495
|
on_update: An optional callback triggered when the DOM updates
|
3496
|
+
on_change: An optional callback triggered when the DOM changes
|
3448
3497
|
_initial_format: The initial format of the data being displayed
|
3449
3498
|
|
3450
3499
|
"""
|
@@ -3454,6 +3503,7 @@ class HTML:
|
|
3454
3503
|
|
3455
3504
|
self.browser_css = browser_css or _BROWSER_CSS
|
3456
3505
|
self.css: CssSelectors = css or {}
|
3506
|
+
self.defer_assets = defer_assets
|
3457
3507
|
|
3458
3508
|
self.render_count = 0
|
3459
3509
|
self.width = width
|
@@ -3473,8 +3523,12 @@ class HTML:
|
|
3473
3523
|
self.fixed_mask: StyleAndTextTuples = []
|
3474
3524
|
# self.anchors = []
|
3475
3525
|
self.on_update = Event(self, on_update)
|
3526
|
+
self.on_change = Event(self, on_change)
|
3476
3527
|
|
3477
|
-
self.
|
3528
|
+
self._dom_processed = False
|
3529
|
+
self._assets_loaded = False
|
3530
|
+
self._url_cbs: dict[str, Callable[[Any], None]] = {}
|
3531
|
+
self._url_fs_map: dict[str, AbstractFileSystem] = {}
|
3478
3532
|
|
3479
3533
|
# Lazily load attributes
|
3480
3534
|
|
@@ -3488,12 +3542,11 @@ class HTML:
|
|
3488
3542
|
"""Parse the markup."""
|
3489
3543
|
return self.parser.parse(self.markup)
|
3490
3544
|
|
3491
|
-
|
3545
|
+
def process_dom(self) -> None:
|
3492
3546
|
"""Load CSS styles and image resources.
|
3493
3547
|
|
3494
3548
|
Do not touch element's themes!
|
3495
3549
|
"""
|
3496
|
-
url_cbs = {}
|
3497
3550
|
|
3498
3551
|
def _process_css(data: bytes) -> None:
|
3499
3552
|
try:
|
@@ -3529,7 +3582,9 @@ class HTML:
|
|
3529
3582
|
and (href := attrs.get("href", ""))
|
3530
3583
|
):
|
3531
3584
|
url = urljoin(str(self.base), href)
|
3532
|
-
|
3585
|
+
fs, url = url_to_fs(url)
|
3586
|
+
self._url_fs_map[url] = fs
|
3587
|
+
self._url_cbs[url] = _process_css
|
3533
3588
|
|
3534
3589
|
# In case of a <style> tab, load first child's text
|
3535
3590
|
elif child.name == "style":
|
@@ -3542,22 +3597,40 @@ class HTML:
|
|
3542
3597
|
elif child.name == "img" and (src := child.attrs.get("src")):
|
3543
3598
|
child.attrs["_missing"] = "true"
|
3544
3599
|
url = urljoin(str(self.base), src)
|
3545
|
-
|
3600
|
+
fs, url = url_to_fs(url)
|
3601
|
+
self._url_fs_map[url] = fs
|
3602
|
+
self._url_cbs[url] = partial(_process_img, child)
|
3603
|
+
|
3604
|
+
self._dom_processed = True
|
3605
|
+
|
3606
|
+
async def load_assets(self) -> None:
|
3607
|
+
"""Load remote assets asynchronously."""
|
3608
|
+
self._assets_loaded = True
|
3546
3609
|
|
3547
3610
|
# Load all remote assets for each protocol using fsspec (where they are loaded
|
3548
3611
|
# asynchronously in their own thread) and trigger callbacks if the file is
|
3549
3612
|
# loaded successfully
|
3550
|
-
|
3551
|
-
|
3552
|
-
|
3553
|
-
|
3554
|
-
|
3555
|
-
|
3556
|
-
|
3557
|
-
|
3558
|
-
|
3613
|
+
fs_url_map = {
|
3614
|
+
fs: [url for url, f in self._url_fs_map.items() if f == fs]
|
3615
|
+
for fs in set(self._url_fs_map.values())
|
3616
|
+
}
|
3617
|
+
for fs, urls in fs_url_map.items():
|
3618
|
+
try:
|
3619
|
+
results = fs.cat(urls, recursive=False, on_error="return")
|
3620
|
+
except Exception:
|
3621
|
+
log.warning("Error connecting to %s", fs)
|
3622
|
+
else:
|
3623
|
+
# for url, result in zip(urls, results.values()):
|
3624
|
+
for url, result in results.items():
|
3625
|
+
if not isinstance(result, Exception):
|
3626
|
+
log.debug("File %s loaded", url)
|
3627
|
+
self._url_cbs[url](result)
|
3628
|
+
else:
|
3629
|
+
log.warning("Error loading %s", url)
|
3559
3630
|
|
3560
|
-
|
3631
|
+
# Reset all nodes so they will update with the new CSS from assets
|
3632
|
+
if self.defer_assets:
|
3633
|
+
self.soup.reset()
|
3561
3634
|
|
3562
3635
|
def render(self, width: int | None, height: int | None) -> StyleAndTextTuples:
|
3563
3636
|
"""Render the current markup at a given size."""
|
@@ -3585,7 +3658,10 @@ class HTML:
|
|
3585
3658
|
assert self.width is not None
|
3586
3659
|
assert self.height is not None
|
3587
3660
|
|
3588
|
-
|
3661
|
+
# The soup gets parsed when we load assets, and asset data gets attached to it
|
3662
|
+
if not self._dom_processed:
|
3663
|
+
self.process_dom()
|
3664
|
+
if not self.defer_assets and not self._assets_loaded:
|
3589
3665
|
await self.load_assets()
|
3590
3666
|
|
3591
3667
|
ft = await self.render_element(
|
@@ -3651,6 +3727,12 @@ class HTML:
|
|
3651
3727
|
self.render_count += 1
|
3652
3728
|
self.formatted_text = ft
|
3653
3729
|
|
3730
|
+
# Load assets after initial render and requuest a re-render when loaded
|
3731
|
+
if self.defer_assets and not self._assets_loaded:
|
3732
|
+
loop = asyncio.get_event_loop()
|
3733
|
+
task = loop.create_task(self.load_assets())
|
3734
|
+
task.add_done_callback(lambda fut: self.on_change.fire())
|
3735
|
+
|
3654
3736
|
return ft
|
3655
3737
|
|
3656
3738
|
async def render_element(
|
@@ -3676,6 +3758,9 @@ class HTML:
|
|
3676
3758
|
elif element.theme.d_grid:
|
3677
3759
|
render_func = self.render_grid_content
|
3678
3760
|
|
3761
|
+
elif element.theme.latex:
|
3762
|
+
render_func = self.render_latex_content
|
3763
|
+
|
3679
3764
|
else:
|
3680
3765
|
name = element.name.lstrip(":")
|
3681
3766
|
render_func = getattr(
|
@@ -3711,14 +3796,15 @@ class HTML:
|
|
3711
3796
|
|
3712
3797
|
"""
|
3713
3798
|
ft: StyleAndTextTuples = []
|
3714
|
-
|
3715
|
-
|
3716
|
-
|
3717
|
-
|
3718
|
-
|
3719
|
-
|
3720
|
-
|
3721
|
-
|
3799
|
+
|
3800
|
+
text = await element.theme.text_transform(element.text)
|
3801
|
+
|
3802
|
+
if parent_theme := element.theme.parent_theme:
|
3803
|
+
style = parent_theme.style
|
3804
|
+
else:
|
3805
|
+
style = ""
|
3806
|
+
|
3807
|
+
ft.append((style, text))
|
3722
3808
|
return ft
|
3723
3809
|
|
3724
3810
|
async def render_details_content(
|
@@ -4178,6 +4264,47 @@ class HTML:
|
|
4178
4264
|
|
4179
4265
|
return table.render()
|
4180
4266
|
|
4267
|
+
async def render_latex_content(
|
4268
|
+
self,
|
4269
|
+
element: Node,
|
4270
|
+
left: int = 0,
|
4271
|
+
fill: bool = True,
|
4272
|
+
align_content: bool = True,
|
4273
|
+
) -> StyleAndTextTuples:
|
4274
|
+
"""Render LaTeX math content."""
|
4275
|
+
theme = element.theme
|
4276
|
+
|
4277
|
+
# Render text representation
|
4278
|
+
ft: StyleAndTextTuples = await self.render_node_content(
|
4279
|
+
element, left, fill, align_content
|
4280
|
+
)
|
4281
|
+
|
4282
|
+
# Render graphic representation
|
4283
|
+
latex = element.text + "".join(
|
4284
|
+
child.text for child in element.renderable_descendents
|
4285
|
+
)
|
4286
|
+
latex = f"${latex}$"
|
4287
|
+
if element.theme.d_blocky:
|
4288
|
+
latex = f"${latex}$"
|
4289
|
+
datum = Datum(
|
4290
|
+
latex,
|
4291
|
+
"latex",
|
4292
|
+
fg=theme.color,
|
4293
|
+
bg=theme.background_color,
|
4294
|
+
align=WindowAlign.CENTER,
|
4295
|
+
)
|
4296
|
+
self.graphic_data.add(datum)
|
4297
|
+
|
4298
|
+
# Calculate size and pad text representation
|
4299
|
+
cols, aspect = await datum.cell_size_async()
|
4300
|
+
rows = max(len(list(split_lines(ft))), ceil(cols * aspect))
|
4301
|
+
cols = max(cols, max_line_width(ft))
|
4302
|
+
|
4303
|
+
key = datum.add_size(Size(rows, cols))
|
4304
|
+
ft = [(f"[Graphic_{key}]", ""), *ft]
|
4305
|
+
ft = valign(ft, height=rows, how=FormattedTextVerticalAlign.TOP)
|
4306
|
+
return ft
|
4307
|
+
|
4181
4308
|
@overload
|
4182
4309
|
async def _render_image(
|
4183
4310
|
self, data: bytes, format_: str, theme: Theme, path: Path | None = None
|
@@ -4249,7 +4376,7 @@ class HTML:
|
|
4249
4376
|
content_width = theme.content_width
|
4250
4377
|
# content_height = theme.content_height
|
4251
4378
|
src = str(element.attrs.get("src", ""))
|
4252
|
-
path = self.base
|
4379
|
+
path = UPath(urljoin(str(self.base), src))
|
4253
4380
|
|
4254
4381
|
if not element.attrs.get("_missing") and (data := element.attrs.get("_data")):
|
4255
4382
|
# Display it graphically
|
@@ -4825,20 +4952,6 @@ class HTML:
|
|
4825
4952
|
padding_style=parent_style,
|
4826
4953
|
)
|
4827
4954
|
|
4828
|
-
# Ensure hidden content is blank and styled like the parent
|
4829
|
-
if theme.hidden:
|
4830
|
-
ft = cast(
|
4831
|
-
"StyleAndTextTuples",
|
4832
|
-
[
|
4833
|
-
(
|
4834
|
-
parent_style,
|
4835
|
-
"\n".join([" " * len(x) for x in text.split("\n")]),
|
4836
|
-
*rest,
|
4837
|
-
)
|
4838
|
-
for style, text, *rest in ft
|
4839
|
-
],
|
4840
|
-
)
|
4841
|
-
|
4842
4955
|
# Apply mouse handler to links
|
4843
4956
|
if (
|
4844
4957
|
(parent := element.parent)
|