euporie 2.8.1__py3-none-any.whl → 2.8.5__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 (129) hide show
  1. euporie/console/_commands.py +143 -0
  2. euporie/console/_settings.py +58 -0
  3. euporie/console/app.py +25 -71
  4. euporie/console/tabs/console.py +267 -147
  5. euporie/core/__init__.py +1 -9
  6. euporie/core/__main__.py +31 -5
  7. euporie/core/_settings.py +104 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +70 -0
  10. euporie/core/app/_settings.py +427 -0
  11. euporie/core/{app.py → app/app.py} +214 -572
  12. euporie/core/app/base.py +51 -0
  13. euporie/core/{current.py → app/current.py} +13 -4
  14. euporie/core/app/cursor.py +35 -0
  15. euporie/core/app/dummy.py +12 -0
  16. euporie/core/app/launch.py +28 -0
  17. euporie/core/bars/__init__.py +11 -0
  18. euporie/core/bars/command.py +182 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +154 -57
  21. euporie/core/{widgets → bars}/status.py +9 -26
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +21 -12
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +11 -5
  27. euporie/core/completion.py +3 -2
  28. euporie/core/config.py +368 -341
  29. euporie/core/convert/__init__.py +0 -30
  30. euporie/core/convert/datum.py +131 -60
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +46 -30
  33. euporie/core/convert/formats/common.py +11 -23
  34. euporie/core/convert/formats/html.py +45 -40
  35. euporie/core/convert/formats/pil.py +1 -1
  36. euporie/core/convert/formats/png.py +3 -5
  37. euporie/core/convert/formats/sixel.py +3 -3
  38. euporie/core/convert/registry.py +11 -8
  39. euporie/core/convert/utils.py +50 -23
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +72 -82
  42. euporie/core/format.py +13 -2
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +36 -36
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +216 -124
  48. euporie/core/history.py +2 -2
  49. euporie/core/inspection.py +3 -2
  50. euporie/core/io.py +207 -28
  51. euporie/core/kernel/__init__.py +1 -0
  52. euporie/core/{kernel.py → kernel/client.py} +100 -139
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +2 -8
  55. euporie/core/key_binding/bindings/basic.py +47 -7
  56. euporie/core/key_binding/bindings/completion.py +3 -8
  57. euporie/core/key_binding/bindings/micro.py +5 -7
  58. euporie/core/key_binding/bindings/mouse.py +26 -24
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/bindings/vi.py +46 -0
  61. euporie/core/key_binding/key_processor.py +43 -2
  62. euporie/core/key_binding/registry.py +2 -0
  63. euporie/core/key_binding/utils.py +22 -2
  64. euporie/core/keys.py +7156 -93
  65. euporie/core/layout/cache.py +35 -25
  66. euporie/core/layout/containers.py +280 -74
  67. euporie/core/layout/decor.py +5 -5
  68. euporie/core/layout/mouse.py +1 -1
  69. euporie/core/layout/print.py +16 -3
  70. euporie/core/layout/scroll.py +26 -28
  71. euporie/core/log.py +75 -60
  72. euporie/core/lsp.py +118 -24
  73. euporie/core/margins.py +60 -31
  74. euporie/core/path.py +2 -1
  75. euporie/core/renderer.py +58 -17
  76. euporie/core/style.py +60 -40
  77. euporie/core/suggest.py +103 -85
  78. euporie/core/tabs/__init__.py +34 -0
  79. euporie/core/tabs/_settings.py +113 -0
  80. euporie/core/tabs/base.py +11 -435
  81. euporie/core/tabs/kernel.py +420 -0
  82. euporie/core/tabs/notebook.py +20 -54
  83. euporie/core/utils.py +98 -6
  84. euporie/core/validation.py +1 -1
  85. euporie/core/widgets/_settings.py +188 -0
  86. euporie/core/widgets/cell.py +90 -158
  87. euporie/core/widgets/cell_outputs.py +25 -36
  88. euporie/core/widgets/decor.py +11 -41
  89. euporie/core/widgets/dialog.py +55 -44
  90. euporie/core/widgets/display.py +27 -24
  91. euporie/core/widgets/file_browser.py +5 -26
  92. euporie/core/widgets/forms.py +16 -12
  93. euporie/core/widgets/inputs.py +37 -81
  94. euporie/core/widgets/layout.py +7 -6
  95. euporie/core/widgets/logo.py +49 -0
  96. euporie/core/widgets/menu.py +13 -11
  97. euporie/core/widgets/pager.py +8 -11
  98. euporie/core/widgets/palette.py +6 -6
  99. euporie/hub/app.py +52 -31
  100. euporie/notebook/_commands.py +24 -0
  101. euporie/notebook/_settings.py +107 -0
  102. euporie/notebook/app.py +109 -210
  103. euporie/notebook/filters.py +1 -1
  104. euporie/notebook/tabs/__init__.py +46 -7
  105. euporie/notebook/tabs/_commands.py +714 -0
  106. euporie/notebook/tabs/_settings.py +32 -0
  107. euporie/notebook/tabs/display.py +2 -2
  108. euporie/notebook/tabs/edit.py +12 -7
  109. euporie/notebook/tabs/json.py +3 -3
  110. euporie/notebook/tabs/log.py +1 -18
  111. euporie/notebook/tabs/notebook.py +21 -674
  112. euporie/notebook/widgets/_commands.py +11 -0
  113. euporie/notebook/widgets/_settings.py +19 -0
  114. euporie/notebook/widgets/side_bar.py +14 -34
  115. euporie/preview/_settings.py +104 -0
  116. euporie/preview/app.py +8 -30
  117. euporie/preview/tabs/notebook.py +15 -86
  118. euporie/web/tabs/web.py +4 -6
  119. euporie/web/widgets/webview.py +5 -12
  120. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
  121. euporie-2.8.5.dist-info/RECORD +172 -0
  122. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
  123. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
  124. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
  125. euporie/core/launch.py +0 -59
  126. euporie/core/terminal.py +0 -527
  127. euporie-2.8.1.dist-info/RECORD +0 -146
  128. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
  129. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from functools import lru_cache
6
+ from functools import cache
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  from prompt_toolkit.data_structures import Point
@@ -17,7 +17,7 @@ from prompt_toolkit.layout.layout import walk
17
17
  from prompt_toolkit.layout.mouse_handlers import MouseHandlers
18
18
  from prompt_toolkit.mouse_events import MouseEvent
19
19
 
20
- from euporie.core.current import get_app
20
+ from euporie.core.app.current import get_app
21
21
  from euporie.core.data_structures import DiInt
22
22
  from euporie.core.layout.screen import BoundedWritePosition, Screen
23
23
 
@@ -256,7 +256,10 @@ class CachedContainer(Container):
256
256
  cols: The columns to copy
257
257
  rows: The rows to copy
258
258
  .
259
- """ # Copy write positions
259
+ """
260
+ # Copy write positions
261
+ new_wps = {}
262
+
260
263
  for win, wp in self.screen.visible_windows_to_write_positions.items():
261
264
  new_wp = BoundedWritePosition(
262
265
  xpos=wp.xpos + left,
@@ -270,10 +273,9 @@ class CachedContainer(Container):
270
273
  left=max(0, wp.width - (cols.stop - wp.xpos)),
271
274
  ),
272
275
  )
273
- screen.visible_windows_to_write_positions[win] = new_wp
274
- screen.height = max(screen.height, self.screen.height)
276
+ new_wps[win] = new_wp
275
277
 
276
- # # Modify render info
278
+ # Modify render info
277
279
  info = win.render_info
278
280
  if info is not None:
279
281
  visible_line_to_row_col = {
@@ -308,7 +310,10 @@ class CachedContainer(Container):
308
310
  win.render_info, "horizontal_scroll", horizontal_scroll
309
311
  )
310
312
 
311
- @lru_cache
313
+ screen.visible_windows_to_write_positions.update(new_wps)
314
+ screen.height = max(screen.height, self.screen.height)
315
+
316
+ @cache
312
317
  def _wrap_mouse_handler(handler: Callable) -> MouseHandler:
313
318
  def _wrapped(mouse_event: MouseEvent) -> NotImplementedOrNone:
314
319
  # Modify mouse events to reflect position of content
@@ -334,14 +339,17 @@ class CachedContainer(Container):
334
339
  output_db = screen.data_buffer
335
340
  output_zwes = screen.zero_width_escapes
336
341
  output_mhs = mouse_handlers.mouse_handlers
337
- for y in range(max(0, rows.start), rows.stop):
342
+
343
+ rows_range = range(max(0, rows.start), rows.stop)
344
+ cols_range = range(max(0, cols.start), cols.stop)
345
+ for y in rows_range:
338
346
  input_db_row = input_db[y]
339
347
  input_zwes_row = input_zwes[y]
340
348
  input_mhs_row = input_mhs[y]
341
349
  output_dbs_row = output_db[top + y]
342
350
  output_zwes_row = output_zwes[top + y]
343
351
  output_mhs_row = output_mhs[top + y]
344
- for x in range(max(0, cols.start), cols.stop):
352
+ for x in cols_range:
345
353
  # Data
346
354
  output_dbs_row[left + x] = input_db_row[x]
347
355
  # Escape sequences
@@ -353,24 +361,26 @@ class CachedContainer(Container):
353
361
  layout = get_app().layout
354
362
  if self.screen.show_cursor:
355
363
  for window, point in self.screen.cursor_positions.items():
356
- if layout.current_control == window.content:
357
- assert window.render_info is not None
358
- if (
359
- (
360
- window.render_info.ui_content.show_cursor
361
- and not window.always_hide_cursor()
362
- )
363
- and point.x in range(cols.start, cols.stop)
364
- and point.y in range(rows.start, rows.stop)
365
- ):
366
- screen.cursor_positions[window] = Point(
367
- x=left + point.x, y=top + point.y
368
- )
369
- screen.show_cursor = True
364
+ if (
365
+ layout.current_control == window.content
366
+ and window.render_info is not None
367
+ and window.render_info.ui_content.show_cursor
368
+ and not window.always_hide_cursor()
369
+ and cols.start <= point.x < cols.stop
370
+ and rows.start <= point.y < rows.stop
371
+ ):
372
+ screen.cursor_positions[window] = Point(
373
+ x=left + point.x, y=top + point.y
374
+ )
375
+ screen.show_cursor = True
370
376
 
371
377
  # Copy menu positions
372
- for window, point in self.screen.menu_positions.items():
373
- screen.menu_positions[window] = Point(x=left + point.x, y=top + point.y)
378
+ screen.menu_positions.update(
379
+ {
380
+ window: Point(x=left + point.x, y=top + point.y)
381
+ for window, point in self.screen.menu_positions.items()
382
+ }
383
+ )
374
384
 
375
385
  def get_children(self) -> list[Container]:
376
386
  """Return a list of all child containers."""
@@ -7,31 +7,47 @@ from functools import lru_cache, 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 collections.abc import Sequence
39
+ from typing import Any, Callable
30
40
 
31
41
  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
42
+ from prompt_toolkit.key_binding.key_bindings import (
43
+ KeyBindingsBase,
44
+ NotImplementedOrNone,
45
+ )
46
+ from prompt_toolkit.layout.containers import (
47
+ AnyContainer,
48
+ AnyDimension,
49
+ Float,
50
+ )
35
51
  from prompt_toolkit.layout.margins import Margin
36
52
  from prompt_toolkit.layout.mouse_handlers import MouseHandlers
37
53
  from prompt_toolkit.layout.screen import Screen, WritePosition
@@ -40,9 +56,87 @@ if TYPE_CHECKING:
40
56
  log = logging.getLogger(__name__)
41
57
 
42
58
 
43
- class HSplit(containers.HSplit):
59
+ @lru_cache(maxsize=None)
60
+ class DummyContainer(Container):
61
+ """Base class for user interface layout."""
62
+
63
+ def __init__(self, width: int = 0, height: int = 0) -> None:
64
+ """Define width and height if any."""
65
+ self.width = width
66
+ self.height = height
67
+
68
+ def reset(self) -> None:
69
+ """Reset the state of this container (does nothing)."""
70
+
71
+ def preferred_width(self, max_available_width: int) -> Dimension:
72
+ """Return a zero-width dimension."""
73
+ return Dimension.exact(self.width)
74
+
75
+ def preferred_height(self, width: int, max_available_height: int) -> Dimension:
76
+ """Return a zero-height dimension."""
77
+ return Dimension.exact(self.height)
78
+
79
+ def write_to_screen(
80
+ self,
81
+ screen: Screen,
82
+ mouse_handlers: MouseHandlers,
83
+ write_position: WritePosition,
84
+ parent_style: str,
85
+ erase_bg: bool,
86
+ z_index: int | None,
87
+ ) -> None:
88
+ """Write the actual content to the screen. Does nothing."""
89
+
90
+ def get_children(self) -> list[Container]:
91
+ """Return an empty list of child :class:`.Container` objects."""
92
+ return []
93
+
94
+
95
+ class HSplit(ptk_containers.HSplit):
44
96
  """Several layouts, one stacked above/under the other."""
45
97
 
98
+ _pad_window: Window
99
+
100
+ def __init__(
101
+ self,
102
+ children: Sequence[AnyContainer],
103
+ window_too_small: Container | None = None,
104
+ align: VerticalAlign = VerticalAlign.JUSTIFY,
105
+ padding: AnyDimension = 0,
106
+ padding_char: str | None = None,
107
+ padding_style: str = "",
108
+ width: AnyDimension = None,
109
+ height: AnyDimension = None,
110
+ z_index: int | None = None,
111
+ modal: bool = False,
112
+ key_bindings: KeyBindingsBase | None = None,
113
+ style: str | Callable[[], str] = "",
114
+ ) -> None:
115
+ """Initialize the HSplit with a cache."""
116
+ if window_too_small is None:
117
+ window_too_small = DummyContainer()
118
+ super().__init__(
119
+ children=children,
120
+ window_too_small=window_too_small,
121
+ align=align,
122
+ padding=padding,
123
+ padding_char=padding_char,
124
+ padding_style=padding_style,
125
+ width=width,
126
+ height=height,
127
+ z_index=z_index,
128
+ modal=modal,
129
+ key_bindings=key_bindings,
130
+ style=style,
131
+ )
132
+ _split_cache_getter = super()._divide_heights
133
+ self._split_cache: FastDictCache[
134
+ tuple[int, WritePosition], list[int] | None
135
+ ] = FastDictCache(lambda rc, wp: _split_cache_getter(wp), size=100)
136
+
137
+ def _divide_heights(self, write_position: WritePosition) -> list[int] | None:
138
+ return self._split_cache[get_app().render_counter, write_position]
139
+
46
140
  def write_to_screen(
47
141
  self,
48
142
  screen: Screen,
@@ -132,68 +226,87 @@ class HSplit(containers.HSplit):
132
226
  z_index,
133
227
  )
134
228
 
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]
229
+ @property
230
+ def pad_window(self) -> Window:
231
+ """Create a single instance of the padding window."""
232
+ try:
233
+ return self._pad_window
234
+ except AttributeError:
235
+ self._pad_window = Window(
236
+ height=self.padding,
237
+ char=self.padding_char,
238
+ style=self.padding_style,
239
+ )
240
+ return self._pad_window
241
+
242
+ @property
243
+ def _all_children(self) -> list[Container]:
244
+ """List of child objects, including padding."""
245
+
246
+ def get() -> list[Container]:
247
+ result: list[Container] = []
248
+ # Padding Top.
249
+ if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM):
250
+ result.append(Window(width=Dimension(preferred=0)))
251
+ # The children with padding.
252
+ for child in self.children:
253
+ result.append(child)
254
+ result.append(self.pad_window)
255
+ if result:
256
+ result.pop()
257
+ # Padding right.
258
+ if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP):
259
+ result.append(Window(width=Dimension(preferred=0)))
260
+ return result
176
261
 
177
- while sum(sizes) < preferred_stop:
178
- if sizes[i] < preferred_dimensions[i]:
179
- sizes[i] += 1
180
- i = next(child_generator)
262
+ return self._children_cache.get(tuple(self.children), get)
181
263
 
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
264
 
187
- while sum(sizes) < max_stop:
188
- if sizes[i] < max_dimensions[i]:
189
- sizes[i] += 1
190
- i = next(child_generator)
265
+ class VSplit(ptk_containers.VSplit):
266
+ """Several layouts, one stacked left/right of the other."""
191
267
 
192
- return sizes
268
+ _pad_window: Window
193
269
 
270
+ def __init__(
271
+ self,
272
+ children: Sequence[AnyContainer],
273
+ window_too_small: Container | None = None,
274
+ align: HorizontalAlign = HorizontalAlign.JUSTIFY,
275
+ padding: AnyDimension = 0,
276
+ padding_char: str | None = None,
277
+ padding_style: str = "",
278
+ width: AnyDimension = None,
279
+ height: AnyDimension = None,
280
+ z_index: int | None = None,
281
+ modal: bool = False,
282
+ key_bindings: KeyBindingsBase | None = None,
283
+ style: str | Callable[[], str] = "",
284
+ ) -> None:
285
+ """Initialize the VSplit with a cache."""
286
+ if window_too_small is None:
287
+ window_too_small = DummyContainer()
288
+ super().__init__(
289
+ children=children,
290
+ window_too_small=window_too_small,
291
+ align=align,
292
+ padding=padding,
293
+ padding_char=padding_char,
294
+ padding_style=padding_style,
295
+ width=width,
296
+ height=height,
297
+ z_index=z_index,
298
+ modal=modal,
299
+ key_bindings=key_bindings,
300
+ style=style,
301
+ )
302
+ _split_cache_getter = super()._divide_widths
303
+ self._split_cache: FastDictCache[tuple[int, int], list[int] | None] = (
304
+ FastDictCache(lambda rc, w: _split_cache_getter(w), size=100)
305
+ )
194
306
 
195
- class VSplit(containers.VSplit):
196
- """Several layouts, one stacked left/right of the other."""
307
+ def _divide_widths(self, width: int) -> list[int] | None:
308
+ """Calculate and cache widths for all columns."""
309
+ return self._split_cache[get_app().render_counter, width]
197
310
 
198
311
  def write_to_screen(
199
312
  self,
@@ -294,10 +407,53 @@ class VSplit(containers.VSplit):
294
407
  z_index,
295
408
  )
296
409
 
410
+ @property
411
+ def pad_window(self) -> Window:
412
+ """Create a single instance of the padding window."""
413
+ try:
414
+ return self._pad_window
415
+ except AttributeError:
416
+ self._pad_window = Window(
417
+ width=self.padding,
418
+ char=self.padding_char,
419
+ style=self.padding_style,
420
+ )
421
+ return self._pad_window
422
+
423
+ @property
424
+ def _all_children(self) -> list[Container]:
425
+ """List of child objects, including padding."""
426
+
427
+ def get() -> list[Container]:
428
+ result: list[Container] = []
429
+
430
+ # Padding left.
431
+ if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT):
432
+ result.append(Window(width=Dimension(preferred=0)))
433
+ # The children with padding.
434
+ for child in self.children:
435
+ result.append(child)
436
+ result.append(self.pad_window)
437
+ if result:
438
+ result.pop()
439
+ # Padding right.
440
+ if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT):
441
+ result.append(Window(width=Dimension(preferred=0)))
442
+
443
+ return result
444
+
445
+ return self._children_cache.get(tuple(self.children), get)
446
+
297
447
 
298
- class Window(containers.Window):
448
+ class Window(ptk_containers.Window):
299
449
  """Container that holds a control."""
300
450
 
451
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
452
+ """Initialize `Windows`, updating the default control for empty windows."""
453
+ super().__init__(*args, **kwargs)
454
+ if isinstance(self.content, PtkDummyControl):
455
+ self.content = DummyControl()
456
+
301
457
  def write_to_screen(
302
458
  self,
303
459
  screen: Screen,
@@ -308,17 +464,20 @@ class Window(containers.Window):
308
464
  z_index: int | None,
309
465
  ) -> None:
310
466
  """Write window to screen."""
311
- assert isinstance(write_position, BoundedWritePosition)
312
467
  # If dont_extend_width/height was given, then reduce width/height in
313
468
  # WritePosition, if the parent wanted us to paint in a bigger area.
314
469
  # (This happens if this window is bundled with another window in a
315
470
  # HSplit/VSplit, but with different size requirements.)
471
+ if isinstance(write_position, BoundedWritePosition):
472
+ bbox = write_position.bbox
473
+ else:
474
+ bbox = DiInt(0, 0, 0, 0)
316
475
  write_position = BoundedWritePosition(
317
476
  xpos=write_position.xpos,
318
477
  ypos=write_position.ypos,
319
478
  width=write_position.width,
320
479
  height=write_position.height,
321
- bbox=write_position.bbox,
480
+ bbox=bbox,
322
481
  )
323
482
 
324
483
  if self.dont_extend_width():
@@ -856,8 +1015,55 @@ class Window(containers.Window):
856
1015
  else:
857
1016
  new_screen.fill_area(write_position, "class:last-line", after=True)
858
1017
 
1018
+ def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
1019
+ """Mouse handler. Called when the UI control doesn't handle this particular event.
1020
+
1021
+ Return `NotImplemented` if nothing was done as a consequence of this
1022
+ key binding (no UI invalidate required in that case).
1023
+ """
1024
+ if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
1025
+ return self._scroll_down()
1026
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
1027
+ return self._scroll_up()
1028
+
1029
+ return NotImplemented
1030
+
1031
+ def _scroll_down(self) -> NotImplementedOrNone: # type: ignore [override]
1032
+ """Scroll window down."""
1033
+ info = self.render_info
1034
+
1035
+ if info is None:
1036
+ return NotImplemented
1037
+
1038
+ if self.vertical_scroll < info.content_height - info.window_height:
1039
+ if info.cursor_position.y <= info.configured_scroll_offsets.top:
1040
+ self.content.move_cursor_down()
1041
+ self.vertical_scroll += 1
1042
+ return None
1043
+
1044
+ return NotImplemented
1045
+
1046
+ def _scroll_up(self) -> NotImplementedOrNone: # type: ignore [override]
1047
+ """Scroll window up."""
1048
+ info = self.render_info
1049
+
1050
+ if info is None:
1051
+ return NotImplemented
1052
+
1053
+ if info.vertical_scroll > 0:
1054
+ # TODO: not entirely correct yet in case of line wrapping and long lines.
1055
+ if (
1056
+ info.cursor_position.y
1057
+ >= info.window_height - 1 - info.configured_scroll_offsets.bottom
1058
+ ):
1059
+ self.content.move_cursor_up()
1060
+ self.vertical_scroll -= 1
1061
+ return None
1062
+
1063
+ return NotImplemented
1064
+
859
1065
 
860
- class FloatContainer(containers.FloatContainer):
1066
+ class FloatContainer(ptk_containers.FloatContainer):
861
1067
  """A `FloatContainer` which uses :py`BoundedWritePosition`s."""
862
1068
 
863
1069
  def _draw_float(
@@ -1005,7 +1211,7 @@ class FloatContainer(containers.FloatContainer):
1005
1211
  )
1006
1212
 
1007
1213
 
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]
1214
+ ptk_containers.HSplit = HSplit # type: ignore[misc]
1215
+ ptk_containers.VSplit = VSplit # type: ignore[misc]
1216
+ ptk_containers.Window = Window # type: ignore[misc]
1217
+ ptk_containers.FloatContainer = FloatContainer # type: ignore[misc]
@@ -15,8 +15,8 @@ from prompt_toolkit.layout.dimension import Dimension
15
15
  from prompt_toolkit.layout.screen import Char, Screen, WritePosition
16
16
  from prompt_toolkit.mouse_events import MouseEventType
17
17
 
18
+ from euporie.core.app.current import get_app
18
19
  from euporie.core.border import ThinLine
19
- from euporie.core.current import get_app
20
20
  from euporie.core.style import ColorPaletteColor
21
21
 
22
22
  if TYPE_CHECKING:
@@ -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):
@@ -341,7 +341,7 @@ class DropShadow(Container):
341
341
  ) -> None:
342
342
  """Draw the wrapped container with the additional style."""
343
343
  attr_cache = self.renderer._attrs_for_style
344
- if attr_cache is not None:
344
+ if attr_cache is not None and self.amount:
345
345
  ypos = write_position.ypos
346
346
  xpos = write_position.xpos
347
347
  amount = self.amount
@@ -13,7 +13,7 @@ from prompt_toolkit.layout.containers import (
13
13
  )
14
14
  from prompt_toolkit.mouse_events import MouseEventType
15
15
 
16
- from euporie.core.current import get_app
16
+ from euporie.core.app.current import get_app
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from typing import Callable
@@ -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,
@@ -15,7 +16,8 @@ from prompt_toolkit.layout.dimension import Dimension, to_dimension
15
16
  from euporie.core.layout.screen import BoundedWritePosition
16
17
 
17
18
  if TYPE_CHECKING:
18
- from typing import Callable, Sequence
19
+ from collections.abc import Sequence
20
+ from typing import Callable
19
21
 
20
22
  from prompt_toolkit.key_binding.key_bindings import (
21
23
  KeyBindingsBase,
@@ -42,8 +44,9 @@ class PrintingContainer(Container):
42
44
  ) -> None:
43
45
  """Initiate the container."""
44
46
  self.width = width
45
- self.rendered = False
46
47
  self._children = children
48
+ self._render_count = -1
49
+ self._cached_children: Sequence[AnyContainer] | None = None
47
50
  self.key_bindings = key_bindings
48
51
 
49
52
  def get_key_bindings(self) -> KeyBindingsBase | None:
@@ -53,7 +56,17 @@ class PrintingContainer(Container):
53
56
  @property
54
57
  def children(self) -> Sequence[AnyContainer]:
55
58
  """Return the container's children."""
56
- children = self._children() if callable(self._children) else self._children
59
+ # Only load the children from a callable once per render cycle
60
+ if callable(self._children):
61
+ if (
62
+ self._cached_children is None
63
+ or self._render_count != get_app().render_counter
64
+ ):
65
+ self._cached_children = self._children()
66
+ self._render_count = get_app().render_counter
67
+ children = self._cached_children
68
+ else:
69
+ children = self._children
57
70
  return children or [Window()]
58
71
 
59
72
  def get_children(self) -> list[Container]: