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
@@ -54,17 +54,18 @@ from euporie.notebook.filters import (
54
54
  )
55
55
 
56
56
  if TYPE_CHECKING:
57
+ from pathlib import Path
57
58
  from typing import Any, Deque, MutableSequence, Sequence
58
59
 
59
- from prompt_toolkit.formatted_text.base import AnyFormattedText, StyleAndTextTuples
60
+ from prompt_toolkit.formatted_text.base import StyleAndTextTuples
60
61
  from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
61
62
  from prompt_toolkit.layout.containers import AnyContainer
62
63
  from prompt_toolkit.mouse_events import MouseEvent
63
- from upath import UPath
64
64
 
65
65
  from euporie.core.app import BaseApp
66
66
  from euporie.core.comm.base import Comm
67
67
  from euporie.core.kernel import Kernel
68
+ from euporie.core.widgets.status_bar import StatusBarFields
68
69
 
69
70
  log = logging.getLogger(__name__)
70
71
 
@@ -75,12 +76,23 @@ class Notebook(BaseNotebook):
75
76
  A tab which allows running and editing a notebook.
76
77
  """
77
78
 
79
+ name = "Notebook Editor"
80
+ weight = 3
81
+ mime_types = {"application/x-ipynb+json"}
82
+ file_extensions = {".ipynb"}
83
+ try:
84
+ from jupytext.formats import NOTEBOOK_EXTENSIONS
85
+ except ModuleNotFoundError:
86
+ pass
87
+ else:
88
+ file_extensions |= set(NOTEBOOK_EXTENSIONS)
89
+
78
90
  allow_stdin = True
79
91
 
80
92
  def __init__(
81
93
  self,
82
94
  app: BaseApp,
83
- path: UPath | None = None,
95
+ path: Path | None = None,
84
96
  kernel: Kernel | None = None,
85
97
  comms: dict[str, Comm] | None = None,
86
98
  use_kernel_history: bool = True,
@@ -102,9 +114,6 @@ class Notebook(BaseNotebook):
102
114
  self.clipboard: list[Cell] = []
103
115
  self.undo_buffer: Deque[tuple[int, list[Cell]]] = deque(maxlen=10)
104
116
 
105
- if not kernel:
106
- self.kernel.start(cb=self.kernel_started, wait=False)
107
-
108
117
  # Tab stuff
109
118
 
110
119
  def _statusbar_kernel_handeler(self, event: MouseEvent) -> NotImplementedOrNone:
@@ -115,29 +124,45 @@ class Notebook(BaseNotebook):
115
124
  else:
116
125
  return NotImplemented
117
126
 
118
- def statusbar_fields(
119
- self,
120
- ) -> tuple[Sequence[AnyFormattedText], Sequence[AnyFormattedText]]:
127
+ def __pt_status__(self) -> StatusBarFields | None:
121
128
  """Generate the formatted text for the statusbar."""
122
- rendered = self.page.pre_rendered
123
- return (
124
- [
125
- self.mode(),
126
- f"Cell {self.page.selected_slice.start+1}",
127
- f"Rendering… ({rendered:.0%})" if rendered < 1 else "",
128
- "Saving…" if self.saving else "",
129
- ],
130
- [
131
- lambda: cast(
132
- "StyleAndTextTuples",
133
- [("", self.kernel_display_name, self._statusbar_kernel_handeler)],
134
- ),
135
- KERNEL_STATUS_REPR[self.kernel.status] if self.kernel else ".",
136
- ],
137
- )
129
+ if self.loaded:
130
+ rendered = self.page.pre_rendered
131
+ return (
132
+ [
133
+ self.mode(),
134
+ f"Cell {self.page.selected_slice.start+1}",
135
+ f"Rendering ({rendered:.0%})" if rendered < 1 else "",
136
+ "Saving…" if self.saving else "",
137
+ ],
138
+ [
139
+ lambda: cast(
140
+ "StyleAndTextTuples",
141
+ [
142
+ (
143
+ "",
144
+ self.kernel_display_name,
145
+ self._statusbar_kernel_handeler,
146
+ )
147
+ ],
148
+ ),
149
+ KERNEL_STATUS_REPR[self.kernel.status] if self.kernel else ".",
150
+ ],
151
+ )
152
+ else:
153
+ return ([], [])
138
154
 
139
155
  # Notebook stuff
140
156
 
157
+ def post_init_kernel(self) -> None:
158
+ """Start the kernel after if has been loaded."""
159
+ # Start kernel
160
+ if self.kernel._status == "stopped":
161
+ self.kernel.start(cb=self.kernel_started, wait=False)
162
+
163
+ # Load container
164
+ super().post_init_kernel()
165
+
141
166
  @property
142
167
  def selected_indices(self) -> list[int]:
143
168
  """Return a list of the currently selected cell indices."""
@@ -219,7 +244,8 @@ class Notebook(BaseNotebook):
219
244
  width=Dimension(weight=1),
220
245
  height=Dimension(min=1, weight=2),
221
246
  key_bindings=load_registered_bindings(
222
- "euporie.notebook.tabs.notebook.Notebook"
247
+ "euporie.core.tabs.base.Tab",
248
+ "euporie.notebook.tabs.notebook.Notebook",
223
249
  ),
224
250
  )
225
251
 
@@ -315,7 +341,7 @@ class Notebook(BaseNotebook):
315
341
 
316
342
  def refresh_cell(self, cell: Cell) -> None:
317
343
  """Trigger the refresh of a notebook cell."""
318
- self.page.get_child_render_info(cell.index).refresh = True
344
+ self.page.get_child_render_info(cell.index).invalidate()
319
345
 
320
346
  def add_cell_above(self) -> None:
321
347
  """Inert a cell above the current selection."""
@@ -568,16 +594,6 @@ class Notebook(BaseNotebook):
568
594
 
569
595
  # ################################### Commands ####################################
570
596
 
571
- @staticmethod
572
- @add_cmd(
573
- filter=notebook_has_focus,
574
- )
575
- def _save_notebook() -> None:
576
- """Save the current notebook."""
577
- tab = get_app().tab
578
- if isinstance(tab, Notebook):
579
- tab.save()
580
-
581
597
  @staticmethod
582
598
  @add_cmd(
583
599
  filter=cell_has_focus & ~buffer_has_focus,
@@ -930,6 +946,11 @@ class Notebook(BaseNotebook):
930
946
  if isinstance(nb, Notebook):
931
947
  for cell in nb.cells:
932
948
  cell.set_cell_type("markdown", clear=True)
949
+ # Remove unallowed additional properties
950
+ json = cell.json
951
+ json.pop("execution_count", None)
952
+ json.pop("outputs", None)
953
+ cell.run_or_render()
933
954
 
934
955
  @staticmethod
935
956
  @add_cmd(
@@ -993,7 +1014,7 @@ class Notebook(BaseNotebook):
993
1014
  title="Collapse cell inputs",
994
1015
  )
995
1016
  def _hide_cell_inputs() -> None:
996
- """Collape the selected cells' inputs."""
1017
+ """Collapse the selected cells' inputs."""
997
1018
  nb = get_app().tab
998
1019
  if isinstance(nb, Notebook):
999
1020
  for cell in nb.cells:
@@ -1028,7 +1049,7 @@ class Notebook(BaseNotebook):
1028
1049
  title="Collapse cell outputs",
1029
1050
  )
1030
1051
  def _hide_cell_outputs() -> None:
1031
- """Collape the selected cells' outputs."""
1052
+ """Collapse the selected cells' outputs."""
1032
1053
  nb = get_app().tab
1033
1054
  if isinstance(nb, Notebook):
1034
1055
  for cell in nb.cells:
@@ -1187,7 +1208,6 @@ class Notebook(BaseNotebook):
1187
1208
  register_bindings(
1188
1209
  {
1189
1210
  "euporie.notebook.tabs.notebook.Notebook": {
1190
- "save-notebook": "c-s",
1191
1211
  "enter-cell-edit-mode": "enter",
1192
1212
  "exit-edit-mode": "escape",
1193
1213
  "run-selected-cells": ["c-enter", "c-e"],
@@ -1234,7 +1254,6 @@ class Notebook(BaseNotebook):
1234
1254
  "scroll-output-right": "right",
1235
1255
  "toggle-expand": "w",
1236
1256
  "notebook-toggle-line-numbers": "l",
1237
- "reset-tab": "f5",
1238
1257
  }
1239
1258
  }
1240
1259
  )
@@ -3,9 +3,11 @@
3
3
 
4
4
  def main() -> "None":
5
5
  """Call the main entrypoint to the application."""
6
+ import sys
7
+
6
8
  from euporie.core import __main__
7
9
 
8
- __main__.main(__name__.split(".")[1])
10
+ __main__.main(str(sys.modules[__name__].__package__).rpartition(".")[2])
9
11
 
10
12
 
11
13
  if __name__ == "__main__":
euporie/preview/app.py CHANGED
@@ -20,6 +20,7 @@ from euporie.core.key_binding.registry import register_bindings
20
20
  from euporie.preview.tabs.notebook import PreviewNotebook
21
21
 
22
22
  if TYPE_CHECKING:
23
+ from pathlib import Path
23
24
  from typing import IO, Any, TextIO
24
25
 
25
26
  from prompt_toolkit.application.application import _AppResult
@@ -91,7 +92,7 @@ class PreviewApp(BaseApp):
91
92
  # Select the first tab after files are opened
92
93
  self.post_load_callables.append(partial(setattr, self, "tab_idx", 0))
93
94
 
94
- def get_file_tab(self, path: UPath) -> type[Tab]:
95
+ def get_file_tab(self, path: Path) -> type[Tab]:
95
96
  """Return the tab to use for a file path."""
96
97
  return PreviewNotebook
97
98
 
@@ -21,12 +21,12 @@ from euporie.core.widgets.cell import Cell
21
21
  from euporie.core.widgets.page import PrintingContainer
22
22
 
23
23
  if TYPE_CHECKING:
24
+ from pathlib import Path
24
25
  from typing import Any, Callable
25
26
 
26
27
  from prompt_toolkit.application.application import Application
27
28
  from prompt_toolkit.formatted_text.base import StyleAndTextTuples
28
29
  from prompt_toolkit.layout.containers import AnyContainer
29
- from upath import UPath
30
30
 
31
31
  from euporie.core.app import BaseApp
32
32
 
@@ -36,25 +36,28 @@ log = logging.getLogger(__name__)
36
36
  class PreviewNotebook(BaseNotebook):
37
37
  """A notebook tab which renders cells sequentially."""
38
38
 
39
+ bg_init = False
40
+
39
41
  def __init__(
40
42
  self,
41
43
  app: BaseApp,
42
- path: UPath | None = None,
44
+ path: Path | None = None,
43
45
  use_kernel_history: bool = False,
44
46
  ) -> None:
45
47
  """Create a new instance."""
46
- super().__init__(app, path, use_kernel_history=use_kernel_history)
47
48
  self.cell_index = 0
48
- self.app.before_render += self.before_render
49
- self.app.after_render += self.after_render
50
49
  self.cells: FastDictCache[tuple[int], Cell] = FastDictCache(
51
50
  get_value=self.get_cell
52
51
  )
53
52
 
54
- # If we are running the notebook, pause rendering util the kernel has started
55
- if self.app.config.run:
56
- self.app.pause_rendering()
57
- self.kernel.start(cb=self.kernel_started, wait=True)
53
+ super().__init__(app, path, use_kernel_history=use_kernel_history)
54
+
55
+ self.app.before_render += self.before_render
56
+ self.app.after_render += self.after_render
57
+
58
+ def pre_init_kernel(self) -> None:
59
+ """Filter cells before kernel is loaded."""
60
+ super().pre_init_kernel()
58
61
 
59
62
  # Filter the cells to be shown
60
63
  n_cells = len(self.json["cells"]) - 1
@@ -67,8 +70,17 @@ class PreviewNotebook(BaseNotebook):
67
70
  log.debug("Showing cells %s to %s", start, stop)
68
71
  self.json["cells"] = self.json["cells"][start:stop]
69
72
 
73
+ def post_init_kernel(self) -> None:
74
+ """Optionally start kernel after it is loaded."""
75
+ super().post_init_kernel()
76
+ # If we are running the notebook, pause rendering util the kernel has started
77
+ if self.app.config.run:
78
+ self.app.pause_rendering()
79
+ self.kernel.start(cb=self.kernel_started, wait=True)
80
+
70
81
  def print_title(self) -> None:
71
82
  """Print a notebook's filename."""
83
+ from euporie.core.border import DoubleLine
72
84
  from euporie.core.formatted_text.utils import (
73
85
  FormattedTextAlign,
74
86
  add_border,
@@ -80,7 +92,8 @@ class PreviewNotebook(BaseNotebook):
80
92
  ft: StyleAndTextTuples = [("bold", str(self.path))]
81
93
  ft = wrap(ft, width - 4)
82
94
  ft = align(ft, how=FormattedTextAlign.CENTER, width=width - 4)
83
- ft = add_border(ft, width=width)
95
+ ft = add_border(ft, width=width, border_grid=DoubleLine.grid)
96
+ ft.append(("", "\n"))
84
97
  self.app.print_text(ft)
85
98
 
86
99
  def kernel_started(self, result: dict | None = None) -> None:
@@ -0,0 +1,149 @@
1
+ """Defines a web-view tab."""
2
+
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from typing import TYPE_CHECKING
8
+
9
+ from prompt_toolkit.eventloop.utils import run_in_executor_with_context
10
+ from prompt_toolkit.filters import Condition
11
+ from prompt_toolkit.layout.containers import HSplit, VSplit
12
+ from prompt_toolkit.layout.dimension import Dimension
13
+ from upath import UPath
14
+
15
+ from euporie.core.convert.core import get_mime
16
+ from euporie.core.current import get_app
17
+ from euporie.core.data_structures import DiBool
18
+ from euporie.core.margins import MarginContainer, ScrollbarMargin
19
+ from euporie.core.tabs.base import Tab
20
+ from euporie.core.widgets.decor import FocusedStyle
21
+ from euporie.core.widgets.display import DisplayWindow
22
+ from euporie.core.widgets.forms import Button, Text
23
+ from euporie.web.widgets.webview import WebViewControl
24
+
25
+ if TYPE_CHECKING:
26
+ from pathlib import Path
27
+ from typing import Any, Callable
28
+
29
+ from prompt_toolkit.layout.containers import AnyContainer
30
+
31
+ from euporie.core.app import BaseApp
32
+ from euporie.core.widgets.status_bar import StatusBarFields
33
+
34
+ log = logging.getLogger(__name__)
35
+
36
+
37
+ class WebTab(Tab):
38
+ """Tab class for displaying files."""
39
+
40
+ name = "Web Viewer"
41
+ weight = 2
42
+ mime_types = {"text/html"}
43
+
44
+ def __init__(self, app: BaseApp, path: Path | None) -> None:
45
+ """Call when the tab is created."""
46
+ super().__init__(app, path)
47
+ self.status: Callable[[], StatusBarFields] | None = None
48
+
49
+ def _load() -> None:
50
+ self.container = self.load_container()
51
+
52
+ run_in_executor_with_context(_load)
53
+
54
+ @property
55
+ def title(self) -> str:
56
+ """Return the tab title."""
57
+ if webview := getattr(self, "webview", None):
58
+ if title := webview.title:
59
+ return title
60
+ if self.path is not None:
61
+ return str(self.path.name) or str(self.path)
62
+ else:
63
+ return "<Web Page>"
64
+
65
+ def load_url(self, url: str | Path, new_tab: bool = False, **kwargs: Any) -> bool:
66
+ """Load a new URL, or the URL in the address-bar."""
67
+ log.debug("Loading %s", url)
68
+ if not url:
69
+ return False
70
+ if isinstance(url, str):
71
+ url = UPath(url)
72
+
73
+ if not new_tab and get_mime(url) == "text/html":
74
+ self.webview.load_url(url, **kwargs)
75
+ else:
76
+ get_app().open_file(url)
77
+ return True
78
+
79
+ def _url_loaded(self, webview: WebViewControl) -> None:
80
+ """Trigger callback when the URL is loaded."""
81
+ url = webview.url
82
+ if url is not None:
83
+ self.path = UPath(url)
84
+ self.url_bar.text = str(url)
85
+
86
+ def load_container(self) -> AnyContainer:
87
+ """Abcract method for loading the notebook's main container."""
88
+ assert self.path is not None
89
+ path = self.path
90
+ self.webview = WebViewControl(url=path, link_handler=self.load_url)
91
+ self.webview.rendered += self._url_loaded
92
+
93
+ def _status() -> StatusBarFields:
94
+ return ([self.webview.status] if self.webview is not None else [], [])
95
+
96
+ self.status = _status
97
+
98
+ button_prev = Button(
99
+ "◀",
100
+ show_borders=DiBool(top=True, right=False, bottom=True, left=True),
101
+ disabled=Condition(lambda: not self.webview.prev_stack),
102
+ on_click=lambda x: self.webview.nav_prev(),
103
+ )
104
+ button_next = Button(
105
+ "▶",
106
+ show_borders=DiBool(top=True, right=True, bottom=True, left=False),
107
+ disabled=Condition(lambda: not self.webview.next_stack),
108
+ on_click=lambda x: self.webview.nav_next(),
109
+ )
110
+ self.url_bar = Text(
111
+ text=str(path),
112
+ show_borders=DiBool(top=True, right=False, bottom=True, left=True),
113
+ accept_handler=lambda buf: self.load_url(buf.text),
114
+ )
115
+ button_go = Button(
116
+ "➜",
117
+ show_borders=DiBool(top=True, right=True, bottom=True, left=False),
118
+ on_click=lambda x: (self.load_url(self.url_bar.text) and None)
119
+ or None, # Magical typing magic
120
+ )
121
+
122
+ return HSplit(
123
+ [
124
+ VSplit(
125
+ [
126
+ FocusedStyle(button_prev),
127
+ FocusedStyle(button_next),
128
+ FocusedStyle(self.url_bar),
129
+ FocusedStyle(button_go),
130
+ ],
131
+ style="class:tab.toolbar",
132
+ ),
133
+ VSplit(
134
+ [
135
+ window := DisplayWindow(self.webview),
136
+ MarginContainer(ScrollbarMargin(), target=window),
137
+ ]
138
+ ),
139
+ ],
140
+ width=Dimension(weight=1),
141
+ height=Dimension(weight=1),
142
+ )
143
+
144
+ def __pt_status__(self) -> StatusBarFields:
145
+ """Return a list of statusbar field values shown then this tab is active."""
146
+ if callable(self.status):
147
+ return self.status()
148
+ else:
149
+ return ([], [])