euporie 2.8.0__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 -92
  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 +26 -37
  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 +9 -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.0.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.0.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
  123. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
  124. {euporie-2.8.0.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.0.dist-info/RECORD +0 -146
  128. {euporie-2.8.0.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
  129. {euporie-2.8.0.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -4,22 +4,18 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from abc import ABCMeta, abstractmethod
7
+ from functools import cache
7
8
  from pathlib import PurePath
8
9
  from typing import TYPE_CHECKING
9
10
 
10
11
  from prompt_toolkit.cache import SimpleCache
11
- from prompt_toolkit.filters import buffer_has_focus
12
12
  from prompt_toolkit.layout.containers import (
13
13
  DynamicContainer,
14
14
  to_container,
15
15
  )
16
16
 
17
- from euporie.core.config import add_setting
18
- from euporie.core.convert.datum import Datum
19
- from euporie.core.convert.formats import BASE64_FORMATS
20
- from euporie.core.convert.mime import MIME_FORMATS
17
+ from euporie.core.app.current import get_app
21
18
  from euporie.core.convert.registry import find_route
22
- from euporie.core.current import get_app
23
19
  from euporie.core.layout.containers import HSplit
24
20
  from euporie.core.widgets.display import Display
25
21
  from euporie.core.widgets.layout import Box
@@ -30,7 +26,7 @@ if TYPE_CHECKING:
30
26
 
31
27
  from prompt_toolkit.layout.containers import AnyContainer
32
28
 
33
- from euporie.core.comm.base import KernelTab
29
+ from euporie.core.tabs.kernel import KernelTab
34
30
 
35
31
  KTParent = TypeVar("KTParent", bound=KernelTab)
36
32
 
@@ -98,6 +94,10 @@ class CellOutputDataElement(CellOutputElement):
98
94
  metadata: Any metadata relating to the data
99
95
  parent: The cell the output-element is attached to
100
96
  """
97
+ from euporie.core.convert.datum import Datum
98
+ from euporie.core.convert.formats import BASE64_FORMATS
99
+ from euporie.core.convert.mime import MIME_FORMATS
100
+
101
101
  self.parent = parent
102
102
 
103
103
  # Get foreground and background colors
@@ -129,16 +129,14 @@ class CellOutputDataElement(CellOutputElement):
129
129
  self._datum,
130
130
  focusable=False,
131
131
  focus_on_click=False,
132
- wrap_lines=config.filter("wrap_cell_outputs"),
132
+ wrap_lines=config.filters.wrap_cell_outputs,
133
133
  always_hide_cursor=True,
134
- style=f"class:mime-{mime.replace('/','-')}",
134
+ style=f"class:mime-{mime.replace('/', '-')}",
135
135
  scrollbar=False,
136
136
  )
137
137
 
138
138
  # Ensure container gets invalidated if `wrap_cell_output` changes
139
- self.container.control.invalidate_events.append(
140
- config.settings["wrap_cell_outputs"].event
141
- )
139
+ self.container.control.invalidate_events.append(config.events.wrap_cell_outputs)
142
140
 
143
141
  @property
144
142
  def data(self) -> Any:
@@ -148,6 +146,8 @@ class CellOutputDataElement(CellOutputElement):
148
146
  @data.setter
149
147
  def data(self, value: Any) -> None:
150
148
  """Set the cell output's data."""
149
+ from euporie.core.convert.datum import Datum
150
+
151
151
  self._datum = Datum(
152
152
  value,
153
153
  self._datum.format,
@@ -260,12 +260,12 @@ MIME_ORDER = [
260
260
  ]
261
261
 
262
262
 
263
- def _calculate_mime_rank(mime_data: tuple[str, Any]) -> int:
263
+ @cache
264
+ def _calculate_mime_rank(mime: str, have_escapes: bool) -> int:
264
265
  """Score the richness of mime output types."""
265
- mime, data = mime_data
266
266
  for i, ranked_mime in enumerate(MIME_ORDER):
267
267
  # Uprank plain text with escape sequences
268
- if mime == "text/plain" and "\x1b[" in data:
268
+ if mime == "text/plain" and have_escapes:
269
269
  i -= 7
270
270
  if PurePath(mime).match(ranked_mime):
271
271
  return i
@@ -273,6 +273,12 @@ def _calculate_mime_rank(mime_data: tuple[str, Any]) -> int:
273
273
  return 999
274
274
 
275
275
 
276
+ def _mime_ranker(mime_data: tuple[str, Any]) -> int:
277
+ """Score the richness of mime output types."""
278
+ mime, data = mime_data
279
+ return _calculate_mime_rank(mime, isinstance(data, str) and "\x1b[" in data)
280
+
281
+
276
282
  class CellOutput:
277
283
  """Represent a single cell output.
278
284
 
@@ -319,7 +325,7 @@ class CellOutput:
319
325
  data = {}
320
326
  output_type = self.json.get("output_type", "unknown")
321
327
  if output_type == "stream":
322
- data = {f'stream/{self.json.get("name")}': self.json.get("text", "")}
328
+ data = {f"stream/{self.json.get('name')}": self.json.get("text", "")}
323
329
  elif output_type == "error":
324
330
  ename = self.json.get("ename", "")
325
331
  evalue = self.json.get("evalue", "")
@@ -327,7 +333,7 @@ class CellOutput:
327
333
  data = {"text/x-python-traceback": f"{ename}: {evalue}\n{traceback}"}
328
334
  else:
329
335
  data = self.json.get("data", {"text/plain": ""})
330
- return dict(sorted(data.items(), key=_calculate_mime_rank))
336
+ return dict(sorted(data.items(), key=_mime_ranker))
331
337
 
332
338
  def update(self) -> None:
333
339
  """Update the output by updating all child containers."""
@@ -358,7 +364,7 @@ class CellOutput:
358
364
  metadata=self.json.get("metadata", {}).get(mime, {}),
359
365
  parent=self.parent,
360
366
  )
361
- except NotImplementedError:
367
+ except (NotImplementedError, KeyError):
362
368
  self.selected_mime = mime = list(data.keys())[-1]
363
369
  continue
364
370
  else:
@@ -372,9 +378,8 @@ class CellOutput:
372
378
  if mime not in self._elements:
373
379
  element = self.make_element(mime)
374
380
  self._elements[mime] = element
375
- else:
376
- element = self._elements[mime]
377
- return element
381
+ return element
382
+ return self._elements[mime]
378
383
 
379
384
  @property
380
385
  def element(self) -> CellOutputElement:
@@ -511,19 +516,3 @@ class CellOutputArea:
511
516
  ):
512
517
  outputs.append(to_plain_text(line))
513
518
  return "\n".join(outputs)
514
-
515
- # ################################### Settings ####################################
516
-
517
- add_setting(
518
- name="wrap_cell_outputs",
519
- title="wrap cell outputs",
520
- flags=["--wrap-cell-outputs"],
521
- type_=bool,
522
- help_="Wrap cell output text.",
523
- default=False,
524
- schema={"type": "boolean"},
525
- description="""
526
- Whether text-based cell outputs should be wrapped.
527
- """,
528
- cmd_filter=~buffer_has_focus,
529
- )
@@ -9,15 +9,12 @@ from prompt_toolkit.filters import to_filter
9
9
  from prompt_toolkit.layout.containers import (
10
10
  ConditionalContainer,
11
11
  DynamicContainer,
12
- Float,
13
- FloatContainer,
14
12
  )
15
13
 
14
+ from euporie.core.app.current import get_app
16
15
  from euporie.core.border import ThinLine
17
- from euporie.core.config import add_setting
18
- from euporie.core.current import get_app
19
16
  from euporie.core.data_structures import DiBool
20
- from euporie.core.layout.containers import HSplit, VSplit, Window
17
+ from euporie.core.layout.containers import DummyContainer, HSplit, VSplit, Window
21
18
  from euporie.core.layout.decor import DropShadow
22
19
 
23
20
  if TYPE_CHECKING:
@@ -183,8 +180,7 @@ class Border:
183
180
  class Shadow:
184
181
  """Draw a shadow underneath/behind this container.
185
182
 
186
- This is a globally configurable version of the
187
- :py:class:`prompt_toolkit.widows.base.Shadow` class.
183
+ The container must be in a float.
188
184
  """
189
185
 
190
186
  def __init__(self, body: AnyContainer) -> None:
@@ -193,27 +189,14 @@ class Shadow:
193
189
  Args:
194
190
  body: Another container object.
195
191
  """
196
- filter_ = get_app().config.filter("show_shadows")
197
- shadow = FloatContainer(
198
- content=body,
199
- floats=[
200
- Float(
201
- bottom=-1,
202
- height=1,
203
- left=1,
204
- right=0,
205
- transparent=True,
206
- content=DropShadow(),
207
- ),
208
- Float(
209
- bottom=-1,
210
- top=1,
211
- width=1,
212
- right=-1,
213
- transparent=True,
214
- content=DropShadow(),
215
- ),
216
- ],
192
+ filter_ = get_app().config.filters.show_shadows
193
+
194
+ spacer = DummyContainer(width=1, height=1)
195
+ shadow = VSplit(
196
+ [
197
+ HSplit([body, VSplit([spacer, DropShadow()])]),
198
+ HSplit([spacer, DropShadow()]),
199
+ ]
217
200
  )
218
201
 
219
202
  def get_contents() -> AnyContainer:
@@ -227,16 +210,3 @@ class Shadow:
227
210
  def __pt_container__(self) -> AnyContainer:
228
211
  """Return the container's content."""
229
212
  return self.container
230
-
231
- # ################################### Settings ####################################
232
-
233
- add_setting(
234
- name="show_shadows",
235
- flags=["--show-shadows"],
236
- type_=bool,
237
- help_="Show or hide shadows under menus and dialogs",
238
- default=True,
239
- description="""
240
- Sets whether shadows are shown under dialogs and popup-menus.
241
- """,
242
- )
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- import traceback
7
6
  from abc import ABCMeta, abstractmethod
8
7
  from functools import partial
9
8
  from pathlib import Path
@@ -26,6 +25,7 @@ from prompt_toolkit.layout.containers import (
26
25
  ConditionalContainer,
27
26
  DynamicContainer,
28
27
  Float,
28
+ to_container,
29
29
  )
30
30
  from prompt_toolkit.layout.controls import FormattedTextControl, UIContent, UIControl
31
31
  from prompt_toolkit.layout.dimension import Dimension
@@ -33,7 +33,7 @@ from prompt_toolkit.layout.screen import WritePosition
33
33
  from prompt_toolkit.mouse_events import MouseButton, MouseEventType
34
34
  from prompt_toolkit.widgets.base import Label
35
35
 
36
- from euporie.core.app import get_app
36
+ from euporie.core.app.current import get_app
37
37
  from euporie.core.border import (
38
38
  FullLine,
39
39
  LowerLeftHalfLine,
@@ -45,14 +45,14 @@ from euporie.core.ft.utils import FormattedTextAlign, align, lex
45
45
  from euporie.core.key_binding.registry import register_bindings
46
46
  from euporie.core.layout.containers import HSplit, VSplit, Window
47
47
  from euporie.core.layout.decor import FocusedStyle
48
- from euporie.core.tabs.base import Tab
49
48
  from euporie.core.widgets.decor import Border, Shadow
50
49
  from euporie.core.widgets.file_browser import FileBrowser
51
50
  from euporie.core.widgets.forms import Button, LabelledWidget, Select, Text
52
51
  from euporie.core.widgets.layout import Box
53
52
 
54
53
  if TYPE_CHECKING:
55
- from typing import Any, Callable, Hashable
54
+ from collections.abc import Hashable
55
+ from typing import Any, Callable
56
56
 
57
57
  from prompt_toolkit.buffer import Buffer
58
58
  from prompt_toolkit.data_structures import Point
@@ -63,9 +63,9 @@ if TYPE_CHECKING:
63
63
  from prompt_toolkit.layout.layout import FocusableElement
64
64
  from prompt_toolkit.mouse_events import MouseEvent
65
65
 
66
- from euporie.core.app import BaseApp
67
- from euporie.core.tabs.base import KernelTab
68
- from euporie.core.widgets.file_browser import FileBrowserControl
66
+ from euporie.core.app.app import BaseApp
67
+ from euporie.core.tabs.base import Tab
68
+ from euporie.core.tabs.kernel import KernelTab
69
69
 
70
70
  log = logging.getLogger(__name__)
71
71
 
@@ -138,8 +138,12 @@ class DialogTitleControl(UIControl):
138
138
  dl_width, max_y
139
139
  ).preferred
140
140
  # Calculate new dialog position
141
- new_x = max(1, min(gx - self.drag_start.x, max_x - dl_width))
142
- new_y = max(1, min(gy - self.drag_start.y, max_y - dl_height))
141
+ new_x = max(
142
+ 1, min(gx - self.drag_start.x, max_x - dl_width + 1)
143
+ )
144
+ new_y = max(
145
+ 1, min(gy - self.drag_start.y, max_y - dl_height + 1)
146
+ )
143
147
  # Move dialog
144
148
  self.dialog.left = new_x - 1
145
149
  self.dialog.top = new_y - 1
@@ -189,7 +193,7 @@ class Dialog(Float, metaclass=ABCMeta):
189
193
  # Set default body & buttons
190
194
  self.body: AnyContainer = Window()
191
195
  self.buttons: dict[str, Callable | None] = {"OK": None}
192
- self.button_widgets: list[AnyContainer] = []
196
+ self._button_widgets: list[AnyContainer] = []
193
197
 
194
198
  # Create key-bindings
195
199
  kb = KeyBindings()
@@ -225,16 +229,12 @@ class Dialog(Float, metaclass=ABCMeta):
225
229
  )
226
230
 
227
231
  # The buttons.
232
+ self.button_split = VSplit(self.button_widgets, padding=1)
228
233
  buttons_row = ConditionalContainer(
229
234
  Box(
230
- body=DynamicContainer(
231
- lambda: VSplit(
232
- self.button_widgets,
233
- padding=1,
234
- key_bindings=DynamicKeyBindings(lambda: self.buttons_kb),
235
- )
236
- ),
235
+ body=self.button_split,
237
236
  height=Dimension(min=1, max=3, preferred=3),
237
+ key_bindings=DynamicKeyBindings(lambda: self.buttons_kb),
238
238
  ),
239
239
  filter=Condition(lambda: bool(self.buttons)),
240
240
  )
@@ -263,6 +263,16 @@ class Dialog(Float, metaclass=ABCMeta):
263
263
  # Set the body as the float's contents
264
264
  super().__init__(content=self.container)
265
265
 
266
+ @property
267
+ def button_widgets(self) -> list[AnyContainer]:
268
+ """A list of button widgets to show in the dialog's row of buttons."""
269
+ return self._button_widgets
270
+
271
+ @button_widgets.setter
272
+ def button_widgets(self, value: list[AnyContainer]) -> None:
273
+ self._button_widgets = list(value)
274
+ self.button_split.children = [to_container(c) for c in value]
275
+
266
276
  def _button_handler(
267
277
  self, button: str = "", event: KeyPressEvent | None = None
268
278
  ) -> None:
@@ -280,8 +290,7 @@ class Dialog(Float, metaclass=ABCMeta):
280
290
  self.load(**params)
281
291
 
282
292
  # Create button widgets & callbacks
283
- self.button_widgets.clear()
284
-
293
+ new_button_widgets: list[AnyContainer] = []
285
294
  if self.buttons:
286
295
  width = max(map(len, self.buttons)) + 2
287
296
  used_keys = set()
@@ -296,7 +305,7 @@ class Dialog(Float, metaclass=ABCMeta):
296
305
  rest = text
297
306
  # Add a button with a handler
298
307
  handler = partial(self._button_handler, text)
299
- self.button_widgets.append(
308
+ new_button_widgets.append(
300
309
  FocusedStyle(
301
310
  Button(
302
311
  [("underline", key), ("", rest)],
@@ -309,6 +318,7 @@ class Dialog(Float, metaclass=ABCMeta):
309
318
  # Add a key-handler
310
319
  if key:
311
320
  self.buttons_kb.add(f"A-{key.lower()}", is_global=True)(handler)
321
+ self.button_widgets = new_button_widgets
312
322
 
313
323
  # When a button is selected, handle left/right key bindings.
314
324
  if len(self.button_widgets) > 1:
@@ -333,15 +343,13 @@ class Dialog(Float, metaclass=ABCMeta):
333
343
  self._load(**params)
334
344
  self.last_focused = self.app.layout.current_control
335
345
  self._visible = True
336
- if self.to_focus is not None:
337
- self.app.layout.focus(self.to_focus)
338
- else:
339
- try:
340
- self.app.layout.focus(self.container)
341
- except ValueError:
342
- pass
343
- self.app.layout.focus(self.container)
344
- self.app.invalidate()
346
+ to_focus = self.to_focus or self.container
347
+ try:
348
+ self.app.layout.focus(to_focus)
349
+ except ValueError:
350
+ pass
351
+ finally:
352
+ self.app.invalidate()
345
353
 
346
354
  def hide(self, event: KeyPressEvent | None = None) -> None:
347
355
  """Hide the dialog."""
@@ -404,7 +412,7 @@ class AboutDialog(Dialog):
404
412
  @add_cmd()
405
413
  def _about() -> None:
406
414
  """Show the about dialog."""
407
- from euporie.core.current import get_app
415
+ from euporie.core.app.current import get_app
408
416
 
409
417
  if dialog := get_app().dialogs.get("about"):
410
418
  dialog.toggle()
@@ -493,12 +501,13 @@ class OpenFileDialog(FileDialog):
493
501
 
494
502
  self.tab_dd = tab_dd = Dropdown(options=[])
495
503
 
496
- def _update_options(fb: FileBrowserControl) -> None:
497
- tabs = get_app().get_file_tabs(path) if (path := fb.path).is_file() else []
504
+ def _update_options(path: Path) -> None:
505
+ tabs = get_app().get_file_tabs(path) if path.is_file() else []
498
506
  tab_dd.options = tabs
499
507
  tab_dd.labels = [tab.name for tab in tabs]
500
508
 
501
- self.file_browser.control.on_select += _update_options
509
+ self.file_browser.control.on_select += lambda fb: _update_options(fb.path)
510
+ self.filepath.buffer.on_text_changed += lambda b: _update_options(Path(b.text))
502
511
 
503
512
  if isinstance(self.body, HSplit):
504
513
  self.body.children.append(
@@ -538,7 +547,7 @@ class OpenFileDialog(FileDialog):
538
547
  self.file_browser.control.dir = path
539
548
  elif path.is_file():
540
549
  self.hide()
541
- self.app.open_file(path, tab_class=self.tab_dd.value)
550
+ self.app.open_file(path, tab_class=self.tab_dd.value.tab_class)
542
551
  return
543
552
  else:
544
553
  self.show(
@@ -551,7 +560,7 @@ class OpenFileDialog(FileDialog):
551
560
  @add_cmd(menu_title="Open File…")
552
561
  def _open_file() -> None:
553
562
  """Open a file."""
554
- from euporie.core.current import get_app
563
+ from euporie.core.app.current import get_app
555
564
 
556
565
  if dialog := get_app().dialogs.get("open-file"):
557
566
  dialog.show()
@@ -560,7 +569,7 @@ class OpenFileDialog(FileDialog):
560
569
 
561
570
  register_bindings(
562
571
  {
563
- "euporie.core.app.BaseApp": {
572
+ "euporie.core.app.app:BaseApp": {
564
573
  "open-file": "c-o",
565
574
  }
566
575
  }
@@ -597,7 +606,7 @@ class SaveAsDialog(FileDialog):
597
606
  )
598
607
  def _save_as() -> None:
599
608
  """Save the current file at a new location."""
600
- from euporie.core.current import get_app
609
+ from euporie.core.app.current import get_app
601
610
 
602
611
  app = get_app()
603
612
  if dialog := app.dialogs.get("save-as"):
@@ -607,7 +616,7 @@ class SaveAsDialog(FileDialog):
607
616
 
608
617
  register_bindings(
609
618
  {
610
- "euporie.core.app.BaseApp": {
619
+ "euporie.core.app.app:BaseApp": {
611
620
  "save-as": ("A-s"),
612
621
  }
613
622
  }
@@ -768,6 +777,8 @@ class ErrorDialog(Dialog):
768
777
 
769
778
  def load(self, exception: Exception | None = None, when: str = "") -> None:
770
779
  """Load dialog body & buttons."""
780
+ import traceback
781
+
771
782
  from euporie.core.margins import MarginContainer, ScrollbarMargin
772
783
  from euporie.core.widgets.formatted_text_area import FormattedTextArea
773
784
  from euporie.core.widgets.forms import Checkbox
@@ -793,7 +804,7 @@ class ErrorDialog(Dialog):
793
804
  ("bold", f" when {when}" if when else ""),
794
805
  ("bold", ":"),
795
806
  ("", "\n\n"),
796
- ("fg:ansired", exception.__repr__()),
807
+ ("fg:ansired", repr(exception)),
797
808
  ("", "\n"),
798
809
  ],
799
810
  )
@@ -854,6 +865,8 @@ class UnsavedDialog(Dialog):
854
865
  tab.save(cb=partial(tab.close, cb))
855
866
 
856
867
  def no_cb() -> None:
868
+ from euporie.core.tabs.base import Tab
869
+
857
870
  assert tab is not None
858
871
  self.hide()
859
872
  Tab.close(tab, cb)
@@ -898,7 +911,7 @@ class ShortcutsDialog(Dialog):
898
911
 
899
912
  def format_key_info(self) -> StyleAndTextTuples:
900
913
  """Generate a table with the current key bindings."""
901
- import importlib
914
+ import pkgutil
902
915
  from textwrap import dedent
903
916
 
904
917
  from prompt_toolkit.formatted_text.base import to_formatted_text
@@ -915,9 +928,7 @@ class ShortcutsDialog(Dialog):
915
928
 
916
929
  for group, bindings in BINDINGS.items():
917
930
  if any(not get_cmd(cmd_name).hidden() for cmd_name in bindings):
918
- mod_name, cls_name = group.rsplit(".", maxsplit=1)
919
- mod = importlib.import_module(mod_name)
920
- app_cls = getattr(mod, cls_name)
931
+ app_cls = pkgutil.resolve_name(group)
921
932
  section_title = (
922
933
  dedent(app_cls.__doc__).strip().split("\n")[0].rstrip(".")
923
934
  )
@@ -960,7 +971,7 @@ class ShortcutsDialog(Dialog):
960
971
  @add_cmd()
961
972
  def _keyboard_shortcuts() -> None:
962
973
  """Display details of registered key-bindings in a dialog."""
963
- from euporie.core.current import get_app
974
+ from euporie.core.app.current import get_app
964
975
 
965
976
  if dialog := get_app().dialogs.get("shortcuts"):
966
977
  dialog.toggle()
@@ -16,13 +16,10 @@ from prompt_toolkit.layout.controls import GetLinePrefixCallable, UIContent, UIC
16
16
  from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
17
17
  from prompt_toolkit.utils import Event, to_str
18
18
 
19
+ from euporie.core.app.current import get_app
19
20
  from euporie.core.commands import add_cmd
20
21
  from euporie.core.convert.datum import Datum
21
- from euporie.core.current import get_app
22
- from euporie.core.filters import (
23
- display_has_focus,
24
- scrollable,
25
- )
22
+ from euporie.core.filters import display_has_focus, scrollable
26
23
  from euporie.core.ft.utils import wrap
27
24
  from euporie.core.graphics import GraphicProcessor
28
25
  from euporie.core.key_binding.registry import (
@@ -34,7 +31,8 @@ from euporie.core.margins import MarginContainer, ScrollbarMargin
34
31
  from euporie.core.utils import run_in_thread_with_context
35
32
 
36
33
  if TYPE_CHECKING:
37
- from typing import Any, Callable, Iterable
34
+ from collections.abc import Iterable
35
+ from typing import Any, Callable
38
36
 
39
37
  from prompt_toolkit.filters import FilterOrBool
40
38
  from prompt_toolkit.formatted_text import StyleAndTextTuples
@@ -89,7 +87,8 @@ class DisplayControl(UIControl):
89
87
  self.height = 0
90
88
 
91
89
  self.key_bindings = load_registered_bindings(
92
- "euporie.core.widgets.display.DisplayControl"
90
+ "euporie.core.widgets.display.DisplayControl",
91
+ config=get_app().config,
93
92
  )
94
93
 
95
94
  self.rendered = Event(self)
@@ -159,7 +158,7 @@ class DisplayControl(UIControl):
159
158
  extend=not self.dont_extend_width(),
160
159
  )
161
160
  if width and height:
162
- key = Datum.add_size(datum, Size(height, self.width))
161
+ key = Datum.add_size(datum, Size(height, width))
163
162
  ft = [(f"[Graphic_{key}]", ""), *ft]
164
163
  lines = list(split_lines(ft))
165
164
  if wrap_lines and width:
@@ -196,13 +195,14 @@ class DisplayControl(UIControl):
196
195
  )
197
196
 
198
197
  def render(self) -> None:
199
- """Render the HTML DOM in a thread."""
198
+ """Render the content in a thread."""
200
199
  datum = self.datum
201
200
  wrap_lines = self.wrap_lines()
202
201
 
203
- max_cols, aspect = self.datum.cell_size()
204
- cols = min(max_cols, self.width) if max_cols else self.width
205
- rows = ceil(cols * aspect) if aspect else self.height
202
+ cols = self.preferred_width(self.width)
203
+ rows = self.preferred_height(
204
+ self.width, self.height, wrap_lines=wrap_lines, get_line_prefix=None
205
+ )
206
206
 
207
207
  def _render() -> None:
208
208
  cp = self.color_palette
@@ -226,12 +226,7 @@ class DisplayControl(UIControl):
226
226
 
227
227
  def preferred_width(self, max_available_width: int) -> int | None:
228
228
  """Calculate and return the preferred width of the control."""
229
- max_cols, aspect = self.datum.cell_size()
230
- if max_cols:
231
- return min(max_cols, max_available_width)
232
- return self._max_line_width_cache[
233
- self.datum, max_available_width, None, self.wrap_lines()
234
- ]
229
+ return max_available_width
235
230
 
236
231
  def preferred_height(
237
232
  self,
@@ -241,12 +236,18 @@ class DisplayControl(UIControl):
241
236
  get_line_prefix: GetLinePrefixCallable | None,
242
237
  ) -> int | None:
243
238
  """Calculate and return the preferred height of the control."""
239
+ height = None
244
240
  max_cols, aspect = self.datum.cell_size()
245
241
  if aspect:
246
- return ceil(min(width, max_cols) * aspect)
242
+ height = ceil(min(width, max_cols) * aspect)
247
243
  cp = self.color_palette
248
244
  self.lines = self._line_cache[
249
- self.datum, width, None, cp.fg.base_hex, cp.bg.base_hex, self.wrap_lines()
245
+ self.datum,
246
+ width,
247
+ height,
248
+ cp.fg.base_hex,
249
+ cp.bg.base_hex,
250
+ self.wrap_lines(),
250
251
  ]
251
252
  return len(self.lines)
252
253
 
@@ -299,17 +300,19 @@ class DisplayControl(UIControl):
299
300
  A :py:class:`UIContent` instance.
300
301
  """
301
302
  # Trigger a re-render in the future if things have changed
303
+ render = False
302
304
  if self.loading:
303
- self.render()
304
- if width != self.width:
305
+ render = True
306
+ if width != self.width or height != self.height:
305
307
  self.resizing = True
306
308
  self.width = width
307
309
  self.height = height
308
- self.render()
310
+ render = True
309
311
  if (cp := get_app().color_palette) != self.color_palette:
310
312
  self.color_palette = cp
313
+ render = True
314
+ if render:
311
315
  self.render()
312
-
313
316
  content = self._content_cache[
314
317
  self.datum, width, height, self.loading, self.cursor_position, cp
315
318
  ]