euporie 2.8.4__py3-none-any.whl → 2.8.6__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 (131) 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 +58 -62
  5. euporie/core/__init__.py +1 -1
  6. euporie/core/__main__.py +28 -11
  7. euporie/core/_settings.py +109 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +95 -0
  10. euporie/core/app/_settings.py +457 -0
  11. euporie/core/{app.py → app/app.py} +212 -576
  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 +205 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +20 -16
  21. euporie/core/{widgets → bars}/status.py +6 -23
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +16 -7
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +10 -20
  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 +116 -53
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +9 -23
  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 +4 -6
  39. euporie/core/convert/utils.py +41 -4
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +98 -40
  42. euporie/core/format.py +2 -3
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +12 -21
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +386 -133
  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} +45 -108
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +1 -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 +1 -6
  58. euporie/core/key_binding/bindings/mouse.py +2 -2
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/key_processor.py +43 -2
  61. euporie/core/key_binding/registry.py +2 -0
  62. euporie/core/key_binding/utils.py +22 -2
  63. euporie/core/keys.py +7156 -93
  64. euporie/core/layout/cache.py +3 -3
  65. euporie/core/layout/containers.py +48 -4
  66. euporie/core/layout/decor.py +2 -2
  67. euporie/core/layout/mouse.py +1 -1
  68. euporie/core/layout/print.py +2 -1
  69. euporie/core/layout/scroll.py +39 -34
  70. euporie/core/log.py +76 -64
  71. euporie/core/lsp.py +118 -24
  72. euporie/core/margins.py +1 -1
  73. euporie/core/path.py +62 -13
  74. euporie/core/renderer.py +58 -17
  75. euporie/core/style.py +57 -39
  76. euporie/core/suggest.py +103 -85
  77. euporie/core/tabs/__init__.py +32 -0
  78. euporie/core/tabs/_settings.py +113 -0
  79. euporie/core/tabs/base.py +80 -470
  80. euporie/core/tabs/kernel.py +419 -0
  81. euporie/core/tabs/notebook.py +24 -101
  82. euporie/core/utils.py +92 -15
  83. euporie/core/validation.py +1 -1
  84. euporie/core/widgets/_settings.py +188 -0
  85. euporie/core/widgets/cell.py +19 -50
  86. euporie/core/widgets/cell_outputs.py +25 -36
  87. euporie/core/widgets/decor.py +11 -41
  88. euporie/core/widgets/dialog.py +62 -27
  89. euporie/core/widgets/display.py +12 -15
  90. euporie/core/widgets/file_browser.py +2 -23
  91. euporie/core/widgets/forms.py +8 -5
  92. euporie/core/widgets/inputs.py +13 -70
  93. euporie/core/widgets/layout.py +2 -1
  94. euporie/core/widgets/logo.py +49 -0
  95. euporie/core/widgets/menu.py +10 -8
  96. euporie/core/widgets/pager.py +6 -10
  97. euporie/core/widgets/palette.py +6 -6
  98. euporie/hub/app.py +52 -35
  99. euporie/notebook/_commands.py +24 -0
  100. euporie/notebook/_settings.py +107 -0
  101. euporie/notebook/app.py +49 -171
  102. euporie/notebook/filters.py +1 -1
  103. euporie/notebook/tabs/__init__.py +46 -7
  104. euporie/notebook/tabs/_commands.py +714 -0
  105. euporie/notebook/tabs/_settings.py +32 -0
  106. euporie/notebook/tabs/display.py +4 -4
  107. euporie/notebook/tabs/edit.py +11 -44
  108. euporie/notebook/tabs/json.py +5 -5
  109. euporie/notebook/tabs/log.py +1 -18
  110. euporie/notebook/tabs/notebook.py +11 -660
  111. euporie/notebook/widgets/_commands.py +11 -0
  112. euporie/notebook/widgets/_settings.py +19 -0
  113. euporie/notebook/widgets/side_bar.py +14 -34
  114. euporie/preview/_settings.py +104 -0
  115. euporie/preview/app.py +6 -31
  116. euporie/preview/tabs/notebook.py +6 -72
  117. euporie/web/__init__.py +1 -0
  118. euporie/web/tabs/__init__.py +14 -0
  119. euporie/web/tabs/web.py +11 -6
  120. euporie/web/widgets/__init__.py +1 -0
  121. euporie/web/widgets/webview.py +5 -15
  122. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
  123. euporie-2.8.6.dist-info/RECORD +175 -0
  124. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
  125. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
  126. {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
  127. euporie/core/launch.py +0 -64
  128. euporie/core/terminal.py +0 -522
  129. euporie-2.8.4.dist-info/RECORD +0 -147
  130. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
  131. {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -0,0 +1,419 @@
1
+ """Contain kernel tab base class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from abc import ABCMeta
8
+ from collections import deque
9
+ from functools import partial
10
+ from typing import TYPE_CHECKING
11
+ from weakref import WeakKeyDictionary
12
+
13
+ from prompt_toolkit.auto_suggest import DummyAutoSuggest
14
+ from prompt_toolkit.completion.base import (
15
+ DynamicCompleter,
16
+ _MergedCompleter,
17
+ )
18
+ from prompt_toolkit.history import DummyHistory, InMemoryHistory
19
+
20
+ from euporie.core.app.current import get_app
21
+ from euporie.core.comm.registry import open_comm
22
+ from euporie.core.commands import add_cmd
23
+ from euporie.core.completion import DeduplicateCompleter, KernelCompleter, LspCompleter
24
+ from euporie.core.diagnostics import Report
25
+ from euporie.core.filters import kernel_tab_has_focus
26
+ from euporie.core.format import LspFormatter
27
+ from euporie.core.history import KernelHistory
28
+ from euporie.core.inspection import (
29
+ FirstInspector,
30
+ KernelInspector,
31
+ LspInspector,
32
+ )
33
+ from euporie.core.kernel.client import Kernel, MsgCallbacks
34
+ from euporie.core.suggest import HistoryAutoSuggest
35
+ from euporie.core.tabs.base import Tab
36
+
37
+ if TYPE_CHECKING:
38
+ from collections.abc import Sequence
39
+ from pathlib import Path
40
+ from typing import Any, Callable
41
+
42
+ from prompt_toolkit.auto_suggest import AutoSuggest
43
+ from prompt_toolkit.completion.base import Completer
44
+ from prompt_toolkit.history import History
45
+
46
+ from euporie.core.app.app import BaseApp
47
+ from euporie.core.comm.base import Comm
48
+ from euporie.core.format import Formatter
49
+ from euporie.core.inspection import Inspector
50
+ from euporie.core.lsp import LspClient
51
+ from euporie.core.widgets.inputs import KernelInput
52
+
53
+ log = logging.getLogger(__name__)
54
+
55
+
56
+ class KernelTab(Tab, metaclass=ABCMeta):
57
+ """A Tab which connects to a kernel."""
58
+
59
+ kernel: Kernel
60
+ kernel_language: str
61
+ _metadata: dict[str, Any]
62
+ bg_init = False
63
+
64
+ default_callbacks: MsgCallbacks
65
+ allow_stdin: bool
66
+
67
+ def __init__(
68
+ self,
69
+ app: BaseApp,
70
+ path: Path | None = None,
71
+ kernel: Kernel | None = None,
72
+ comms: dict[str, Comm] | None = None,
73
+ use_kernel_history: bool = False,
74
+ connection_file: Path | None = None,
75
+ ) -> None:
76
+ """Create a new instance of a tab with a kernel."""
77
+ # Init tab
78
+ super().__init__(app, path)
79
+
80
+ self.lsps: list[LspClient] = []
81
+ self.history: History = DummyHistory()
82
+ self.inspectors: list[Inspector] = []
83
+ self.inspector = FirstInspector(lambda: self.inspectors)
84
+ self.suggester: AutoSuggest = DummyAutoSuggest()
85
+ self.completers: list[Completer] = []
86
+ self.completer = DeduplicateCompleter(
87
+ DynamicCompleter(lambda: _MergedCompleter(self.completers))
88
+ )
89
+ self.formatters: list[Formatter] = self.app.formatters
90
+ self.reports: WeakKeyDictionary[LspClient, Report] = WeakKeyDictionary()
91
+
92
+ # The client-side comm states
93
+ self.comms: dict[str, Comm] = {}
94
+ # The current kernel input
95
+ self._current_input: KernelInput | None = None
96
+
97
+ if self.bg_init:
98
+ # Load kernel in a background thread
99
+ app.create_background_task(
100
+ asyncio.to_thread(
101
+ self.init_kernel, kernel, comms, use_kernel_history, connection_file
102
+ )
103
+ )
104
+ else:
105
+ self.init_kernel(kernel, comms, use_kernel_history, connection_file)
106
+
107
+ async def load_lsps(self) -> None:
108
+ """Load the LSP clients."""
109
+ path = self.path
110
+
111
+ # Load list of LSP clients for the tab's language
112
+ self.lsps.extend(self.app.get_language_lsps(self.language))
113
+
114
+ # Wait for all lsps to be initialized, and setup hooks as they become ready
115
+ async def _await_load(lsp: LspClient) -> LspClient:
116
+ await lsp.initialized.wait()
117
+ return lsp
118
+
119
+ for ready in asyncio.as_completed([_await_load(lsp) for lsp in self.lsps]):
120
+ lsp = await ready
121
+ # Apply open, save, and close hooks to the tab
122
+ change_handler = partial(lambda lsp, tab: self.lsp_change_handler(lsp), lsp)
123
+ close_handler = partial(lambda lsp, tab: self.lsp_close_handler(lsp), lsp)
124
+ before_save_handler = partial(
125
+ lambda lsp, tab: self.lsp_before_save_handler(lsp), lsp
126
+ )
127
+ after_save_handler = partial(
128
+ lambda lsp, tab: self.lsp_after_save_handler(lsp), lsp
129
+ )
130
+
131
+ self.on_close += close_handler
132
+ self.on_change += change_handler
133
+ self.before_save += before_save_handler
134
+ self.after_save += after_save_handler
135
+
136
+ # Listen for LSP diagnostics
137
+ lsp.on_diagnostics += self.lsp_update_diagnostics
138
+
139
+ # Add completer
140
+ completer = LspCompleter(lsp=lsp, path=path)
141
+ self.completers.append(completer)
142
+
143
+ # Add inspector
144
+ inspector = LspInspector(lsp, path)
145
+ self.inspectors.append(inspector)
146
+
147
+ # Add formatter
148
+ formatter = LspFormatter(lsp, path)
149
+ self.formatters.append(formatter)
150
+
151
+ # Remove hooks if the LSP exits
152
+ def lsp_unload(lsp: LspClient) -> None:
153
+ self.on_change -= change_handler # noqa: B023
154
+ self.before_save -= before_save_handler # noqa: B023
155
+ self.after_save -= after_save_handler # noqa: B023
156
+ self.on_close -= close_handler # noqa: B023
157
+ if completer in self.completers: # noqa: B023
158
+ self.completers.remove(completer) # noqa: B023
159
+ if inspector in self.completers: # noqa: B023
160
+ self.inspectors.remove(inspector) # noqa: B023
161
+ if formatter in self.completers: # noqa: B023
162
+ self.formatters.remove(formatter) # noqa: B023
163
+ if completer in self.completers: # noqa: B023
164
+ self.completers.remove(completer) # noqa: B023
165
+ if inspector in self.inspectors: # noqa: B023
166
+ self.inspectors.remove(inspector) # noqa: B023
167
+ if formatter in self.formatters: # noqa: B023
168
+ self.formatters.remove(formatter) # noqa: B023
169
+
170
+ lsp.on_exit += lsp_unload
171
+
172
+ # Remove the lsp exit handler if this tab closes
173
+ self.on_close += lambda tab: (
174
+ (lsp.on_exit.__isub__(lsp_unload) and None) or None # noqa: B023
175
+ ) # Magical typing
176
+
177
+ # Tell the LSP we have an open file
178
+ self.lsp_open_handler(lsp)
179
+
180
+ def pre_init_kernel(self) -> None:
181
+ """Run stuff before the kernel is loaded."""
182
+
183
+ def post_init_kernel(self) -> None:
184
+ """Run stuff after the kernel is loaded."""
185
+
186
+ def init_kernel(
187
+ self,
188
+ kernel: Kernel | None = None,
189
+ comms: dict[str, Comm] | None = None,
190
+ use_kernel_history: bool = False,
191
+ connection_file: Path | None = None,
192
+ ) -> None:
193
+ """Set up the tab's kernel and related components."""
194
+ self.pre_init_kernel()
195
+
196
+ self.kernel_queue: deque[Callable] = deque()
197
+
198
+ if kernel:
199
+ self.kernel = kernel
200
+ self.kernel.default_callbacks = self.default_callbacks
201
+ else:
202
+ self.kernel = Kernel(
203
+ kernel_tab=self,
204
+ allow_stdin=self.allow_stdin,
205
+ default_callbacks=self.default_callbacks,
206
+ connection_file=connection_file,
207
+ )
208
+ self.comms = comms or {} # The client-side comm states
209
+ self.completers.append(KernelCompleter(self.kernel))
210
+ self.inspectors.append(KernelInspector(self.kernel))
211
+ self.use_kernel_history = use_kernel_history
212
+ self.history = (
213
+ KernelHistory(self.kernel) if use_kernel_history else InMemoryHistory()
214
+ )
215
+ self.suggester = HistoryAutoSuggest(self.history)
216
+
217
+ self.app.create_background_task(self.load_lsps())
218
+
219
+ self.post_init_kernel()
220
+
221
+ def close(self, cb: Callable | None = None) -> None:
222
+ """Shut down kernel when tab is closed."""
223
+ if hasattr(self, "kernel"):
224
+ self.kernel.shutdown()
225
+ super().close(cb)
226
+
227
+ def interrupt_kernel(self) -> None:
228
+ """Interrupt the current `Notebook`'s kernel."""
229
+ self.kernel.interrupt()
230
+
231
+ def restart_kernel(self, cb: Callable | None = None) -> None:
232
+ """Restart the current `Notebook`'s kernel."""
233
+
234
+ def _cb(result: dict[str, Any]) -> None:
235
+ if callable(cb):
236
+ cb()
237
+
238
+ if confirm := self.app.dialogs.get("confirm"):
239
+ confirm.show(
240
+ message="Are you sure you want to restart the kernel?",
241
+ cb=partial(self.kernel.restart, cb=_cb),
242
+ )
243
+ else:
244
+ self.kernel.restart(cb=_cb)
245
+
246
+ def kernel_started(self, result: dict[str, Any] | None = None) -> None:
247
+ """Task to run when the kernel has started."""
248
+ # Check kernel has not failed
249
+ if not self.kernel_name or self.kernel.missing:
250
+ if not self.kernel_name:
251
+ msg = "No kernel selected"
252
+ else:
253
+ msg = f"Kernel '{self.kernel_display_name}' not installed"
254
+ self.change_kernel(
255
+ msg=msg,
256
+ startup=True,
257
+ )
258
+
259
+ elif self.kernel.status == "error":
260
+ self.report_kernel_error(self.kernel.error)
261
+
262
+ else:
263
+ # Wait for an idle kernel
264
+ if self.kernel.status != "idle":
265
+ self.kernel.wait_for_status("idle")
266
+
267
+ # Load widget comm info
268
+ # self.kernel.comm_info(target_name="jupyter.widget")
269
+
270
+ # Load kernel info
271
+ self.kernel.info(set_kernel_info=self.set_kernel_info)
272
+
273
+ # Load kernel history
274
+ if self.use_kernel_history:
275
+ self.app.create_background_task(self.load_history())
276
+
277
+ # Run queued kernel tasks when the kernel is idle
278
+ log.debug("Running %d kernel tasks", len(self.kernel_queue))
279
+ while self.kernel_queue:
280
+ self.kernel_queue.popleft()()
281
+
282
+ self.app.invalidate()
283
+
284
+ def report_kernel_error(self, error: Exception | None) -> None:
285
+ """Report a kernel error to the user."""
286
+ log.debug("Kernel error", exc_info=error)
287
+
288
+ async def load_history(self) -> None:
289
+ """Load kernel history."""
290
+ try:
291
+ await self.history.load().__anext__()
292
+ except StopAsyncIteration:
293
+ pass
294
+
295
+ @property
296
+ def metadata(self) -> dict[str, Any]:
297
+ """Return a dictionary to hold notebook / kernel metadata."""
298
+ return self._metadata
299
+
300
+ @property
301
+ def kernel_name(self) -> str:
302
+ """Return the name of the kernel defined in the notebook JSON."""
303
+ return self.metadata.get("kernelspec", {}).get(
304
+ "name", self.app.config.kernel_name
305
+ )
306
+
307
+ @kernel_name.setter
308
+ def kernel_name(self, value: str) -> None:
309
+ """Return the name of the kernel defined in the notebook JSON."""
310
+ self.metadata.setdefault("kernelspec", {})["name"] = value
311
+
312
+ @property
313
+ def language(self) -> str:
314
+ """Return the name of the kernel defined in the notebook JSON."""
315
+ return self.metadata.get("kernelspec", {}).get("language")
316
+
317
+ @property
318
+ def kernel_display_name(self) -> str:
319
+ """Return the display name of the kernel defined in the notebook JSON."""
320
+ return self.metadata.get("kernelspec", {}).get("display_name", self.kernel_name)
321
+
322
+ @property
323
+ def kernel_lang_file_ext(self) -> str:
324
+ """Return the display name of the kernel defined in the notebook JSON."""
325
+ return self.metadata.get("language_info", {}).get("file_extension", ".py")
326
+
327
+ @property
328
+ def current_input(self) -> KernelInput:
329
+ """Return the currently active kernel input, if any."""
330
+ from euporie.core.widgets.inputs import KernelInput
331
+
332
+ return self._current_input or KernelInput(self)
333
+
334
+ def set_kernel_info(self, info: dict) -> None:
335
+ """Handle kernel info requests."""
336
+ self.metadata["language_info"] = info.get("language_info", {})
337
+
338
+ def change_kernel(self, msg: str | None = None, startup: bool = False) -> None:
339
+ """Prompt the user to select a new kernel."""
340
+ kernel_specs = self.kernel.specs
341
+
342
+ # Warn the user if no kernels are installed
343
+ if not kernel_specs:
344
+ if startup and "no-kernels" in self.app.dialogs:
345
+ self.app.dialogs["no-kernels"].show()
346
+ return
347
+
348
+ # Automatically select the only kernel if there is only one
349
+ if startup and len(kernel_specs) == 1:
350
+ self.kernel.change(next(iter(kernel_specs)))
351
+ return
352
+
353
+ self.app.dialogs["change-kernel"].show(
354
+ tab=self, message=msg, kernel_specs=kernel_specs
355
+ )
356
+
357
+ def comm_open(self, content: dict, buffers: Sequence[bytes]) -> None:
358
+ """Register a new kernel Comm object in the notebook."""
359
+ comm_id = str(content.get("comm_id"))
360
+ self.comms[comm_id] = open_comm(
361
+ comm_container=self, content=content, buffers=buffers
362
+ )
363
+
364
+ def comm_msg(self, content: dict, buffers: Sequence[bytes]) -> None:
365
+ """Respond to a Comm message from the kernel."""
366
+ comm_id = str(content.get("comm_id"))
367
+ if comm := self.comms.get(comm_id):
368
+ comm.process_data(content.get("data", {}), buffers)
369
+
370
+ def comm_close(self, content: dict, buffers: Sequence[bytes]) -> None:
371
+ """Close a notebook Comm."""
372
+ comm_id = content.get("comm_id")
373
+ if comm_id in self.comms:
374
+ del self.comms[comm_id]
375
+
376
+ def lsp_open_handler(self, lsp: LspClient) -> None:
377
+ """Tell the LSP we opened a file."""
378
+ lsp.open_doc(
379
+ path=self.path, language=self.language, text=self.current_input.buffer.text
380
+ )
381
+
382
+ def lsp_change_handler(self, lsp: LspClient) -> None:
383
+ """Tell the LSP server a file has changed."""
384
+ lsp.change_doc(
385
+ path=self.path,
386
+ language=self.language,
387
+ text=self.current_input.buffer.text,
388
+ )
389
+
390
+ def lsp_before_save_handler(self, lsp: LspClient) -> None:
391
+ """Tell the the LSP we are about to save a document."""
392
+ lsp.will_save_doc(self.path)
393
+
394
+ def lsp_after_save_handler(self, lsp: LspClient) -> None:
395
+ """Tell the the LSP we saved a document."""
396
+ lsp.save_doc(self.path, text=self.current_input.buffer.text)
397
+
398
+ def lsp_close_handler(self, lsp: LspClient) -> None:
399
+ """Tell the LSP we opened a file."""
400
+ lsp.close_doc(path=self.path)
401
+
402
+ def lsp_update_diagnostics(self, lsp: LspClient) -> None:
403
+ """Process a new diagnostic report from the LSP."""
404
+ if (diagnostics := lsp.reports.pop(self.path.as_uri(), None)) is not None:
405
+ self.reports[lsp] = Report.from_lsp(self.current_input.text, diagnostics)
406
+ self.app.invalidate()
407
+
408
+ def report(self) -> Report:
409
+ """Return the current diagnostic reports."""
410
+ return Report.from_reports(*self.reports.values())
411
+
412
+ # ################################### Commands ####################################
413
+
414
+ @staticmethod
415
+ @add_cmd(filter=kernel_tab_has_focus)
416
+ def _change_kernel() -> None:
417
+ """Change the notebook's kernel."""
418
+ if isinstance(kt := get_app().tab, KernelTab):
419
+ kt.change_kernel()
@@ -1,4 +1,4 @@
1
- """Contain the main class for a notebook file."""
1
+ """Contain the base class for a notebook tabs."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -9,15 +9,14 @@ from functools import partial
9
9
  from typing import TYPE_CHECKING
10
10
 
11
11
  import nbformat
12
- from prompt_toolkit.filters import Never, buffer_has_focus
12
+ from prompt_toolkit.filters import Never
13
13
 
14
14
  from euporie.core.comm.registry import open_comm
15
15
  from euporie.core.commands import get_cmd
16
- from euporie.core.config import add_setting
17
- from euporie.core.kernel import MsgCallbacks
16
+ from euporie.core.io import edit_in_editor
17
+ from euporie.core.kernel.client import MsgCallbacks
18
18
  from euporie.core.path import UntitledPath
19
- from euporie.core.tabs.base import KernelTab
20
- from euporie.core.terminal import edit_in_editor
19
+ from euporie.core.tabs.kernel import KernelTab
21
20
  from euporie.core.widgets.cell import Cell, get_cell_id
22
21
 
23
22
  try:
@@ -34,9 +33,9 @@ if TYPE_CHECKING:
34
33
  from prompt_toolkit.filters import Filter
35
34
  from prompt_toolkit.layout.containers import AnyContainer
36
35
 
37
- from euporie.core.app import BaseApp
36
+ from euporie.core.app.app import BaseApp
38
37
  from euporie.core.comm.base import Comm
39
- from euporie.core.kernel import Kernel
38
+ from euporie.core.kernel.client import Kernel
40
39
  from euporie.core.lsp import LspClient
41
40
  from euporie.core.widgets.inputs import KernelInput
42
41
 
@@ -107,11 +106,14 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
107
106
 
108
107
  def pre_init_kernel(self) -> None:
109
108
  """Run stuff before the kernel is loaded."""
109
+ super().pre_init_kernel()
110
110
  # Load notebook file
111
111
  self.load()
112
112
 
113
113
  def post_init_kernel(self) -> None:
114
114
  """Load the notebook container after the kernel has been loaded."""
115
+ super().post_init_kernel()
116
+
115
117
  # Replace the tab's container
116
118
  prev = self.container
117
119
  self.container = self.load_container()
@@ -273,18 +275,15 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
273
275
  else:
274
276
  super().close(cb)
275
277
 
276
- def save(self, path: Path | None = None, cb: Callable | None = None) -> None:
278
+ def write_file(self, path: Path) -> None:
277
279
  """Write the notebook's JSON to the current notebook's file.
278
280
 
279
281
  Additionally save the widget state to the notebook metadata.
280
282
 
281
283
  Args:
282
- path: An optional new path at which to save the tab
283
- cb: A callback to run if after saving the notebook.
284
+ path: An path at which to save the file
284
285
 
285
286
  """
286
- if path is not None:
287
- self.path = path
288
287
  if self.app.config.save_widget_state:
289
288
  self.json.setdefault("metadata", {})["widgets"] = {
290
289
  "application/vnd.jupyter.widget-state+json": {
@@ -297,56 +296,21 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
297
296
  },
298
297
  }
299
298
  }
300
- if self.path is None or isinstance(self.path, UntitledPath):
301
- if dialog := self.app.dialogs.get("save-as"):
302
- dialog.show(tab=self, cb=cb)
303
- else:
304
- log.debug("Saving notebook..")
305
- self.saving = True
306
- self.app.invalidate()
307
-
308
- # Save to a temp file, then replace the original
309
- temp_path = self.path.parent / f".{self.path.stem}.tmp{self.path.suffix}"
310
- log.debug("Using temporary file %s", temp_path.name)
299
+ with path.open("w") as open_file:
311
300
  try:
312
- open_file = temp_path.open("w")
313
- except NotImplementedError:
314
- get_cmd("save-as").run()
315
- else:
301
+ write_nb(nb=nbformat.from_dict(self.json), fp=open_file)
302
+ except AssertionError:
316
303
  try:
317
- try:
318
- write_nb(nb=nbformat.from_dict(self.json), fp=open_file)
319
- except AssertionError:
320
- try:
321
- # Jupytext requires a filename if we don't give it a format
322
- write_nb(nb=nbformat.from_dict(self.json), fp=temp_path)
323
- except Exception:
324
- # Jupytext requires a format if the path has no extension
325
- # We just use ipynb as the default format
326
- write_nb(
327
- nb=nbformat.from_dict(self.json),
328
- fp=open_file,
329
- fmt="ipynb",
330
- )
304
+ # Jupytext requires a filename if we don't give it a format
305
+ write_nb(nb=nbformat.from_dict(self.json), fp=path)
331
306
  except Exception:
332
- log.exception("An error occurred while saving the file")
333
- if dialog := self.app.dialogs.get("save-as"):
334
- dialog.show(tab=self, cb=cb)
335
- else:
336
- open_file.close()
337
- try:
338
- temp_path.rename(self.path)
339
- except Exception:
340
- if dialog := self.app.dialogs.get("save-as"):
341
- dialog.show(tab=self, cb=cb)
342
- else:
343
- self.dirty = False
344
- self.saving = False
345
- self.app.invalidate()
346
- log.debug("Notebook saved")
347
- # Run the callback
348
- if callable(cb):
349
- cb()
307
+ # Jupytext requires a format if the path has no extension
308
+ # We just use ipynb as the default format
309
+ write_nb(
310
+ nb=nbformat.from_dict(self.json),
311
+ fp=open_file,
312
+ fmt="ipynb",
313
+ )
350
314
 
351
315
  def run_cell(
352
316
  self,
@@ -458,44 +422,3 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
458
422
  def lsp_update_diagnostics(self, lsp: LspClient) -> None:
459
423
  """Process a new diagnostic report from the LSP."""
460
424
  # Do nothing, these are handled by cells
461
-
462
- # ################################### Settings ####################################
463
-
464
- add_setting(
465
- name="save_widget_state",
466
- flags=["--save-widget-state"],
467
- type_=bool,
468
- help_="Save a notebook's widget state in the notebook metadata",
469
- default=True,
470
- description="""
471
- When set to ``True``, the state of any widgets in the current notebook will
472
- be saves in the notebook's metadata. This enables widgets to be displayed
473
- when the notebook is re-opened without having to re-run the notebook.
474
- """,
475
- )
476
-
477
- add_setting(
478
- name="max_notebook_width",
479
- flags=["--max-notebook-width"],
480
- type_=int,
481
- help_="Maximum width of notebooks",
482
- default=120,
483
- schema={
484
- "minimum": 1,
485
- },
486
- description="""
487
- The maximum width at which to display a notebook.
488
- """,
489
- )
490
-
491
- add_setting(
492
- name="expand",
493
- flags=["--expand"],
494
- type_=bool,
495
- help_="Use the full width to display notebooks",
496
- default=False,
497
- description="""
498
- Whether the notebook page should expand to fill the available width
499
- """,
500
- cmd_filter=~buffer_has_focus,
501
- )