euporie 2.8.2__tar.gz → 2.8.4__tar.gz

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 (145) hide show
  1. {euporie-2.8.2 → euporie-2.8.4}/PKG-INFO +5 -5
  2. {euporie-2.8.2 → euporie-2.8.4}/euporie/console/tabs/console.py +227 -104
  3. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/__init__.py +1 -1
  4. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/__main__.py +1 -1
  5. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/app.py +31 -18
  6. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/clipboard.py +1 -1
  7. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/comm/ipywidgets.py +5 -5
  8. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/commands.py +1 -1
  9. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/config.py +4 -4
  10. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/datum.py +4 -1
  11. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/registry.py +7 -2
  12. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/filters.py +3 -1
  13. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/ft/html.py +2 -4
  14. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/graphics.py +6 -6
  15. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/kernel.py +56 -32
  16. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/bindings/__init__.py +2 -1
  17. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/bindings/mouse.py +24 -22
  18. euporie-2.8.4/euporie/core/key_binding/bindings/vi.py +46 -0
  19. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/cache.py +33 -23
  20. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/containers.py +235 -73
  21. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/decor.py +3 -3
  22. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/print.py +14 -2
  23. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/scroll.py +15 -21
  24. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/margins.py +59 -30
  25. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/style.py +7 -5
  26. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/tabs/base.py +32 -0
  27. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/tabs/notebook.py +6 -3
  28. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/terminal.py +12 -17
  29. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/utils.py +2 -4
  30. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/cell.py +64 -109
  31. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/dialog.py +25 -20
  32. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/file_browser.py +3 -3
  33. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/forms.py +8 -7
  34. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/inputs.py +21 -9
  35. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/layout.py +5 -5
  36. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/status.py +3 -3
  37. {euporie-2.8.2 → euporie-2.8.4}/euporie/hub/app.py +7 -3
  38. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/app.py +68 -47
  39. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/tabs/log.py +1 -1
  40. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/tabs/notebook.py +5 -3
  41. {euporie-2.8.2 → euporie-2.8.4}/euporie/preview/app.py +3 -0
  42. {euporie-2.8.2 → euporie-2.8.4}/euporie/preview/tabs/notebook.py +9 -14
  43. {euporie-2.8.2 → euporie-2.8.4}/euporie/web/tabs/web.py +0 -1
  44. {euporie-2.8.2 → euporie-2.8.4}/pyproject.toml +18 -10
  45. {euporie-2.8.2 → euporie-2.8.4}/.gitignore +0 -0
  46. {euporie-2.8.2 → euporie-2.8.4}/LICENSE +0 -0
  47. {euporie-2.8.2 → euporie-2.8.4}/README.rst +0 -0
  48. {euporie-2.8.2 → euporie-2.8.4}/euporie/console/__init__.py +0 -0
  49. {euporie-2.8.2 → euporie-2.8.4}/euporie/console/__main__.py +0 -0
  50. {euporie-2.8.2 → euporie-2.8.4}/euporie/console/app.py +0 -0
  51. {euporie-2.8.2 → euporie-2.8.4}/euporie/console/py.typed +0 -0
  52. {euporie-2.8.2 → euporie-2.8.4}/euporie/console/tabs/__init__.py +0 -0
  53. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/border.py +0 -0
  54. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/comm/__init__.py +0 -0
  55. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/comm/base.py +0 -0
  56. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/comm/registry.py +0 -0
  57. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/completion.py +0 -0
  58. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/__init__.py +0 -0
  59. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/__init__.py +0 -0
  60. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/ansi.py +0 -0
  61. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/base64.py +0 -0
  62. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/common.py +0 -0
  63. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/ft.py +0 -0
  64. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/html.py +0 -0
  65. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/jpeg.py +0 -0
  66. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/markdown.py +0 -0
  67. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/pdf.py +0 -0
  68. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/pil.py +0 -0
  69. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/png.py +0 -0
  70. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/rich.py +0 -0
  71. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/sixel.py +0 -0
  72. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/formats/svg.py +0 -0
  73. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/mime.py +0 -0
  74. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/convert/utils.py +0 -0
  75. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/current.py +0 -0
  76. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/data_structures.py +0 -0
  77. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/diagnostics.py +0 -0
  78. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/format.py +0 -0
  79. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/ft/__init__.py +0 -0
  80. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/ft/ansi.py +0 -0
  81. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/ft/table.py +0 -0
  82. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/ft/utils.py +0 -0
  83. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/history.py +0 -0
  84. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/inspection.py +0 -0
  85. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/io.py +0 -0
  86. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/__init__.py +0 -0
  87. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/bindings/basic.py +0 -0
  88. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/bindings/completion.py +0 -0
  89. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/bindings/micro.py +0 -0
  90. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/bindings/page_navigation.py +0 -0
  91. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/key_processor.py +0 -0
  92. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/micro_state.py +0 -0
  93. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/registry.py +0 -0
  94. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/utils.py +0 -0
  95. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/key_binding/vi_state.py +0 -0
  96. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/keys.py +0 -0
  97. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/launch.py +0 -0
  98. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/__init__.py +0 -0
  99. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/controls.py +0 -0
  100. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/mouse.py +0 -0
  101. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/layout/screen.py +0 -0
  102. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/lexers.py +0 -0
  103. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/log.py +0 -0
  104. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/lsp.py +0 -0
  105. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/path.py +0 -0
  106. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/processors.py +0 -0
  107. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/py.typed +0 -0
  108. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/pygments.py +0 -0
  109. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/reference.py +0 -0
  110. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/renderer.py +0 -0
  111. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/suggest.py +0 -0
  112. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/tabs/__init__.py +0 -0
  113. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/validation.py +0 -0
  114. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/__init__.py +0 -0
  115. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/cell_outputs.py +0 -0
  116. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/decor.py +0 -0
  117. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/display.py +0 -0
  118. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/formatted_text_area.py +0 -0
  119. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/menu.py +0 -0
  120. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/pager.py +0 -0
  121. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/palette.py +0 -0
  122. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/search.py +0 -0
  123. {euporie-2.8.2 → euporie-2.8.4}/euporie/core/widgets/tree.py +0 -0
  124. {euporie-2.8.2 → euporie-2.8.4}/euporie/data/desktop/euporie-console.desktop +0 -0
  125. {euporie-2.8.2 → euporie-2.8.4}/euporie/data/desktop/euporie-notebook.desktop +0 -0
  126. {euporie-2.8.2 → euporie-2.8.4}/euporie/hub/__init__.py +0 -0
  127. {euporie-2.8.2 → euporie-2.8.4}/euporie/hub/__main__.py +0 -0
  128. {euporie-2.8.2 → euporie-2.8.4}/euporie/hub/py.typed +0 -0
  129. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/__init__.py +0 -0
  130. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/__main__.py +0 -0
  131. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/current.py +0 -0
  132. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/enums.py +0 -0
  133. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/filters.py +0 -0
  134. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/py.typed +0 -0
  135. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/tabs/__init__.py +0 -0
  136. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/tabs/display.py +0 -0
  137. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/tabs/edit.py +0 -0
  138. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/tabs/json.py +0 -0
  139. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/widgets/__init__.py +0 -0
  140. {euporie-2.8.2 → euporie-2.8.4}/euporie/notebook/widgets/side_bar.py +0 -0
  141. {euporie-2.8.2 → euporie-2.8.4}/euporie/preview/__init__.py +0 -0
  142. {euporie-2.8.2 → euporie-2.8.4}/euporie/preview/__main__.py +0 -0
  143. {euporie-2.8.2 → euporie-2.8.4}/euporie/preview/py.typed +0 -0
  144. {euporie-2.8.2 → euporie-2.8.4}/euporie/preview/tabs/__init__.py +0 -0
  145. {euporie-2.8.2 → euporie-2.8.4}/euporie/web/widgets/webview.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: euporie
3
- Version: 2.8.2
3
+ Version: 2.8.4
4
4
  Summary: Euporie is a suite of terminal applications for interacting with Jupyter kernels
5
5
  Project-URL: Documentation, https://euporie.readthedocs.io/en/latest
6
6
  Project-URL: Issues, https://github.com/joouha/euporie/issues
@@ -28,16 +28,16 @@ Requires-Dist: fsspec[http]>=2022.12.0
28
28
  Requires-Dist: imagesize~=1.3
29
29
  Requires-Dist: jupyter-client>=7.1
30
30
  Requires-Dist: jupytext>=1.14.0
31
- Requires-Dist: linkify-it-py~=1.0
32
- Requires-Dist: markdown-it-py~=2.1.0
33
- Requires-Dist: mdit-py-plugins~=0.3.0
31
+ Requires-Dist: linkify-it-py~=2.0
32
+ Requires-Dist: markdown-it-py~=3.0
33
+ Requires-Dist: mdit-py-plugins~=0.4.2
34
34
  Requires-Dist: nbformat~=5.0
35
35
  Requires-Dist: pillow>=9.0
36
36
  Requires-Dist: platformdirs~=3.5
37
37
  Requires-Dist: prompt-toolkit~=3.0.36
38
38
  Requires-Dist: pygments~=2.11
39
39
  Requires-Dist: pyperclip~=1.8
40
- Requires-Dist: sixelcrop~=0.1.6
40
+ Requires-Dist: sixelcrop~=0.1.8
41
41
  Requires-Dist: timg~=1.1.6
42
42
  Requires-Dist: typing-extensions~=4.5
43
43
  Requires-Dist: universal-pathlib~=0.2.1
@@ -62,7 +62,7 @@ if TYPE_CHECKING:
62
62
  from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples
63
63
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
64
64
  from prompt_toolkit.key_binding.key_processor import KeyPressEvent
65
- from prompt_toolkit.layout.containers import Float
65
+ from prompt_toolkit.layout.containers import Container, Float
66
66
 
67
67
  from euporie.core.app import BaseApp
68
68
  from euporie.core.lsp import LspClient
@@ -103,6 +103,7 @@ class Console(KernelTab):
103
103
  prompt, password
104
104
  ),
105
105
  set_execution_count=partial(setattr, self, "execution_count"),
106
+ add_input=self.new_input,
106
107
  add_output=self.new_output,
107
108
  clear_output=self.clear_output,
108
109
  # set_metadata=self.misc_callback,
@@ -132,6 +133,7 @@ class Console(KernelTab):
132
133
 
133
134
  self.json = nbformat.v4.new_notebook()
134
135
  self.json["metadata"] = self._metadata
136
+ self.render_queue: list[dict[str, Any]] = []
135
137
 
136
138
  self.container = self.load_container()
137
139
 
@@ -192,13 +194,12 @@ class Console(KernelTab):
192
194
  completeness_status = self.kernel.is_complete(code, wait=True).get(
193
195
  "status", "unknown"
194
196
  )
195
- if (
197
+ return not (
196
198
  not code.strip()
197
199
  or completeness_status == "incomplete"
198
- or (completeness_status == "unknown" and code[-2:] != "\n\n")
199
- ):
200
- return False
201
- return True
200
+ or completeness_status == "unknown"
201
+ and code[-2:] != "\n\n"
202
+ )
202
203
 
203
204
  def run(self, buffer: Buffer | None = None) -> None:
204
205
  """Run the code in the input box."""
@@ -208,78 +209,125 @@ class Console(KernelTab):
208
209
  # Auto-reformat code
209
210
  if app.config.autoformat:
210
211
  self.input_box.reformat()
211
- # Get the code to run
212
+ # # Get the code to run
212
213
  text = buffer.text
213
- # Remove any selections from input
214
+ # # Remove any selections from input
214
215
  buffer.selection_state = None
215
216
  # Disable existing output
216
- self.live_output.style = "class:disabled"
217
+ # self.live_output.style = "class:disabled"
217
218
  # Reset the diagnostics
218
219
  self.reports.clear()
220
+ # Increment this for display purposes until we get the response from the kernel
221
+ self.execution_count += 1
219
222
  # Move cursor to the start of the input
220
223
  buffer.cursor_position = 0
221
- # Re-render the app and move to below the current output
222
- original_layout = app.layout
223
- app.layout = self.input_layout
224
- app.draw()
225
- app.layout = original_layout
226
- # Prevent displayed graphics on terminal being cleaned up (bit of a hack)
227
- app.graphics.clear()
224
+ # Render input
225
+ self.new_input({"code": text}, own=True, force=True)
228
226
  # Run the previous entry
229
227
  if self.kernel.status == "starting":
230
228
  self.kernel_queue.append(partial(self.kernel.run, text, wait=False))
231
229
  else:
232
230
  self.kernel.run(text, wait=False)
233
- # Increment this for display purposes until we get the response from the kernel
234
- self.execution_count += 1
235
231
  # Reset the input & output
236
232
  buffer.reset(append_to_history=True)
237
- # Remove any live outputs and disable mouse support
238
- self.live_output.reset()
233
+ self.on_advance()
234
+
235
+ def new_input(
236
+ self, input_json: dict[str, Any], own: bool, force: bool = False
237
+ ) -> None:
238
+ """Create new cell inputs in response to kernel ``execute_input`` messages."""
239
+ # Skip our own inputs when relayed from the kernel
240
+ # We render them immediately when they are run to avoid delays in the UI
241
+ if own and not force:
242
+ return
243
+
244
+ app = self.app
245
+ if not own and not app.config.show_remote_inputs:
246
+ return
247
+
248
+ self.flush_live_output()
249
+
239
250
  # Record the input as a cell in the json
240
- self.json["cells"].append(
241
- nbformat.v4.new_code_cell(source=text, execution_count=self.execution_count)
251
+ cell_json = nbformat.v4.new_code_cell(
252
+ source=input_json["code"],
253
+ execution_count=input_json.get("execution_count", self.execution_count),
242
254
  )
255
+ self.render_queue.append(cell_json)
256
+ self.json["cells"].append(cell_json)
243
257
  if (
244
258
  app.config.max_stored_outputs
245
259
  and len(self.json["cells"]) > app.config.max_stored_outputs
246
260
  ):
247
261
  del self.json["cells"][0]
248
- self.on_advance()
249
262
 
250
- def new_output(self, output_json: dict[str, Any]) -> None:
263
+ # Invalidate the app so the new input gets printed
264
+ app.invalidate()
265
+
266
+ def new_output(self, output_json: dict[str, Any], own: bool) -> None:
251
267
  """Print the previous output and replace it with the new one."""
268
+ if not own and not self.app.config.show_remote_outputs:
269
+ return
270
+
252
271
  # Clear the output if we were previously asked to
253
272
  if self.clear_outputs_on_output:
254
273
  self.clear_outputs_on_output = False
255
274
  # Clear the screen
256
275
  get_cmd("clear-screen").run()
257
276
 
258
- # Add to record
259
- if self.json["cells"]:
260
- self.json["cells"][-1]["outputs"].append(output_json)
277
+ # If there is no cell in the virtual notebook, add an empty cell
278
+ if not self.json["cells"]:
279
+ self.json["cells"].append(
280
+ nbformat.v4.new_code_cell(execution_count=self.execution_count)
281
+ )
282
+ cell = self.json.cells[-1]
283
+
284
+ # If there is no code cell in the render queue, add a dummy cell with no input
285
+ if cell not in self.render_queue:
286
+ # Add to end of previous cell in virtual notebook
287
+ # cell["outputs"].append(output_json)
288
+ # Create virtual cell
289
+ cell = nbformat.v4.new_code_cell(
290
+ id=cell.id, execution_count=self.execution_count
291
+ )
292
+ self.render_queue.append(cell)
261
293
 
262
- # Set output
294
+ # Add widgets to the live output
263
295
  if "application/vnd.jupyter.widget-view+json" in output_json.get("data", {}):
264
- # Use a live output to display widgets
265
296
  self.live_output.add_output(output_json)
266
297
  else:
267
- # Queue the output json
268
- self.output.add_output(output_json)
269
- # Invalidate the app so the output get printed
270
- self.app.invalidate()
298
+ cell["outputs"].append(output_json)
299
+
300
+ # Invalidate the app so the output get printed
301
+ self.app.invalidate()
302
+
303
+ def flush_live_output(self) -> None:
304
+ """Flush any active live outputs to the terminal."""
305
+ if self.live_output.json:
306
+ self.render_queue.append(
307
+ nbformat.v4.new_code_cell(
308
+ execution_count=None,
309
+ outputs=self.live_output.json[:],
310
+ ),
311
+ )
312
+ self.live_output.reset()
271
313
 
272
314
  def render_outputs(self, app: Application[Any]) -> None:
273
315
  """Request that any unrendered outputs be rendered."""
274
- if self.output.json:
316
+ # Check for unrendered cells or new output
317
+
318
+ # Clear the render queue so it does not get rendered again
319
+ render_queue = list(self.render_queue)
320
+ self.render_queue.clear()
321
+
322
+ if render_queue:
323
+ # Render the echo layout with any new cells / outputs
275
324
  app = self.app
276
325
  original_layout = self.app.layout
277
- app.layout = self.output_layout
278
- app.renderer.render(self.app, self.output_layout, is_done=True)
279
- app.renderer.request_absolute_cursor_position()
326
+ new_layout = self.echo_layout(render_queue)
327
+ app.layout = new_layout
328
+ app.renderer.render(app, new_layout, is_done=True)
280
329
  app.layout = original_layout
281
- # Remove the outputs so they do not get rendered again
282
- self.output.reset()
330
+ app.renderer.request_absolute_cursor_position()
283
331
 
284
332
  def reset(self) -> None:
285
333
  """Reset the state of the tab."""
@@ -293,10 +341,16 @@ class Console(KernelTab):
293
341
  self.app.invalidate()
294
342
 
295
343
  def prompt(
296
- self, text: str, offset: int = 0, show_busy: bool = False
344
+ self,
345
+ text: str,
346
+ count: int | None = None,
347
+ offset: int = 0,
348
+ show_busy: bool = False,
297
349
  ) -> StyleAndTextTuples:
298
350
  """Determine what should be displayed in the prompt of the cell."""
299
- prompt = str(self.execution_count + offset)
351
+ if count is None:
352
+ count = self.execution_count
353
+ prompt = str(count + offset)
300
354
  if show_busy and self.kernel.status in ("busy", "queued"):
301
355
  prompt = "*".center(len(prompt))
302
356
  ft: StyleAndTextTuples = [
@@ -317,59 +371,140 @@ class Console(KernelTab):
317
371
  """Return the file extension for scripts in the notebook's language."""
318
372
  return self.lang_info.get("file_extension", ".py")
319
373
 
320
- def load_container(self) -> HSplit:
321
- """Build the main application layout."""
322
- # Output area
323
-
324
- self.output = CellOutputArea([], parent=self)
325
-
326
- @Condition
327
- def first_output() -> bool:
328
- """Check if the current outputs contain the first output."""
329
- if self.output.json:
330
- for output in self.json["cells"][-1].get("outputs", []):
331
- if output in self.live_output.json:
332
- continue
333
- return output == self.output.json[0]
334
- return False
335
-
336
- output_prompt = Window(
337
- FormattedTextControl(partial(self.prompt, "Out", show_busy=False)),
338
- dont_extend_width=True,
339
- style="class:cell,output,prompt",
340
- height=1,
341
- )
342
- output_margin = Window(
343
- char=" ", width=lambda: len(str(self.execution_count)) + 7
344
- )
374
+ # Echo area
375
+
376
+ def echo_layout(self, render_queue: list) -> Layout:
377
+ """Generate a layout for displaying executed cells."""
378
+ children: list[Container] = []
379
+ height_known = renderer_height_is_known()
380
+ rows_above_layout = self.app.renderer.rows_above_layout if height_known else 1
381
+ json_cells = self.json.cells
382
+ for i, cell in enumerate(render_queue):
383
+ if cell.source:
384
+ # Spacing between cells
385
+ if ((json_cells and cell.id != json_cells[0].id) or i > 0) and (
386
+ (height_known and rows_above_layout > 0) or not height_known
387
+ ):
388
+ children.append(
389
+ Window(
390
+ height=1,
391
+ dont_extend_height=True,
392
+ )
393
+ )
345
394
 
346
- output_area = PrintingContainer(
347
- [
348
- ConditionalContainer(
349
- Window(height=1, dont_extend_height=True), filter=first_output
350
- ),
351
- VSplit(
352
- [
353
- ConditionalContainer(output_prompt, filter=first_output),
354
- ConditionalContainer(output_margin, filter=~first_output),
355
- self.output,
356
- ]
357
- ),
358
- ],
359
- )
360
- # We need to ensure the output layout also has the application's floats so that
361
- # graphics are displayed
362
- self.output_layout = Layout(
395
+ # Cell input
396
+ children.append(
397
+ VSplit(
398
+ [
399
+ Window(
400
+ FormattedTextControl(
401
+ partial(
402
+ self.prompt,
403
+ "In ",
404
+ count=cell.execution_count,
405
+ )
406
+ ),
407
+ dont_extend_width=True,
408
+ style="class:cell,input,prompt",
409
+ height=1,
410
+ ),
411
+ KernelInput(
412
+ text=cell.source,
413
+ kernel_tab=self,
414
+ language=lambda: self.language,
415
+ read_only=True,
416
+ ),
417
+ ],
418
+ ),
419
+ )
420
+
421
+ # Outputs
422
+ if outputs := cell.outputs:
423
+ children.append(
424
+ Window(
425
+ height=1,
426
+ dont_extend_height=True,
427
+ )
428
+ )
429
+
430
+ def _flush(
431
+ buffer: list[dict[str, Any]], prompt: AnyFormattedText
432
+ ) -> None:
433
+ if buffer:
434
+ children.append(
435
+ VSplit(
436
+ [
437
+ Window(
438
+ FormattedTextControl(prompt),
439
+ dont_extend_width=True,
440
+ dont_extend_height=True,
441
+ style="class:cell,output,prompt",
442
+ height=1,
443
+ ),
444
+ CellOutputArea(
445
+ buffer, parent=self, style="class:disabled"
446
+ ),
447
+ ]
448
+ ),
449
+ )
450
+ buffer.clear()
451
+
452
+ buffer: list[dict[str, Any]] = []
453
+ ec = cell.execution_count
454
+ prompt: AnyFormattedText = ""
455
+ next_prompt: AnyFormattedText
456
+ for output in outputs:
457
+ if (next_ec := output.get("execution_count")) is None:
458
+ next_prompt = " " * (len(str(ec)) + 7)
459
+ else:
460
+ next_prompt = self.prompt("Out", count=next_ec, show_busy=False)
461
+ ec = next_ec
462
+ if next_prompt != prompt:
463
+ _flush(buffer, prompt)
464
+ prompt = next_prompt
465
+ buffer.append(output)
466
+ _flush(buffer, prompt)
467
+
468
+ return Layout(
363
469
  FloatContainer(
364
- output_area,
470
+ PrintingContainer(children),
365
471
  floats=cast("list[Float]", self.app.graphics),
366
472
  )
367
473
  )
368
474
 
475
+ def load_container(self) -> HSplit:
476
+ """Build the main application layout."""
369
477
  # Live output area
370
478
 
371
479
  self.live_output = CellOutputArea([], parent=self)
372
480
 
481
+ live_output_row = ConditionalContainer(
482
+ HSplit(
483
+ [
484
+ Window(height=1, dont_extend_height=True),
485
+ VSplit(
486
+ [
487
+ Window(
488
+ FormattedTextControl(
489
+ lambda: self.prompt(
490
+ "Out",
491
+ count=self.live_output.json[0][
492
+ "execution_count"
493
+ ],
494
+ )
495
+ ),
496
+ dont_extend_width=True,
497
+ style="class:cell,output,prompt",
498
+ height=1,
499
+ ),
500
+ self.live_output,
501
+ ]
502
+ ),
503
+ ]
504
+ ),
505
+ filter=Condition(lambda: bool(self.live_output.json)),
506
+ )
507
+
373
508
  # Input area
374
509
 
375
510
  input_kb = KeyBindings()
@@ -425,13 +560,6 @@ class Console(KernelTab):
425
560
  """Force new line on Shift-Enter."""
426
561
  event.current_buffer.newline(copy_margin=not in_paste_mode())
427
562
 
428
- input_prompt = Window(
429
- FormattedTextControl(partial(self.prompt, "In ", offset=1)),
430
- dont_extend_width=True,
431
- style="class:cell,input,prompt",
432
- height=1,
433
- )
434
-
435
563
  def _handler(buffer: Buffer) -> bool:
436
564
  self.run(buffer)
437
565
  return True
@@ -460,7 +588,7 @@ class Console(KernelTab):
460
588
  # Spacing
461
589
  ConditionalContainer(
462
590
  Window(height=1, dont_extend_height=True),
463
- filter=Condition(lambda: self.execution_count > 0)
591
+ filter=Condition(lambda: len(self.json["cells"]) > 0)
464
592
  & (
465
593
  (
466
594
  renderer_height_is_known
@@ -473,7 +601,12 @@ class Console(KernelTab):
473
601
  ConditionalContainer(
474
602
  VSplit(
475
603
  [
476
- input_prompt,
604
+ Window(
605
+ FormattedTextControl(partial(self.prompt, "In ", offset=1)),
606
+ dont_extend_width=True,
607
+ style="class:cell,input,prompt",
608
+ height=1,
609
+ ),
477
610
  self.input_box,
478
611
  ],
479
612
  ),
@@ -481,19 +614,9 @@ class Console(KernelTab):
481
614
  ),
482
615
  ]
483
616
 
484
- self.input_layout = Layout(PrintingContainer(input_row))
485
-
486
617
  return HSplit(
487
618
  [
488
- ConditionalContainer(
489
- HSplit(
490
- [
491
- Window(height=1, dont_extend_height=True),
492
- VSplit([output_margin, self.live_output]),
493
- ]
494
- ),
495
- filter=Condition(lambda: bool(self.live_output.json)),
496
- ),
619
+ live_output_row,
497
620
  # StdIn
498
621
  self.stdin_box,
499
622
  ConditionalContainer(
@@ -1,7 +1,7 @@
1
1
  """This package defines the euporie application and its components."""
2
2
 
3
3
  __app_name__ = "euporie"
4
- __version__ = "2.8.2"
4
+ __version__ = "2.8.4"
5
5
  __logo__ = "⚈"
6
6
  __strapline__ = "Jupyter in the terminal"
7
7
  __author__ = "Josiah Outram Halstead"
@@ -14,7 +14,7 @@ def main(name: "str" = "launch") -> "None":
14
14
  # Monkey-patch prompt_toolkit
15
15
  from euporie.core.layout import containers # noqa: F401
16
16
 
17
- eps = entry_points() # group="euporie.apps")
17
+ eps = entry_points()
18
18
  if isinstance(eps, dict):
19
19
  points = eps.get("euporie.apps")
20
20
  else:
@@ -30,10 +30,7 @@ from prompt_toolkit.key_binding.bindings.emacs import (
30
30
  from prompt_toolkit.key_binding.bindings.mouse import (
31
31
  load_mouse_bindings as load_ptk_mouse_bindings,
32
32
  )
33
- from prompt_toolkit.key_binding.bindings.vi import (
34
- load_vi_bindings,
35
- load_vi_search_bindings,
36
- )
33
+ from prompt_toolkit.key_binding.bindings.vi import load_vi_search_bindings
37
34
  from prompt_toolkit.key_binding.key_bindings import (
38
35
  ConditionalKeyBindings,
39
36
  merge_key_bindings,
@@ -54,6 +51,7 @@ from prompt_toolkit.styles import (
54
51
  merge_styles,
55
52
  style_from_pygments_cls,
56
53
  )
54
+ from prompt_toolkit.utils import Event
57
55
  from pygments.styles import STYLE_MAP as pygments_styles
58
56
  from pygments.styles import get_style_by_name
59
57
  from upath import UPath
@@ -241,6 +239,7 @@ class BaseApp(Application):
241
239
  )
242
240
  # Contains the opened tab containers
243
241
  self.tabs: list[Tab] = []
242
+ self.on_tabs_change = Event(self)
244
243
  # Holds the search bar to pass to cell inputs
245
244
  self.search_bar: SearchBar | None = None
246
245
  # Holds the index of the current tab
@@ -367,12 +366,12 @@ class BaseApp(Application):
367
366
  xcursor=True,
368
367
  ycursor=True,
369
368
  )
370
- # Open any files we need to
371
- self.open_files()
372
369
  # Load the layout
373
370
  # We delay this until we have terminal responses to allow terminal graphics
374
371
  # support to be detected first
375
372
  self.layout = Layout(self.load_container(), self.focused_element)
373
+ # Open any files we need to
374
+ self.open_files()
376
375
 
377
376
  async def run_async(
378
377
  self,
@@ -462,6 +461,7 @@ class BaseApp(Application):
462
461
  from euporie.core.key_binding.bindings.basic import load_basic_bindings
463
462
  from euporie.core.key_binding.bindings.micro import load_micro_bindings
464
463
  from euporie.core.key_binding.bindings.mouse import load_mouse_bindings
464
+ from euporie.core.key_binding.bindings.vi import load_vi_bindings
465
465
 
466
466
  self._default_bindings = merge_key_bindings(
467
467
  [
@@ -648,7 +648,7 @@ class BaseApp(Application):
648
648
  log.error("Unable to display file %s", path)
649
649
  else:
650
650
  tab = tab_class(self, ppath)
651
- self.tabs.append(tab)
651
+ self.add_tab(tab)
652
652
  # Ensure the opened tab is focused at app start
653
653
  self.focused_element = tab
654
654
  # Ensure the newly opened tab is selected
@@ -688,7 +688,7 @@ class BaseApp(Application):
688
688
  try:
689
689
  self.layout.focus(container)
690
690
  except ValueError:
691
- self.to_focus = container
691
+ log.exception("Cannot focus tab")
692
692
 
693
693
  def focus_tab(self, tab: Tab) -> None:
694
694
  """Make a tab visible and focuses it."""
@@ -704,20 +704,22 @@ class BaseApp(Application):
704
704
  # Remove tab
705
705
  if tab in self.tabs:
706
706
  self.tabs.remove(tab)
707
- # Update body container to reflect new tab list
708
- # assert isinstance(self.body_container.body, HSplit)
709
- # self.body_container.body.children[0] = VSplit(self.tabs)
710
- # Focus another tab if one exists
711
- if self.tab:
712
- self.layout.focus(self.tab)
713
- # If a tab is not open, the status bar is not shown, so focus the logo, so
714
- # pressing tab focuses the menu
707
+ self.on_tabs_change()
708
+ # Focus the next active tab if one exists
709
+ if next_tab := self.tab:
710
+ next_tab.focus()
711
+ # If no tab is open, ensure something is focused
715
712
  else:
716
713
  try:
717
714
  self.layout.focus_next()
718
715
  except ValueError:
719
716
  pass
720
717
 
718
+ def add_tab(self, tab: Tab) -> None:
719
+ """Add a tab to the current tabs list."""
720
+ self.tabs.append(tab)
721
+ self.on_tabs_change()
722
+
721
723
  def close_tab(self, tab: Tab | None = None) -> None:
722
724
  """Close a notebook tab.
723
725
 
@@ -1056,6 +1058,17 @@ class BaseApp(Application):
1056
1058
  """,
1057
1059
  )
1058
1060
 
1061
+ add_setting(
1062
+ name="syntax_highlighting",
1063
+ flags=["--syntax-highlighting"],
1064
+ type_=bool,
1065
+ help_="Syntax highlighting",
1066
+ default=True,
1067
+ description="""
1068
+ Enable or disable syntax highlighting in code input fields.
1069
+ """,
1070
+ )
1071
+
1059
1072
  add_setting(
1060
1073
  name="syntax_theme",
1061
1074
  flags=["--syntax-theme"],
@@ -1291,8 +1304,8 @@ class BaseApp(Application):
1291
1304
  "close-tab": "c-w",
1292
1305
  "next-tab": "c-pagedown",
1293
1306
  "previous-tab": "c-pageup",
1294
- "focus-next": "s-tab",
1295
- "focus-previous": "tab",
1307
+ "focus-next": "tab",
1308
+ "focus-previous": "s-tab",
1296
1309
  "clear-screen": "c-l",
1297
1310
  }
1298
1311
  }
@@ -47,7 +47,7 @@ class Osc52Clipboard(Clipboard):
47
47
  if isinstance(output, Vt100_Output):
48
48
  output.get_clipboard()
49
49
  output.flush()
50
- self.app.term_info.clipboard_data.await_response()
50
+ self.app.term_info.clipboard_data.await_response(timeout=5)
51
51
  return self._data
52
52
 
53
53
  def _update_clipboard(self, query: TerminalQuery) -> None:
@@ -122,7 +122,7 @@ def _separate_buffers(
122
122
  cloned_substrate = dict(substate) # clone list/tuple
123
123
  cloned_substrate[k] = vnew
124
124
  else:
125
- raise ValueError("expected state to be a list or dict, not %r" % substate)
125
+ raise ValueError(f"Expected state to be a list or dict, not {substate!r}")
126
126
  return cloned_substrate if cloned_substrate is not None else substate
127
127
 
128
128
 
@@ -250,7 +250,7 @@ class OutputModel(IpyWidgetComm):
250
250
  {"outputs": partial(setattr, container, "json")},
251
251
  )
252
252
 
253
- def add_output(self, json: dict[str, Any]) -> None:
253
+ def add_output(self, json: dict[str, Any], own: bool) -> None:
254
254
  """Add a new output to this widget."""
255
255
  if self.clear_output_wait:
256
256
  self.set_state("outputs", [json])
@@ -285,9 +285,9 @@ class OutputModel(IpyWidgetComm):
285
285
  else:
286
286
  # Restore the message's callbacks
287
287
  if self.original_callbacks:
288
- self.comm_container.kernel.msg_id_callbacks[
289
- self.prev_msg_id
290
- ] = self.original_callbacks
288
+ self.comm_container.kernel.msg_id_callbacks[self.prev_msg_id] = (
289
+ self.original_callbacks
290
+ )
291
291
  self.original_callbacks = MsgCallbacks()
292
292
  else:
293
293
  del self.comm_container.kernel.msg_id_callbacks[self.prev_msg_id]