euporie 2.3.2__py3-none-any.whl → 2.4.1__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/__main__.py +3 -1
- euporie/console/app.py +6 -4
- euporie/console/tabs/console.py +34 -9
- euporie/core/__init__.py +6 -1
- euporie/core/__main__.py +1 -1
- euporie/core/app.py +79 -109
- euporie/core/border.py +44 -14
- euporie/core/comm/base.py +5 -4
- euporie/core/comm/ipywidgets.py +11 -11
- euporie/core/comm/registry.py +12 -6
- euporie/core/commands.py +30 -23
- euporie/core/completion.py +1 -4
- euporie/core/config.py +15 -5
- euporie/core/convert/{base.py → core.py} +117 -53
- euporie/core/convert/formats/ansi.py +46 -25
- euporie/core/convert/formats/base64.py +3 -3
- euporie/core/convert/formats/common.py +38 -13
- euporie/core/convert/formats/formatted_text.py +54 -12
- euporie/core/convert/formats/html.py +5 -5
- euporie/core/convert/formats/jpeg.py +1 -1
- euporie/core/convert/formats/markdown.py +4 -4
- euporie/core/convert/formats/pdf.py +1 -1
- euporie/core/convert/formats/pil.py +5 -3
- euporie/core/convert/formats/png.py +7 -6
- euporie/core/convert/formats/rich.py +4 -3
- euporie/core/convert/formats/sixel.py +5 -5
- euporie/core/convert/utils.py +1 -1
- euporie/core/current.py +11 -5
- euporie/core/formatted_text/ansi.py +4 -8
- euporie/core/formatted_text/html.py +1630 -856
- euporie/core/formatted_text/markdown.py +177 -166
- euporie/core/formatted_text/table.py +20 -14
- euporie/core/formatted_text/utils.py +21 -10
- euporie/core/io.py +14 -14
- euporie/core/kernel.py +48 -37
- euporie/core/key_binding/bindings/micro.py +5 -1
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/keys.py +3 -0
- euporie/core/launch.py +5 -2
- euporie/core/lexers.py +13 -2
- euporie/core/log.py +135 -139
- euporie/core/margins.py +32 -14
- euporie/core/path.py +273 -0
- euporie/core/processors.py +35 -0
- euporie/core/renderer.py +21 -5
- euporie/core/style.py +34 -19
- euporie/core/tabs/base.py +101 -17
- euporie/core/tabs/notebook.py +72 -30
- euporie/core/terminal.py +56 -48
- euporie/core/utils.py +12 -16
- euporie/core/widgets/cell.py +6 -5
- euporie/core/widgets/cell_outputs.py +2 -2
- euporie/core/widgets/decor.py +74 -82
- euporie/core/widgets/dialog.py +132 -28
- euporie/core/widgets/display.py +76 -24
- euporie/core/widgets/file_browser.py +87 -31
- euporie/core/widgets/formatted_text_area.py +1 -3
- euporie/core/widgets/forms.py +79 -40
- euporie/core/widgets/inputs.py +23 -13
- euporie/core/widgets/layout.py +4 -3
- euporie/core/widgets/menu.py +368 -216
- euporie/core/widgets/page.py +99 -58
- euporie/core/widgets/pager.py +1 -1
- euporie/core/widgets/palette.py +30 -27
- euporie/core/widgets/search_bar.py +38 -25
- euporie/core/widgets/status_bar.py +103 -5
- euporie/data/desktop/euporie-console.desktop +7 -0
- euporie/data/desktop/euporie-notebook.desktop +7 -0
- euporie/hub/__main__.py +3 -1
- euporie/hub/app.py +9 -7
- euporie/notebook/__main__.py +3 -1
- euporie/notebook/app.py +7 -30
- euporie/notebook/tabs/__init__.py +7 -3
- euporie/notebook/tabs/display.py +18 -9
- euporie/notebook/tabs/edit.py +106 -23
- euporie/notebook/tabs/json.py +73 -0
- euporie/notebook/tabs/log.py +18 -8
- euporie/notebook/tabs/notebook.py +60 -41
- euporie/preview/__main__.py +3 -1
- euporie/preview/app.py +2 -1
- euporie/preview/tabs/notebook.py +23 -10
- euporie/web/tabs/web.py +149 -0
- euporie/web/widgets/webview.py +563 -0
- euporie-2.4.1.data/data/share/applications/euporie-console.desktop +7 -0
- euporie-2.4.1.data/data/share/applications/euporie-notebook.desktop +7 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/METADATA +6 -5
- euporie-2.4.1.dist-info/RECORD +129 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/WHEEL +1 -1
- euporie/core/url.py +0 -64
- euporie-2.3.2.dist-info/RECORD +0 -122
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/entry_points.txt +0 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/licenses/LICENSE +0 -0
euporie/core/widgets/page.py
CHANGED
@@ -2,14 +2,13 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
-
import asyncio
|
6
|
-
import contextvars
|
7
5
|
import logging
|
8
6
|
import weakref
|
9
7
|
from typing import TYPE_CHECKING, cast
|
10
8
|
|
11
9
|
from prompt_toolkit.application.current import get_app
|
12
10
|
from prompt_toolkit.data_structures import Point
|
11
|
+
from prompt_toolkit.eventloop.utils import run_in_executor_with_context
|
13
12
|
from prompt_toolkit.filters import is_searching
|
14
13
|
from prompt_toolkit.layout.containers import (
|
15
14
|
Container,
|
@@ -20,6 +19,7 @@ from prompt_toolkit.layout.containers import (
|
|
20
19
|
)
|
21
20
|
from prompt_toolkit.layout.controls import UIContent
|
22
21
|
from prompt_toolkit.layout.dimension import Dimension, to_dimension
|
22
|
+
from prompt_toolkit.layout.layout import walk
|
23
23
|
from prompt_toolkit.layout.mouse_handlers import MouseHandlers
|
24
24
|
from prompt_toolkit.layout.screen import Char, Screen, WritePosition
|
25
25
|
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseModifier
|
@@ -27,7 +27,7 @@ from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseModifie
|
|
27
27
|
from euporie.core.data_structures import DiInt
|
28
28
|
|
29
29
|
if TYPE_CHECKING:
|
30
|
-
from typing import Callable, Sequence
|
30
|
+
from typing import Callable, Iterable, Sequence
|
31
31
|
|
32
32
|
from prompt_toolkit.key_binding.key_bindings import (
|
33
33
|
KeyBindingsBase,
|
@@ -35,6 +35,7 @@ if TYPE_CHECKING:
|
|
35
35
|
)
|
36
36
|
from prompt_toolkit.layout.containers import AnyContainer
|
37
37
|
from prompt_toolkit.layout.dimension import AnyDimension
|
38
|
+
from prompt_toolkit.utils import Event
|
38
39
|
|
39
40
|
MouseHandler = Callable[[MouseEvent], object]
|
40
41
|
|
@@ -84,10 +85,24 @@ class ChildRenderInfo:
|
|
84
85
|
self.screen = Screen(default_char=Char(char=" "))
|
85
86
|
self.mouse_handlers = MouseHandlers()
|
86
87
|
|
88
|
+
self.render_counter = -1
|
89
|
+
self._invalid = True
|
90
|
+
self._invalidate_events: list[Event[object]] = []
|
91
|
+
self._layout_hash = 0
|
87
92
|
self.height = 0
|
88
93
|
self.width = 0
|
89
94
|
|
90
|
-
|
95
|
+
def invalidate(self) -> None:
|
96
|
+
"""Flag the child's rendering as out-of-date."""
|
97
|
+
self._invalid = True
|
98
|
+
|
99
|
+
def _invalidate_handler(self, sender: object) -> None:
|
100
|
+
self.invalidate()
|
101
|
+
|
102
|
+
@property
|
103
|
+
def layout_hash(self) -> int:
|
104
|
+
"""Return a hash of the child's current layout."""
|
105
|
+
return sum(hash(container) for container in walk(self.container))
|
91
106
|
|
92
107
|
def render(
|
93
108
|
self,
|
@@ -103,9 +118,24 @@ class ChildRenderInfo:
|
|
103
118
|
style: The parent style to apply when rendering
|
104
119
|
|
105
120
|
"""
|
106
|
-
if
|
107
|
-
|
121
|
+
# Check if refresh is needed
|
122
|
+
refresh = False
|
123
|
+
new_layout_hash = self.layout_hash
|
124
|
+
if self.render_counter != (new_render_counter := get_app().render_counter):
|
125
|
+
if (
|
126
|
+
self._invalid
|
127
|
+
or self.width != available_width
|
128
|
+
or self._layout_hash != (new_layout_hash := self.layout_hash)
|
129
|
+
):
|
130
|
+
self.render_counter = new_render_counter
|
131
|
+
refresh = True
|
132
|
+
|
133
|
+
# Refresh if needed
|
134
|
+
if refresh:
|
135
|
+
self._invalid = False
|
136
|
+
self._layout_hash = new_layout_hash
|
108
137
|
# log.debug("Re-rendering cell %s", self.child.index)
|
138
|
+
self.width = available_width
|
109
139
|
self.height = self.container.preferred_height(
|
110
140
|
available_width, available_height
|
111
141
|
).preferred
|
@@ -128,6 +158,20 @@ class ChildRenderInfo:
|
|
128
158
|
)
|
129
159
|
self.screen.draw_all_floats()
|
130
160
|
|
161
|
+
# Collect invalidation events
|
162
|
+
def gather_events() -> Iterable[Event[object]]:
|
163
|
+
for container in walk(self.container):
|
164
|
+
if isinstance(container, Window):
|
165
|
+
for event in container.content.get_invalidate_events():
|
166
|
+
event += self._invalidate_handler
|
167
|
+
yield event
|
168
|
+
|
169
|
+
# Remove all the original event handlers
|
170
|
+
for event in self._invalidate_events:
|
171
|
+
event -= self._invalidate_handler
|
172
|
+
# Update the list of handlers
|
173
|
+
self._invalidate_events = list(gather_events())
|
174
|
+
|
131
175
|
def blit(
|
132
176
|
self,
|
133
177
|
screen: Screen,
|
@@ -238,7 +282,7 @@ class ChildRenderInfo:
|
|
238
282
|
|
239
283
|
# Refresh the child if there was a response
|
240
284
|
if response is None:
|
241
|
-
self.
|
285
|
+
self.invalidate()
|
242
286
|
return response
|
243
287
|
|
244
288
|
# This would work if windows returned NotImplemented when scrolled
|
@@ -260,7 +304,7 @@ class ChildRenderInfo:
|
|
260
304
|
else:
|
261
305
|
self.parent.select(index, extend=False)
|
262
306
|
response = None
|
263
|
-
self.
|
307
|
+
self.invalidate()
|
264
308
|
|
265
309
|
# Attempt to focus the container
|
266
310
|
layout.focus(self.child)
|
@@ -316,6 +360,8 @@ class ChildRenderInfo:
|
|
316
360
|
class ScrollingContainer(Container):
|
317
361
|
"""A scrollable container which renders only the currently visible children."""
|
318
362
|
|
363
|
+
render_info: WindowRenderInfo | None
|
364
|
+
|
319
365
|
def __init__(
|
320
366
|
self,
|
321
367
|
children: Callable[[], Sequence[AnyContainer]] | Sequence[AnyContainer],
|
@@ -361,41 +407,27 @@ class ScrollingContainer(Container):
|
|
361
407
|
|
362
408
|
def pre_render_children(self, width: int, height: int) -> None:
|
363
409
|
"""Render all unrendered children in a background thread."""
|
364
|
-
#
|
365
|
-
|
410
|
+
# Prevent multiple calls
|
411
|
+
self.pre_rendered = 0.00001
|
366
412
|
|
367
413
|
def render_in_thread() -> None:
|
368
|
-
"""
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
n_children = len(self.children)
|
375
|
-
for i in range(n_children):
|
376
|
-
if i < len(self.children):
|
377
|
-
self.get_child_render_info(i).render(width, height)
|
378
|
-
self.pre_rendered = i / n_children
|
379
|
-
get_app().invalidate()
|
380
|
-
self.pre_rendered = 1.0
|
414
|
+
"""Render children in thread."""
|
415
|
+
n_children = len(self.children)
|
416
|
+
for i in range(n_children):
|
417
|
+
if i < len(self.children):
|
418
|
+
self.get_child_render_info(i).render(width, height)
|
419
|
+
self.pre_rendered = i / n_children
|
381
420
|
get_app().invalidate()
|
421
|
+
self.pre_rendered = 1.0
|
422
|
+
get_app().invalidate()
|
382
423
|
|
383
|
-
|
384
|
-
|
385
|
-
async def trigger_render() -> None:
|
386
|
-
"""Use an executor thread from the current event loop."""
|
387
|
-
await asyncio.get_event_loop().run_in_executor(
|
388
|
-
None, ctx.run, render_in_thread
|
389
|
-
)
|
390
|
-
|
391
|
-
get_app().create_background_task(trigger_render())
|
424
|
+
run_in_executor_with_context(render_in_thread)
|
392
425
|
|
393
426
|
def reset(self) -> None:
|
394
427
|
"""Reet the state of this container and all the children."""
|
395
|
-
|
396
|
-
for meta in meta_data:
|
428
|
+
for meta in self.child_render_infos.values():
|
397
429
|
meta.container.reset()
|
398
|
-
meta.
|
430
|
+
meta.invalidate()
|
399
431
|
|
400
432
|
def preferred_width(self, max_available_width: int) -> Dimension:
|
401
433
|
"""Do not provide a preferred width - grow to fill the available space."""
|
@@ -446,15 +478,15 @@ class ScrollingContainer(Container):
|
|
446
478
|
render_info: ChildRenderInfo | None
|
447
479
|
for render_info in self._selected_child_render_infos:
|
448
480
|
if render_info:
|
449
|
-
render_info.
|
481
|
+
render_info.invalidate()
|
450
482
|
# If a child currently has focus, request to refresh it
|
451
|
-
for child in self.children:
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
483
|
+
# for child in self.children:
|
484
|
+
# if (
|
485
|
+
# render_info := self.child_render_infos.get(hash(child))
|
486
|
+
# ) is not None:
|
487
|
+
# if app.layout.has_focus(render_info.child):
|
488
|
+
# render_info.invalidate()
|
489
|
+
# break
|
458
490
|
# Get the first selected child and focus it
|
459
491
|
child = self.children[new_slice.start]
|
460
492
|
if not app.layout.has_focus(child):
|
@@ -565,7 +597,11 @@ class ScrollingContainer(Container):
|
|
565
597
|
"""
|
566
598
|
# self.refresh_children = True
|
567
599
|
if n > 0:
|
568
|
-
if
|
600
|
+
if (
|
601
|
+
min(self.visible_indicies) == 0
|
602
|
+
and self.index_positions
|
603
|
+
and self.index_positions[0] is not None
|
604
|
+
):
|
569
605
|
n = min(n, 0 - self.index_positions[0] - self.scrolling)
|
570
606
|
if self.index_positions[0] + self.scrolling + n > 0:
|
571
607
|
return NotImplemented
|
@@ -636,10 +672,6 @@ class ScrollingContainer(Container):
|
|
636
672
|
available_width = write_position.width
|
637
673
|
available_height = write_position.height
|
638
674
|
|
639
|
-
# Trigger pre-rendering of children
|
640
|
-
if not self.pre_rendered:
|
641
|
-
self.pre_render_children(available_width, available_height)
|
642
|
-
|
643
675
|
# Update screen height
|
644
676
|
screen.height = max(screen.height, ypos + write_position.height)
|
645
677
|
|
@@ -651,23 +683,19 @@ class ScrollingContainer(Container):
|
|
651
683
|
self._selected_child_render_infos = []
|
652
684
|
for index in selected_indices:
|
653
685
|
render_info = self.get_child_render_info(index)
|
654
|
-
# Do not bother to re-render children if we are scrolling
|
686
|
+
# Do not bother to re-render selected children if we are scrolling
|
655
687
|
if not self.scrolling:
|
656
|
-
render_info.
|
688
|
+
render_info.invalidate()
|
657
689
|
self._selected_child_render_infos.append(render_info)
|
658
690
|
self.index_positions[index] = None
|
659
691
|
|
660
|
-
# Refresh all children if the width has changed
|
661
|
-
if self.last_write_position.width != write_position.width:
|
662
|
-
for child_render_info in self.child_render_infos.values():
|
663
|
-
child_render_info.refresh = True
|
664
|
-
|
665
692
|
# Refresh visible children if searching
|
666
693
|
if is_searching():
|
667
694
|
for index in self.visible_indicies:
|
668
|
-
self.get_child_render_info(index).
|
695
|
+
self.get_child_render_info(index).invalidate()
|
669
696
|
|
670
697
|
# Scroll to make the cursor visible
|
698
|
+
layout = get_app().layout
|
671
699
|
if self.scroll_to_cursor:
|
672
700
|
selected_child_render_info = self._selected_child_render_infos[0]
|
673
701
|
selected_child_render_info.render(
|
@@ -675,7 +703,7 @@ class ScrollingContainer(Container):
|
|
675
703
|
available_height=available_height,
|
676
704
|
style=f"{parent_style} {self.style}",
|
677
705
|
)
|
678
|
-
current_window =
|
706
|
+
current_window = layout.current_window
|
679
707
|
cursor_position = selected_child_render_info.screen.cursor_positions.get(
|
680
708
|
current_window
|
681
709
|
)
|
@@ -805,13 +833,22 @@ class ScrollingContainer(Container):
|
|
805
833
|
|
806
834
|
# Update which children will appear in the layout
|
807
835
|
self.visible_indicies = visible_indicies
|
836
|
+
|
837
|
+
# Update parent relations in layout
|
838
|
+
def _walk(e: Container) -> None:
|
839
|
+
for c in e.get_children():
|
840
|
+
layout._child_to_parent[c] = e
|
841
|
+
_walk(c)
|
842
|
+
|
843
|
+
_walk(self)
|
844
|
+
|
808
845
|
# Record where the contain was last drawn so we can determine if cell outputs
|
809
846
|
# are partially obscured
|
810
847
|
self.last_write_position = write_position
|
811
848
|
|
812
849
|
# Calculate scrollbar info
|
813
850
|
sizes = self.known_sizes
|
814
|
-
avg_size = sum(sizes.values()) / len(sizes)
|
851
|
+
avg_size = sum(sizes.values()) / len(sizes) if sizes else 0
|
815
852
|
n_children = len(self.children)
|
816
853
|
for i in range(n_children):
|
817
854
|
if i not in sizes:
|
@@ -836,6 +873,10 @@ class ScrollingContainer(Container):
|
|
836
873
|
# Signal that we are no longer scrolling
|
837
874
|
self.scrolling = 0
|
838
875
|
|
876
|
+
# Trigger pre-rendering of children
|
877
|
+
if not self.pre_rendered:
|
878
|
+
self.pre_render_children(available_width, available_height)
|
879
|
+
|
839
880
|
@property
|
840
881
|
def vertical_scroll(self) -> int:
|
841
882
|
"""The best guess at the absolute vertical scroll position."""
|
euporie/core/widgets/pager.py
CHANGED
@@ -15,7 +15,7 @@ from prompt_toolkit.layout.containers import (
|
|
15
15
|
from prompt_toolkit.widgets import Box
|
16
16
|
|
17
17
|
from euporie.core.commands import add_cmd
|
18
|
-
from euporie.core.convert.
|
18
|
+
from euporie.core.convert.core import BASE64_FORMATS, MIME_FORMATS, find_route
|
19
19
|
from euporie.core.current import get_app
|
20
20
|
from euporie.core.filters import pager_has_focus
|
21
21
|
from euporie.core.key_binding.registry import (
|
euporie/core/widgets/palette.py
CHANGED
@@ -17,10 +17,11 @@ from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
|
17
17
|
from euporie.core.commands import Command, add_cmd, commands
|
18
18
|
from euporie.core.current import get_app
|
19
19
|
from euporie.core.key_binding.registry import register_bindings
|
20
|
-
from euporie.core.margins import ScrollbarMargin
|
20
|
+
from euporie.core.margins import MarginContainer, ScrollbarMargin
|
21
21
|
from euporie.core.widgets.decor import FocusedStyle
|
22
22
|
from euporie.core.widgets.dialog import Dialog
|
23
23
|
from euporie.core.widgets.forms import Text
|
24
|
+
from euporie.core.widgets.status_bar import StatusContainer
|
24
25
|
|
25
26
|
if TYPE_CHECKING:
|
26
27
|
from prompt_toolkit.buffer import Buffer
|
@@ -29,7 +30,8 @@ if TYPE_CHECKING:
|
|
29
30
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
30
31
|
from prompt_toolkit.layout.controls import GetLinePrefixCallable
|
31
32
|
|
32
|
-
from euporie.core.app import BaseApp
|
33
|
+
from euporie.core.app import BaseApp
|
34
|
+
from euporie.core.widgets.status_bar import StatusBarFields
|
33
35
|
|
34
36
|
log = logging.getLogger(__name__)
|
35
37
|
|
@@ -192,39 +194,34 @@ class CommandPalette(Dialog):
|
|
192
194
|
placeholder=" Type to search…",
|
193
195
|
)
|
194
196
|
self.text_area.buffer.on_text_changed += self.text_changed
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
197
|
+
|
198
|
+
self.body = StatusContainer(
|
199
|
+
body=HSplit(
|
200
|
+
[
|
201
|
+
VSplit(
|
202
|
+
[FocusedStyle(self.text_area)],
|
203
|
+
padding=1,
|
204
|
+
),
|
205
|
+
VSplit(
|
206
|
+
[
|
207
|
+
window := Window(
|
208
|
+
CommandMenuControl(self),
|
209
|
+
scroll_offsets=ScrollOffsets(bottom=1),
|
210
|
+
),
|
211
|
+
MarginContainer(ScrollbarMargin(), target=window),
|
212
|
+
]
|
213
|
+
),
|
214
|
+
],
|
215
|
+
),
|
216
|
+
status=self.__pt_status__,
|
209
217
|
)
|
210
218
|
self.buttons = {}
|
211
219
|
|
212
|
-
get_app().container_statuses[self.container] = self.statusbar_fields
|
213
|
-
|
214
220
|
def load(self) -> None:
|
215
221
|
"""Reset the dialog ready for display."""
|
216
222
|
self.text_area.buffer.text = ""
|
217
223
|
self.to_focus = self.text_area
|
218
224
|
|
219
|
-
def statusbar_fields(
|
220
|
-
self,
|
221
|
-
) -> StatusBarFields:
|
222
|
-
"""Return a list of statusbar field values shown then this tab is active."""
|
223
|
-
if self.matches:
|
224
|
-
return ([self.matches[self.index].command.description], [])
|
225
|
-
else:
|
226
|
-
return ([], [])
|
227
|
-
|
228
225
|
def select(self, n: int, event: KeyPressEvent | None = None) -> None:
|
229
226
|
"""Change the index of the selected command.
|
230
227
|
|
@@ -267,6 +264,12 @@ class CommandPalette(Dialog):
|
|
267
264
|
else:
|
268
265
|
return False
|
269
266
|
|
267
|
+
def __pt_status__(self) -> StatusBarFields | None:
|
268
|
+
"""Return a list of statusbar field values shown then this tab is active."""
|
269
|
+
if self.matches:
|
270
|
+
return ([self.matches[self.index].command.description], [])
|
271
|
+
return None
|
272
|
+
|
270
273
|
# ################################### Commands ####################################
|
271
274
|
|
272
275
|
@staticmethod
|
@@ -9,6 +9,7 @@ from prompt_toolkit.filters.app import is_searching
|
|
9
9
|
from prompt_toolkit.key_binding.vi_state import InputMode
|
10
10
|
from prompt_toolkit.layout.controls import BufferControl, SearchBufferControl
|
11
11
|
from prompt_toolkit.search import SearchDirection
|
12
|
+
from prompt_toolkit.selection import SelectionState
|
12
13
|
from prompt_toolkit.widgets import SearchToolbar as PtkSearchToolbar
|
13
14
|
|
14
15
|
from euporie.core.commands import add_cmd
|
@@ -57,6 +58,20 @@ class SearchBar(PtkSearchToolbar):
|
|
57
58
|
"euporie.core.widgets.search_bar.SearchBar"
|
58
59
|
)
|
59
60
|
|
61
|
+
register_bindings(
|
62
|
+
{
|
63
|
+
"euporie.core.app.BaseApp": {
|
64
|
+
"find": ["c-f", "f3", "f7"],
|
65
|
+
"find-next": "c-g",
|
66
|
+
"find-previous": "c-p",
|
67
|
+
},
|
68
|
+
"euporie.core.widgets.search_bar.SearchBar": {
|
69
|
+
"accept-search": "enter",
|
70
|
+
"stop-search": "escape",
|
71
|
+
},
|
72
|
+
}
|
73
|
+
)
|
74
|
+
|
60
75
|
|
61
76
|
def start_global_search(
|
62
77
|
buffer_control: BufferControl | None = None,
|
@@ -94,6 +109,7 @@ def start_global_search(
|
|
94
109
|
control.search_state.direction = direction
|
95
110
|
# Add it to our list
|
96
111
|
searchable_controls.append(control)
|
112
|
+
|
97
113
|
# Stop the search if we did not find any searchable controls
|
98
114
|
if not searchable_controls:
|
99
115
|
return
|
@@ -113,10 +129,7 @@ def start_global_search(
|
|
113
129
|
app.vi_state.input_mode = InputMode.INSERT
|
114
130
|
|
115
131
|
|
116
|
-
@add_cmd(
|
117
|
-
menu_title="Find",
|
118
|
-
# filter=control_is_searchable,
|
119
|
-
)
|
132
|
+
@add_cmd(menu_title="Find")
|
120
133
|
def find() -> None:
|
121
134
|
"""Enter search mode."""
|
122
135
|
start_global_search(direction=SearchDirection.FORWARD)
|
@@ -142,10 +155,13 @@ def find_prev_next(direction: SearchDirection) -> None:
|
|
142
155
|
search_state = control.search_state
|
143
156
|
search_state.direction = direction
|
144
157
|
# Apply search to buffer
|
145
|
-
control.buffer
|
146
|
-
|
158
|
+
buffer = control.buffer
|
159
|
+
buffer.apply_search(search_state, include_current_position=False, count=1)
|
160
|
+
# Set selection
|
161
|
+
buffer.selection_state = SelectionState(
|
162
|
+
buffer.cursor_position + len(search_state.text)
|
147
163
|
)
|
148
|
-
|
164
|
+
buffer.selection_state.enter_shift_mode()
|
149
165
|
|
150
166
|
|
151
167
|
@add_cmd()
|
@@ -170,8 +186,8 @@ def stop_search() -> None:
|
|
170
186
|
if buffer_control is None:
|
171
187
|
return
|
172
188
|
search_buffer_control = buffer_control.search_buffer_control
|
173
|
-
# Focus the
|
174
|
-
layout.focus(
|
189
|
+
# Focus the previous control
|
190
|
+
layout.focus(layout.previous_control)
|
175
191
|
# Close the search toolbar
|
176
192
|
if search_buffer_control is not None:
|
177
193
|
del layout.search_links[search_buffer_control]
|
@@ -202,23 +218,20 @@ def accept_search() -> None:
|
|
202
218
|
if search_buffer_control.buffer.text:
|
203
219
|
search_state.text = search_buffer_control.buffer.text
|
204
220
|
# Apply search.
|
205
|
-
control.buffer.apply_search(
|
221
|
+
control.buffer.apply_search(
|
222
|
+
search_state, include_current_position=True, count=1
|
223
|
+
)
|
224
|
+
|
225
|
+
# Set selection on target control
|
226
|
+
buffer_control = layout.search_target_buffer_control
|
227
|
+
if buffer_control and control.is_focusable():
|
228
|
+
buffer = buffer_control.buffer
|
229
|
+
buffer.selection_state = SelectionState(
|
230
|
+
buffer.cursor_position + len(search_state.text)
|
231
|
+
)
|
232
|
+
buffer.selection_state.enter_shift_mode()
|
233
|
+
|
206
234
|
# Add query to history of search line.
|
207
235
|
search_buffer_control.buffer.append_to_history()
|
208
236
|
# Stop the search
|
209
237
|
stop_search()
|
210
|
-
|
211
|
-
|
212
|
-
register_bindings(
|
213
|
-
{
|
214
|
-
"euporie.core.app.BaseApp": {
|
215
|
-
"find": ["c-f", "f3", "f7"],
|
216
|
-
"find-next": "c-g",
|
217
|
-
"find-previous": "c-p",
|
218
|
-
},
|
219
|
-
"euporie.core.widgets.search_bar.SearchBar": {
|
220
|
-
"accept-search": "enter",
|
221
|
-
"stop-search": "escape",
|
222
|
-
},
|
223
|
-
}
|
224
|
-
)
|
@@ -2,15 +2,23 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import logging
|
5
6
|
from typing import TYPE_CHECKING
|
7
|
+
from weakref import WeakKeyDictionary
|
6
8
|
|
9
|
+
from prompt_toolkit.cache import FastDictCache
|
7
10
|
from prompt_toolkit.filters.utils import to_filter
|
11
|
+
from prompt_toolkit.formatted_text import to_formatted_text
|
12
|
+
from prompt_toolkit.layout import containers
|
8
13
|
from prompt_toolkit.layout.containers import (
|
9
14
|
ConditionalContainer,
|
10
15
|
VSplit,
|
11
16
|
Window,
|
12
17
|
WindowAlign,
|
13
18
|
)
|
19
|
+
from prompt_toolkit.layout.containers import (
|
20
|
+
to_container as ptk_to_container,
|
21
|
+
)
|
14
22
|
from prompt_toolkit.layout.controls import FormattedTextControl
|
15
23
|
|
16
24
|
from euporie.core.config import add_setting
|
@@ -18,28 +26,76 @@ from euporie.core.current import get_app
|
|
18
26
|
from euporie.core.filters import is_searching
|
19
27
|
|
20
28
|
if TYPE_CHECKING:
|
29
|
+
from typing import Callable, Sequence
|
30
|
+
|
21
31
|
from prompt_toolkit.filters.base import FilterOrBool
|
22
|
-
from prompt_toolkit.
|
32
|
+
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
33
|
+
from prompt_toolkit.formatted_text.base import AnyFormattedText
|
34
|
+
from prompt_toolkit.layout.containers import AnyContainer, Container
|
35
|
+
|
36
|
+
StatusBarFields = tuple[Sequence[AnyFormattedText], Sequence[AnyFormattedText]]
|
37
|
+
|
38
|
+
log = logging.getLogger(__name__)
|
39
|
+
|
40
|
+
|
41
|
+
_CONTAINER_STATUSES: WeakKeyDictionary[
|
42
|
+
Container, Callable[[], StatusBarFields | None]
|
43
|
+
] = WeakKeyDictionary()
|
44
|
+
|
45
|
+
|
46
|
+
def to_container(container: AnyContainer) -> Container:
|
47
|
+
"""Monkey-patch `to_container` to collect container status functions."""
|
48
|
+
result = ptk_to_container(container)
|
49
|
+
if hasattr(container, "__pt_status__"):
|
50
|
+
_CONTAINER_STATUSES[result] = container.__pt_status__
|
51
|
+
return result
|
52
|
+
|
53
|
+
|
54
|
+
containers.to_container = to_container
|
55
|
+
|
56
|
+
|
57
|
+
class StatusContainer:
|
58
|
+
"""A container which allows attaching a status function."""
|
59
|
+
|
60
|
+
def __init__(
|
61
|
+
self, body: AnyContainer, status: Callable[[], StatusBarFields | None]
|
62
|
+
) -> None:
|
63
|
+
"""Initiate a new instance with a body and status function."""
|
64
|
+
self.body = body
|
65
|
+
self.status = status
|
66
|
+
|
67
|
+
def __pt_status__(self) -> StatusBarFields | None:
|
68
|
+
"""Return the status fields."""
|
69
|
+
return self.status()
|
70
|
+
|
71
|
+
def __pt_container__(self) -> AnyContainer:
|
72
|
+
"""Return the body container."""
|
73
|
+
return self.body
|
23
74
|
|
24
75
|
|
25
76
|
class StatusBar:
|
26
77
|
"""A status bar which shows the status of the current tab."""
|
27
78
|
|
28
|
-
def __init__(
|
79
|
+
def __init__(
|
80
|
+
self, extra_filter: FilterOrBool = True, default: StatusBarFields | None = None
|
81
|
+
) -> None:
|
29
82
|
"""Create a new status bar instance."""
|
30
|
-
self.
|
83
|
+
self.default: StatusBarFields = default or ([], [])
|
84
|
+
self._status_cache: FastDictCache[
|
85
|
+
tuple[int], list[StyleAndTextTuples]
|
86
|
+
] = FastDictCache(self._status, size=1)
|
31
87
|
self.container = ConditionalContainer(
|
32
88
|
content=VSplit(
|
33
89
|
[
|
34
90
|
Window(
|
35
91
|
FormattedTextControl(
|
36
|
-
lambda: self.
|
92
|
+
lambda: self._status_cache[get_app().render_counter,][0]
|
37
93
|
),
|
38
94
|
style="class:status",
|
39
95
|
),
|
40
96
|
Window(
|
41
97
|
FormattedTextControl(
|
42
|
-
lambda: self.
|
98
|
+
lambda: self._status_cache[get_app().render_counter,][1]
|
43
99
|
),
|
44
100
|
style="class:status.right",
|
45
101
|
align=WindowAlign.RIGHT,
|
@@ -52,6 +108,48 @@ class StatusBar:
|
|
52
108
|
& to_filter(extra_filter),
|
53
109
|
)
|
54
110
|
|
111
|
+
def _status(self, render_counter: int = 0) -> list[StyleAndTextTuples]:
|
112
|
+
"""Load and format the current status bar entries."""
|
113
|
+
layout = get_app().layout
|
114
|
+
current: Container = layout.current_window
|
115
|
+
|
116
|
+
entries = self.default
|
117
|
+
while True:
|
118
|
+
if callable(
|
119
|
+
func := (
|
120
|
+
_CONTAINER_STATUSES.get(current)
|
121
|
+
or getattr(current, "__pt_status__", None)
|
122
|
+
)
|
123
|
+
):
|
124
|
+
result = func()
|
125
|
+
if result is not None:
|
126
|
+
entries = result
|
127
|
+
break
|
128
|
+
elif current in layout._child_to_parent:
|
129
|
+
current = layout._child_to_parent[current]
|
130
|
+
continue
|
131
|
+
break
|
132
|
+
|
133
|
+
output: list[StyleAndTextTuples] = []
|
134
|
+
# Show the tab's status fields
|
135
|
+
for entry in entries:
|
136
|
+
output.append([])
|
137
|
+
for field in entry:
|
138
|
+
if field:
|
139
|
+
if isinstance(field, tuple):
|
140
|
+
ft = [field]
|
141
|
+
else:
|
142
|
+
ft = to_formatted_text(field, style="class:status.field")
|
143
|
+
output[-1] += [
|
144
|
+
("class:status.field", " "),
|
145
|
+
*ft,
|
146
|
+
("class:status.field", " "),
|
147
|
+
("class:status", " "),
|
148
|
+
]
|
149
|
+
if output[-1]:
|
150
|
+
output[-1].pop()
|
151
|
+
return output
|
152
|
+
|
55
153
|
def __pt_container__(self) -> AnyContainer:
|
56
154
|
"""Return the widget's container."""
|
57
155
|
return self.container
|