euporie 2.8.2__py3-none-any.whl → 2.8.4__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 (49) hide show
  1. euporie/console/tabs/console.py +227 -104
  2. euporie/core/__init__.py +1 -1
  3. euporie/core/__main__.py +1 -1
  4. euporie/core/app.py +31 -18
  5. euporie/core/clipboard.py +1 -1
  6. euporie/core/comm/ipywidgets.py +5 -5
  7. euporie/core/commands.py +1 -1
  8. euporie/core/config.py +4 -4
  9. euporie/core/convert/datum.py +4 -1
  10. euporie/core/convert/registry.py +7 -2
  11. euporie/core/filters.py +3 -1
  12. euporie/core/ft/html.py +2 -4
  13. euporie/core/graphics.py +6 -6
  14. euporie/core/kernel.py +56 -32
  15. euporie/core/key_binding/bindings/__init__.py +2 -1
  16. euporie/core/key_binding/bindings/mouse.py +24 -22
  17. euporie/core/key_binding/bindings/vi.py +46 -0
  18. euporie/core/layout/cache.py +33 -23
  19. euporie/core/layout/containers.py +235 -73
  20. euporie/core/layout/decor.py +3 -3
  21. euporie/core/layout/print.py +14 -2
  22. euporie/core/layout/scroll.py +15 -21
  23. euporie/core/margins.py +59 -30
  24. euporie/core/style.py +7 -5
  25. euporie/core/tabs/base.py +32 -0
  26. euporie/core/tabs/notebook.py +6 -3
  27. euporie/core/terminal.py +12 -17
  28. euporie/core/utils.py +2 -4
  29. euporie/core/widgets/cell.py +64 -109
  30. euporie/core/widgets/dialog.py +25 -20
  31. euporie/core/widgets/file_browser.py +3 -3
  32. euporie/core/widgets/forms.py +8 -7
  33. euporie/core/widgets/inputs.py +21 -9
  34. euporie/core/widgets/layout.py +5 -5
  35. euporie/core/widgets/status.py +3 -3
  36. euporie/hub/app.py +7 -3
  37. euporie/notebook/app.py +68 -47
  38. euporie/notebook/tabs/log.py +1 -1
  39. euporie/notebook/tabs/notebook.py +5 -3
  40. euporie/preview/app.py +3 -0
  41. euporie/preview/tabs/notebook.py +9 -14
  42. euporie/web/tabs/web.py +0 -1
  43. {euporie-2.8.2.dist-info → euporie-2.8.4.dist-info}/METADATA +5 -5
  44. {euporie-2.8.2.dist-info → euporie-2.8.4.dist-info}/RECORD +49 -48
  45. {euporie-2.8.2.data → euporie-2.8.4.data}/data/share/applications/euporie-console.desktop +0 -0
  46. {euporie-2.8.2.data → euporie-2.8.4.data}/data/share/applications/euporie-notebook.desktop +0 -0
  47. {euporie-2.8.2.dist-info → euporie-2.8.4.dist-info}/WHEEL +0 -0
  48. {euporie-2.8.2.dist-info → euporie-2.8.4.dist-info}/entry_points.txt +0 -0
  49. {euporie-2.8.2.dist-info → euporie-2.8.4.dist-info}/licenses/LICENSE +0 -0
@@ -3,35 +3,50 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from functools import lru_cache, partial
6
+ from functools import partial
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from prompt_toolkit.application.current import get_app
10
+ from prompt_toolkit.cache import FastDictCache
10
11
  from prompt_toolkit.data_structures import Point
11
- from prompt_toolkit.layout import containers
12
- from prompt_toolkit.layout.containers import WindowAlign, WindowRenderInfo
12
+ from prompt_toolkit.layout import containers as ptk_containers
13
+ from prompt_toolkit.layout.containers import (
14
+ Container,
15
+ HorizontalAlign,
16
+ VerticalAlign,
17
+ WindowAlign,
18
+ WindowRenderInfo,
19
+ )
20
+ from prompt_toolkit.layout.controls import DummyControl as PtkDummyControl
13
21
  from prompt_toolkit.layout.controls import (
14
22
  FormattedTextControl,
15
23
  UIContent,
16
24
  fragment_list_width,
17
25
  to_formatted_text,
18
26
  )
19
- from prompt_toolkit.layout.dimension import sum_layout_dimensions
27
+ from prompt_toolkit.layout.dimension import Dimension
20
28
  from prompt_toolkit.layout.screen import _CHAR_CACHE
21
29
  from prompt_toolkit.layout.utils import explode_text_fragments
22
- from prompt_toolkit.mouse_events import MouseEvent
23
- from prompt_toolkit.utils import get_cwidth, take_using_weights, to_str
30
+ from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
31
+ from prompt_toolkit.utils import get_cwidth, to_str
24
32
 
25
33
  from euporie.core.data_structures import DiInt
34
+ from euporie.core.layout.controls import DummyControl
26
35
  from euporie.core.layout.screen import BoundedWritePosition
27
36
 
28
37
  if TYPE_CHECKING:
29
- from typing import Callable
38
+ from typing import Any, Callable, Sequence
30
39
 
31
40
  from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples
32
- from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
33
- from prompt_toolkit.layout.containers import Float
34
- from prompt_toolkit.layout.dimension import Dimension
41
+ from prompt_toolkit.key_binding.key_bindings import (
42
+ KeyBindingsBase,
43
+ NotImplementedOrNone,
44
+ )
45
+ from prompt_toolkit.layout.containers import (
46
+ AnyContainer,
47
+ AnyDimension,
48
+ Float,
49
+ )
35
50
  from prompt_toolkit.layout.margins import Margin
36
51
  from prompt_toolkit.layout.mouse_handlers import MouseHandlers
37
52
  from prompt_toolkit.layout.screen import Screen, WritePosition
@@ -40,9 +55,49 @@ if TYPE_CHECKING:
40
55
  log = logging.getLogger(__name__)
41
56
 
42
57
 
43
- class HSplit(containers.HSplit):
58
+ class HSplit(ptk_containers.HSplit):
44
59
  """Several layouts, one stacked above/under the other."""
45
60
 
61
+ _pad_window: Window
62
+
63
+ def __init__(
64
+ self,
65
+ children: Sequence[AnyContainer],
66
+ window_too_small: Container | None = None,
67
+ align: VerticalAlign = VerticalAlign.JUSTIFY,
68
+ padding: AnyDimension = 0,
69
+ padding_char: str | None = None,
70
+ padding_style: str = "",
71
+ width: AnyDimension = None,
72
+ height: AnyDimension = None,
73
+ z_index: int | None = None,
74
+ modal: bool = False,
75
+ key_bindings: KeyBindingsBase | None = None,
76
+ style: str | Callable[[], str] = "",
77
+ ) -> None:
78
+ """Initialize the HSplit with a cache."""
79
+ super().__init__(
80
+ children=children,
81
+ window_too_small=window_too_small,
82
+ align=align,
83
+ padding=padding,
84
+ padding_char=padding_char,
85
+ padding_style=padding_style,
86
+ width=width,
87
+ height=height,
88
+ z_index=z_index,
89
+ modal=modal,
90
+ key_bindings=key_bindings,
91
+ style=style,
92
+ )
93
+ _split_cache_getter = super()._divide_heights
94
+ self._split_cache: FastDictCache[
95
+ tuple[int, WritePosition], list[int] | None
96
+ ] = FastDictCache(lambda rc, wp: _split_cache_getter(wp), size=100)
97
+
98
+ def _divide_heights(self, write_position: WritePosition) -> list[int] | None:
99
+ return self._split_cache[get_app().render_counter, write_position]
100
+
46
101
  def write_to_screen(
47
102
  self,
48
103
  screen: Screen,
@@ -132,68 +187,85 @@ class HSplit(containers.HSplit):
132
187
  z_index,
133
188
  )
134
189
 
135
- def _divide_heights(self, write_position: WritePosition) -> list[int] | None:
136
- """Return the heights for all rows, or None when there is not enough space."""
137
- if not self.children:
138
- return []
139
-
140
- # Calculate heights.
141
- width = write_position.width
142
- height = write_position.height
143
-
144
- return _get_divided_heights(
145
- width,
146
- height,
147
- tuple(c.preferred_height(width, height) for c in self._all_children),
148
- )
149
-
150
-
151
- @lru_cache(maxsize=2048)
152
- def _get_divided_heights(
153
- width: int, height: int, dimensions: tuple[Dimension, ...]
154
- ) -> list[int] | None:
155
- # Sum dimensions
156
- sum_dimensions = sum_layout_dimensions(list(dimensions))
157
-
158
- # If there is not enough space for both.
159
- # Don't do anything.
160
- if sum_dimensions.min > height:
161
- return None
162
-
163
- # Find optimal sizes. (Start with minimal size, increase until we cover
164
- # the whole height.)
165
- sizes = [d.min for d in dimensions]
166
-
167
- child_generator = take_using_weights(
168
- items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
169
- )
170
-
171
- i = next(child_generator)
172
-
173
- # Increase until we meet at least the 'preferred' size.
174
- preferred_stop = min(height, sum_dimensions.preferred)
175
- preferred_dimensions = [d.preferred for d in dimensions]
190
+ @property
191
+ def pad_window(self) -> Window:
192
+ """Create a single instance of the padding window."""
193
+ try:
194
+ return self._pad_window
195
+ except AttributeError:
196
+ self._pad_window = Window(
197
+ height=self.padding,
198
+ char=self.padding_char,
199
+ style=self.padding_style,
200
+ )
201
+ return self._pad_window
202
+
203
+ @property
204
+ def _all_children(self) -> list[Container]:
205
+ """List of child objects, including padding."""
206
+
207
+ def get() -> list[Container]:
208
+ result: list[Container] = []
209
+ # Padding Top.
210
+ if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM):
211
+ result.append(Window(width=Dimension(preferred=0)))
212
+ # The children with padding.
213
+ for child in self.children:
214
+ result.append(child)
215
+ result.append(self.pad_window)
216
+ if result:
217
+ result.pop()
218
+ # Padding right.
219
+ if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP):
220
+ result.append(Window(width=Dimension(preferred=0)))
221
+ return result
176
222
 
177
- while sum(sizes) < preferred_stop:
178
- if sizes[i] < preferred_dimensions[i]:
179
- sizes[i] += 1
180
- i = next(child_generator)
223
+ return self._children_cache.get(tuple(self.children), get)
181
224
 
182
- # Increase until we use all the available space. (or until "max")
183
- if not get_app().is_done:
184
- max_stop = min(height, sum_dimensions.max)
185
- max_dimensions = [d.max for d in dimensions]
186
225
 
187
- while sum(sizes) < max_stop:
188
- if sizes[i] < max_dimensions[i]:
189
- sizes[i] += 1
190
- i = next(child_generator)
226
+ class VSplit(ptk_containers.VSplit):
227
+ """Several layouts, one stacked left/right of the other."""
191
228
 
192
- return sizes
229
+ _pad_window: Window
193
230
 
231
+ def __init__(
232
+ self,
233
+ children: Sequence[AnyContainer],
234
+ window_too_small: Container | None = None,
235
+ align: HorizontalAlign = HorizontalAlign.JUSTIFY,
236
+ padding: AnyDimension = 0,
237
+ padding_char: str | None = None,
238
+ padding_style: str = "",
239
+ width: AnyDimension = None,
240
+ height: AnyDimension = None,
241
+ z_index: int | None = None,
242
+ modal: bool = False,
243
+ key_bindings: KeyBindingsBase | None = None,
244
+ style: str | Callable[[], str] = "",
245
+ ) -> None:
246
+ """Initialize the VSplit with a cache."""
247
+ super().__init__(
248
+ children=children,
249
+ window_too_small=window_too_small,
250
+ align=align,
251
+ padding=padding,
252
+ padding_char=padding_char,
253
+ padding_style=padding_style,
254
+ width=width,
255
+ height=height,
256
+ z_index=z_index,
257
+ modal=modal,
258
+ key_bindings=key_bindings,
259
+ style=style,
260
+ )
261
+ _split_cache_getter = super()._divide_widths
262
+ self._split_cache: FastDictCache[tuple[int, int], list[int] | None] = (
263
+ FastDictCache(lambda rc, w: _split_cache_getter(w), size=100)
264
+ )
194
265
 
195
- class VSplit(containers.VSplit):
196
- """Several layouts, one stacked left/right of the other."""
266
+ def _divide_widths(self, width: int) -> list[int] | None:
267
+ """Calculate and cache widths for all columns."""
268
+ return self._split_cache[get_app().render_counter, width]
197
269
 
198
270
  def write_to_screen(
199
271
  self,
@@ -294,10 +366,53 @@ class VSplit(containers.VSplit):
294
366
  z_index,
295
367
  )
296
368
 
369
+ @property
370
+ def pad_window(self) -> Window:
371
+ """Create a single instance of the padding window."""
372
+ try:
373
+ return self._pad_window
374
+ except AttributeError:
375
+ self._pad_window = Window(
376
+ width=self.padding,
377
+ char=self.padding_char,
378
+ style=self.padding_style,
379
+ )
380
+ return self._pad_window
381
+
382
+ @property
383
+ def _all_children(self) -> list[Container]:
384
+ """List of child objects, including padding."""
385
+
386
+ def get() -> list[Container]:
387
+ result: list[Container] = []
388
+
389
+ # Padding left.
390
+ if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT):
391
+ result.append(Window(width=Dimension(preferred=0)))
392
+ # The children with padding.
393
+ for child in self.children:
394
+ result.append(child)
395
+ result.append(self.pad_window)
396
+ if result:
397
+ result.pop()
398
+ # Padding right.
399
+ if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT):
400
+ result.append(Window(width=Dimension(preferred=0)))
401
+
402
+ return result
297
403
 
298
- class Window(containers.Window):
404
+ return self._children_cache.get(tuple(self.children), get)
405
+
406
+
407
+ class Window(ptk_containers.Window):
299
408
  """Container that holds a control."""
300
409
 
410
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
411
+ """Initialize `Windows`, updating the default control for empty windows."""
412
+ super().__init__(*args, **kwargs)
413
+ if isinstance(self.content, PtkDummyControl):
414
+ self.content = DummyControl()
415
+
301
416
  def write_to_screen(
302
417
  self,
303
418
  screen: Screen,
@@ -856,8 +971,55 @@ class Window(containers.Window):
856
971
  else:
857
972
  new_screen.fill_area(write_position, "class:last-line", after=True)
858
973
 
974
+ def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
975
+ """Mouse handler. Called when the UI control doesn't handle this particular event.
976
+
977
+ Return `NotImplemented` if nothing was done as a consequence of this
978
+ key binding (no UI invalidate required in that case).
979
+ """
980
+ if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
981
+ return self._scroll_down()
982
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
983
+ return self._scroll_up()
984
+
985
+ return NotImplemented
986
+
987
+ def _scroll_down(self) -> NotImplementedOrNone: # type: ignore [override]
988
+ """Scroll window down."""
989
+ info = self.render_info
990
+
991
+ if info is None:
992
+ return NotImplemented
993
+
994
+ if self.vertical_scroll < info.content_height - info.window_height:
995
+ if info.cursor_position.y <= info.configured_scroll_offsets.top:
996
+ self.content.move_cursor_down()
997
+ self.vertical_scroll += 1
998
+ return None
999
+
1000
+ return NotImplemented
1001
+
1002
+ def _scroll_up(self) -> NotImplementedOrNone: # type: ignore [override]
1003
+ """Scroll window up."""
1004
+ info = self.render_info
1005
+
1006
+ if info is None:
1007
+ return NotImplemented
1008
+
1009
+ if info.vertical_scroll > 0:
1010
+ # TODO: not entirely correct yet in case of line wrapping and long lines.
1011
+ if (
1012
+ info.cursor_position.y
1013
+ >= info.window_height - 1 - info.configured_scroll_offsets.bottom
1014
+ ):
1015
+ self.content.move_cursor_up()
1016
+ self.vertical_scroll -= 1
1017
+ return None
1018
+
1019
+ return NotImplemented
1020
+
859
1021
 
860
- class FloatContainer(containers.FloatContainer):
1022
+ class FloatContainer(ptk_containers.FloatContainer):
861
1023
  """A `FloatContainer` which uses :py`BoundedWritePosition`s."""
862
1024
 
863
1025
  def _draw_float(
@@ -1005,7 +1167,7 @@ class FloatContainer(containers.FloatContainer):
1005
1167
  )
1006
1168
 
1007
1169
 
1008
- containers.HSplit = HSplit # type: ignore[misc]
1009
- containers.VSplit = VSplit # type: ignore[misc]
1010
- containers.Window = Window # type: ignore[misc]
1011
- containers.FloatContainer = FloatContainer # type: ignore[misc]
1170
+ ptk_containers.HSplit = HSplit # type: ignore[misc]
1171
+ ptk_containers.VSplit = VSplit # type: ignore[misc]
1172
+ ptk_containers.Window = Window # type: ignore[misc]
1173
+ ptk_containers.FloatContainer = FloatContainer # type: ignore[misc]
@@ -280,9 +280,9 @@ class FocusedStyle(Container):
280
280
 
281
281
  return wrapped_mouse_handler
282
282
 
283
- mouse_handler_wrappers: FastDictCache[
284
- tuple[Callable], MouseHandler
285
- ] = FastDictCache(get_value=_wrap_mouse_handler)
283
+ mouse_handler_wrappers: FastDictCache[tuple[Callable], MouseHandler] = (
284
+ FastDictCache(get_value=_wrap_mouse_handler)
285
+ )
286
286
  for y in range(y_min, y_max):
287
287
  row = mouse_handlers.mouse_handlers[y]
288
288
  for x in range(x_min, x_max):
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from prompt_toolkit.application.current import get_app
8
9
  from prompt_toolkit.layout.containers import (
9
10
  Container,
10
11
  Window,
@@ -42,8 +43,9 @@ class PrintingContainer(Container):
42
43
  ) -> None:
43
44
  """Initiate the container."""
44
45
  self.width = width
45
- self.rendered = False
46
46
  self._children = children
47
+ self._render_count = -1
48
+ self._cached_children: Sequence[AnyContainer] | None = None
47
49
  self.key_bindings = key_bindings
48
50
 
49
51
  def get_key_bindings(self) -> KeyBindingsBase | None:
@@ -53,7 +55,17 @@ class PrintingContainer(Container):
53
55
  @property
54
56
  def children(self) -> Sequence[AnyContainer]:
55
57
  """Return the container's children."""
56
- children = self._children() if callable(self._children) else self._children
58
+ # Only load the children from a callable once per render cycle
59
+ if callable(self._children):
60
+ if (
61
+ self._cached_children is None
62
+ or self._render_count != get_app().render_counter
63
+ ):
64
+ self._cached_children = self._children()
65
+ self._render_count = get_app().render_counter
66
+ children = self._cached_children
67
+ else:
68
+ children = self._children
57
69
  return children or [Window()]
58
70
 
59
71
  def get_children(self) -> list[Container]:
@@ -284,25 +284,19 @@ class ScrollingContainer(Container):
284
284
  def wrapped(mouse_event: MouseEvent) -> NotImplementedOrNone:
285
285
  response: NotImplementedOrNone = NotImplemented
286
286
 
287
- if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
288
- response = self.scroll(-1)
289
- elif mouse_event.event_type == MouseEventType.SCROLL_UP:
290
- response = self.scroll(1)
291
- elif callable(handler):
287
+ if callable(handler):
292
288
  response = handler(mouse_event)
293
289
 
290
+ if response is NotImplemented:
291
+ if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
292
+ response = self.scroll(-1)
293
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
294
+ response = self.scroll(1)
295
+
294
296
  # Refresh the child if there was a response
295
297
  if response is None:
296
298
  return response
297
299
 
298
- # This relies on windows returning NotImplemented when scrolled
299
- # to the start or end
300
- # if response is NotImplemented:
301
- # if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
302
- # response = self.scroll(-1)
303
- # elif mouse_event.event_type == MouseEventType.SCROLL_UP:
304
- # response = self.scroll(1)
305
-
306
300
  # Select the clicked child if clicked
307
301
  if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
308
302
  if child:
@@ -480,10 +474,10 @@ class ScrollingContainer(Container):
480
474
  )
481
475
  for y in range(ypos + line, ypos + available_height):
482
476
  for x in range(xpos, xpos + available_width):
483
- mouse_handlers.mouse_handlers[y][
484
- x
485
- ] = self._mouse_handler_wrapper(
486
- mouse_handlers.mouse_handlers[y][x]
477
+ mouse_handlers.mouse_handlers[y][x] = (
478
+ self._mouse_handler_wrapper(
479
+ mouse_handlers.mouse_handlers[y][x]
480
+ )
487
481
  )
488
482
  # Blit children above the selected that are on screen
489
483
  line = self.selected_child_position
@@ -525,10 +519,10 @@ class ScrollingContainer(Container):
525
519
  )
526
520
  for y in range(ypos, ypos + line):
527
521
  for x in range(xpos, xpos + available_width):
528
- mouse_handlers.mouse_handlers[y][
529
- x
530
- ] = self._mouse_handler_wrapper(
531
- mouse_handlers.mouse_handlers[y][x]
522
+ mouse_handlers.mouse_handlers[y][x] = (
523
+ self._mouse_handler_wrapper(
524
+ mouse_handlers.mouse_handlers[y][x]
525
+ )
532
526
  )
533
527
 
534
528
  # Dont bother drawing floats