euporie 2.3.2__py3-none-any.whl → 2.4.1__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 (92) hide show
  1. euporie/console/__main__.py +3 -1
  2. euporie/console/app.py +6 -4
  3. euporie/console/tabs/console.py +34 -9
  4. euporie/core/__init__.py +6 -1
  5. euporie/core/__main__.py +1 -1
  6. euporie/core/app.py +79 -109
  7. euporie/core/border.py +44 -14
  8. euporie/core/comm/base.py +5 -4
  9. euporie/core/comm/ipywidgets.py +11 -11
  10. euporie/core/comm/registry.py +12 -6
  11. euporie/core/commands.py +30 -23
  12. euporie/core/completion.py +1 -4
  13. euporie/core/config.py +15 -5
  14. euporie/core/convert/{base.py → core.py} +117 -53
  15. euporie/core/convert/formats/ansi.py +46 -25
  16. euporie/core/convert/formats/base64.py +3 -3
  17. euporie/core/convert/formats/common.py +38 -13
  18. euporie/core/convert/formats/formatted_text.py +54 -12
  19. euporie/core/convert/formats/html.py +5 -5
  20. euporie/core/convert/formats/jpeg.py +1 -1
  21. euporie/core/convert/formats/markdown.py +4 -4
  22. euporie/core/convert/formats/pdf.py +1 -1
  23. euporie/core/convert/formats/pil.py +5 -3
  24. euporie/core/convert/formats/png.py +7 -6
  25. euporie/core/convert/formats/rich.py +4 -3
  26. euporie/core/convert/formats/sixel.py +5 -5
  27. euporie/core/convert/utils.py +1 -1
  28. euporie/core/current.py +11 -5
  29. euporie/core/formatted_text/ansi.py +4 -8
  30. euporie/core/formatted_text/html.py +1630 -856
  31. euporie/core/formatted_text/markdown.py +177 -166
  32. euporie/core/formatted_text/table.py +20 -14
  33. euporie/core/formatted_text/utils.py +21 -10
  34. euporie/core/io.py +14 -14
  35. euporie/core/kernel.py +48 -37
  36. euporie/core/key_binding/bindings/micro.py +5 -1
  37. euporie/core/key_binding/bindings/mouse.py +2 -2
  38. euporie/core/keys.py +3 -0
  39. euporie/core/launch.py +5 -2
  40. euporie/core/lexers.py +13 -2
  41. euporie/core/log.py +135 -139
  42. euporie/core/margins.py +32 -14
  43. euporie/core/path.py +273 -0
  44. euporie/core/processors.py +35 -0
  45. euporie/core/renderer.py +21 -5
  46. euporie/core/style.py +34 -19
  47. euporie/core/tabs/base.py +101 -17
  48. euporie/core/tabs/notebook.py +72 -30
  49. euporie/core/terminal.py +56 -48
  50. euporie/core/utils.py +12 -16
  51. euporie/core/widgets/cell.py +6 -5
  52. euporie/core/widgets/cell_outputs.py +2 -2
  53. euporie/core/widgets/decor.py +74 -82
  54. euporie/core/widgets/dialog.py +132 -28
  55. euporie/core/widgets/display.py +76 -24
  56. euporie/core/widgets/file_browser.py +87 -31
  57. euporie/core/widgets/formatted_text_area.py +1 -3
  58. euporie/core/widgets/forms.py +79 -40
  59. euporie/core/widgets/inputs.py +23 -13
  60. euporie/core/widgets/layout.py +4 -3
  61. euporie/core/widgets/menu.py +368 -216
  62. euporie/core/widgets/page.py +99 -58
  63. euporie/core/widgets/pager.py +1 -1
  64. euporie/core/widgets/palette.py +30 -27
  65. euporie/core/widgets/search_bar.py +38 -25
  66. euporie/core/widgets/status_bar.py +103 -5
  67. euporie/data/desktop/euporie-console.desktop +7 -0
  68. euporie/data/desktop/euporie-notebook.desktop +7 -0
  69. euporie/hub/__main__.py +3 -1
  70. euporie/hub/app.py +9 -7
  71. euporie/notebook/__main__.py +3 -1
  72. euporie/notebook/app.py +7 -30
  73. euporie/notebook/tabs/__init__.py +7 -3
  74. euporie/notebook/tabs/display.py +18 -9
  75. euporie/notebook/tabs/edit.py +106 -23
  76. euporie/notebook/tabs/json.py +73 -0
  77. euporie/notebook/tabs/log.py +18 -8
  78. euporie/notebook/tabs/notebook.py +60 -41
  79. euporie/preview/__main__.py +3 -1
  80. euporie/preview/app.py +2 -1
  81. euporie/preview/tabs/notebook.py +23 -10
  82. euporie/web/tabs/web.py +149 -0
  83. euporie/web/widgets/webview.py +563 -0
  84. euporie-2.4.1.data/data/share/applications/euporie-console.desktop +7 -0
  85. euporie-2.4.1.data/data/share/applications/euporie-notebook.desktop +7 -0
  86. {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/METADATA +6 -5
  87. euporie-2.4.1.dist-info/RECORD +129 -0
  88. {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/WHEEL +1 -1
  89. euporie/core/url.py +0 -64
  90. euporie-2.3.2.dist-info/RECORD +0 -122
  91. {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/entry_points.txt +0 -0
  92. {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/licenses/LICENSE +0 -0
euporie/core/tabs/base.py CHANGED
@@ -8,8 +8,10 @@ from collections import deque
8
8
  from functools import partial
9
9
  from typing import TYPE_CHECKING
10
10
 
11
+ from prompt_toolkit.eventloop.utils import run_in_executor_with_context
11
12
  from prompt_toolkit.history import InMemoryHistory
12
- from prompt_toolkit.layout.containers import Window
13
+ from prompt_toolkit.layout.containers import Window, WindowAlign
14
+ from prompt_toolkit.layout.controls import FormattedTextControl
13
15
 
14
16
  from euporie.core.comm.registry import open_comm
15
17
  from euporie.core.commands import add_cmd
@@ -19,41 +21,55 @@ from euporie.core.current import get_app
19
21
  from euporie.core.filters import kernel_tab_has_focus, tab_has_focus
20
22
  from euporie.core.history import KernelHistory
21
23
  from euporie.core.kernel import Kernel, MsgCallbacks
24
+ from euporie.core.key_binding.registry import (
25
+ register_bindings,
26
+ )
22
27
  from euporie.core.suggest import HistoryAutoSuggest
23
28
 
24
29
  if TYPE_CHECKING:
30
+ from pathlib import Path
25
31
  from typing import Any, Callable, Deque, Sequence
26
32
 
27
33
  from prompt_toolkit.auto_suggest import AutoSuggest
28
34
  from prompt_toolkit.completion.base import Completer
29
- from prompt_toolkit.formatted_text import AnyFormattedText
30
35
  from prompt_toolkit.history import History
31
36
  from prompt_toolkit.layout.containers import AnyContainer
32
- from upath import UPath
33
37
 
34
38
  from euporie.core.app import BaseApp
35
39
  from euporie.core.comm.base import Comm
40
+ from euporie.core.widgets.status_bar import StatusBarFields
36
41
 
37
42
  log = logging.getLogger(__name__)
38
43
 
39
44
 
40
45
  class Tab(metaclass=ABCMeta):
41
- """Bae class for interface tabs."""
46
+ """Base class for interface tabs."""
47
+
48
+ _registry: set[type[Tab]] = set()
49
+ name: str | None = None
50
+ weight: int = 0
51
+ mime_types: set[str] = set()
52
+ file_extensions: set[str] = set()
42
53
 
43
54
  container: AnyContainer
44
55
 
45
- def __init__(self, app: BaseApp, path: UPath | None = None) -> None:
56
+ def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
57
+ """Compile a registry of named tabs."""
58
+ super().__init_subclass__(**kwargs)
59
+ if cls.name:
60
+ Tab._registry.add(cls)
61
+
62
+ def __init__(self, app: BaseApp, path: Path | None = None) -> None:
46
63
  """Call when the tab is created."""
47
64
  self.app = app
48
65
  self.path = path
49
- self.app.container_statuses[self] = self.statusbar_fields
50
- self.container = Window()
66
+ self.container = Window(
67
+ FormattedTextControl([("fg:#888888", "\nLoading…")], focusable=True),
68
+ align=WindowAlign.CENTER,
69
+ )
51
70
 
52
- def statusbar_fields(
53
- self,
54
- ) -> tuple[Sequence[AnyFormattedText], Sequence[AnyFormattedText]]:
55
- """Return a list of statusbar field values shown then this tab is active."""
56
- return ([], [])
71
+ self.dirty = False
72
+ self.saving = False
57
73
 
58
74
  @property
59
75
  def title(self) -> str:
@@ -71,8 +87,7 @@ class Tab(metaclass=ABCMeta):
71
87
  cb: A function to call after the tab is closed.
72
88
 
73
89
  """
74
- if self in self.app.container_statuses:
75
- del self.app.container_statuses[self]
90
+ # Run callback
76
91
  if callable(cb):
77
92
  cb()
78
93
 
@@ -80,10 +95,18 @@ class Tab(metaclass=ABCMeta):
80
95
  """Focus the tab (or make it visible)."""
81
96
  self.app.focus_tab(self)
82
97
 
83
- def save(self, path: UPath | None = None, cb: Callable | None = None) -> None:
98
+ def _save(self, path: Path | None = None, cb: Callable | None = None) -> None:
99
+ """Perform the file save in a background thread."""
100
+ run_in_executor_with_context(self.save, path, cb)
101
+
102
+ def save(self, path: Path | None = None, cb: Callable | None = None) -> None:
84
103
  """Save the current notebook."""
85
104
  raise NotImplementedError
86
105
 
106
+ def __pt_status__(self) -> StatusBarFields | None:
107
+ """Return a list of statusbar field values shown then this tab is active."""
108
+ return ([], [])
109
+
87
110
  def __pt_container__(self) -> AnyContainer:
88
111
  """Return the main container object."""
89
112
  return self.container
@@ -97,6 +120,27 @@ class Tab(metaclass=ABCMeta):
97
120
  if (tab := get_app().tab) is not None:
98
121
  tab.reset()
99
122
 
123
+ @staticmethod
124
+ @add_cmd(filter=tab_has_focus)
125
+ def _save_file() -> None:
126
+ """Save the current file."""
127
+ if (tab := get_app().tab) is not None:
128
+ try:
129
+ tab._save()
130
+ except NotImplementedError:
131
+ pass
132
+
133
+ # ################################# Key Bindings ##################################
134
+
135
+ register_bindings(
136
+ {
137
+ "euporie.core.tabs.base.Tab": {
138
+ "save-file": "c-s",
139
+ "reset-tab": "f5",
140
+ }
141
+ }
142
+ )
143
+
100
144
 
101
145
  class KernelTab(Tab, metaclass=ABCMeta):
102
146
  """A Tab which connects to a kernel."""
@@ -104,6 +148,7 @@ class KernelTab(Tab, metaclass=ABCMeta):
104
148
  kernel: Kernel
105
149
  kernel_language: str
106
150
  _metadata: dict[str, Any]
151
+ bg_init = True
107
152
 
108
153
  default_callbacks: MsgCallbacks
109
154
  allow_stdin: bool
@@ -111,14 +156,44 @@ class KernelTab(Tab, metaclass=ABCMeta):
111
156
  def __init__(
112
157
  self,
113
158
  app: BaseApp,
114
- path: UPath | None = None,
159
+ path: Path | None = None,
115
160
  kernel: Kernel | None = None,
116
161
  comms: dict[str, Comm] | None = None,
117
162
  use_kernel_history: bool = False,
163
+ connection_file: Path | None = None,
118
164
  ) -> None:
119
165
  """Create a new instance of a tab with a kernel."""
166
+ # Init tab
120
167
  super().__init__(app, path)
121
168
 
169
+ if self.bg_init:
170
+ # Load kernel in a background thread
171
+ run_in_executor_with_context(
172
+ partial(
173
+ self.init_kernel, kernel, comms, use_kernel_history, connection_file
174
+ )
175
+ )
176
+ else:
177
+ self.init_kernel(kernel, comms, use_kernel_history, connection_file)
178
+
179
+ def pre_init_kernel(self) -> None:
180
+ """Run stuff before the kernel is loaded."""
181
+ pass
182
+
183
+ def post_init_kernel(self) -> None:
184
+ """Run stuff after the kernel is loaded."""
185
+ pass
186
+
187
+ def init_kernel(
188
+ self,
189
+ kernel: Kernel | None = None,
190
+ comms: dict[str, Comm] | None = None,
191
+ use_kernel_history: bool = False,
192
+ connection_file: Path | None = None,
193
+ ) -> None:
194
+ """Set up the tab's kernel and related components."""
195
+ self.pre_init_kernel()
196
+
122
197
  self.kernel_queue: Deque[Callable] = deque()
123
198
 
124
199
  if kernel:
@@ -129,6 +204,7 @@ class KernelTab(Tab, metaclass=ABCMeta):
129
204
  kernel_tab=self,
130
205
  allow_stdin=self.allow_stdin,
131
206
  default_callbacks=self.default_callbacks,
207
+ connection_file=connection_file,
132
208
  )
133
209
  self.comms: dict[str, Comm] = comms or {} # The client-side comm states
134
210
  self.completer: Completer = KernelCompleter(self.kernel)
@@ -138,6 +214,14 @@ class KernelTab(Tab, metaclass=ABCMeta):
138
214
  )
139
215
  self.suggester: AutoSuggest = HistoryAutoSuggest(self.history)
140
216
 
217
+ self.post_init_kernel()
218
+
219
+ def close(self, cb: Callable | None = None) -> None:
220
+ """Shut down kernel when tab is closed."""
221
+ if hasattr(self, "kernel"):
222
+ self.kernel.shutdown()
223
+ super().close(cb)
224
+
141
225
  def interrupt_kernel(self) -> None:
142
226
  """Interrupt the current `Notebook`'s kernel."""
143
227
  self.kernel.interrupt()
@@ -293,7 +377,7 @@ class KernelTab(Tab, metaclass=ABCMeta):
293
377
 
294
378
  add_setting(
295
379
  name="kernel_name",
296
- flags=["--kernel-name"],
380
+ flags=["--kernel-name", "--kernel"],
297
381
  type_=str,
298
382
  help_="The name of the kernel to start by default",
299
383
  default="python3",
@@ -15,16 +15,23 @@ from euporie.core.comm.registry import open_comm
15
15
  from euporie.core.commands import get_cmd
16
16
  from euporie.core.config import add_setting
17
17
  from euporie.core.kernel import MsgCallbacks
18
+ from euporie.core.path import parse_path
18
19
  from euporie.core.tabs.base import KernelTab
19
- from euporie.core.utils import parse_path
20
20
  from euporie.core.widgets.cell import Cell, get_cell_id
21
21
 
22
+ try:
23
+ from jupytext import read as read_nb
24
+ from jupytext import write as write_nb
25
+ except ModuleNotFoundError:
26
+ from nbformat import read as read_nb
27
+ from nbformat import write as write_nb
28
+
22
29
  if TYPE_CHECKING:
30
+ from pathlib import Path
23
31
  from typing import Any, Callable
24
32
 
25
33
  from prompt_toolkit.filters import Filter
26
34
  from prompt_toolkit.layout.containers import AnyContainer
27
- from upath import UPath
28
35
 
29
36
  from euporie.core.app import BaseApp
30
37
  from euporie.core.comm.base import Comm
@@ -42,7 +49,7 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
42
49
  def __init__(
43
50
  self,
44
51
  app: BaseApp,
45
- path: UPath | None = None,
52
+ path: Path | None = None,
46
53
  kernel: Kernel | None = None,
47
54
  comms: dict[str, Comm] | None = None,
48
55
  use_kernel_history: bool = False,
@@ -73,25 +80,16 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
73
80
  "dead": self.kernel_died,
74
81
  }
75
82
  )
76
-
77
- # Load the notebook file
78
- self.path = parse_path(path)
79
- log.debug("Loading notebooks %s", self.path)
80
-
81
- self.load(json)
83
+ self.json = json or {}
84
+ self._rendered_cells: dict[str, Cell] = {}
85
+ self.multiple_cells_selected: Filter = Never()
86
+ self.path = parse_path(path) if path else None
87
+ self.loaded = False
82
88
 
83
89
  super().__init__(
84
90
  app, path, kernel=kernel, comms=comms, use_kernel_history=use_kernel_history
85
91
  )
86
92
 
87
- self._rendered_cells: dict[str, Cell] = {}
88
- self.load_widgets_from_metadata()
89
- self.dirty = False
90
- self.saving = False
91
- self.multiple_cells_selected: Filter = Never()
92
-
93
- self.container = self.load_container()
94
-
95
93
  # Tab stuff
96
94
 
97
95
  def reset(self) -> None:
@@ -104,6 +102,30 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
104
102
 
105
103
  # KernelTab stuff
106
104
 
105
+ def pre_init_kernel(self) -> None:
106
+ """Run stuff before the kernel is loaded."""
107
+ # Load notebook file
108
+ self.load()
109
+
110
+ def post_init_kernel(self) -> None:
111
+ """Load the notebook container after the kernel has been loaded."""
112
+ # Replace the tab's container
113
+ prev = self.container
114
+ self.container = self.load_container()
115
+ self.loaded = True
116
+ self.app.invalidate()
117
+
118
+ # Update the focus if the old container had focus
119
+ if (layout := self.app.layout).has_focus(prev):
120
+
121
+ async def _focus_new_container() -> None:
122
+ layout.focus(self.container)
123
+
124
+ self.app.create_background_task(_focus_new_container())
125
+
126
+ # Load widgets
127
+ self.load_widgets_from_metadata()
128
+
107
129
  @property
108
130
  def metadata(self) -> dict[str, Any]:
109
131
  """Return a dictionary to hold notebook / kernel metadata."""
@@ -118,21 +140,22 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
118
140
  if confirm := self.app.dialogs.get("confirm"):
119
141
  confirm.show(
120
142
  title="Kernel connection lost",
121
- message="The kernel appears to have died\nas it can no longer be reached.\n\n"
143
+ message="The kernel appears to have died\n"
144
+ "as it can no longer be reached.\n\n"
122
145
  "Do you want to restart the kernel?",
123
146
  cb=self.kernel.restart,
124
147
  )
125
148
 
126
149
  # Notebook stuff
127
150
 
128
- def load(self, json: dict[str, Any] | None = None) -> None:
151
+ def load(self) -> None:
129
152
  """Load the notebook file from the file-system."""
130
153
  # Open json file, or load from passed json object
131
154
  if self.path is not None and self.path.exists():
132
155
  with self.path.open() as f:
133
- self.json = nbformat.read(f, as_version=4)
156
+ self.json = read_nb(f, as_version=4)
134
157
  else:
135
- self.json = json or nbformat.v4.new_notebook()
158
+ self.json = self.json or nbformat.v4.new_notebook()
136
159
  # Ensure there is always at least one cell
137
160
  if not self.json.setdefault("cells", []):
138
161
  self.json["cells"] = [nbformat.v4.new_code_cell()]
@@ -242,7 +265,7 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
242
265
  else:
243
266
  super().close(cb)
244
267
 
245
- def save(self, path: UPath | None = None, cb: Callable | None = None) -> None:
268
+ def save(self, path: Path | None = None, cb: Callable | None = None) -> None:
246
269
  """Write the notebook's JSON to the current notebook's file.
247
270
 
248
271
  Additionally save the widget state to the notebook metadata.
@@ -267,24 +290,43 @@ class BaseNotebook(KernelTab, metaclass=ABCMeta):
267
290
  }
268
291
  }
269
292
  if self.path is None:
270
- # get_cmd("save-as").run()
271
293
  if dialog := self.app.dialogs.get("save-as"):
272
294
  dialog.show(tab=self, cb=cb)
273
295
  else:
274
296
  log.debug("Saving notebook..")
275
297
  self.saving = True
276
298
  self.app.invalidate()
299
+
300
+ # Save to a temp file, then replace the original
301
+ temp_path = self.path.parent / f".{self.path.stem}.tmp{self.path.suffix}"
302
+ log.debug("Using temporary file %s", temp_path.name)
277
303
  try:
278
- open_file = self.path.open("w")
304
+ open_file = temp_path.open("w")
279
305
  except NotImplementedError:
280
306
  get_cmd("save-as").run()
281
307
  else:
282
- with open_file as f:
283
- nbformat.write(nb=nbformat.from_dict(self.json), fp=f)
284
- self.dirty = False
285
- self.saving = False
286
- self.app.invalidate()
287
- log.debug("Notebook saved")
308
+ try:
309
+ try:
310
+ with open_file as f:
311
+ write_nb(nb=nbformat.from_dict(self.json), fp=f)
312
+ except AssertionError:
313
+ # Jupytext requires a filename if we don't give it a format
314
+ write_nb(nb=nbformat.from_dict(self.json), fp=temp_path)
315
+ except Exception:
316
+ if dialog := self.app.dialogs.get("save-as"):
317
+ dialog.show(tab=self, cb=cb)
318
+ else:
319
+ open_file.close()
320
+ try:
321
+ temp_path.rename(self.path)
322
+ except Exception:
323
+ if dialog := self.app.dialogs.get("save-as"):
324
+ dialog.show(tab=self, cb=cb)
325
+ else:
326
+ self.dirty = False
327
+ self.saving = False
328
+ self.app.invalidate()
329
+ log.debug("Notebook saved")
288
330
  # Run the callback
289
331
  if callable(cb):
290
332
  cb()
euporie/core/terminal.py CHANGED
@@ -132,24 +132,6 @@ class TerminalQuery:
132
132
  return self._value or self.default
133
133
 
134
134
 
135
- class ColorQueryMixin:
136
- """A mixin for terminal queries which check a terminal colour."""
137
-
138
- pattern: re.Pattern
139
-
140
- def verify(self, data: str) -> str | None:
141
- """Verify the response contains a colour."""
142
- if match := self.pattern.match(data):
143
- if colors := match.groupdict():
144
- r, g, b = (
145
- colors.get("r", "00"),
146
- colors.get("g", "00"),
147
- colors.get("b", "00"),
148
- )
149
- return f"#{r[:2]}{g[:2]}{b[:2]}"
150
- return None
151
-
152
-
153
135
  class Colors(TerminalQuery):
154
136
  """A terminal query to retrieve colours as hex codes."""
155
137
 
@@ -237,6 +219,7 @@ class KittyGraphicsStatus(TerminalQuery):
237
219
  pattern = re.compile(r"^\x1b_Gi=(4294967295|0);(?P<status>OK)\x1b\\\Z")
238
220
 
239
221
  def _cmd(self) -> str:
222
+ """Hide the command in case the terminal does not support this sequence."""
240
223
  return "\x1b[s" + tmuxify(self.cmd) + "\x1b[u\x1b[2K"
241
224
 
242
225
  def verify(self, data: str) -> bool:
@@ -328,30 +311,47 @@ class SgrPixelStatus(TerminalQuery):
328
311
 
329
312
  default = False
330
313
  cache = True
331
- cmd = "\x1b[?1016$p"
314
+ cmd = "\x1b[?1016h\x1b[?1016$p\x1b[?1016l" # Enable, check, disable
332
315
  pattern = re.compile(r"^\x1b\[\?1016;(?P<Pm>\d)\$\Z")
333
316
 
334
317
  def verify(self, data: str) -> bool:
335
318
  """Verify the terminal response means sixel graphics are supported."""
336
319
  if match := self.pattern.match(data):
337
320
  if values := match.groupdict():
338
- if values.get("Pm") != 0:
321
+ if values.get("Pm") in {"1", "3"}:
339
322
  return True
340
323
  return False
341
324
 
342
325
 
326
+ class CsiUStatus(TerminalQuery):
327
+ """A terminal query to check for CSI-u support."""
328
+
329
+ default = False
330
+ cache = True
331
+ cmd = "\x1b[?u"
332
+ pattern = re.compile(r"^\x1b\[\?\d+u")
333
+
334
+ def verify(self, data: str) -> bool:
335
+ """Verify the terminal responds."""
336
+ if match := self.pattern.match(data):
337
+ if match:
338
+ return True
339
+ return False
340
+
341
+
343
342
  class TerminalInfo:
344
343
  """A class to gather and hold information about the terminal."""
345
344
 
346
345
  input: Input
347
346
  output: Output
348
347
 
348
+ _queries: dict[type[TerminalQuery], TerminalQuery] = {}
349
+
349
350
  def __init__(self, input_: Input, output: Output, config: Config) -> None:
350
351
  """Instantiate the terminal information class."""
351
352
  self.input = input_
352
353
  self.output = output
353
354
  self.config = config
354
- self._queries: list[TerminalQuery] = []
355
355
 
356
356
  self.colors = self.register(Colors)
357
357
  self.pixel_dimensions = self.register(PixelDimensions)
@@ -360,36 +360,44 @@ class TerminalInfo:
360
360
  self.iterm_graphics_status = self.register(ItermGraphicsStatus)
361
361
  self.depth_of_color = self.register(DepthOfColor)
362
362
  self.sgr_pixel_status = self.register(SgrPixelStatus)
363
+ self.csiu_status = self.register(CsiUStatus)
363
364
 
364
365
  def register(self, query: type[TerminalQuery]) -> TerminalQuery:
365
366
  """Instantiate and registers a query's response with the input parser."""
366
367
  # Create an instance of this query
367
- query_inst = query(self.output, config=self.config)
368
- self._queries.append(query_inst)
369
-
370
- # If the query expects a response from the terminal, we need to add a
371
- # key-binding for it and register it with the input parser
372
- if query.pattern:
373
- name = re.sub(r"(?<!^)(?=[A-Z])", "-", query.__name__).lower()
374
- title = name.replace("-", " ")
375
-
376
- # Add a "key" definition for this query
377
- key_name = f"{query.__name__}Response"
378
- key_code = f"<{name}-response>"
379
- # Do not register the same key multiple times
380
- if not hasattr(Keys, key_name):
381
- extend_enum(Keys, key_name, key_code)
382
- key = getattr(Keys, key_name)
383
-
384
- # Register this key with the parser if supported
385
- if parser := getattr(self.input, "vt100_parser", None):
386
- # Register the key
387
- parser.queries[key] = query.pattern
388
-
389
- # Add a command for the query's key-binding
390
- add_cmd(name=name, title=title, hidden=True)(query_inst._handle_response)
391
- # Add key-binding
392
- register_bindings({"euporie.core.app.BaseApp": {name: key}})
368
+ query_inst: TerminalQuery | None
369
+
370
+ if (query_inst := self._queries.get(query)) is None:
371
+ query_inst = query(self.output, config=self.config)
372
+ self._queries[query] = query_inst
373
+
374
+ # If the query expects a response from the terminal, we need to add a
375
+ # key-binding for it and register it with the input parser
376
+ if query.pattern:
377
+ name = re.sub(r"(?<!^)(?=[A-Z])", "-", query.__name__).lower()
378
+ title = name.replace("-", " ")
379
+
380
+ # Add a "key" definition for this query
381
+ key_name = f"{query.__name__}Response"
382
+ key_code = f"<{name}-response>"
383
+ # Do not register the same key multiple times
384
+ if not hasattr(Keys, key_name):
385
+ extend_enum(Keys, key_name, key_code)
386
+ key = getattr(Keys, key_name)
387
+
388
+ # Register this key with the parser if supported
389
+ if (parser := getattr(self.input, "vt100_parser", None)) and hasattr(
390
+ parser, "queries"
391
+ ):
392
+ # Register the key
393
+ parser.queries[key] = query.pattern
394
+
395
+ # Add a command for the query's key-binding
396
+ add_cmd(name=name, title=title, hidden=True)(
397
+ query_inst._handle_response
398
+ )
399
+ # Add key-binding
400
+ register_bindings({"euporie.core.app.BaseApp": {name: key}})
393
401
 
394
402
  return query_inst
395
403
 
@@ -397,7 +405,7 @@ class TerminalInfo:
397
405
  """Send the command for all queries."""
398
406
  # Ensure line wrapping is off before sending queries
399
407
  self.output.disable_autowrap()
400
- for query in self._queries:
408
+ for query in self._queries.values():
401
409
  query.send()
402
410
 
403
411
  def _tiocgwnsz(self) -> tuple[int, int, int, int]:
euporie/core/utils.py CHANGED
@@ -6,10 +6,8 @@ from itertools import chain
6
6
  from typing import TYPE_CHECKING, Sequence, TypeVar, overload
7
7
 
8
8
  from prompt_toolkit.mouse_events import MouseButton, MouseEventType
9
- from upath import UPath
10
9
 
11
10
  if TYPE_CHECKING:
12
- from os import PathLike
13
11
  from typing import Callable, Iterable
14
12
 
15
13
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
@@ -48,20 +46,18 @@ class ChainedList(Sequence[T]):
48
46
  return len(self.data)
49
47
 
50
48
 
51
- def parse_path(path: str | PathLike | None) -> UPath | None:
52
- """Pare and resolve a path."""
53
- if path is None:
54
- return None
55
- upath = UPath(path)
56
- try:
57
- upath = upath.expanduser()
58
- except NotImplementedError:
59
- pass
60
- try:
61
- upath = upath.resolve()
62
- except (AttributeError, NotImplementedError):
63
- pass
64
- return upath
49
+ def dict_merge(target_dict: dict, input_dict: dict) -> None:
50
+ """Merge the second dictionary onto the first."""
51
+ for k in input_dict:
52
+ if k in target_dict:
53
+ if isinstance(target_dict[k], dict) and isinstance(input_dict[k], dict):
54
+ dict_merge(target_dict[k], input_dict[k])
55
+ elif isinstance(target_dict[k], list) and isinstance(input_dict[k], list):
56
+ target_dict[k] = [*target_dict[k], *input_dict[k]]
57
+ else:
58
+ target_dict[k] = input_dict[k]
59
+ else:
60
+ target_dict[k] = input_dict[k]
65
61
 
66
62
 
67
63
  def on_click(func: Callable) -> MouseHandler:
@@ -151,6 +151,10 @@ class Cell:
151
151
 
152
152
  # Now we generate the main container used to represent a kernel_tab cell
153
153
 
154
+ source_hidden = Condition(
155
+ lambda: self.json["metadata"].get("jupyter", {}).get("source_hidden", False)
156
+ )
157
+
154
158
  self.input_box = KernelInput(
155
159
  kernel_tab=self.kernel_tab,
156
160
  text=self.input,
@@ -170,6 +174,7 @@ class Cell:
170
174
  )
171
175
  ),
172
176
  accept_handler=lambda buffer: self.run_or_render() or True,
177
+ focusable=show_input & ~source_hidden,
173
178
  )
174
179
  self.input_box.buffer.name = self.cell_type
175
180
 
@@ -228,10 +233,6 @@ class Cell:
228
233
  height=1,
229
234
  )
230
235
 
231
- source_hidden = Condition(
232
- lambda: self.json["metadata"].get("jupyter", {}).get("source_hidden", False)
233
- )
234
-
235
236
  input_row = ConditionalContainer(
236
237
  VSplit(
237
238
  [
@@ -588,7 +589,7 @@ class Cell:
588
589
  if self.cell_type == "markdown":
589
590
  return "markdown"
590
591
  elif self.cell_type == "code":
591
- lang_info = self.kernel_tab.json.metadata.get("language_info", {})
592
+ lang_info = self.kernel_tab.metadata.get("language_info", {})
592
593
  return lang_info.get("name", lang_info.get("pygments_lexer", "python"))
593
594
  else:
594
595
  return "raw"
@@ -11,7 +11,7 @@ from prompt_toolkit.cache import SimpleCache
11
11
  from prompt_toolkit.layout.containers import DynamicContainer, HSplit, to_container
12
12
  from prompt_toolkit.widgets.base import Box
13
13
 
14
- from euporie.core.convert.base import BASE64_FORMATS, MIME_FORMATS, find_route
14
+ from euporie.core.convert.core import BASE64_FORMATS, MIME_FORMATS, find_route
15
15
  from euporie.core.current import get_app
16
16
  from euporie.core.widgets.display import Display
17
17
  from euporie.core.widgets.tree import JsonView
@@ -39,7 +39,7 @@ log = logging.getLogger(__name__)
39
39
 
40
40
 
41
41
  class CellOutputElement(metaclass=ABCMeta):
42
- """Bae class for the various types of cell outputs (display data or widgets)."""
42
+ """Base class for the various types of cell outputs (display data or widgets)."""
43
43
 
44
44
  data: Any
45
45