euporie 2.8.5__py3-none-any.whl → 2.8.7__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/app.py +2 -0
- euporie/console/tabs/console.py +27 -17
- euporie/core/__init__.py +2 -2
- euporie/core/__main__.py +2 -2
- euporie/core/_settings.py +7 -2
- euporie/core/app/_commands.py +20 -12
- euporie/core/app/_settings.py +34 -4
- euporie/core/app/app.py +31 -18
- euporie/core/bars/command.py +53 -27
- euporie/core/bars/search.py +43 -2
- euporie/core/border.py +7 -2
- euporie/core/comm/base.py +2 -2
- euporie/core/comm/ipywidgets.py +3 -3
- euporie/core/commands.py +44 -24
- euporie/core/completion.py +14 -6
- euporie/core/convert/datum.py +7 -7
- euporie/core/data_structures.py +20 -1
- euporie/core/filters.py +40 -9
- euporie/core/format.py +2 -3
- euporie/core/ft/html.py +47 -40
- euporie/core/graphics.py +199 -31
- euporie/core/history.py +15 -5
- euporie/core/inspection.py +16 -9
- euporie/core/kernel/__init__.py +53 -1
- euporie/core/kernel/base.py +571 -0
- euporie/core/kernel/{client.py → jupyter.py} +173 -430
- euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
- euporie/core/kernel/local.py +694 -0
- euporie/core/key_binding/bindings/basic.py +6 -3
- euporie/core/keys.py +26 -25
- euporie/core/layout/cache.py +31 -7
- euporie/core/layout/containers.py +88 -13
- euporie/core/layout/scroll.py +69 -170
- euporie/core/log.py +2 -5
- euporie/core/path.py +61 -13
- euporie/core/style.py +2 -1
- euporie/core/suggest.py +155 -74
- euporie/core/tabs/__init__.py +12 -4
- euporie/core/tabs/_commands.py +76 -0
- euporie/core/tabs/_settings.py +16 -0
- euporie/core/tabs/base.py +89 -9
- euporie/core/tabs/kernel.py +83 -38
- euporie/core/tabs/notebook.py +28 -76
- euporie/core/utils.py +2 -19
- euporie/core/validation.py +8 -8
- euporie/core/widgets/_settings.py +19 -2
- euporie/core/widgets/cell.py +32 -32
- euporie/core/widgets/cell_outputs.py +10 -1
- euporie/core/widgets/dialog.py +60 -76
- euporie/core/widgets/display.py +2 -2
- euporie/core/widgets/forms.py +71 -59
- euporie/core/widgets/inputs.py +7 -4
- euporie/core/widgets/layout.py +281 -93
- euporie/core/widgets/menu.py +56 -16
- euporie/core/widgets/palette.py +3 -1
- euporie/core/widgets/tree.py +86 -76
- euporie/notebook/app.py +35 -16
- euporie/notebook/tabs/display.py +2 -2
- euporie/notebook/tabs/edit.py +11 -46
- euporie/notebook/tabs/json.py +8 -4
- euporie/notebook/tabs/notebook.py +26 -8
- euporie/preview/tabs/notebook.py +17 -13
- euporie/web/__init__.py +1 -0
- euporie/web/tabs/__init__.py +14 -0
- euporie/web/tabs/web.py +30 -5
- euporie/web/widgets/__init__.py +1 -0
- euporie/web/widgets/webview.py +5 -4
- {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/METADATA +4 -2
- {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/RECORD +74 -68
- {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/entry_points.txt +1 -1
- {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/licenses/LICENSE +1 -1
- {euporie-2.8.5.data → euporie-2.8.7.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.5.data → euporie-2.8.7.data}/data/share/applications/euporie-notebook.desktop +0 -0
- {euporie-2.8.5.dist-info → euporie-2.8.7.dist-info}/WHEEL +0 -0
euporie/console/app.py
CHANGED
@@ -32,6 +32,7 @@ from euporie.core.filters import has_dialog
|
|
32
32
|
from euporie.core.layout.mouse import DisableMouseOnScroll
|
33
33
|
from euporie.core.widgets.dialog import (
|
34
34
|
AboutDialog,
|
35
|
+
ConfirmDialog,
|
35
36
|
NoKernelsDialog,
|
36
37
|
SaveAsDialog,
|
37
38
|
SelectKernelDialog,
|
@@ -112,6 +113,7 @@ class ConsoleApp(BaseApp):
|
|
112
113
|
self.dialogs["no-kernels"] = NoKernelsDialog(self)
|
113
114
|
self.dialogs["change-kernel"] = SelectKernelDialog(self)
|
114
115
|
self.dialogs["shortcuts"] = ShortcutsDialog(self)
|
116
|
+
self.dialogs["confirm"] = ConfirmDialog(self)
|
115
117
|
|
116
118
|
return FloatContainer(
|
117
119
|
DisableMouseOnScroll(
|
euporie/console/tabs/console.py
CHANGED
@@ -40,7 +40,7 @@ from euporie.core.filters import (
|
|
40
40
|
)
|
41
41
|
from euporie.core.format import LspFormatter
|
42
42
|
from euporie.core.io import edit_in_editor
|
43
|
-
from euporie.core.kernel.
|
43
|
+
from euporie.core.kernel.base import MsgCallbacks
|
44
44
|
from euporie.core.key_binding.registry import (
|
45
45
|
load_registered_bindings,
|
46
46
|
register_bindings,
|
@@ -137,10 +137,7 @@ class Console(KernelTab):
|
|
137
137
|
|
138
138
|
self.container = self.load_container()
|
139
139
|
|
140
|
-
self.kernel.start(cb=self.kernel_started, wait=False)
|
141
|
-
|
142
140
|
self.app.before_render += self.render_outputs
|
143
|
-
|
144
141
|
self.on_advance = Event(self)
|
145
142
|
|
146
143
|
async def load_lsps(self) -> None:
|
@@ -162,9 +159,26 @@ class Console(KernelTab):
|
|
162
159
|
|
163
160
|
lsp.on_exit += lsp_unload
|
164
161
|
|
162
|
+
def post_init_kernel(self) -> None:
|
163
|
+
"""Start the kernel after if has been loaded."""
|
164
|
+
# Load container
|
165
|
+
super().post_init_kernel()
|
166
|
+
|
167
|
+
# Start kernel
|
168
|
+
if self.kernel._status == "stopped":
|
169
|
+
self.kernel.start(cb=self.kernel_started, wait=False)
|
170
|
+
|
165
171
|
def kernel_died(self) -> None:
|
166
|
-
"""Call
|
172
|
+
"""Call if the kernel dies."""
|
167
173
|
log.error("The kernel has died")
|
174
|
+
if confirm := self.app.dialogs.get("confirm"):
|
175
|
+
confirm.show(
|
176
|
+
title="Kernel connection lost",
|
177
|
+
message="The kernel appears to have died\n"
|
178
|
+
"as it can no longer be reached.\n\n"
|
179
|
+
"Do you want to restart the kernel?",
|
180
|
+
cb=self.kernel.restart,
|
181
|
+
)
|
168
182
|
|
169
183
|
async def load_history(self) -> None:
|
170
184
|
"""Load kernel history."""
|
@@ -191,9 +205,7 @@ class Console(KernelTab):
|
|
191
205
|
def validate_input(self, code: str) -> bool:
|
192
206
|
"""Determine if the entered code is ready to run."""
|
193
207
|
assert self.kernel is not None
|
194
|
-
completeness_status = self.kernel.is_complete(code
|
195
|
-
"status", "unknown"
|
196
|
-
)
|
208
|
+
completeness_status = self.kernel.is_complete(code).get("status", "unknown")
|
197
209
|
return not (
|
198
210
|
not code.strip()
|
199
211
|
or completeness_status == "incomplete"
|
@@ -404,12 +416,7 @@ class Console(KernelTab):
|
|
404
416
|
if ((json_cells and cell.id != json_cells[0].id) or i > 0) and (
|
405
417
|
(height_known and rows_above_layout > 0) or not height_known
|
406
418
|
):
|
407
|
-
children.append(
|
408
|
-
Window(
|
409
|
-
height=1,
|
410
|
-
dont_extend_height=True,
|
411
|
-
)
|
412
|
-
)
|
419
|
+
children.append(Window(height=1, dont_extend_height=True))
|
413
420
|
|
414
421
|
# Cell input
|
415
422
|
children.append(
|
@@ -441,9 +448,12 @@ class Console(KernelTab):
|
|
441
448
|
if outputs := cell.outputs:
|
442
449
|
# Add space before an output if last rendered cell did not have outputs
|
443
450
|
# or we are rendering a new output
|
444
|
-
if self.last_rendered is
|
445
|
-
|
446
|
-
|
451
|
+
if self.last_rendered is None or (
|
452
|
+
self.last_rendered is not None
|
453
|
+
and (
|
454
|
+
not self.last_rendered.outputs
|
455
|
+
or cell.execution_count != self.last_rendered.execution_count
|
456
|
+
)
|
447
457
|
):
|
448
458
|
children.append(
|
449
459
|
Window(
|
euporie/core/__init__.py
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
"""This package defines the euporie application and its components."""
|
2
2
|
|
3
3
|
__app_name__ = "euporie"
|
4
|
-
__version__ = "2.8.
|
4
|
+
__version__ = "2.8.7"
|
5
5
|
__logo__ = "⚈"
|
6
6
|
__strapline__ = "Jupyter in the terminal"
|
7
7
|
__author__ = "Josiah Outram Halstead"
|
8
8
|
__email__ = "josiah@halstead.email"
|
9
|
-
__copyright__ = f"©
|
9
|
+
__copyright__ = f"© 2025, {__author__}"
|
10
10
|
__license__ = "MIT"
|
euporie/core/__main__.py
CHANGED
@@ -7,13 +7,13 @@ from importlib.metadata import entry_points
|
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
|
9
9
|
if TYPE_CHECKING:
|
10
|
-
from importlib.metadata import EntryPoint, EntryPoints
|
10
|
+
from importlib.metadata import EntryPoint, EntryPoints
|
11
11
|
|
12
12
|
|
13
13
|
@cache
|
14
14
|
def available_apps() -> dict[str, EntryPoint]:
|
15
15
|
"""Return a list of loadable euporie apps."""
|
16
|
-
eps: dict |
|
16
|
+
eps: dict | EntryPoints
|
17
17
|
try:
|
18
18
|
eps = entry_points(group="euporie.apps")
|
19
19
|
except TypeError:
|
euporie/core/_settings.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
"""Defines core settings."""
|
2
2
|
|
3
|
+
import json
|
4
|
+
|
3
5
|
from euporie.core import __version__
|
4
6
|
from euporie.core.config import add_setting
|
5
7
|
|
@@ -73,8 +75,11 @@ add_setting(
|
|
73
75
|
name="log_config",
|
74
76
|
group="euporie.core.log",
|
75
77
|
flags=["--log-config"],
|
76
|
-
type_=
|
77
|
-
default=
|
78
|
+
type_=json.loads,
|
79
|
+
default={},
|
80
|
+
schema={
|
81
|
+
"type": "object",
|
82
|
+
},
|
78
83
|
title="additional logging configuration",
|
79
84
|
help_="Additional logging configuration",
|
80
85
|
description="""
|
euporie/core/app/_commands.py
CHANGED
@@ -8,7 +8,7 @@ from prompt_toolkit.filters import buffer_has_focus
|
|
8
8
|
|
9
9
|
from euporie.core.app.current import get_app
|
10
10
|
from euporie.core.commands import add_cmd
|
11
|
-
from euporie.core.filters import tab_has_focus
|
11
|
+
from euporie.core.filters import tab_has_focus, tab_type_has_focus
|
12
12
|
|
13
13
|
if TYPE_CHECKING:
|
14
14
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
@@ -20,18 +20,12 @@ def _quit() -> None:
|
|
20
20
|
get_app().exit()
|
21
21
|
|
22
22
|
|
23
|
-
@add_cmd(aliases=["
|
24
|
-
def
|
25
|
-
"""
|
26
|
-
from
|
23
|
+
@add_cmd(aliases=["q!"])
|
24
|
+
def _force_quit() -> None:
|
25
|
+
"""Quit euporie without saving any changes."""
|
26
|
+
from prompt_toolkit.application.application import Application
|
27
27
|
|
28
|
-
|
29
|
-
if (tab := get_app().tab) is not None:
|
30
|
-
try:
|
31
|
-
tab._save(UPath(event._arg) if event._arg else None)
|
32
|
-
except NotImplementedError:
|
33
|
-
pass
|
34
|
-
app.exit()
|
28
|
+
Application.exit(get_app())
|
35
29
|
|
36
30
|
|
37
31
|
@add_cmd(aliases=["bc"], filter=tab_has_focus, menu_title="Close File")
|
@@ -68,3 +62,17 @@ def _focus_previous() -> None:
|
|
68
62
|
def _clear_screen() -> None:
|
69
63
|
"""Clear the screen."""
|
70
64
|
get_app().renderer.clear()
|
65
|
+
|
66
|
+
|
67
|
+
@add_cmd(hidden=True, aliases=[""])
|
68
|
+
def _go_to(event: KeyPressEvent, index: int = 0) -> None:
|
69
|
+
"""Go to a line or cell by number."""
|
70
|
+
index = max(0, index - 1)
|
71
|
+
if buffer_has_focus():
|
72
|
+
buffer = get_app().current_buffer
|
73
|
+
buffer.cursor_position = len("".join(buffer.text.splitlines(True)[:index]))
|
74
|
+
elif tab_type_has_focus("euporie.notebook.tabs.notebook:Notebook")():
|
75
|
+
from euporie.notebook.tabs.notebook import Notebook
|
76
|
+
|
77
|
+
if isinstance(nb := get_app().tab, Notebook):
|
78
|
+
nb.select(index)
|
euporie/core/app/_settings.py
CHANGED
@@ -140,6 +140,8 @@ add_setting(
|
|
140
140
|
|
141
141
|
e.g.
|
142
142
|
|
143
|
+
.. code-block:: json
|
144
|
+
|
143
145
|
[
|
144
146
|
{"command": ["ruff", "format", "-"], "languages": ["python"]},
|
145
147
|
{"command": ["black", "-"], "languages": ["python"]},
|
@@ -281,6 +283,30 @@ add_setting(
|
|
281
283
|
""",
|
282
284
|
)
|
283
285
|
|
286
|
+
add_setting(
|
287
|
+
name="custom_styles",
|
288
|
+
group="euporie.core.style",
|
289
|
+
flags=["--custom-styles"],
|
290
|
+
type_=json.loads,
|
291
|
+
default={},
|
292
|
+
schema={
|
293
|
+
"type": "object",
|
294
|
+
},
|
295
|
+
help_="Additional style settings",
|
296
|
+
description="""
|
297
|
+
A JSON object mapping style names to prompt-toolkit style values.
|
298
|
+
|
299
|
+
The style keys used in euporie can be found in :py:func:`euporie.core.style.build_style`.
|
300
|
+
|
301
|
+
e.g.:
|
302
|
+
|
303
|
+
.. code-block:: json
|
304
|
+
|
305
|
+
{ "cell input prompt":"fg:purple", "cell output prompt": "fg:green" }
|
306
|
+
|
307
|
+
""",
|
308
|
+
)
|
309
|
+
|
284
310
|
add_setting(
|
285
311
|
name="key_bindings",
|
286
312
|
group="euporie.core.app.app",
|
@@ -288,19 +314,19 @@ add_setting(
|
|
288
314
|
type_=json.loads,
|
289
315
|
help_="Additional key binding definitions",
|
290
316
|
default={},
|
291
|
-
description="""
|
292
|
-
A mapping of component names to mappings of command name to key-binding lists.
|
293
|
-
""",
|
294
317
|
schema={
|
295
318
|
"type": "object",
|
296
319
|
},
|
320
|
+
description="""
|
321
|
+
A mapping of component names to mappings of command name to key-binding lists.
|
322
|
+
""",
|
297
323
|
)
|
298
324
|
|
299
325
|
add_setting(
|
300
326
|
name="graphics",
|
301
327
|
group="euporie.core.app.app",
|
302
328
|
flags=["--graphics"],
|
303
|
-
choices=["none", "sixel", "kitty", "iterm"],
|
329
|
+
choices=["none", "sixel", "kitty", "kitty-unicode", "iterm"],
|
304
330
|
type_=str,
|
305
331
|
default=None,
|
306
332
|
help_="The preferred graphics protocol",
|
@@ -375,6 +401,8 @@ add_setting(
|
|
375
401
|
description="""
|
376
402
|
Additional language servers can be defined here, e.g.:
|
377
403
|
|
404
|
+
.. code-block:: json
|
405
|
+
|
378
406
|
{
|
379
407
|
"ruff": {"command": ["ruff-lsp"], "languages": ["python"]},
|
380
408
|
"pylsp": {"command": ["pylsp"], "languages": ["python"]},
|
@@ -392,6 +420,8 @@ add_setting(
|
|
392
420
|
empty dictionary. For example, the following would disable the awk language
|
393
421
|
server:
|
394
422
|
|
423
|
+
.. code-block:: json
|
424
|
+
|
395
425
|
{
|
396
426
|
"awk-language-server": {},
|
397
427
|
}
|
euporie/core/app/app.py
CHANGED
@@ -520,14 +520,17 @@ class BaseApp(ConfigurableApp, Application, ABC):
|
|
520
520
|
@classmethod
|
521
521
|
def launch(cls) -> None:
|
522
522
|
"""Launch the app."""
|
523
|
+
from prompt_toolkit.utils import in_main_thread
|
524
|
+
|
523
525
|
super().launch()
|
524
526
|
# Run the application
|
525
527
|
with create_app_session(input=cls.load_input(), output=cls.load_output()):
|
526
528
|
# Create an instance of the app and run it
|
527
529
|
app = cls()
|
528
|
-
|
529
|
-
|
530
|
-
|
530
|
+
if in_main_thread():
|
531
|
+
# Handle SIGTERM while the app is running
|
532
|
+
original_sigterm = signal.getsignal(signal.SIGTERM)
|
533
|
+
signal.signal(signal.SIGTERM, app.cleanup)
|
531
534
|
# Set and run the app
|
532
535
|
with set_app(app):
|
533
536
|
try:
|
@@ -535,7 +538,8 @@ class BaseApp(ConfigurableApp, Application, ABC):
|
|
535
538
|
except (EOFError, KeyboardInterrupt):
|
536
539
|
result = None
|
537
540
|
finally:
|
538
|
-
|
541
|
+
if in_main_thread():
|
542
|
+
signal.signal(signal.SIGTERM, original_sigterm)
|
539
543
|
# Shut down any remaining LSP clients at exit
|
540
544
|
app.shutdown_lsps()
|
541
545
|
return result
|
@@ -582,14 +586,16 @@ class BaseApp(ConfigurableApp, Application, ABC):
|
|
582
586
|
path_mime = get_mime(path) or "text/plain"
|
583
587
|
log.debug("File %s has mime type: %s", path, path_mime)
|
584
588
|
|
585
|
-
|
589
|
+
# Use a set to automatically handle duplicates
|
590
|
+
tab_options: set[TabRegistryEntry] = set()
|
586
591
|
for entry in self.tab_registry:
|
587
592
|
for mime_type in entry.mime_types:
|
588
593
|
if PurePath(path_mime).match(mime_type):
|
589
|
-
tab_options.
|
594
|
+
tab_options.add(entry)
|
590
595
|
if path.suffix in entry.file_extensions:
|
591
|
-
tab_options.
|
596
|
+
tab_options.add(entry)
|
592
597
|
|
598
|
+
# Sort by weight (TabRegistryEntry.__lt__ handles this)
|
593
599
|
return sorted(tab_options, reverse=True)
|
594
600
|
|
595
601
|
def get_file_tab(self, path: Path) -> type[Tab] | None:
|
@@ -791,6 +797,13 @@ class BaseApp(ConfigurableApp, Application, ABC):
|
|
791
797
|
syntax_theme = "tango" if self.color_palette.bg.is_light else "euporie"
|
792
798
|
return syntax_theme
|
793
799
|
|
800
|
+
base_styles = (
|
801
|
+
Style(MIME_STYLE),
|
802
|
+
Style(HTML_STYLE),
|
803
|
+
Style(LOG_STYLE),
|
804
|
+
Style(IPYWIDGET_STYLE),
|
805
|
+
)
|
806
|
+
|
794
807
|
def create_merged_style(self) -> BaseStyle:
|
795
808
|
"""Generate a new merged style for the application.
|
796
809
|
|
@@ -801,6 +814,11 @@ class BaseApp(ConfigurableApp, Application, ABC):
|
|
801
814
|
Return a combined style to use for the application
|
802
815
|
|
803
816
|
"""
|
817
|
+
styles: list[BaseStyle] = [
|
818
|
+
style_from_pygments_cls(get_style_by_name(self.syntax_theme)),
|
819
|
+
*self.base_styles,
|
820
|
+
]
|
821
|
+
|
804
822
|
# Get foreground and background colors based on the configured colour scheme
|
805
823
|
theme_colors: dict[str, dict[str, str]] = {
|
806
824
|
"default": {},
|
@@ -844,7 +862,7 @@ class BaseApp(ConfigurableApp, Application, ABC):
|
|
844
862
|
)
|
845
863
|
|
846
864
|
# Build app style
|
847
|
-
|
865
|
+
styles.append(build_style(cp))
|
848
866
|
|
849
867
|
# Apply style transformations based on the configured color scheme
|
850
868
|
self.style_transformation = merge_style_transformations(
|
@@ -862,16 +880,11 @@ class BaseApp(ConfigurableApp, Application, ABC):
|
|
862
880
|
]
|
863
881
|
)
|
864
882
|
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
Style(LOG_STYLE),
|
871
|
-
Style(IPYWIDGET_STYLE),
|
872
|
-
app_style,
|
873
|
-
]
|
874
|
-
)
|
883
|
+
# Add user style customizations
|
884
|
+
if custom_style_dict := self.config.custom_styles:
|
885
|
+
styles.append(Style.from_dict(custom_style_dict))
|
886
|
+
|
887
|
+
return merge_styles(styles)
|
875
888
|
|
876
889
|
def update_style(self, query: Setting | None = None) -> None:
|
877
890
|
"""Update the application's style when the syntax theme is changed."""
|
euporie/core/bars/command.py
CHANGED
@@ -3,14 +3,14 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import logging
|
6
|
+
import re
|
7
|
+
from functools import lru_cache
|
6
8
|
from typing import TYPE_CHECKING
|
7
9
|
|
8
10
|
from prompt_toolkit.buffer import Buffer
|
9
11
|
from prompt_toolkit.completion.base import Completer, Completion
|
10
|
-
from prompt_toolkit.filters import
|
11
|
-
|
12
|
-
has_focus,
|
13
|
-
)
|
12
|
+
from prompt_toolkit.filters import buffer_has_focus, has_focus, vi_navigation_mode
|
13
|
+
from prompt_toolkit.key_binding.vi_state import InputMode
|
14
14
|
from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window
|
15
15
|
from prompt_toolkit.layout.controls import (
|
16
16
|
BufferControl,
|
@@ -29,6 +29,7 @@ from euporie.core.key_binding.registry import (
|
|
29
29
|
|
30
30
|
if TYPE_CHECKING:
|
31
31
|
from collections.abc import Iterable
|
32
|
+
from typing import Unpack
|
32
33
|
|
33
34
|
from prompt_toolkit.completion.base import CompleteEvent
|
34
35
|
from prompt_toolkit.document import Document
|
@@ -39,6 +40,23 @@ if TYPE_CHECKING:
|
|
39
40
|
log = logging.getLogger(__name__)
|
40
41
|
|
41
42
|
|
43
|
+
@lru_cache
|
44
|
+
def _parse_cmd(text: str) -> tuple[Command | None, str]:
|
45
|
+
"""Parse a command line to command and arguments.
|
46
|
+
|
47
|
+
Command names cannot start with digits, so lines staring with digits have an empty
|
48
|
+
command string (this is used for the go-to-cell/go-to-line shortcuts).
|
49
|
+
"""
|
50
|
+
if match := re.fullmatch(r"^(?P<cmd>[^\d][^\s]*|)\s*(?P<args>.*)$", text):
|
51
|
+
cmd, args = match.groups()
|
52
|
+
else:
|
53
|
+
cmd, args = "", ""
|
54
|
+
try:
|
55
|
+
return get_cmd(cmd), args
|
56
|
+
except KeyError:
|
57
|
+
return None, args
|
58
|
+
|
59
|
+
|
42
60
|
class CommandCompleter(Completer):
|
43
61
|
"""Completer of commands."""
|
44
62
|
|
@@ -49,7 +67,11 @@ class CommandCompleter(Completer):
|
|
49
67
|
prefix = document.text
|
50
68
|
found_so_far: set[Command] = set()
|
51
69
|
for alias, command in commands.items():
|
52
|
-
if
|
70
|
+
if (
|
71
|
+
alias.startswith(prefix)
|
72
|
+
and command not in found_so_far
|
73
|
+
and not command.hidden()
|
74
|
+
):
|
53
75
|
yield Completion(
|
54
76
|
command.name,
|
55
77
|
start_position=-len(prefix),
|
@@ -105,21 +127,18 @@ class CommandBar:
|
|
105
127
|
|
106
128
|
def _validate(self, text: str) -> bool:
|
107
129
|
"""Verify that a valid command has been entered."""
|
108
|
-
cmd,
|
109
|
-
|
110
|
-
get_cmd(cmd)
|
111
|
-
except KeyError:
|
112
|
-
return False
|
113
|
-
else:
|
114
|
-
return True
|
130
|
+
cmd, _args = _parse_cmd(text)
|
131
|
+
return bool(cmd)
|
115
132
|
|
116
133
|
def _accept(self, buffer: Buffer) -> bool:
|
117
134
|
"""Return value determines if the text is kept."""
|
118
|
-
|
119
|
-
|
135
|
+
app = get_app()
|
136
|
+
app.vi_state.input_mode = InputMode.NAVIGATION
|
137
|
+
app.layout.focus_last()
|
120
138
|
text = buffer.text.strip()
|
121
|
-
cmd,
|
122
|
-
|
139
|
+
cmd, args = _parse_cmd(text)
|
140
|
+
if cmd:
|
141
|
+
cmd.run(args)
|
123
142
|
return False
|
124
143
|
|
125
144
|
def __pt_container__(self) -> Container:
|
@@ -135,25 +154,28 @@ class CommandBar:
|
|
135
154
|
"activate-command-bar-shell-alt": "A-!",
|
136
155
|
},
|
137
156
|
"euporie.core.bars.command:CommandBar": {
|
138
|
-
"deactivate-command-bar": "escape",
|
157
|
+
"deactivate-command-bar": ["escape", "c-c"],
|
139
158
|
},
|
140
159
|
}
|
141
160
|
)
|
142
161
|
|
143
162
|
@staticmethod
|
144
163
|
@add_cmd(name="activate-command-bar-alt", hidden=True)
|
145
|
-
@add_cmd(filter=~buffer_has_focus)
|
164
|
+
@add_cmd(filter=~buffer_has_focus | vi_navigation_mode)
|
146
165
|
def _activate_command_bar(event: KeyPressEvent) -> None:
|
147
166
|
"""Enter command mode."""
|
148
167
|
event.app.layout.focus(COMMAND_BAR_BUFFER)
|
168
|
+
event.app.vi_state.input_mode = InputMode.INSERT
|
149
169
|
|
150
170
|
@staticmethod
|
151
171
|
@add_cmd(filter=~buffer_has_focus)
|
152
172
|
@add_cmd(name="activate-command-bar-shell-alt", hidden=True)
|
153
173
|
def _activate_command_bar_shell(event: KeyPressEvent) -> None:
|
154
174
|
"""Enter command mode."""
|
155
|
-
|
175
|
+
app = event.app
|
176
|
+
layout = app.layout
|
156
177
|
layout.focus(COMMAND_BAR_BUFFER)
|
178
|
+
app.vi_state.input_mode = InputMode.INSERT
|
157
179
|
if isinstance(control := layout.current_control, BufferControl):
|
158
180
|
buffer = control.buffer
|
159
181
|
buffer.text = "shell "
|
@@ -163,20 +185,24 @@ class CommandBar:
|
|
163
185
|
@add_cmd(hidden=True)
|
164
186
|
def _deactivate_command_bar(event: KeyPressEvent) -> None:
|
165
187
|
"""Exit command mode."""
|
166
|
-
|
188
|
+
app = event.app
|
189
|
+
layout = app.layout
|
167
190
|
layout.focus(COMMAND_BAR_BUFFER)
|
168
191
|
if isinstance(control := layout.current_control, BufferControl):
|
192
|
+
app.vi_state.input_mode = InputMode.NAVIGATION
|
169
193
|
buffer = control.buffer
|
170
194
|
buffer.reset()
|
171
|
-
|
195
|
+
app.layout.focus_previous()
|
172
196
|
|
173
197
|
@staticmethod
|
174
198
|
@add_cmd(aliases=["shell"])
|
175
|
-
async def _run_shell_command(
|
199
|
+
async def _run_shell_command(
|
200
|
+
event: KeyPressEvent, *cmd_arg: Unpack[tuple[str]]
|
201
|
+
) -> None:
|
176
202
|
"""Run system command."""
|
177
|
-
|
178
|
-
if
|
179
|
-
await app.run_system_command(
|
180
|
-
|
181
|
-
display_before_text=[("bold", "$ "), ("", f"{
|
203
|
+
command = " ".join(str(x) for x in cmd_arg)
|
204
|
+
if command:
|
205
|
+
await event.app.run_system_command(
|
206
|
+
command,
|
207
|
+
display_before_text=[("bold", "$ "), ("", f"{command}\n")],
|
182
208
|
)
|
euporie/core/bars/search.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import logging
|
6
|
+
import re
|
6
7
|
from typing import TYPE_CHECKING
|
7
8
|
|
8
9
|
from prompt_toolkit.buffer import Buffer
|
@@ -27,6 +28,7 @@ from euporie.core.key_binding.registry import (
|
|
27
28
|
if TYPE_CHECKING:
|
28
29
|
from prompt_toolkit.filters import FilterOrBool
|
29
30
|
from prompt_toolkit.formatted_text.base import AnyFormattedText
|
31
|
+
from prompt_toolkit.layout.controls import UIControl
|
30
32
|
|
31
33
|
log = logging.getLogger(__name__)
|
32
34
|
|
@@ -119,9 +121,17 @@ def find_searchable_controls(
|
|
119
121
|
search_buffer_control: SearchBufferControl, current_control: BufferControl | None
|
120
122
|
) -> list[BufferControl]:
|
121
123
|
"""Find list of searchable controls and the index of the next control."""
|
122
|
-
|
124
|
+
# If a tab provides a list of buffers to search, use that. Otherwise, trawl the
|
125
|
+
# layout for buffer controls with this as its search control
|
126
|
+
long_list: list[UIControl]
|
127
|
+
if tab := get_app().tab:
|
128
|
+
try:
|
129
|
+
long_list = list(tab.__pt_searchables__())
|
130
|
+
except NotImplementedError:
|
131
|
+
long_list = list(get_app().layout.find_all_controls())
|
123
132
|
next_control_index = 0
|
124
|
-
|
133
|
+
searchable_controls: list[BufferControl] = []
|
134
|
+
for control in long_list:
|
125
135
|
# Find the index of the next searchable control so we can link the search
|
126
136
|
# control to it if the currently focused control is not searchable. This is so
|
127
137
|
# that the next searchable control can be focused when search is completed.
|
@@ -134,6 +144,7 @@ def find_searchable_controls(
|
|
134
144
|
):
|
135
145
|
# Add it to our list
|
136
146
|
searchable_controls.append(control)
|
147
|
+
# Cut list based on current control index
|
137
148
|
searchable_controls = (
|
138
149
|
searchable_controls[next_control_index:]
|
139
150
|
+ searchable_controls[:next_control_index]
|
@@ -332,3 +343,33 @@ def accept_search() -> None:
|
|
332
343
|
search_buffer_control.buffer.append_to_history()
|
333
344
|
# Stop the search
|
334
345
|
stop_search()
|
346
|
+
|
347
|
+
|
348
|
+
@add_cmd()
|
349
|
+
def _replace_all(find_str: str, replace_str: str) -> None:
|
350
|
+
"""Find and replace text in all searchable buffers.
|
351
|
+
|
352
|
+
Args:
|
353
|
+
find_str: String pattern to find (will be converted to regex)
|
354
|
+
replace_str: Replacement string
|
355
|
+
"""
|
356
|
+
# Convert find string to regex pattern
|
357
|
+
pattern = re.compile(find_str)
|
358
|
+
|
359
|
+
# Get searchable controls
|
360
|
+
search_buffer_control, current_control = find_search_control()
|
361
|
+
if search_buffer_control is None:
|
362
|
+
return
|
363
|
+
searchable_controls = find_searchable_controls(
|
364
|
+
search_buffer_control, current_control
|
365
|
+
)
|
366
|
+
|
367
|
+
# Apply replacements to each buffer
|
368
|
+
for control in searchable_controls:
|
369
|
+
if isinstance(control, BufferControl):
|
370
|
+
buffer = control.buffer
|
371
|
+
text = buffer.text
|
372
|
+
new_text = pattern.sub(replace_str, text)
|
373
|
+
if new_text != text:
|
374
|
+
buffer.text = new_text
|
375
|
+
buffer.on_text_changed()
|
euporie/core/border.py
CHANGED
@@ -512,6 +512,11 @@ _GRID_CHARS = {
|
|
512
512
|
GridChar(LowerLeftQuarterLine, NoLine, NoLine, UpperRightEighthLine): " ",
|
513
513
|
GridChar(UpperRightQuarterLine, UpperRightEighthLine, NoLine, NoLine): " ",
|
514
514
|
|
515
|
+
GridChar(NoLine, NoLine, UpperRightQuarterLine, UpperRightEighthLine): "▁",
|
516
|
+
GridChar(UpperRightQuarterLine, NoLine, NoLine, UpperRightEighthLine): "▔",
|
517
|
+
GridChar(LowerLeftQuarterLine, UpperRightEighthLine, NoLine, NoLine): "▔",
|
518
|
+
GridChar(NoLine, LowerLeftEighthLine, LowerLeftQuarterLine, NoLine): "▁",
|
519
|
+
|
515
520
|
# LowerLeftQuarterLine
|
516
521
|
GridChar(LowerLeftQuarterLine, NoLine, LowerLeftQuarterLine, NoLine): "▎",
|
517
522
|
GridChar(NoLine, LowerLeftQuarterLine, NoLine, LowerLeftQuarterLine): "▂",
|
@@ -875,9 +880,9 @@ InsetGrid = (
|
|
875
880
|
|
876
881
|
OutsetGrid = (
|
877
882
|
LowerLeftEighthLine.top_edge
|
878
|
-
+
|
883
|
+
+ UpperRightQuarterLine.right_edge
|
879
884
|
+ UpperRightEighthLine.bottom_edge
|
880
|
-
+
|
885
|
+
+ LowerLeftQuarterLine.left_edge
|
881
886
|
+ ThinLine.inner
|
882
887
|
)
|
883
888
|
|
euporie/core/comm/base.py
CHANGED
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|
15
15
|
|
16
16
|
from prompt_toolkit.layout.containers import AnyContainer
|
17
17
|
|
18
|
-
from euporie.core.kernel.
|
18
|
+
from euporie.core.kernel.jupyter import JupyterKernel
|
19
19
|
from euporie.core.tabs.kernel import KernelTab
|
20
20
|
from euporie.core.widgets.cell_outputs import OutputParent
|
21
21
|
|
@@ -40,7 +40,7 @@ class CommView:
|
|
40
40
|
"""
|
41
41
|
self.container = container
|
42
42
|
self.setters: dict[str, Callable[..., None]] = dict(setters or {})
|
43
|
-
self.kernel:
|
43
|
+
self.kernel: JupyterKernel | None = None
|
44
44
|
|
45
45
|
def update(self, changes: dict[str, Any]) -> None:
|
46
46
|
"""Update the view to reflect changes in the Comm.
|