euporie 2.8.1__py3-none-any.whl → 2.8.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. euporie/console/_commands.py +143 -0
  2. euporie/console/_settings.py +58 -0
  3. euporie/console/app.py +25 -71
  4. euporie/console/tabs/console.py +267 -147
  5. euporie/core/__init__.py +1 -9
  6. euporie/core/__main__.py +31 -5
  7. euporie/core/_settings.py +104 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +70 -0
  10. euporie/core/app/_settings.py +427 -0
  11. euporie/core/{app.py → app/app.py} +214 -572
  12. euporie/core/app/base.py +51 -0
  13. euporie/core/{current.py → app/current.py} +13 -4
  14. euporie/core/app/cursor.py +35 -0
  15. euporie/core/app/dummy.py +12 -0
  16. euporie/core/app/launch.py +28 -0
  17. euporie/core/bars/__init__.py +11 -0
  18. euporie/core/bars/command.py +182 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +154 -57
  21. euporie/core/{widgets → bars}/status.py +9 -26
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +21 -12
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +11 -5
  27. euporie/core/completion.py +3 -2
  28. euporie/core/config.py +368 -341
  29. euporie/core/convert/__init__.py +0 -30
  30. euporie/core/convert/datum.py +131 -60
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +46 -30
  33. euporie/core/convert/formats/common.py +11 -23
  34. euporie/core/convert/formats/html.py +45 -40
  35. euporie/core/convert/formats/pil.py +1 -1
  36. euporie/core/convert/formats/png.py +3 -5
  37. euporie/core/convert/formats/sixel.py +3 -3
  38. euporie/core/convert/registry.py +11 -8
  39. euporie/core/convert/utils.py +50 -23
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +72 -82
  42. euporie/core/format.py +13 -2
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +36 -36
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +216 -124
  48. euporie/core/history.py +2 -2
  49. euporie/core/inspection.py +3 -2
  50. euporie/core/io.py +207 -28
  51. euporie/core/kernel/__init__.py +1 -0
  52. euporie/core/{kernel.py → kernel/client.py} +100 -139
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +2 -8
  55. euporie/core/key_binding/bindings/basic.py +47 -7
  56. euporie/core/key_binding/bindings/completion.py +3 -8
  57. euporie/core/key_binding/bindings/micro.py +5 -7
  58. euporie/core/key_binding/bindings/mouse.py +26 -24
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/bindings/vi.py +46 -0
  61. euporie/core/key_binding/key_processor.py +43 -2
  62. euporie/core/key_binding/registry.py +2 -0
  63. euporie/core/key_binding/utils.py +22 -2
  64. euporie/core/keys.py +7156 -93
  65. euporie/core/layout/cache.py +35 -25
  66. euporie/core/layout/containers.py +280 -74
  67. euporie/core/layout/decor.py +5 -5
  68. euporie/core/layout/mouse.py +1 -1
  69. euporie/core/layout/print.py +16 -3
  70. euporie/core/layout/scroll.py +26 -28
  71. euporie/core/log.py +75 -60
  72. euporie/core/lsp.py +118 -24
  73. euporie/core/margins.py +60 -31
  74. euporie/core/path.py +2 -1
  75. euporie/core/renderer.py +58 -17
  76. euporie/core/style.py +60 -40
  77. euporie/core/suggest.py +103 -85
  78. euporie/core/tabs/__init__.py +34 -0
  79. euporie/core/tabs/_settings.py +113 -0
  80. euporie/core/tabs/base.py +11 -435
  81. euporie/core/tabs/kernel.py +420 -0
  82. euporie/core/tabs/notebook.py +20 -54
  83. euporie/core/utils.py +98 -6
  84. euporie/core/validation.py +1 -1
  85. euporie/core/widgets/_settings.py +188 -0
  86. euporie/core/widgets/cell.py +90 -158
  87. euporie/core/widgets/cell_outputs.py +25 -36
  88. euporie/core/widgets/decor.py +11 -41
  89. euporie/core/widgets/dialog.py +55 -44
  90. euporie/core/widgets/display.py +27 -24
  91. euporie/core/widgets/file_browser.py +5 -26
  92. euporie/core/widgets/forms.py +16 -12
  93. euporie/core/widgets/inputs.py +37 -81
  94. euporie/core/widgets/layout.py +7 -6
  95. euporie/core/widgets/logo.py +49 -0
  96. euporie/core/widgets/menu.py +13 -11
  97. euporie/core/widgets/pager.py +8 -11
  98. euporie/core/widgets/palette.py +6 -6
  99. euporie/hub/app.py +52 -31
  100. euporie/notebook/_commands.py +24 -0
  101. euporie/notebook/_settings.py +107 -0
  102. euporie/notebook/app.py +109 -210
  103. euporie/notebook/filters.py +1 -1
  104. euporie/notebook/tabs/__init__.py +46 -7
  105. euporie/notebook/tabs/_commands.py +714 -0
  106. euporie/notebook/tabs/_settings.py +32 -0
  107. euporie/notebook/tabs/display.py +2 -2
  108. euporie/notebook/tabs/edit.py +12 -7
  109. euporie/notebook/tabs/json.py +3 -3
  110. euporie/notebook/tabs/log.py +1 -18
  111. euporie/notebook/tabs/notebook.py +21 -674
  112. euporie/notebook/widgets/_commands.py +11 -0
  113. euporie/notebook/widgets/_settings.py +19 -0
  114. euporie/notebook/widgets/side_bar.py +14 -34
  115. euporie/preview/_settings.py +104 -0
  116. euporie/preview/app.py +8 -30
  117. euporie/preview/tabs/notebook.py +15 -86
  118. euporie/web/tabs/web.py +4 -6
  119. euporie/web/widgets/webview.py +5 -12
  120. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
  121. euporie-2.8.5.dist-info/RECORD +172 -0
  122. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
  123. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
  124. {euporie-2.8.1.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
  125. euporie/core/launch.py +0 -59
  126. euporie/core/terminal.py +0 -527
  127. euporie-2.8.1.dist-info/RECORD +0 -146
  128. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
  129. {euporie-2.8.1.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -31,7 +31,6 @@ from prompt_toolkit.utils import Event
31
31
  from upath import UPath
32
32
 
33
33
  from euporie.core.commands import add_cmd, get_cmd
34
- from euporie.core.config import add_setting
35
34
  from euporie.core.diagnostics import Report
36
35
  from euporie.core.filters import (
37
36
  at_end_of_buffer,
@@ -40,7 +39,8 @@ from euporie.core.filters import (
40
39
  kernel_tab_has_focus,
41
40
  )
42
41
  from euporie.core.format import LspFormatter
43
- from euporie.core.kernel import MsgCallbacks
42
+ from euporie.core.io import edit_in_editor
43
+ from euporie.core.kernel.client import MsgCallbacks
44
44
  from euporie.core.key_binding.registry import (
45
45
  load_registered_bindings,
46
46
  register_bindings,
@@ -48,23 +48,24 @@ from euporie.core.key_binding.registry import (
48
48
  from euporie.core.layout.print import PrintingContainer
49
49
  from euporie.core.lsp import LspCell
50
50
  from euporie.core.style import KERNEL_STATUS_REPR
51
- from euporie.core.tabs.base import KernelTab
52
- from euporie.core.terminal import edit_in_editor
51
+ from euporie.core.tabs.kernel import KernelTab
53
52
  from euporie.core.validation import KernelValidator
54
53
  from euporie.core.widgets.cell_outputs import CellOutputArea
55
54
  from euporie.core.widgets.inputs import KernelInput, StdInput
56
55
 
57
56
  if TYPE_CHECKING:
57
+ from collections.abc import Sequence
58
58
  from pathlib import Path
59
- from typing import Any, Callable, Sequence
59
+ from typing import Any, Callable
60
60
 
61
+ from nbformat.notebooknode import NotebookNode
61
62
  from prompt_toolkit.application.application import Application
62
63
  from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples
63
64
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
64
65
  from prompt_toolkit.key_binding.key_processor import KeyPressEvent
65
- from prompt_toolkit.layout.containers import Float
66
+ from prompt_toolkit.layout.containers import Container, Float
66
67
 
67
- from euporie.core.app import BaseApp
68
+ from euporie.core.app.app import BaseApp
68
69
  from euporie.core.lsp import LspClient
69
70
 
70
71
  log = logging.getLogger(__name__)
@@ -77,8 +78,6 @@ class Console(KernelTab):
77
78
 
78
79
  """
79
80
 
80
- bg_init = False
81
-
82
81
  def __init__(
83
82
  self,
84
83
  app: BaseApp,
@@ -103,6 +102,7 @@ class Console(KernelTab):
103
102
  prompt, password
104
103
  ),
105
104
  set_execution_count=partial(setattr, self, "execution_count"),
105
+ add_input=self.new_input,
106
106
  add_output=self.new_output,
107
107
  clear_output=self.clear_output,
108
108
  # set_metadata=self.misc_callback,
@@ -132,6 +132,8 @@ class Console(KernelTab):
132
132
 
133
133
  self.json = nbformat.v4.new_notebook()
134
134
  self.json["metadata"] = self._metadata
135
+ self.render_queue: list[dict[str, Any]] = []
136
+ self.last_rendered: NotebookNode | None = None
135
137
 
136
138
  self.container = self.load_container()
137
139
 
@@ -192,13 +194,11 @@ 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
200
  or (completeness_status == "unknown" and code[-2:] != "\n\n")
199
- ):
200
- return False
201
- return True
201
+ )
202
202
 
203
203
  def run(self, buffer: Buffer | None = None) -> None:
204
204
  """Run the code in the input box."""
@@ -208,82 +208,149 @@ class Console(KernelTab):
208
208
  # Auto-reformat code
209
209
  if app.config.autoformat:
210
210
  self.input_box.reformat()
211
- # Get the code to run
211
+ # # Get the code to run
212
212
  text = buffer.text
213
- # Remove any selections from input
213
+ # # Remove any selections from input
214
214
  buffer.selection_state = None
215
215
  # Disable existing output
216
- self.live_output.style = "class:disabled"
216
+ # self.live_output.style = "class:disabled"
217
217
  # Reset the diagnostics
218
218
  self.reports.clear()
219
+ # Increment this for display purposes until we get the response from the kernel
220
+ self.execution_count += 1
219
221
  # Move cursor to the start of the input
220
222
  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()
223
+ # Render input
224
+ self.new_input({"code": text}, own=True, force=True)
228
225
  # Run the previous entry
229
226
  if self.kernel.status == "starting":
230
227
  self.kernel_queue.append(partial(self.kernel.run, text, wait=False))
231
228
  else:
232
229
  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
230
  # Reset the input & output
236
231
  buffer.reset(append_to_history=True)
237
- # Remove any live outputs and disable mouse support
238
- self.live_output.reset()
232
+ self.on_advance()
233
+
234
+ def new_input(
235
+ self, input_json: dict[str, Any], own: bool, force: bool = False
236
+ ) -> None:
237
+ """Create new cell inputs in response to kernel ``execute_input`` messages."""
238
+ # Skip our own inputs when relayed from the kernel
239
+ # We render them immediately when they are run to avoid delays in the UI
240
+ if own and not force:
241
+ return
242
+
243
+ app = self.app
244
+ if not own and not app.config.show_remote_inputs:
245
+ return
246
+
247
+ self.flush_live_output()
248
+
239
249
  # 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)
250
+ cell_json = nbformat.v4.new_code_cell(
251
+ source=input_json["code"],
252
+ execution_count=input_json.get("execution_count", self.execution_count),
242
253
  )
254
+ self.render_queue.append(cell_json)
255
+ self.json["cells"].append(cell_json)
243
256
  if (
244
257
  app.config.max_stored_outputs
245
258
  and len(self.json["cells"]) > app.config.max_stored_outputs
246
259
  ):
247
260
  del self.json["cells"][0]
248
- self.on_advance()
249
261
 
250
- def new_output(self, output_json: dict[str, Any]) -> None:
262
+ # Invalidate the app so the new input gets printed
263
+ app.invalidate()
264
+
265
+ def new_output(self, output_json: dict[str, Any], own: bool) -> None:
251
266
  """Print the previous output and replace it with the new one."""
267
+ if not own and not self.app.config.show_remote_outputs:
268
+ return
269
+
252
270
  # Clear the output if we were previously asked to
253
271
  if self.clear_outputs_on_output:
254
272
  self.clear_outputs_on_output = False
255
273
  # Clear the screen
256
274
  get_cmd("clear-screen").run()
257
275
 
258
- # Add to record
259
- if self.json["cells"]:
260
- self.json["cells"][-1]["outputs"].append(output_json)
261
-
262
- # Set output
263
- if "application/vnd.jupyter.widget-view+json" in output_json.get("data", {}):
264
- # Use a live output to display widgets
265
- self.live_output.add_output(output_json)
276
+ # If there is no cell in the virtual notebook, add an empty cell
277
+ if not self.json["cells"]:
278
+ self.json["cells"].append(
279
+ nbformat.v4.new_code_cell(execution_count=self.execution_count)
280
+ )
281
+ cell = self.json.cells[-1]
282
+
283
+ # If there is no code cell in the render queue, add a dummy cell with no input
284
+ if cell not in self.render_queue:
285
+ # Add to end of previous cell in virtual notebook
286
+ # cell["outputs"].append(output_json)
287
+ # Create virtual cell
288
+ cell = nbformat.v4.new_code_cell(
289
+ id=cell.id, execution_count=self.execution_count
290
+ )
291
+ self.render_queue.append(cell)
292
+
293
+ # Add widgets to the live output
294
+ if output_json.get("output_type") == "stream":
295
+ # Use live output to enable emulation of carriage returns
296
+ text = output_json.get("text", "")
297
+ tail = ""
298
+ _text, _, _tail = text.rpartition("\n")
299
+ if "\r" in _tail: # or "\x1b[" in _tail:
300
+ text, tail = _text, _tail
301
+ if text:
302
+ # Partially Flush live output streams
303
+ cell["outputs"].extend(self.live_output.json)
304
+ self.live_output.reset()
305
+ output_json["text"] = text
306
+ cell["outputs"].append(output_json)
307
+ if tail:
308
+ self.live_output.add_output(
309
+ nbformat.v4.new_output(**{**output_json, "text": tail})
310
+ )
266
311
  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()
312
+ if "application/vnd.jupyter.widget-view+json" in output_json.get(
313
+ "data", {}
314
+ ):
315
+ self.live_output.add_output(output_json)
316
+ else:
317
+ cell["outputs"].append(output_json)
318
+
319
+ # Invalidate the app so the output get printed
320
+ self.app.invalidate()
321
+
322
+ def flush_live_output(self) -> None:
323
+ """Flush any active live outputs to the terminal."""
324
+ if self.live_output.json:
325
+ self.render_queue.append(
326
+ nbformat.v4.new_code_cell(
327
+ execution_count=None,
328
+ outputs=self.live_output.json[:],
329
+ ),
330
+ )
331
+ self.live_output.reset()
271
332
 
272
333
  def render_outputs(self, app: Application[Any]) -> None:
273
334
  """Request that any unrendered outputs be rendered."""
274
- if self.output.json:
335
+ # Check for unrendered cells or new output
336
+
337
+ # Clear the render queue so it does not get rendered again
338
+ render_queue = list(self.render_queue)
339
+ self.render_queue.clear()
340
+
341
+ if render_queue:
342
+ # Render the echo layout with any new cells / outputs
275
343
  app = self.app
276
344
  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()
345
+ new_layout = self.echo_layout(render_queue)
346
+ app.layout = new_layout
347
+ app.renderer.render(app, new_layout, is_done=True)
280
348
  app.layout = original_layout
281
- # Remove the outputs so they do not get rendered again
282
- self.output.reset()
349
+ app.renderer.request_absolute_cursor_position()
283
350
 
284
351
  def reset(self) -> None:
285
352
  """Reset the state of the tab."""
286
- from euporie.core.widgets.search import stop_search
353
+ from euporie.core.bars.search import stop_search
287
354
 
288
355
  self.live_output.reset()
289
356
  stop_search()
@@ -293,10 +360,16 @@ class Console(KernelTab):
293
360
  self.app.invalidate()
294
361
 
295
362
  def prompt(
296
- self, text: str, offset: int = 0, show_busy: bool = False
363
+ self,
364
+ text: str,
365
+ count: int | None = None,
366
+ offset: int = 0,
367
+ show_busy: bool = False,
297
368
  ) -> StyleAndTextTuples:
298
369
  """Determine what should be displayed in the prompt of the cell."""
299
- prompt = str(self.execution_count + offset)
370
+ if count is None:
371
+ return [("", " " * (len(text) + 4 + len(str(self.execution_count))))]
372
+ prompt = str(count + offset)
300
373
  if show_busy and self.kernel.status in ("busy", "queued"):
301
374
  prompt = "*".center(len(prompt))
302
375
  ft: StyleAndTextTuples = [
@@ -317,59 +390,145 @@ class Console(KernelTab):
317
390
  """Return the file extension for scripts in the notebook's language."""
318
391
  return self.lang_info.get("file_extension", ".py")
319
392
 
320
- def load_container(self) -> HSplit:
321
- """Build the main application layout."""
322
- # Output area
323
-
324
- self.output = CellOutputArea([], parent=self)
393
+ # Echo area
394
+
395
+ def echo_layout(self, render_queue: list) -> Layout:
396
+ """Generate a layout for displaying executed cells."""
397
+ children: list[Container] = []
398
+ height_known = renderer_height_is_known()
399
+ rows_above_layout = self.app.renderer.rows_above_layout if height_known else 1
400
+ json_cells = self.json.cells
401
+ for i, cell in enumerate(render_queue):
402
+ if cell.source:
403
+ # Spacing between cells
404
+ if ((json_cells and cell.id != json_cells[0].id) or i > 0) and (
405
+ (height_known and rows_above_layout > 0) or not height_known
406
+ ):
407
+ children.append(
408
+ Window(
409
+ height=1,
410
+ dont_extend_height=True,
411
+ )
412
+ )
325
413
 
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
- )
414
+ # Cell input
415
+ children.append(
416
+ VSplit(
417
+ [
418
+ Window(
419
+ FormattedTextControl(
420
+ partial(
421
+ self.prompt,
422
+ "In ",
423
+ count=cell.execution_count,
424
+ )
425
+ ),
426
+ dont_extend_width=True,
427
+ style="class:cell,input,prompt",
428
+ height=1,
429
+ ),
430
+ KernelInput(
431
+ text=cell.source,
432
+ kernel_tab=self,
433
+ language=lambda: self.language,
434
+ read_only=True,
435
+ ),
436
+ ],
437
+ ),
438
+ )
439
+
440
+ # Outputs
441
+ if outputs := cell.outputs:
442
+ # Add space before an output if last rendered cell did not have outputs
443
+ # or we are rendering a new output
444
+ if self.last_rendered is not None and (
445
+ not self.last_rendered.outputs
446
+ or cell.execution_count != self.last_rendered.execution_count
447
+ ):
448
+ children.append(
449
+ Window(
450
+ height=1,
451
+ dont_extend_height=True,
452
+ )
453
+ )
345
454
 
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(
455
+ def _flush(
456
+ buffer: list[dict[str, Any]], prompt: AnyFormattedText
457
+ ) -> None:
458
+ if buffer:
459
+ children.append(
460
+ VSplit(
461
+ [
462
+ Window(
463
+ FormattedTextControl(prompt),
464
+ dont_extend_width=True,
465
+ dont_extend_height=True,
466
+ style="class:cell,output,prompt",
467
+ height=1,
468
+ ),
469
+ CellOutputArea(
470
+ buffer, parent=self, style="class:disabled"
471
+ ),
472
+ ]
473
+ ),
474
+ )
475
+ buffer.clear()
476
+
477
+ buffer: list[dict[str, Any]] = []
478
+ # ec = cell.execution_count
479
+ prompt: AnyFormattedText = ""
480
+ next_prompt: AnyFormattedText
481
+ for output in outputs:
482
+ next_ec = output.get("execution_count")
483
+ next_prompt = self.prompt("Out", count=next_ec, show_busy=False)
484
+ if next_prompt != prompt:
485
+ _flush(buffer, prompt)
486
+ prompt = next_prompt
487
+ buffer.append(output)
488
+ _flush(buffer, prompt)
489
+
490
+ self.last_rendered = cell
491
+
492
+ return Layout(
363
493
  FloatContainer(
364
- output_area,
494
+ PrintingContainer(children),
365
495
  floats=cast("list[Float]", self.app.graphics),
366
496
  )
367
497
  )
368
498
 
499
+ def load_container(self) -> HSplit:
500
+ """Build the main application layout."""
369
501
  # Live output area
370
502
 
371
503
  self.live_output = CellOutputArea([], parent=self)
372
504
 
505
+ live_output_row = ConditionalContainer(
506
+ HSplit(
507
+ [
508
+ Window(height=1, dont_extend_height=True),
509
+ VSplit(
510
+ [
511
+ Window(
512
+ FormattedTextControl(
513
+ lambda: self.prompt(
514
+ "Out",
515
+ count=self.live_output.json[0].get(
516
+ "execution_count",
517
+ ),
518
+ )
519
+ ),
520
+ dont_extend_width=True,
521
+ style="class:cell,output,prompt",
522
+ height=1,
523
+ ),
524
+ self.live_output,
525
+ ]
526
+ ),
527
+ ]
528
+ ),
529
+ filter=Condition(lambda: bool(self.live_output.json)),
530
+ )
531
+
373
532
  # Input area
374
533
 
375
534
  input_kb = KeyBindings()
@@ -425,13 +584,6 @@ class Console(KernelTab):
425
584
  """Force new line on Shift-Enter."""
426
585
  event.current_buffer.newline(copy_margin=not in_paste_mode())
427
586
 
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
587
  def _handler(buffer: Buffer) -> bool:
436
588
  self.run(buffer)
437
589
  return True
@@ -460,7 +612,7 @@ class Console(KernelTab):
460
612
  # Spacing
461
613
  ConditionalContainer(
462
614
  Window(height=1, dont_extend_height=True),
463
- filter=Condition(lambda: self.execution_count > 0)
615
+ filter=Condition(lambda: len(self.json["cells"]) > 0)
464
616
  & (
465
617
  (
466
618
  renderer_height_is_known
@@ -473,7 +625,16 @@ class Console(KernelTab):
473
625
  ConditionalContainer(
474
626
  VSplit(
475
627
  [
476
- input_prompt,
628
+ Window(
629
+ FormattedTextControl(
630
+ lambda: self.prompt(
631
+ "In ", self.execution_count, offset=1
632
+ )
633
+ ),
634
+ dont_extend_width=True,
635
+ style="class:cell,input,prompt",
636
+ height=1,
637
+ ),
477
638
  self.input_box,
478
639
  ],
479
640
  ),
@@ -481,19 +642,9 @@ class Console(KernelTab):
481
642
  ),
482
643
  ]
483
644
 
484
- self.input_layout = Layout(PrintingContainer(input_row))
485
-
486
645
  return HSplit(
487
646
  [
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
- ),
647
+ live_output_row,
497
648
  # StdIn
498
649
  self.stdin_box,
499
650
  ConditionalContainer(
@@ -503,7 +654,8 @@ class Console(KernelTab):
503
654
  *input_row,
504
655
  ],
505
656
  key_bindings=load_registered_bindings(
506
- "euporie.console.tabs.console.Console"
657
+ "euporie.console.tabs.console.Console",
658
+ config=self.app.config,
507
659
  ),
508
660
  )
509
661
 
@@ -714,38 +866,6 @@ class Console(KernelTab):
714
866
  tab.reset()
715
867
  app.layout.focus(tab.input_box)
716
868
 
717
- # ################################### Settings ####################################
718
-
719
- add_setting(
720
- name="max_stored_outputs",
721
- flags=["--max-stored-outputs"],
722
- type_=int,
723
- help_="The number of inputs / outputs to store in an in-memory notebook",
724
- default=100,
725
- schema={
726
- "minimum": 0,
727
- },
728
- description="""
729
- Defines the maximum number of executed "cells" to store in case the console
730
- session is saved to a file or converted into a notebook.
731
- """,
732
- )
733
-
734
- add_setting(
735
- name="connection_file",
736
- flags=["--connection-file", "--kernel-connection-file"],
737
- type_=UPath,
738
- help_="Attempt to connect to an existing kernel using a JSON connection info file",
739
- default=None,
740
- description="""
741
- If the file does not exist, kernel connection information will be written
742
- to the file path provided.
743
-
744
- If the file exists, kernel connection info will be read from the file,
745
- allowing euporie to connect to existing kernels.
746
- """,
747
- )
748
-
749
869
  # ################################# Key Bindings ##################################
750
870
 
751
871
  register_bindings(
euporie/core/__init__.py CHANGED
@@ -1,18 +1,10 @@
1
1
  """This package defines the euporie application and its components."""
2
2
 
3
3
  __app_name__ = "euporie"
4
- __version__ = "2.8.1"
4
+ __version__ = "2.8.5"
5
5
  __logo__ = "⚈"
6
6
  __strapline__ = "Jupyter in the terminal"
7
7
  __author__ = "Josiah Outram Halstead"
8
8
  __email__ = "josiah@halstead.email"
9
9
  __copyright__ = f"© 2024, {__author__}"
10
10
  __license__ = "MIT"
11
-
12
-
13
- # Register extensions to external packages
14
- from euporie.core import path # noqa F401
15
- from euporie.core import pygments # noqa F401
16
-
17
- # Monkey-patch prompt_toolkit
18
- from euporie.core.layout import containers # noqa: F401
euporie/core/__main__.py CHANGED
@@ -1,21 +1,47 @@
1
1
  """Main entry point into euporie.core."""
2
2
 
3
+ from __future__ import annotations
3
4
 
4
- def main(name: "str" = "launch") -> "None":
5
- """Load and launches the application."""
6
- from importlib.metadata import entry_points
5
+ from functools import cache
6
+ from importlib.metadata import entry_points
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from importlib.metadata import EntryPoint, EntryPoints, SelectableGroups
7
11
 
8
- eps = entry_points() # group="euporie.apps")
12
+
13
+ @cache
14
+ def available_apps() -> dict[str, EntryPoint]:
15
+ """Return a list of loadable euporie apps."""
16
+ eps: dict | SelectableGroups | EntryPoints
17
+ try:
18
+ eps = entry_points(group="euporie.apps")
19
+ except TypeError:
20
+ eps = entry_points()
9
21
  if isinstance(eps, dict):
10
22
  points = eps.get("euporie.apps")
11
23
  else:
12
24
  points = eps.select(group="euporie.apps")
13
25
  apps = {x.name: x for x in points} if points else {}
26
+ return apps
27
+
28
+
29
+ def main(name: str = "launch") -> None:
30
+ """Load and launches the application."""
31
+ # Register extensions to external packages
32
+ from euporie.core import (
33
+ path, # noqa F401
34
+ pygments, # noqa F401
35
+ )
36
+
37
+ # Monkey-patch prompt_toolkit
38
+ from euporie.core.layout import containers # noqa: F401
14
39
 
40
+ apps = available_apps()
15
41
  if entry := apps.get(name):
16
42
  return entry.load().launch()
17
43
  else:
18
- raise Exception(f"Euporie app `{name}` not installed")
44
+ raise ModuleNotFoundError(f"Euporie app `{name}` not installed")
19
45
 
20
46
 
21
47
  if __name__ == "__main__":