euporie 2.8.5__py3-none-any.whl → 2.8.7__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 (74) hide show
  1. euporie/console/app.py +2 -0
  2. euporie/console/tabs/console.py +27 -17
  3. euporie/core/__init__.py +2 -2
  4. euporie/core/__main__.py +2 -2
  5. euporie/core/_settings.py +7 -2
  6. euporie/core/app/_commands.py +20 -12
  7. euporie/core/app/_settings.py +34 -4
  8. euporie/core/app/app.py +31 -18
  9. euporie/core/bars/command.py +53 -27
  10. euporie/core/bars/search.py +43 -2
  11. euporie/core/border.py +7 -2
  12. euporie/core/comm/base.py +2 -2
  13. euporie/core/comm/ipywidgets.py +3 -3
  14. euporie/core/commands.py +44 -24
  15. euporie/core/completion.py +14 -6
  16. euporie/core/convert/datum.py +7 -7
  17. euporie/core/data_structures.py +20 -1
  18. euporie/core/filters.py +40 -9
  19. euporie/core/format.py +2 -3
  20. euporie/core/ft/html.py +47 -40
  21. euporie/core/graphics.py +199 -31
  22. euporie/core/history.py +15 -5
  23. euporie/core/inspection.py +16 -9
  24. euporie/core/kernel/__init__.py +53 -1
  25. euporie/core/kernel/base.py +571 -0
  26. euporie/core/kernel/{client.py → jupyter.py} +173 -430
  27. euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
  28. euporie/core/kernel/local.py +694 -0
  29. euporie/core/key_binding/bindings/basic.py +6 -3
  30. euporie/core/keys.py +26 -25
  31. euporie/core/layout/cache.py +31 -7
  32. euporie/core/layout/containers.py +88 -13
  33. euporie/core/layout/scroll.py +69 -170
  34. euporie/core/log.py +2 -5
  35. euporie/core/path.py +61 -13
  36. euporie/core/style.py +2 -1
  37. euporie/core/suggest.py +155 -74
  38. euporie/core/tabs/__init__.py +12 -4
  39. euporie/core/tabs/_commands.py +76 -0
  40. euporie/core/tabs/_settings.py +16 -0
  41. euporie/core/tabs/base.py +89 -9
  42. euporie/core/tabs/kernel.py +83 -38
  43. euporie/core/tabs/notebook.py +28 -76
  44. euporie/core/utils.py +2 -19
  45. euporie/core/validation.py +8 -8
  46. euporie/core/widgets/_settings.py +19 -2
  47. euporie/core/widgets/cell.py +32 -32
  48. euporie/core/widgets/cell_outputs.py +10 -1
  49. euporie/core/widgets/dialog.py +60 -76
  50. euporie/core/widgets/display.py +2 -2
  51. euporie/core/widgets/forms.py +71 -59
  52. euporie/core/widgets/inputs.py +7 -4
  53. euporie/core/widgets/layout.py +281 -93
  54. euporie/core/widgets/menu.py +56 -16
  55. euporie/core/widgets/palette.py +3 -1
  56. euporie/core/widgets/tree.py +86 -76
  57. euporie/notebook/app.py +35 -16
  58. euporie/notebook/tabs/display.py +2 -2
  59. euporie/notebook/tabs/edit.py +11 -46
  60. euporie/notebook/tabs/json.py +8 -4
  61. euporie/notebook/tabs/notebook.py +26 -8
  62. euporie/preview/tabs/notebook.py +17 -13
  63. euporie/web/__init__.py +1 -0
  64. euporie/web/tabs/__init__.py +14 -0
  65. euporie/web/tabs/web.py +30 -5
  66. euporie/web/widgets/__init__.py +1 -0
  67. euporie/web/widgets/webview.py +5 -4
  68. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/METADATA +4 -2
  69. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/RECORD +74 -68
  70. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/entry_points.txt +1 -1
  71. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/licenses/LICENSE +1 -1
  72. {euporie-2.8.5.data → euporie-2.8.7.data}/data/share/applications/euporie-console.desktop +0 -0
  73. {euporie-2.8.5.data → euporie-2.8.7.data}/data/share/applications/euporie-notebook.desktop +0 -0
  74. {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/WHEEL +0 -0
@@ -2,108 +2,118 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, cast
5
+ from functools import partial
6
+ from typing import TYPE_CHECKING
6
7
 
7
- from prompt_toolkit.filters import Condition
8
- from prompt_toolkit.layout.containers import ConditionalContainer
8
+ from prompt_toolkit.layout.containers import Window
9
9
  from prompt_toolkit.layout.controls import FormattedTextControl
10
10
  from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
11
11
 
12
- from euporie.core.layout.containers import HSplit, VSplit, Window
13
-
14
12
  if TYPE_CHECKING:
15
13
  from typing import Any
16
14
 
17
15
  from prompt_toolkit.formatted_text import StyleAndTextTuples
18
16
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
19
- from prompt_toolkit.layout.containers import AnyContainer
20
17
 
21
18
 
22
19
  class JsonView:
23
20
  """A JSON-view container."""
24
21
 
25
22
  def __init__(
26
- self, data: Any, title: str | None = None, expanded: bool = True
23
+ self, data: Any, title: str | None = None, expanded: bool = False
27
24
  ) -> None:
28
25
  """Create a new instance."""
29
- if title is None:
30
- self.title = "root"
31
- else:
32
- self.title = title
33
- self.expanded = expanded
34
-
35
- self.children = []
36
- self.value_style = ""
37
- self.value = ""
38
-
39
- if isinstance(data, list):
40
- data = dict(enumerate(data))
41
- self.value = f"[] {len(data)} items"
42
- self.value_style = "class:pygments.comment"
43
- if isinstance(data, dict):
44
- self.value = f"{{}} {len(data)} items"
45
- self.value_style = "class:pygments.comment"
46
- self.children = [
47
- JsonView(data=value, title=key, expanded=expanded)
48
- for key, value in data.items()
49
- ]
50
- else:
51
- self.value = f"{data!r}"
52
- self.value_style = {
53
- str: "class:pygments.literal.string",
54
- int: "class:pygments.literal.number",
55
- float: "class:pygments.literal.number",
56
- bool: "class:pygments.keyword.constant",
57
- }.get(type(data), "")
58
-
59
- self.container = HSplit(
60
- [
61
- Window(
62
- FormattedTextControl(self.format_title),
63
- dont_extend_height=True,
64
- ),
65
- ConditionalContainer(
66
- VSplit(
67
- [
68
- Window(
69
- width=2,
70
- char=" ",
71
- style="",
72
- ),
73
- HSplit(self.children),
74
- ]
75
- ),
76
- filter=Condition(lambda: self.expanded and bool(self.children)),
77
- ),
78
- ],
26
+ self.data = data
27
+ self.title = "root" if title is None else title
28
+ self.start_expanded = expanded
29
+ self._toggled_paths: set[tuple[str, ...]] = set()
30
+ self.container = Window(
31
+ FormattedTextControl(self._get_formatted_text, focusable=True),
79
32
  style="class:tree",
80
33
  )
81
34
 
82
- def toggle(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
83
- """Toggle the expansion state."""
35
+ def _get_value_style(self, value: Any) -> str:
36
+ """Return the style for a given value type."""
37
+ return {
38
+ str: "class:pygments.literal.string",
39
+ int: "class:pygments.literal.number",
40
+ float: "class:pygments.literal.number",
41
+ bool: "class:pygments.keyword.constant",
42
+ }.get(type(value), "")
43
+
44
+ def _format_value(self, value: Any) -> tuple[str, str]:
45
+ """Return the formatted value and its style."""
46
+ if isinstance(value, (list, dict)):
47
+ length = len(value)
48
+ if isinstance(value, list):
49
+ return f" [] {length} items", "class:pygments.comment"
50
+ return f" {{}} {length} items", "class:pygments.comment"
51
+ return repr(value), self._get_value_style(value)
52
+
53
+ def _get_formatted_text(self) -> StyleAndTextTuples:
54
+ """Generate the complete tree view as formatted text."""
55
+ result: StyleAndTextTuples = []
56
+ toggled_paths = self._toggled_paths
57
+ start_expanded = self.start_expanded
58
+ toggle = self._toggle
59
+ format_value = self._format_value
60
+
61
+ def format_node(
62
+ data: Any, path: tuple[str, ...], indent: int, key: str
63
+ ) -> None:
64
+ is_expanded = (path in toggled_paths) ^ start_expanded
65
+ has_children = isinstance(data, (dict, list))
66
+ mouse_handler = partial(toggle, path=path)
67
+
68
+ value, style = format_value(data)
69
+ row: StyleAndTextTuples = [
70
+ # Add indentation
71
+ ("", " " * indent),
72
+ # Add toggle symbol
73
+ (
74
+ ("class:pygments.operator", "▼" if is_expanded else "▶")
75
+ if has_children
76
+ else ("", " ")
77
+ ),
78
+ ("", " "),
79
+ ("class:pygments.keyword", str(key)),
80
+ ("class:pygments.punctuation", ": "),
81
+ (style, value),
82
+ ("", "\n"),
83
+ ]
84
+
85
+ # Apply mouse_handler to rows with children
86
+ if has_children:
87
+ row = [(style, text, mouse_handler) for (style, text, *_) in row]
88
+
89
+ result.extend(row)
90
+
91
+ if is_expanded and has_children:
92
+ if isinstance(data, list):
93
+ data = {str(i): v for i, v in enumerate(data)}
94
+
95
+ for k, v in data.items():
96
+ new_path = (*path, str(k)) if path else (str(k),)
97
+ format_node(v, new_path, indent + 1, k)
98
+
99
+ format_node(self.data, (), 0, self.title)
100
+ return result
101
+
102
+ def _toggle(
103
+ self, mouse_event: MouseEvent, path: tuple[str, ...]
104
+ ) -> NotImplementedOrNone:
105
+ """Toggle the expansion state of a node."""
84
106
  if (
85
107
  mouse_event.button == MouseButton.LEFT
86
108
  and mouse_event.event_type == MouseEventType.MOUSE_UP
87
109
  ):
88
- self.expanded = not self.expanded
110
+ if path in self._toggled_paths:
111
+ self._toggled_paths.remove(path)
112
+ else:
113
+ self._toggled_paths.add(path)
89
114
  return None
90
115
  return NotImplemented
91
116
 
92
- def format_title(self) -> StyleAndTextTuples:
93
- """Return the tree node toggle and title."""
94
- return cast(
95
- "StyleAndTextTuples",
96
- [
97
- ("class:pygments.operator", "▼" if self.expanded else "▶", self.toggle)
98
- if self.children
99
- else ("", " ", self.toggle),
100
- ("", " ", self.toggle),
101
- ("class:pygments.keyword", f"{self.title}", self.toggle),
102
- ("class:pygments.punctuation", ": ", self.toggle),
103
- (self.value_style, self.value, self.toggle),
104
- ],
105
- )
106
-
107
- def __pt_container__(self) -> AnyContainer:
117
+ def __pt_container__(self) -> Window:
108
118
  """Return the tree-view container's content."""
109
119
  return self.container
euporie/notebook/app.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import logging
6
6
  from functools import partial
7
7
  from typing import TYPE_CHECKING, cast
8
+ from weakref import WeakKeyDictionary
8
9
 
9
10
  from prompt_toolkit.filters import Condition
10
11
  from prompt_toolkit.formatted_text.base import to_formatted_text
@@ -89,6 +90,7 @@ class NotebookApp(BaseApp):
89
90
  super().__init__(**kwargs)
90
91
  self.bindings_to_load.append("euporie.notebook.app.NotebookApp")
91
92
 
93
+ self._tab_bar_tabs: dict[int, WeakKeyDictionary[Tab, TabBarTab]] = {}
92
94
  self.on_tabs_change += self.set_tab_container
93
95
 
94
96
  # Register config hooks
@@ -136,21 +138,36 @@ class NotebookApp(BaseApp):
136
138
 
137
139
  def set_tab_container(self, app: BaseApp | None = None) -> None:
138
140
  """Set the container to use to display opened tabs."""
141
+ tab_mode = TabMode(self.config.tab_mode)
139
142
  if not self.tabs:
140
143
  self._tab_container = Pattern(
141
144
  self.config.background_character,
142
145
  self.config.background_pattern,
143
146
  )
144
- elif TabMode(self.config.tab_mode) == TabMode.TILE_HORIZONTALLY:
147
+ elif tab_mode == TabMode.TILE_HORIZONTALLY:
148
+ children = []
149
+ for tab in self.tabs:
150
+
151
+ def _get_tab_container(tab: Tab = tab) -> Tab:
152
+ return tab
153
+
154
+ children.append(DynamicContainer(_get_tab_container))
145
155
  self._tab_container = HSplit(
146
- children=self.tabs,
156
+ children=children,
147
157
  padding=1,
148
158
  padding_style="class:tab-padding",
149
159
  padding_char="─",
150
160
  )
151
- elif TabMode(self.config.tab_mode) == TabMode.TILE_VERTICALLY:
161
+ elif tab_mode == TabMode.TILE_VERTICALLY:
162
+ children = []
163
+ for tab in self.tabs:
164
+
165
+ def _get_tab_container(tab: Tab = tab) -> Tab:
166
+ return tab
167
+
168
+ children.append(DynamicContainer(_get_tab_container))
152
169
  self._tab_container = VSplit(
153
- children=self.tabs,
170
+ children=children,
154
171
  padding=1,
155
172
  padding_style="class:tab-padding",
156
173
  padding_char="│",
@@ -161,9 +178,7 @@ class NotebookApp(BaseApp):
161
178
  ConditionalContainer(
162
179
  Window(
163
180
  TabBarControl(
164
- tabs=self.tab_bar_tabs,
165
- active=lambda: self._tab_idx,
166
- closeable=True,
181
+ tabs=self.tab_bar_tabs, active=lambda: self._tab_idx
167
182
  ),
168
183
  height=2,
169
184
  style="class:app-tab-bar",
@@ -281,14 +296,18 @@ class NotebookApp(BaseApp):
281
296
 
282
297
  def tab_bar_tabs(self) -> list[TabBarTab]:
283
298
  """Return a list of the current tabs for the tab-bar."""
284
- return [
285
- TabBarTab(
286
- title=partial(lambda x: x.title, tab),
287
- on_activate=partial(setattr, self, "tab_idx", i),
288
- on_close=partial(self.close_tab, tab),
289
- )
290
- for i, tab in enumerate(self.tabs)
291
- ]
299
+ result = []
300
+ for i, tab in enumerate(self.tabs):
301
+ index_dict = self._tab_bar_tabs.setdefault(i, WeakKeyDictionary())
302
+ if tab not in index_dict:
303
+ index_dict[tab] = TabBarTab(
304
+ title=lambda tab=tab: tab.title, # type: ignore [misc]
305
+ on_activate=partial(setattr, self, "tab_idx", i),
306
+ on_close=partial(self.close_tab, tab),
307
+ closeable=True,
308
+ )
309
+ result.append(self._tab_bar_tabs[i][tab])
310
+ return result
292
311
 
293
312
  def _handle_exception(
294
313
  self, loop: AbstractEventLoop, context: dict[str, Any]
@@ -474,9 +493,9 @@ class NotebookApp(BaseApp):
474
493
  children=[
475
494
  get_cmd("toggle-enable-language-servers").menu,
476
495
  separator,
496
+ self.config.menus.autosuggest,
477
497
  get_cmd("toggle-autoformat").menu,
478
498
  get_cmd("toggle-autocomplete").menu,
479
- get_cmd("toggle-autosuggest").menu,
480
499
  get_cmd("toggle-autoinspect").menu,
481
500
  ],
482
501
  description="Turn code assistance tools on or off",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import logging
6
7
  from typing import TYPE_CHECKING, ClassVar
7
8
 
@@ -12,7 +13,6 @@ from euporie.core.convert.datum import Datum
12
13
  from euporie.core.convert.mime import MIME_FORMATS, get_format
13
14
  from euporie.core.margins import MarginContainer, ScrollbarMargin
14
15
  from euporie.core.tabs.base import Tab
15
- from euporie.core.utils import run_in_thread_with_context
16
16
  from euporie.core.widgets.display import Display
17
17
 
18
18
  if TYPE_CHECKING:
@@ -44,7 +44,7 @@ class DisplayTab(Tab):
44
44
  self.app.layout.focus(self.container)
45
45
  self.app.invalidate()
46
46
 
47
- run_in_thread_with_context(_load)
47
+ app.create_background_task(asyncio.to_thread(_load))
48
48
 
49
49
  def __pt_status__(self) -> StatusBarFields | None:
50
50
  """Return a list of statusbar field values shown then this tab is active."""
@@ -3,16 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from functools import partial
6
7
  from typing import TYPE_CHECKING, ClassVar
7
8
 
8
9
  from prompt_toolkit.layout.containers import HSplit
9
10
  from prompt_toolkit.layout.dimension import Dimension
10
11
 
11
12
  from euporie.core.filters import insert_mode, replace_mode
12
- from euporie.core.kernel.client import Kernel, MsgCallbacks
13
+ from euporie.core.kernel.jupyter import JupyterKernel, MsgCallbacks
13
14
  from euporie.core.key_binding.registry import load_registered_bindings
14
15
  from euporie.core.lexers import detect_lexer
15
- from euporie.core.path import UntitledPath
16
16
  from euporie.core.tabs.kernel import KernelTab
17
17
  from euporie.core.widgets.inputs import KernelInput
18
18
 
@@ -41,7 +41,7 @@ class EditorTab(KernelTab):
41
41
  self,
42
42
  app: BaseApp,
43
43
  path: Path | None = None,
44
- kernel: Kernel | None = None,
44
+ kernel: JupyterKernel | None = None,
45
45
  comms: dict[str, Comm] | None = None,
46
46
  use_kernel_history: bool = False,
47
47
  ) -> None:
@@ -93,7 +93,7 @@ class EditorTab(KernelTab):
93
93
  if self.dirty and (unsaved := self.app.dialogs.get("unsaved")):
94
94
  unsaved.show(
95
95
  tab=self,
96
- cb=cb,
96
+ cb=partial(super().close, cb),
97
97
  )
98
98
  else:
99
99
  super().close(cb)
@@ -154,46 +154,11 @@ class EditorTab(KernelTab):
154
154
  ),
155
155
  )
156
156
 
157
- def save(self, path: Path | None = None, cb: Callable | None = None) -> None:
158
- """Save the current file."""
159
- if path is not None:
160
- self.path = path
157
+ def write_file(self, path: Path) -> None:
158
+ """Write the file's text data to a path.
161
159
 
162
- if isinstance(self.path, UntitledPath):
163
- if dialog := self.app.dialogs.get("save-as"):
164
- dialog.show(tab=self, cb=cb)
165
- else:
166
- log.debug("Saving file...")
167
- self.saving = True
168
- self.app.invalidate()
169
- # Ensure parent path exists
170
- parent = self.path.parent
171
- parent.mkdir(exist_ok=True, parents=True)
172
- # Save to a temp file, then replace the original
173
- temp_path = parent / f".{self.path.stem}.tmp{self.path.suffix}"
174
- log.debug("Using temporary file %s", temp_path.name)
175
- try:
176
- open_file = temp_path.open("w")
177
- except NotImplementedError:
178
- if dialog := self.app.dialogs.get("save-as"):
179
- dialog.show(tab=self, cb=cb)
180
- else:
181
- try:
182
- open_file.write(self.input_box.buffer.text)
183
- except Exception:
184
- if dialog := self.app.dialogs.get("save-as"):
185
- dialog.show(tab=self, cb=cb)
186
- else:
187
- try:
188
- temp_path.rename(self.path)
189
- except Exception:
190
- if dialog := self.app.dialogs.get("save-as"):
191
- dialog.show(tab=self, cb=cb)
192
- else:
193
- self.dirty = False
194
- self.saving = False
195
- self.app.invalidate()
196
- log.debug("File saved")
197
- # Run the callback
198
- if callable(cb):
199
- cb()
160
+ Args:
161
+ path: An path at which to save the file
162
+
163
+ """
164
+ path.write_text(self.input_box.buffer.text)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import json
6
7
  import logging
7
8
  from typing import TYPE_CHECKING, ClassVar
@@ -9,8 +10,9 @@ from typing import TYPE_CHECKING, ClassVar
9
10
  from prompt_toolkit.layout.containers import VSplit
10
11
  from prompt_toolkit.layout.dimension import Dimension
11
12
 
13
+ from euporie.core.layout.scroll import ScrollingContainer
14
+ from euporie.core.margins import MarginContainer, ScrollbarMargin
12
15
  from euporie.core.tabs.base import Tab
13
- from euporie.core.utils import run_in_thread_with_context
14
16
  from euporie.core.widgets.tree import JsonView
15
17
 
16
18
  if TYPE_CHECKING:
@@ -43,7 +45,7 @@ class JsonTab(Tab):
43
45
  self.app.layout.focus(self.container)
44
46
  self.app.invalidate()
45
47
 
46
- run_in_thread_with_context(_load)
48
+ app.create_background_task(asyncio.to_thread(_load))
47
49
 
48
50
  def __pt_status__(self) -> StatusBarFields | None:
49
51
  """Return a list of statusbar field values shown then this tab is active."""
@@ -65,8 +67,10 @@ class JsonTab(Tab):
65
67
 
66
68
  return VSplit(
67
69
  [
68
- JsonView(data, title=self.path.name, expanded=True),
69
- # MarginContainer(ScrollbarMargin(), target=self.display.window),
70
+ scroll := ScrollingContainer(
71
+ children=[JsonView(data, title=self.path.name, expanded=True)]
72
+ ),
73
+ MarginContainer(ScrollbarMargin(), target=scroll),
70
74
  ],
71
75
  width=Dimension(weight=1),
72
76
  height=Dimension(weight=1),
@@ -48,7 +48,7 @@ if TYPE_CHECKING:
48
48
  from euporie.core.app.app import BaseApp
49
49
  from euporie.core.bars.status import StatusBarFields
50
50
  from euporie.core.comm.base import Comm
51
- from euporie.core.kernel.client import Kernel
51
+ from euporie.core.kernel.base import BaseKernel
52
52
 
53
53
  log = logging.getLogger(__name__)
54
54
 
@@ -77,7 +77,7 @@ class Notebook(BaseNotebook):
77
77
  self,
78
78
  app: BaseApp,
79
79
  path: Path | None = None,
80
- kernel: Kernel | None = None,
80
+ kernel: BaseKernel | None = None,
81
81
  comms: dict[str, Comm] | None = None,
82
82
  use_kernel_history: bool = True,
83
83
  json: dict[str, Any] | None = None,
@@ -97,7 +97,6 @@ class Notebook(BaseNotebook):
97
97
  self.multiple_cells_selected = multiple_cells_selected
98
98
  self.clipboard: list[Cell] = []
99
99
  self.undo_buffer: deque[tuple[int, list[Cell]]] = deque(maxlen=10)
100
-
101
100
  self.default_callbacks["set_next_input"] = self.set_next_input
102
101
 
103
102
  # Tab stuff
@@ -117,7 +116,11 @@ class Notebook(BaseNotebook):
117
116
 
118
117
  def _kernel_name() -> StyleAndTextTuples:
119
118
  ft: StyleAndTextTuples = [
120
- ("", self.kernel_display_name, self._statusbar_kernel_handler)
119
+ (
120
+ "",
121
+ self.kernel_display_name or "No Kernel",
122
+ self._statusbar_kernel_handler,
123
+ )
121
124
  ]
122
125
  return ft
123
126
 
@@ -164,13 +167,28 @@ class Notebook(BaseNotebook):
164
167
  self.app.dialogs["error"].show(exception=error, when="starting the kernel")
165
168
 
166
169
  def load_container(self) -> AnyContainer:
167
- """Load the main notebook container."""
170
+ """Trigger loading of the main notebook container."""
171
+
172
+ async def _load() -> None:
173
+ # Load notebook file
174
+ self.load()
175
+ # Load an focus container
176
+ prev = self.container
177
+ self.container = self._load_container()
178
+ self.loaded = True
179
+ # Update the focus if the old container had focus
180
+ if self.app.layout.has_focus(prev):
181
+ self.focus()
182
+
183
+ self.app.create_background_task(_load())
184
+
185
+ return self.container
186
+
187
+ def _load_container(self) -> AnyContainer:
188
+ """Actually load the main notebook container."""
168
189
  self.page = ScrollingContainer(
169
190
  self.rendered_cells, width=self.app.config.max_notebook_width
170
191
  )
171
- # Ensure all cells get initialized ASAP to prevent race conditions
172
- # creating multiple version of cells across threads
173
- self.page.all_children()
174
192
 
175
193
  expand = Condition(lambda: self.app.config.expand)
176
194
 
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
27
27
 
28
28
  from euporie.core.app.app import BaseApp
29
29
  from euporie.core.comm.base import Comm
30
- from euporie.core.kernel.client import Kernel
30
+ from euporie.core.kernel.base import BaseKernel
31
31
 
32
32
  log = logging.getLogger(__name__)
33
33
 
@@ -54,17 +54,6 @@ class PreviewNotebook(BaseNotebook):
54
54
  """Filter cells before kernel is loaded."""
55
55
  super().pre_init_kernel()
56
56
 
57
- # Filter the cells to be shown
58
- n_cells = len(self.json["cells"]) - 1
59
- start: int | None = None
60
- stop: int | None = None
61
- if self.app.config.cell_start is not None:
62
- start = min(max(self.app.config.cell_start, -n_cells), n_cells)
63
- if self.app.config.cell_stop is not None:
64
- stop = min(max(self.app.config.cell_stop, -n_cells), n_cells)
65
- log.debug("Showing cells %s to %s", start, stop)
66
- self.json["cells"] = self.json["cells"][start:stop]
67
-
68
57
  def post_init_kernel(self) -> None:
69
58
  """Optionally start kernel after it is loaded."""
70
59
  super().post_init_kernel()
@@ -78,7 +67,7 @@ class PreviewNotebook(BaseNotebook):
78
67
 
79
68
  def init_kernel(
80
69
  self,
81
- kernel: Kernel | None = None,
70
+ kernel: BaseKernel | None = None,
82
71
  comms: dict[str, Comm] | None = None,
83
72
  use_kernel_history: bool = False,
84
73
  connection_file: Path | None = None,
@@ -162,6 +151,21 @@ class PreviewNotebook(BaseNotebook):
162
151
 
163
152
  def load_container(self) -> AnyContainer:
164
153
  """Load the notebook's main container."""
154
+ # Load file
155
+ self.load()
156
+
157
+ # Filter the cells to be shown
158
+ n_cells = len(self.json["cells"]) - 1
159
+ start: int | None = None
160
+ stop: int | None = None
161
+ if self.app.config.cell_start is not None:
162
+ start = min(max(self.app.config.cell_start, -n_cells), n_cells)
163
+ if self.app.config.cell_stop is not None:
164
+ stop = min(max(self.app.config.cell_stop, -n_cells), n_cells)
165
+ log.debug("Showing cells %s to %s", start, stop)
166
+ self.json["cells"] = self.json["cells"][start:stop]
167
+
168
+ # Generate container
165
169
  no_expand = ~self.app.config.filters.expand
166
170
  return PrintingContainer(
167
171
  [
@@ -0,0 +1 @@
1
+ """A euporie web viewer component."""
@@ -0,0 +1,14 @@
1
+ """Tabs for use in euporie notebook editor."""
2
+
3
+ from euporie.core.tabs import _TAB_REGISTRY, TabRegistryEntry
4
+
5
+ _TAB_REGISTRY.extend(
6
+ [
7
+ TabRegistryEntry(
8
+ path="euporie.web.tabs.web:WebTab",
9
+ name="Web Viewer",
10
+ mime_types={"text/html", "text/markdown"},
11
+ weight=2,
12
+ ),
13
+ ]
14
+ )