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
@@ -1,6 +1,6 @@
1
1
  """Decorative widgets."""
2
2
 
3
- # from __future__ import annotations
3
+ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from typing import TYPE_CHECKING
@@ -30,7 +30,7 @@ from euporie.core.data_structures import DiBool
30
30
  from euporie.core.style import ColorPaletteColor
31
31
 
32
32
  if TYPE_CHECKING:
33
- from typing import Callable, Optional, Union
33
+ from typing import Callable
34
34
 
35
35
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
36
36
  from prompt_toolkit.layout.containers import AnyContainer
@@ -48,12 +48,12 @@ class Line(Container):
48
48
 
49
49
  def __init__(
50
50
  self,
51
- char: "Optional[str]" = None,
52
- width: "Optional[int]" = None,
53
- height: "Optional[int]" = None,
54
- collapse: "bool" = False,
55
- style: "str" = "class:grid-line",
56
- ) -> "None":
51
+ char: str | None = None,
52
+ width: int | None = None,
53
+ height: int | None = None,
54
+ collapse: bool = False,
55
+ style: str = "class:grid-line",
56
+ ) -> None:
57
57
  """Initialize a grid line.
58
58
 
59
59
  Args:
@@ -78,28 +78,26 @@ class Line(Container):
78
78
  self.char = Char(char, style)
79
79
  self.collapse = collapse
80
80
 
81
- def reset(self) -> "None":
81
+ def reset(self) -> None:
82
82
  """Reet the state of the line. Does nothing."""
83
83
 
84
- def preferred_width(self, max_available_width: "int") -> "Dimension":
84
+ def preferred_width(self, max_available_width: int) -> Dimension:
85
85
  """Return the preferred width of the line."""
86
86
  return Dimension(min=int(not self.collapse), max=self.width)
87
87
 
88
- def preferred_height(
89
- self, width: "int", max_available_height: "int"
90
- ) -> "Dimension":
88
+ def preferred_height(self, width: int, max_available_height: int) -> Dimension:
91
89
  """Return the preferred height of the line."""
92
90
  return Dimension(min=int(not self.collapse), max=self.height)
93
91
 
94
92
  def write_to_screen(
95
93
  self,
96
- screen: "Screen",
97
- mouse_handlers: "MouseHandlers",
98
- write_position: "WritePosition",
99
- parent_style: "str",
100
- erase_bg: "bool",
101
- z_index: "Optional[int]",
102
- ) -> "None":
94
+ screen: Screen,
95
+ mouse_handlers: MouseHandlers,
96
+ write_position: WritePosition,
97
+ parent_style: str,
98
+ erase_bg: bool,
99
+ z_index: int | None,
100
+ ) -> None:
103
101
  """Draw a continuous line in the ``write_position`` area.
104
102
 
105
103
  Args:
@@ -124,7 +122,7 @@ class Line(Container):
124
122
  for x in range(xpos, xpos + write_position.width):
125
123
  row[x] = self.char
126
124
 
127
- def get_children(self) -> "list":
125
+ def get_children(self) -> list:
128
126
  """Return an empty list of the container's children."""
129
127
  return []
130
128
 
@@ -134,37 +132,35 @@ class Pattern(Container):
134
132
 
135
133
  def __init__(
136
134
  self,
137
- char: "Union[str, Callable[[], str]]",
138
- pattern: "Union[int, Callable[[], int]]" = 1,
139
- ) -> "None":
135
+ char: str | Callable[[], str],
136
+ pattern: int | Callable[[], int] = 1,
137
+ ) -> None:
140
138
  """Initialize the :class:`Pattern`."""
141
139
  self.bg = Char(" ", "class:pattern")
142
140
  self.char = char
143
141
  self.pattern = pattern
144
142
 
145
- def reset(self) -> "None":
143
+ def reset(self) -> None:
146
144
  """Reet the pattern. Does nothing."""
147
145
  pass
148
146
 
149
- def preferred_width(self, max_available_width: "int") -> "Dimension":
147
+ def preferred_width(self, max_available_width: int) -> Dimension:
150
148
  """Return an empty dimension (expand to available width)."""
151
149
  return Dimension()
152
150
 
153
- def preferred_height(
154
- self, width: "int", max_available_height: "int"
155
- ) -> "Dimension":
151
+ def preferred_height(self, width: int, max_available_height: int) -> Dimension:
156
152
  """Return an empty dimension (expand to available height)."""
157
153
  return Dimension()
158
154
 
159
155
  def write_to_screen(
160
156
  self,
161
- screen: "Screen",
162
- mouse_handlers: "MouseHandlers",
163
- write_position: "WritePosition",
164
- parent_style: "str",
165
- erase_bg: "bool",
166
- z_index: "Optional[int]",
167
- ) -> "None":
157
+ screen: Screen,
158
+ mouse_handlers: MouseHandlers,
159
+ write_position: WritePosition,
160
+ parent_style: str,
161
+ erase_bg: bool,
162
+ z_index: int | None,
163
+ ) -> None:
168
164
  """Fill the whole area of write_position with a pattern.
169
165
 
170
166
  Args:
@@ -208,7 +204,7 @@ class Pattern(Container):
208
204
  else:
209
205
  row[x] = bg
210
206
 
211
- def get_children(self) -> "list":
207
+ def get_children(self) -> list:
212
208
  """Return an empty list of the container's children."""
213
209
  return []
214
210
 
@@ -218,11 +214,11 @@ class Border:
218
214
 
219
215
  def __init__(
220
216
  self,
221
- body: "AnyContainer",
222
- border: "Optional[GridStyle]" = ThinLine.grid,
223
- style: "Union[str, Callable[[], str]]" = "class:border",
224
- show_borders: "Optional[DiBool]" = None,
225
- ) -> "None":
217
+ body: AnyContainer,
218
+ border: GridStyle | None = ThinLine.grid,
219
+ style: str | Callable[[], str] = "class:border",
220
+ show_borders: DiBool | None = None,
221
+ ) -> None:
226
222
  """Create a new border widget which wraps another container.
227
223
 
228
224
  Args:
@@ -244,7 +240,7 @@ class Border:
244
240
  border_bottom = to_filter(show_borders.bottom)
245
241
  border_left = to_filter(show_borders.left)
246
242
 
247
- self.container: "AnyContainer"
243
+ self.container: AnyContainer
248
244
  if border is not None and any(show_borders):
249
245
  self.container = HSplit(
250
246
  [
@@ -344,10 +340,10 @@ class Border:
344
340
  else:
345
341
  self.container = body
346
342
 
347
- def add_style(self, extra: "str") -> "Callable[[], str]":
343
+ def add_style(self, extra: str) -> Callable[[], str]:
348
344
  """Return a function which adds a style string to the border style."""
349
345
 
350
- def _style() -> "str":
346
+ def _style() -> str:
351
347
  if callable(self.style):
352
348
  return f"{self.style()} {extra}"
353
349
  else:
@@ -355,7 +351,7 @@ class Border:
355
351
 
356
352
  return _style
357
353
 
358
- def __pt_container__(self) -> "AnyContainer":
354
+ def __pt_container__(self) -> AnyContainer:
359
355
  """Return the border widget's container."""
360
356
  return self.container
361
357
 
@@ -365,10 +361,10 @@ class FocusedStyle(Container):
365
361
 
366
362
  def __init__(
367
363
  self,
368
- body: "AnyContainer",
369
- style_focus: "Union[str, Callable[[], str]]" = "class:focused",
370
- style_hover: "Union[str, Callable[[], str]]" = "class:hovered",
371
- ) -> "None":
364
+ body: AnyContainer,
365
+ style_focus: str | Callable[[], str] = "class:focused",
366
+ style_hover: str | Callable[[], str] = "",
367
+ ) -> None:
372
368
  """Create a new instance of the widget.
373
369
 
374
370
  Args:
@@ -382,29 +378,27 @@ class FocusedStyle(Container):
382
378
  self.hover = False
383
379
  self.has_focus = has_focus(self.body)
384
380
 
385
- def reset(self) -> "None":
381
+ def reset(self) -> None:
386
382
  """Reet the wrapped container."""
387
383
  to_container(self.body).reset()
388
384
 
389
- def preferred_width(self, max_available_width: "int") -> "Dimension":
385
+ def preferred_width(self, max_available_width: int) -> Dimension:
390
386
  """Return the wrapped container's preferred width."""
391
387
  return to_container(self.body).preferred_width(max_available_width)
392
388
 
393
- def preferred_height(
394
- self, width: "int", max_available_height: "int"
395
- ) -> "Dimension":
389
+ def preferred_height(self, width: int, max_available_height: int) -> Dimension:
396
390
  """Return the wrapped container's preferred height."""
397
391
  return to_container(self.body).preferred_height(width, max_available_height)
398
392
 
399
393
  def write_to_screen(
400
394
  self,
401
- screen: "Screen",
402
- mouse_handlers: "MouseHandlers",
403
- write_position: "WritePosition",
404
- parent_style: "str",
405
- erase_bg: "bool",
406
- z_index: "Optional[int]",
407
- ) -> "None":
395
+ screen: Screen,
396
+ mouse_handlers: MouseHandlers,
397
+ write_position: WritePosition,
398
+ parent_style: str,
399
+ erase_bg: bool,
400
+ z_index: int | None,
401
+ ) -> None:
408
402
  """Draw the wrapped container with the additional style."""
409
403
  to_container(self.body).write_to_screen(
410
404
  screen,
@@ -422,10 +416,10 @@ class FocusedStyle(Container):
422
416
  y_max = y_min + write_position.height
423
417
 
424
418
  # Wrap mouse handlers to add "hover" class on hover
425
- def _wrap_mouse_handler(handler: "Callable") -> "MouseHandler":
419
+ def _wrap_mouse_handler(handler: Callable) -> MouseHandler:
426
420
  def wrapped_mouse_handler(
427
- mouse_event: "MouseEvent",
428
- ) -> "NotImplementedOrNone":
421
+ mouse_event: MouseEvent,
422
+ ) -> NotImplementedOrNone:
429
423
  result = handler(mouse_event)
430
424
 
431
425
  if mouse_event.event_type == MouseEventType.MOUSE_MOVE:
@@ -452,7 +446,7 @@ class FocusedStyle(Container):
452
446
  for x in range(x_min, x_max):
453
447
  row[x] = mouse_handler_wrappers[(row[x],)]
454
448
 
455
- def get_style(self) -> "str":
449
+ def get_style(self) -> str:
456
450
  """Determine the style to apply depending on the focus status."""
457
451
  style = ""
458
452
  if self.has_focus():
@@ -465,7 +459,7 @@ class FocusedStyle(Container):
465
459
  )
466
460
  return style
467
461
 
468
- def get_children(self) -> "list[Container]":
462
+ def get_children(self) -> list[Container]:
469
463
  """Return the list of child :class:`.Container` objects."""
470
464
  return [to_container(self.body)]
471
465
 
@@ -480,28 +474,26 @@ class DropShadow(Container):
480
474
  self.cp = app.color_palette
481
475
  self.renderer = app.renderer
482
476
 
483
- def reset(self) -> "None":
477
+ def reset(self) -> None:
484
478
  """Reet the wrapped container - here, do nothing."""
485
479
 
486
- def preferred_width(self, max_available_width: "int") -> "Dimension":
480
+ def preferred_width(self, max_available_width: int) -> Dimension:
487
481
  """Return the wrapped container's preferred width."""
488
482
  return Dimension(weight=1)
489
483
 
490
- def preferred_height(
491
- self, width: "int", max_available_height: "int"
492
- ) -> "Dimension":
484
+ def preferred_height(self, width: int, max_available_height: int) -> Dimension:
493
485
  """Return the wrapped container's preferred height."""
494
486
  return Dimension(weight=1)
495
487
 
496
488
  def write_to_screen(
497
489
  self,
498
- screen: "Screen",
499
- mouse_handlers: "MouseHandlers",
500
- write_position: "WritePosition",
501
- parent_style: "str",
502
- erase_bg: "bool",
503
- z_index: "Optional[int]",
504
- ) -> "None":
490
+ screen: Screen,
491
+ mouse_handlers: MouseHandlers,
492
+ write_position: WritePosition,
493
+ parent_style: str,
494
+ erase_bg: bool,
495
+ z_index: int | None,
496
+ ) -> None:
505
497
  """Draw the wrapped container with the additional style."""
506
498
  attr_cache = self.renderer._attrs_for_style
507
499
  if attr_cache is not None:
@@ -551,7 +543,7 @@ class Shadow:
551
543
  :py:class:`prompt_toolkit.widows.base.Shadow` class.
552
544
  """
553
545
 
554
- def __init__(self, body: "AnyContainer") -> "None":
546
+ def __init__(self, body: AnyContainer) -> None:
555
547
  """Initialize a new drop-shadow container.
556
548
 
557
549
  Args:
@@ -580,7 +572,7 @@ class Shadow:
580
572
  ],
581
573
  )
582
574
 
583
- def get_contents() -> "AnyContainer":
575
+ def get_contents() -> AnyContainer:
584
576
  if filter_():
585
577
  return shadow
586
578
  else:
@@ -588,7 +580,7 @@ class Shadow:
588
580
 
589
581
  self.container = DynamicContainer(get_contents)
590
582
 
591
- def __pt_container__(self) -> "AnyContainer":
583
+ def __pt_container__(self) -> AnyContainer:
592
584
  """Return the container's content."""
593
585
  return self.container
594
586
 
@@ -6,13 +6,19 @@ import logging
6
6
  import traceback
7
7
  from abc import ABCMeta, abstractmethod
8
8
  from functools import partial
9
+ from pathlib import Path
9
10
  from typing import TYPE_CHECKING
10
11
 
11
12
  from prompt_toolkit.cache import SimpleCache
12
13
  from prompt_toolkit.clipboard import ClipboardData
13
14
  from prompt_toolkit.completion import PathCompleter
14
15
  from prompt_toolkit.data_structures import Point
15
- from prompt_toolkit.filters import Condition, has_completions, has_focus
16
+ from prompt_toolkit.filters import (
17
+ Condition,
18
+ buffer_has_focus,
19
+ has_completions,
20
+ has_focus,
21
+ )
16
22
  from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text
17
23
  from prompt_toolkit.formatted_text.utils import split_lines
18
24
  from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
@@ -31,6 +37,7 @@ from prompt_toolkit.layout.screen import WritePosition
31
37
  from prompt_toolkit.mouse_events import MouseButton, MouseEventType
32
38
  from prompt_toolkit.widgets.base import Box, Label
33
39
 
40
+ from euporie.core.app import get_app
34
41
  from euporie.core.border import (
35
42
  FullLine,
36
43
  LowerLeftHalfLine,
@@ -58,6 +65,7 @@ if TYPE_CHECKING:
58
65
 
59
66
  from euporie.core.app import BaseApp
60
67
  from euporie.core.tabs.base import KernelTab
68
+ from euporie.core.widgets.file_browser import FileBrowserControl
61
69
 
62
70
  log = logging.getLogger(__name__)
63
71
 
@@ -184,14 +192,23 @@ class Dialog(Float, metaclass=ABCMeta):
184
192
  self.button_widgets: list[AnyContainer] = []
185
193
 
186
194
  # Create key-bindings
187
- self.kb = KeyBindings()
188
- self.kb.add("escape")(lambda event: self.hide())
189
- self.kb.add("tab", filter=~has_completions)(focus_next)
190
- self.kb.add("s-tab", filter=~has_completions)(focus_previous)
195
+ kb = KeyBindings()
196
+ kb.add("escape")(lambda event: self.hide())
197
+ kb.add("tab", filter=~has_completions)(focus_next)
198
+ kb.add("s-tab", filter=~has_completions)(focus_previous)
199
+
200
+ @kb.add("enter", filter=~has_completions & ~buffer_has_focus)
201
+ def _focus_button(event: KeyPressEvent) -> NotImplementedOrNone:
202
+ if self.button_widgets:
203
+ app.layout.focus(self.button_widgets[0])
204
+ return None
205
+ return NotImplemented
206
+
207
+ self.kb = kb
191
208
  self.buttons_kb = KeyBindings()
192
209
 
193
210
  # Create title row
194
- title_window = Window(height=1, style="class:dialog,title")
211
+ title_window = Window(height=1, style="class:dialog,dialog-title")
195
212
  title_window.content = DialogTitleControl(
196
213
  lambda: self.title, self, title_window
197
214
  )
@@ -234,7 +251,7 @@ class Dialog(Float, metaclass=ABCMeta):
234
251
  ],
235
252
  style="class:dialog,body",
236
253
  key_bindings=self.kb,
237
- modal=True,
254
+ modal=False,
238
255
  ),
239
256
  border=DialogGrid,
240
257
  style="class:dialog,border",
@@ -452,6 +469,7 @@ class FileDialog(Dialog, metaclass=ABCMeta):
452
469
  self.file_browser.control.on_open += lambda path: self.validate(
453
470
  self.filepath.buffer, tab=tab, cb=cb
454
471
  )
472
+ self.file_browser.control.selected = None
455
473
  self.error = error
456
474
  self.to_focus = self.filepath
457
475
 
@@ -467,23 +485,65 @@ class OpenFileDialog(FileDialog):
467
485
 
468
486
  title = "Select a File to Open"
469
487
 
488
+ def __init__(self, app: BaseApp) -> None:
489
+ """Additional body components."""
490
+ from euporie.core.widgets.forms import Dropdown
491
+
492
+ super().__init__(app)
493
+
494
+ self.tab_dd = tab_dd = Dropdown(options=[])
495
+
496
+ def _update_options(fb: FileBrowserControl) -> None:
497
+ tabs = get_app().get_file_tabs(path) if (path := fb.path).is_file() else []
498
+ tab_dd.options = tabs
499
+ tab_dd.labels = [tab.name for tab in tabs]
500
+
501
+ self.file_browser.control.on_select += _update_options
502
+
503
+ if isinstance(self.body, HSplit):
504
+ self.body.children.append(
505
+ ConditionalContainer(
506
+ FocusedStyle(LabelledWidget(tab_dd, "Open with:")),
507
+ filter=Condition(lambda: len(tab_dd.options) > 1),
508
+ )
509
+ )
510
+
511
+ def load(
512
+ self,
513
+ text: str = "",
514
+ tab: Tab | None = None,
515
+ error: str = "",
516
+ cb: Callable | None = None,
517
+ ) -> None:
518
+ """Load the dialog body."""
519
+ super().load(text=text, tab=tab, error=error, cb=cb)
520
+ self.tab_dd.options = []
521
+ self.tab_dd.labels = []
522
+
470
523
  def validate(
471
524
  self, buffer: Buffer, tab: Tab | None, cb: Callable | None = None
472
525
  ) -> None:
473
526
  """Validate the the file to open exists."""
474
- from euporie.core.utils import parse_path
527
+ from upath import UPath
528
+
529
+ from euporie.core.path import parse_path
475
530
 
476
531
  path = parse_path(self.file_browser.control.dir / buffer.text)
477
532
  if path is not None:
478
- if str(path).startswith("http") or path.is_file():
479
- self.hide()
480
- self.app.open_file(path)
481
- elif not path.exists():
533
+ if not path.exists():
534
+ path = UPath(buffer.text)
535
+
536
+ if path.exists():
537
+ if path.is_dir():
538
+ self.file_browser.control.dir = path
539
+ elif path.is_file():
540
+ self.hide()
541
+ self.app.open_file(path, tab_class=self.tab_dd.value)
542
+ return
543
+ else:
482
544
  self.show(
483
545
  error="The file path specified does not exist", text=buffer.text
484
546
  )
485
- elif path.is_dir():
486
- self.file_browser.control.dir = path
487
547
 
488
548
  # ################################### Commands ####################################
489
549
 
@@ -516,7 +576,7 @@ class SaveAsDialog(FileDialog):
516
576
  self, buffer: Buffer, tab: Tab | None, cb: Callable | None = None
517
577
  ) -> None:
518
578
  """Validate the the file to open exists."""
519
- from euporie.core.utils import parse_path
579
+ from euporie.core.path import parse_path
520
580
 
521
581
  path = parse_path(self.file_browser.control.dir / buffer.text)
522
582
  if tab and path is not None:
@@ -589,13 +649,19 @@ class SelectKernelDialog(Dialog):
589
649
  def load(
590
650
  self,
591
651
  kernel_specs: dict[str, Any] | None = None,
652
+ runtime_dirs: dict[str, Path] | None = None,
592
653
  tab: KernelTab | None = None,
593
654
  message: str = "",
594
655
  ) -> None:
595
656
  """Load dialog body & buttons."""
657
+ from jupyter_core.paths import jupyter_runtime_dir
658
+
659
+ from euporie.core.widgets.layout import TabbedSplit
660
+
596
661
  kernel_specs = kernel_specs or {}
662
+ runtime_dirs = runtime_dirs or {}
597
663
 
598
- options = Select(
664
+ options_specs = Select(
599
665
  options=list(kernel_specs.keys()),
600
666
  labels=[
601
667
  kernel_spec.get("spec", {}).get("display_name", kernel_name)
@@ -608,21 +674,51 @@ class SelectKernelDialog(Dialog):
608
674
  rows=5,
609
675
  dont_extend_width=False,
610
676
  )
677
+ self.to_focus = options_specs
678
+
679
+ connection_files = {
680
+ path.name: path
681
+ for path in Path(jupyter_runtime_dir()).glob("kernel-*.json")
682
+ }
683
+ options_files = Select(
684
+ options=list(connection_files.values()),
685
+ labels=list(connection_files.keys()),
686
+ style="class:input,radio-buttons",
687
+ prefix=("○", "◉"),
688
+ multiple=False,
689
+ border=None,
690
+ rows=5,
691
+ dont_extend_width=False,
692
+ )
611
693
 
612
- msg_ft = (f"{message}\n" if message else "") + "Please select a kernel:\n"
694
+ msg_ft = (f"{message}\n\n" if message else "") + "Please select a kernel:"
613
695
 
614
696
  self.body = HSplit(
615
697
  [
616
698
  Label(msg_ft),
617
- FocusedStyle(options),
699
+ tabs := TabbedSplit(
700
+ [
701
+ FocusedStyle(options_specs),
702
+ FocusedStyle(options_files),
703
+ ],
704
+ titles=["New", "Existing"],
705
+ width=Dimension(min=30),
706
+ ),
618
707
  ]
619
708
  )
620
709
 
621
710
  def _change_kernel() -> None:
622
711
  self.hide()
623
712
  assert tab is not None
624
- name = options.options[options.index or 0]
625
- tab.kernel.change(name, cb=tab.kernel_started)
713
+ if tabs.active == 0:
714
+ name = options_specs.value
715
+ connection_file = None
716
+ else:
717
+ name = None
718
+ connection_file = options_files.value
719
+ tab.kernel.change(
720
+ name=name, connection_file=connection_file, cb=tab.kernel_started
721
+ )
626
722
 
627
723
  self.buttons = {
628
724
  "Select": _change_kernel,
@@ -672,6 +768,7 @@ class ErrorDialog(Dialog):
672
768
 
673
769
  def load(self, exception: Exception | None = None, when: str = "") -> None:
674
770
  """Load dialog body & buttons."""
771
+ from euporie.core.margins import MarginContainer, ScrollbarMargin
675
772
  from euporie.core.widgets.formatted_text_area import FormattedTextArea
676
773
  from euporie.core.widgets.forms import Checkbox
677
774
 
@@ -705,12 +802,17 @@ class ErrorDialog(Dialog):
705
802
  Box(checkbox, padding_left=0),
706
803
  ),
707
804
  ConditionalContainer(
708
- FormattedTextArea(
709
- lex([("", tb_text)], "pytb"),
710
- width=80,
711
- height=Dimension(min=10),
712
- wrap_lines=False,
713
- style="",
805
+ VSplit(
806
+ [
807
+ fta := FormattedTextArea(
808
+ lex([("", tb_text)], "pytb"),
809
+ width=80,
810
+ height=Dimension(min=10),
811
+ wrap_lines=False,
812
+ style="",
813
+ ),
814
+ MarginContainer(ScrollbarMargin(), target=fta.window),
815
+ ]
714
816
  ),
715
817
  filter=Condition(lambda: checkbox.selected),
716
818
  ),
@@ -775,6 +877,7 @@ class ShortcutsDialog(Dialog):
775
877
  def load(self, *args: Any, **kwargs: Any) -> None:
776
878
  """Load the dialog body."""
777
879
  from euporie.core.formatted_text.utils import max_line_width
880
+ from euporie.core.margins import MarginContainer, ScrollbarMargin
778
881
  from euporie.core.widgets.formatted_text_area import FormattedTextArea
779
882
 
780
883
  if not self.details:
@@ -783,13 +886,14 @@ class ShortcutsDialog(Dialog):
783
886
 
784
887
  width = max_line_width(self.details)
785
888
 
786
- self.body = FormattedTextArea(
889
+ fta = FormattedTextArea(
787
890
  formatted_text=self.details,
788
891
  multiline=True,
789
892
  focusable=True,
790
893
  wrap_lines=False,
791
- width=width,
894
+ width=width - 2,
792
895
  )
896
+ self.body = VSplit([fta, MarginContainer(ScrollbarMargin(), target=fta.window)])
793
897
 
794
898
  def format_key_info(self) -> StyleAndTextTuples:
795
899
  """Generate a table with the current key bindings."""