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