euporie 2.8.4__py3-none-any.whl → 2.8.6__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/_commands.py +143 -0
- euporie/console/_settings.py +58 -0
- euporie/console/app.py +25 -71
- euporie/console/tabs/console.py +58 -62
- euporie/core/__init__.py +1 -1
- euporie/core/__main__.py +28 -11
- euporie/core/_settings.py +109 -0
- euporie/core/app/__init__.py +3 -0
- euporie/core/app/_commands.py +95 -0
- euporie/core/app/_settings.py +457 -0
- euporie/core/{app.py → app/app.py} +212 -576
- euporie/core/app/base.py +51 -0
- euporie/core/{current.py → app/current.py} +13 -4
- euporie/core/app/cursor.py +35 -0
- euporie/core/app/dummy.py +12 -0
- euporie/core/app/launch.py +28 -0
- euporie/core/bars/__init__.py +11 -0
- euporie/core/bars/command.py +205 -0
- euporie/core/bars/menu.py +258 -0
- euporie/core/{widgets → bars}/search.py +20 -16
- euporie/core/{widgets → bars}/status.py +6 -23
- euporie/core/clipboard.py +19 -80
- euporie/core/comm/base.py +8 -6
- euporie/core/comm/ipywidgets.py +16 -7
- euporie/core/comm/registry.py +2 -1
- euporie/core/commands.py +10 -20
- euporie/core/completion.py +3 -2
- euporie/core/config.py +368 -341
- euporie/core/convert/__init__.py +0 -30
- euporie/core/convert/datum.py +116 -53
- euporie/core/convert/formats/__init__.py +31 -0
- euporie/core/convert/formats/ansi.py +9 -23
- euporie/core/convert/formats/common.py +11 -23
- euporie/core/convert/formats/html.py +45 -40
- euporie/core/convert/formats/pil.py +1 -1
- euporie/core/convert/formats/png.py +3 -5
- euporie/core/convert/formats/sixel.py +3 -3
- euporie/core/convert/registry.py +4 -6
- euporie/core/convert/utils.py +41 -4
- euporie/core/diagnostics.py +2 -2
- euporie/core/filters.py +98 -40
- euporie/core/format.py +2 -3
- euporie/core/ft/ansi.py +1 -1
- euporie/core/ft/html.py +12 -21
- euporie/core/ft/table.py +1 -3
- euporie/core/ft/utils.py +4 -1
- euporie/core/graphics.py +386 -133
- euporie/core/history.py +2 -2
- euporie/core/inspection.py +3 -2
- euporie/core/io.py +207 -28
- euporie/core/kernel/__init__.py +1 -0
- euporie/core/{kernel.py → kernel/client.py} +45 -108
- euporie/core/kernel/manager.py +114 -0
- euporie/core/key_binding/bindings/__init__.py +1 -8
- euporie/core/key_binding/bindings/basic.py +47 -7
- euporie/core/key_binding/bindings/completion.py +3 -8
- euporie/core/key_binding/bindings/micro.py +1 -6
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/key_binding/bindings/terminal.py +193 -0
- euporie/core/key_binding/key_processor.py +43 -2
- euporie/core/key_binding/registry.py +2 -0
- euporie/core/key_binding/utils.py +22 -2
- euporie/core/keys.py +7156 -93
- euporie/core/layout/cache.py +3 -3
- euporie/core/layout/containers.py +48 -4
- euporie/core/layout/decor.py +2 -2
- euporie/core/layout/mouse.py +1 -1
- euporie/core/layout/print.py +2 -1
- euporie/core/layout/scroll.py +39 -34
- euporie/core/log.py +76 -64
- euporie/core/lsp.py +118 -24
- euporie/core/margins.py +1 -1
- euporie/core/path.py +62 -13
- euporie/core/renderer.py +58 -17
- euporie/core/style.py +57 -39
- euporie/core/suggest.py +103 -85
- euporie/core/tabs/__init__.py +32 -0
- euporie/core/tabs/_settings.py +113 -0
- euporie/core/tabs/base.py +80 -470
- euporie/core/tabs/kernel.py +419 -0
- euporie/core/tabs/notebook.py +24 -101
- euporie/core/utils.py +92 -15
- euporie/core/validation.py +1 -1
- euporie/core/widgets/_settings.py +188 -0
- euporie/core/widgets/cell.py +19 -50
- euporie/core/widgets/cell_outputs.py +25 -36
- euporie/core/widgets/decor.py +11 -41
- euporie/core/widgets/dialog.py +62 -27
- euporie/core/widgets/display.py +12 -15
- euporie/core/widgets/file_browser.py +2 -23
- euporie/core/widgets/forms.py +8 -5
- euporie/core/widgets/inputs.py +13 -70
- euporie/core/widgets/layout.py +2 -1
- euporie/core/widgets/logo.py +49 -0
- euporie/core/widgets/menu.py +10 -8
- euporie/core/widgets/pager.py +6 -10
- euporie/core/widgets/palette.py +6 -6
- euporie/hub/app.py +52 -35
- euporie/notebook/_commands.py +24 -0
- euporie/notebook/_settings.py +107 -0
- euporie/notebook/app.py +49 -171
- euporie/notebook/filters.py +1 -1
- euporie/notebook/tabs/__init__.py +46 -7
- euporie/notebook/tabs/_commands.py +714 -0
- euporie/notebook/tabs/_settings.py +32 -0
- euporie/notebook/tabs/display.py +4 -4
- euporie/notebook/tabs/edit.py +11 -44
- euporie/notebook/tabs/json.py +5 -5
- euporie/notebook/tabs/log.py +1 -18
- euporie/notebook/tabs/notebook.py +11 -660
- euporie/notebook/widgets/_commands.py +11 -0
- euporie/notebook/widgets/_settings.py +19 -0
- euporie/notebook/widgets/side_bar.py +14 -34
- euporie/preview/_settings.py +104 -0
- euporie/preview/app.py +6 -31
- euporie/preview/tabs/notebook.py +6 -72
- euporie/web/__init__.py +1 -0
- euporie/web/tabs/__init__.py +14 -0
- euporie/web/tabs/web.py +11 -6
- euporie/web/widgets/__init__.py +1 -0
- euporie/web/widgets/webview.py +5 -15
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/METADATA +10 -8
- euporie-2.8.6.dist-info/RECORD +175 -0
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/WHEEL +1 -1
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/entry_points.txt +2 -2
- {euporie-2.8.4.dist-info → euporie-2.8.6.dist-info}/licenses/LICENSE +1 -1
- euporie/core/launch.py +0 -64
- euporie/core/terminal.py +0 -522
- euporie-2.8.4.dist-info/RECORD +0 -147
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.4.data → euporie-2.8.6.data}/data/share/applications/euporie-notebook.desktop +0 -0
euporie/core/app/base.py
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
"""Define a base class for configurable apps."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from abc import ABC
|
6
|
+
from inspect import isabstract
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from typing import Any, ClassVar
|
11
|
+
|
12
|
+
from euporie.core.config import Config
|
13
|
+
|
14
|
+
|
15
|
+
class ConfigurableApp(ABC):
|
16
|
+
"""An application with configuration."""
|
17
|
+
|
18
|
+
name: str | None = None
|
19
|
+
config: Config
|
20
|
+
_config_defaults: ClassVar[dict[str, Any]] = {"log_level_stdout": "error"}
|
21
|
+
|
22
|
+
def __init_subclass__(cls) -> None:
|
23
|
+
"""Create a config instance for each non-abstract subclass."""
|
24
|
+
if not isabstract(cls):
|
25
|
+
from euporie.core.config import Config
|
26
|
+
|
27
|
+
# Load settings
|
28
|
+
cls.load_settings()
|
29
|
+
cls.config = Config(
|
30
|
+
app=cls.name,
|
31
|
+
_help=cls.__doc__ or "",
|
32
|
+
**cls._config_defaults,
|
33
|
+
)
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
def load_settings(cls) -> None:
|
37
|
+
"""Load all known settings for this class."""
|
38
|
+
from euporie.core.utils import import_submodules, root_module
|
39
|
+
|
40
|
+
roots = {
|
41
|
+
root_module(base.__module__)
|
42
|
+
for base in cls.__mro__
|
43
|
+
if base.__module__.startswith("euporie.")
|
44
|
+
}
|
45
|
+
for root in roots:
|
46
|
+
import_submodules(root, ("_settings", "_commands"))
|
47
|
+
|
48
|
+
@classmethod
|
49
|
+
def launch(cls) -> None:
|
50
|
+
"""Launch the app."""
|
51
|
+
cls.config.load()
|
@@ -7,16 +7,25 @@ from typing import TYPE_CHECKING
|
|
7
7
|
from prompt_toolkit.application.current import _current_app_session
|
8
8
|
|
9
9
|
if TYPE_CHECKING:
|
10
|
-
from euporie.core.app import BaseApp
|
10
|
+
from euporie.core.app.app import BaseApp
|
11
11
|
|
12
12
|
|
13
13
|
def get_app() -> BaseApp:
|
14
14
|
"""Get the current active (running) Application."""
|
15
|
-
from euporie.core.app import BaseApp
|
15
|
+
from euporie.core.app.app import BaseApp
|
16
16
|
|
17
17
|
session = _current_app_session.get()
|
18
18
|
if isinstance(session.app, BaseApp):
|
19
19
|
return session.app
|
20
20
|
|
21
|
-
#
|
22
|
-
|
21
|
+
# Create a dummy application if we really need one
|
22
|
+
from euporie.core.app.dummy import DummyApp
|
23
|
+
|
24
|
+
return DummyApp()
|
25
|
+
|
26
|
+
|
27
|
+
def get_app_cls(name: str) -> BaseApp:
|
28
|
+
"""Load a euporie app by name."""
|
29
|
+
from euporie.core.__main__ import available_apps
|
30
|
+
|
31
|
+
return available_apps()[name].load()
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""Define configurable cursors."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
from prompt_toolkit.cursor_shapes import CursorShape, CursorShapeConfig
|
8
|
+
|
9
|
+
from euporie.core.filters import insert_mode, replace_mode
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from typing import Any
|
13
|
+
|
14
|
+
from prompt_toolkit.application.application import Application
|
15
|
+
|
16
|
+
|
17
|
+
class CursorConfig(CursorShapeConfig):
|
18
|
+
"""Determine which cursor mode to use."""
|
19
|
+
|
20
|
+
def get_cursor_shape(self, app: Application[Any]) -> CursorShape:
|
21
|
+
"""Return the cursor shape to be used in the current state."""
|
22
|
+
from euporie.core.app.app import BaseApp
|
23
|
+
|
24
|
+
if isinstance(app, BaseApp) and app.config.set_cursor_shape:
|
25
|
+
if insert_mode():
|
26
|
+
if app.config.cursor_blink:
|
27
|
+
return CursorShape.BLINKING_BEAM
|
28
|
+
else:
|
29
|
+
return CursorShape.BEAM
|
30
|
+
elif replace_mode():
|
31
|
+
if app.config.cursor_blink:
|
32
|
+
return CursorShape.BLINKING_UNDERLINE
|
33
|
+
else:
|
34
|
+
return CursorShape.UNDERLINE
|
35
|
+
return CursorShape.BLOCK
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"""Define a dummy application."""
|
2
|
+
|
3
|
+
from euporie.core.app.app import BaseApp
|
4
|
+
from euporie.core.layout.containers import DummyContainer
|
5
|
+
|
6
|
+
|
7
|
+
class DummyApp(BaseApp):
|
8
|
+
"""An empty application which does nothing."""
|
9
|
+
|
10
|
+
def load_container(self) -> DummyContainer:
|
11
|
+
"""Load a dummy container as the root container."""
|
12
|
+
return DummyContainer()
|
@@ -0,0 +1,28 @@
|
|
1
|
+
"""Define a simple app for launching euporie apps."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from euporie.core.app import APP_ALIASES
|
6
|
+
from euporie.core.app.base import ConfigurableApp
|
7
|
+
|
8
|
+
|
9
|
+
class LaunchApp(ConfigurableApp):
|
10
|
+
"""Launch a euporie application."""
|
11
|
+
|
12
|
+
@classmethod
|
13
|
+
def launch(cls) -> None:
|
14
|
+
"""Launch an app."""
|
15
|
+
super().launch()
|
16
|
+
|
17
|
+
# Detect selected app
|
18
|
+
chosen_app = cls.config.app
|
19
|
+
chosen_app = APP_ALIASES.get(chosen_app, chosen_app)
|
20
|
+
|
21
|
+
# Run the application
|
22
|
+
from euporie.core.__main__ import main
|
23
|
+
|
24
|
+
main(chosen_app)
|
25
|
+
|
26
|
+
|
27
|
+
if __name__ == "__main__":
|
28
|
+
LaunchApp.launch()
|
@@ -0,0 +1,205 @@
|
|
1
|
+
"""Define the global command toolbar."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import re
|
7
|
+
from functools import lru_cache
|
8
|
+
from typing import TYPE_CHECKING
|
9
|
+
|
10
|
+
from prompt_toolkit.buffer import Buffer
|
11
|
+
from prompt_toolkit.completion.base import Completer, Completion
|
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
|
+
from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window
|
15
|
+
from prompt_toolkit.layout.controls import (
|
16
|
+
BufferControl,
|
17
|
+
)
|
18
|
+
from prompt_toolkit.layout.processors import BeforeInput, HighlightSelectionProcessor
|
19
|
+
from prompt_toolkit.lexers import SimpleLexer
|
20
|
+
from prompt_toolkit.validation import Validator
|
21
|
+
|
22
|
+
from euporie.core.app.current import get_app
|
23
|
+
from euporie.core.bars import COMMAND_BAR_BUFFER
|
24
|
+
from euporie.core.commands import add_cmd, commands, get_cmd
|
25
|
+
from euporie.core.key_binding.registry import (
|
26
|
+
load_registered_bindings,
|
27
|
+
register_bindings,
|
28
|
+
)
|
29
|
+
|
30
|
+
if TYPE_CHECKING:
|
31
|
+
from collections.abc import Iterable
|
32
|
+
|
33
|
+
from prompt_toolkit.completion.base import CompleteEvent
|
34
|
+
from prompt_toolkit.document import Document
|
35
|
+
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
36
|
+
|
37
|
+
from euporie.core.commands import Command
|
38
|
+
|
39
|
+
log = logging.getLogger(__name__)
|
40
|
+
|
41
|
+
|
42
|
+
@lru_cache
|
43
|
+
def _parse_cmd(text: str) -> tuple[Command | None, str]:
|
44
|
+
"""Parse a command line to command and arguments.
|
45
|
+
|
46
|
+
Command names cannot start with digits, so lines staring with digits have an empty
|
47
|
+
command string (this is used for the go-to-cell/go-to-line shortcuts).
|
48
|
+
"""
|
49
|
+
if match := re.fullmatch(r"^(?P<cmd>[^\d][^\s]*|)\s*(?P<args>.*)$", text):
|
50
|
+
cmd, args = match.groups()
|
51
|
+
else:
|
52
|
+
cmd, args = "", ""
|
53
|
+
try:
|
54
|
+
return get_cmd(cmd), args
|
55
|
+
except KeyError:
|
56
|
+
return None, args
|
57
|
+
|
58
|
+
|
59
|
+
class CommandCompleter(Completer):
|
60
|
+
"""Completer of commands."""
|
61
|
+
|
62
|
+
def get_completions(
|
63
|
+
self, document: Document, complete_event: CompleteEvent
|
64
|
+
) -> Iterable[Completion]:
|
65
|
+
"""Complete registered commands."""
|
66
|
+
prefix = document.text
|
67
|
+
found_so_far: set[Command] = set()
|
68
|
+
for alias, command in commands.items():
|
69
|
+
if (
|
70
|
+
alias.startswith(prefix)
|
71
|
+
and command not in found_so_far
|
72
|
+
and not command.hidden()
|
73
|
+
):
|
74
|
+
yield Completion(
|
75
|
+
command.name,
|
76
|
+
start_position=-len(prefix),
|
77
|
+
display=command.name,
|
78
|
+
display_meta=command.description,
|
79
|
+
)
|
80
|
+
found_so_far.add(command)
|
81
|
+
|
82
|
+
|
83
|
+
class CommandBar:
|
84
|
+
"""Command mode toolbar.
|
85
|
+
|
86
|
+
A modal editor like toolbar to allow entry of commands.
|
87
|
+
"""
|
88
|
+
|
89
|
+
def __init__(self) -> None:
|
90
|
+
"""Create a new command bar instance."""
|
91
|
+
self.buffer = Buffer(
|
92
|
+
completer=CommandCompleter(),
|
93
|
+
complete_while_typing=True,
|
94
|
+
name=COMMAND_BAR_BUFFER,
|
95
|
+
multiline=False,
|
96
|
+
accept_handler=self._accept,
|
97
|
+
validator=Validator.from_callable(
|
98
|
+
validate_func=self._validate,
|
99
|
+
error_message="Command not recognised",
|
100
|
+
move_cursor_to_end=True,
|
101
|
+
),
|
102
|
+
)
|
103
|
+
self.control = BufferControl(
|
104
|
+
buffer=self.buffer,
|
105
|
+
lexer=SimpleLexer(style="class:toolbar.text"),
|
106
|
+
input_processors=[
|
107
|
+
BeforeInput(":", style="class:toolbar.title"),
|
108
|
+
HighlightSelectionProcessor(),
|
109
|
+
],
|
110
|
+
include_default_input_processors=False,
|
111
|
+
key_bindings=load_registered_bindings(
|
112
|
+
"euporie.core.bars.command:CommandBar",
|
113
|
+
config=get_app().config,
|
114
|
+
),
|
115
|
+
)
|
116
|
+
self.window = Window(
|
117
|
+
self.control,
|
118
|
+
height=1,
|
119
|
+
style="class:toolbar",
|
120
|
+
)
|
121
|
+
|
122
|
+
self.container = ConditionalContainer(
|
123
|
+
content=self.window,
|
124
|
+
filter=has_focus(self.buffer),
|
125
|
+
)
|
126
|
+
|
127
|
+
def _validate(self, text: str) -> bool:
|
128
|
+
"""Verify that a valid command has been entered."""
|
129
|
+
cmd, _args = _parse_cmd(text)
|
130
|
+
return bool(cmd)
|
131
|
+
|
132
|
+
def _accept(self, buffer: Buffer) -> bool:
|
133
|
+
"""Return value determines if the text is kept."""
|
134
|
+
app = get_app()
|
135
|
+
app.vi_state.input_mode = InputMode.NAVIGATION
|
136
|
+
app.layout.focus_last()
|
137
|
+
text = buffer.text.strip()
|
138
|
+
cmd, args = _parse_cmd(text)
|
139
|
+
if cmd:
|
140
|
+
cmd.run(args)
|
141
|
+
return False
|
142
|
+
|
143
|
+
def __pt_container__(self) -> Container:
|
144
|
+
"""Magic method for widget container."""
|
145
|
+
return self.container
|
146
|
+
|
147
|
+
register_bindings(
|
148
|
+
{
|
149
|
+
"euporie.core.app.app:BaseApp": {
|
150
|
+
"activate-command-bar": ":",
|
151
|
+
"activate-command-bar-alt": "A-:",
|
152
|
+
"activate-command-bar-shell": "!",
|
153
|
+
"activate-command-bar-shell-alt": "A-!",
|
154
|
+
},
|
155
|
+
"euporie.core.bars.command:CommandBar": {
|
156
|
+
"deactivate-command-bar": ["escape", "c-c"],
|
157
|
+
},
|
158
|
+
}
|
159
|
+
)
|
160
|
+
|
161
|
+
@staticmethod
|
162
|
+
@add_cmd(name="activate-command-bar-alt", hidden=True)
|
163
|
+
@add_cmd(filter=~buffer_has_focus | vi_navigation_mode)
|
164
|
+
def _activate_command_bar(event: KeyPressEvent) -> None:
|
165
|
+
"""Enter command mode."""
|
166
|
+
event.app.layout.focus(COMMAND_BAR_BUFFER)
|
167
|
+
event.app.vi_state.input_mode = InputMode.INSERT
|
168
|
+
|
169
|
+
@staticmethod
|
170
|
+
@add_cmd(filter=~buffer_has_focus)
|
171
|
+
@add_cmd(name="activate-command-bar-shell-alt", hidden=True)
|
172
|
+
def _activate_command_bar_shell(event: KeyPressEvent) -> None:
|
173
|
+
"""Enter command mode."""
|
174
|
+
app = event.app
|
175
|
+
layout = app.layout
|
176
|
+
layout.focus(COMMAND_BAR_BUFFER)
|
177
|
+
app.vi_state.input_mode = InputMode.INSERT
|
178
|
+
if isinstance(control := layout.current_control, BufferControl):
|
179
|
+
buffer = control.buffer
|
180
|
+
buffer.text = "shell "
|
181
|
+
buffer.cursor_position = 6
|
182
|
+
|
183
|
+
@staticmethod
|
184
|
+
@add_cmd(hidden=True)
|
185
|
+
def _deactivate_command_bar(event: KeyPressEvent) -> None:
|
186
|
+
"""Exit command mode."""
|
187
|
+
app = event.app
|
188
|
+
layout = app.layout
|
189
|
+
layout.focus(COMMAND_BAR_BUFFER)
|
190
|
+
if isinstance(control := layout.current_control, BufferControl):
|
191
|
+
app.vi_state.input_mode = InputMode.NAVIGATION
|
192
|
+
buffer = control.buffer
|
193
|
+
buffer.reset()
|
194
|
+
app.layout.focus_previous()
|
195
|
+
|
196
|
+
@staticmethod
|
197
|
+
@add_cmd(aliases=["shell"])
|
198
|
+
async def _run_shell_command(event: KeyPressEvent) -> None:
|
199
|
+
"""Run system command."""
|
200
|
+
app = event.app
|
201
|
+
if event._arg:
|
202
|
+
await app.run_system_command(
|
203
|
+
event._arg,
|
204
|
+
display_before_text=[("bold", "$ "), ("", f"{event._arg}\n")],
|
205
|
+
)
|
@@ -0,0 +1,258 @@
|
|
1
|
+
"""Contains a completion menu for toolbars."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from math import ceil
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
from prompt_toolkit.data_structures import Point
|
10
|
+
from prompt_toolkit.filters import Condition, has_completions, is_done
|
11
|
+
from prompt_toolkit.layout.containers import ConditionalContainer, Window
|
12
|
+
from prompt_toolkit.layout.controls import GetLinePrefixCallable, UIContent, UIControl
|
13
|
+
from prompt_toolkit.layout.dimension import Dimension
|
14
|
+
from prompt_toolkit.utils import get_cwidth
|
15
|
+
|
16
|
+
from euporie.core.app.current import get_app
|
17
|
+
from euporie.core.filters import has_toolbar
|
18
|
+
from euporie.core.ft.utils import apply_style, pad, truncate
|
19
|
+
from euporie.core.layout.containers import HSplit
|
20
|
+
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from prompt_toolkit.buffer import CompletionState
|
23
|
+
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
24
|
+
from prompt_toolkit.key_binding.key_bindings import (
|
25
|
+
KeyBindingsBase,
|
26
|
+
NotImplementedOrNone,
|
27
|
+
)
|
28
|
+
from prompt_toolkit.mouse_events import MouseEvent
|
29
|
+
|
30
|
+
log = logging.getLogger(__name__)
|
31
|
+
|
32
|
+
|
33
|
+
class ToolbarCompletionMenuControl(UIControl):
|
34
|
+
"""A completion menu for toolbars."""
|
35
|
+
|
36
|
+
def __init__(self, min_item_width: int = 5, max_item_width: int = 30) -> None:
|
37
|
+
"""Define minimum and maximum item widhth."""
|
38
|
+
self.max_item_width = max_item_width
|
39
|
+
self.min_item_width = min_item_width
|
40
|
+
|
41
|
+
def preferred_width(self, max_available_width: int) -> int | None:
|
42
|
+
"""Fill available width."""
|
43
|
+
return max_available_width
|
44
|
+
|
45
|
+
def preferred_height(
|
46
|
+
self,
|
47
|
+
width: int,
|
48
|
+
max_available_height: int,
|
49
|
+
wrap_lines: bool,
|
50
|
+
get_line_prefix: GetLinePrefixCallable | None,
|
51
|
+
) -> int | None:
|
52
|
+
"""Calculate how many rows to use, filling the width first then overflowing."""
|
53
|
+
complete_state = get_app().current_buffer.complete_state
|
54
|
+
if complete_state is None:
|
55
|
+
return 0
|
56
|
+
|
57
|
+
col_width = self._get_col_width(complete_state, width, max_available_height)
|
58
|
+
height = min(
|
59
|
+
ceil(col_width * len(complete_state.completions) / width),
|
60
|
+
max_available_height,
|
61
|
+
)
|
62
|
+
return height
|
63
|
+
|
64
|
+
def _get_col_width(
|
65
|
+
self, complete_state: CompletionState, width: int, height: int
|
66
|
+
) -> int:
|
67
|
+
"""Calculate the optimal width for the items in the menu."""
|
68
|
+
completions = complete_state.completions
|
69
|
+
item_width = max(
|
70
|
+
min(
|
71
|
+
max(get_cwidth(c.display_text) + 3 for c in completions),
|
72
|
+
self.max_item_width,
|
73
|
+
),
|
74
|
+
self.min_item_width,
|
75
|
+
)
|
76
|
+
col_count = width // item_width
|
77
|
+
# With an overflow reduce column width to show more of the truncated column
|
78
|
+
if len(completions) > col_count * height:
|
79
|
+
col_width = min((width - 6) // col_count, item_width)
|
80
|
+
# With an exact width expand columns to fill the space
|
81
|
+
elif len(completions) == col_count * height:
|
82
|
+
col_width = max(width // col_count, item_width)
|
83
|
+
# Otherwise use the calculated item width
|
84
|
+
else:
|
85
|
+
col_width = item_width
|
86
|
+
return col_width
|
87
|
+
|
88
|
+
def create_content(self, width: int, height: int) -> UIContent:
|
89
|
+
"""Create a UIContent object for this control."""
|
90
|
+
complete_state = get_app().current_buffer.complete_state
|
91
|
+
if complete_state is None:
|
92
|
+
return UIContent()
|
93
|
+
|
94
|
+
completions = complete_state.completions
|
95
|
+
index = complete_state.complete_index # Can be None!
|
96
|
+
|
97
|
+
# Calculate width of completions menu.
|
98
|
+
col_width = self._get_col_width(complete_state, width, height)
|
99
|
+
# Calculate offset to ensure active completion is visible
|
100
|
+
cur_col = (index or 0) // height
|
101
|
+
visible_cols = width // col_width
|
102
|
+
offset = max(0, cur_col - visible_cols + 1) * height
|
103
|
+
|
104
|
+
# Pad and style visible items
|
105
|
+
items: list[StyleAndTextTuples] = []
|
106
|
+
item: StyleAndTextTuples
|
107
|
+
for i in range(offset, offset + ((visible_cols + 1) * height)):
|
108
|
+
if i < len(completions):
|
109
|
+
item = completions[i].display
|
110
|
+
item = truncate(item, col_width - 3)
|
111
|
+
item = pad(item, width=col_width - 3)
|
112
|
+
item = [("", " "), *item, ("", " ")]
|
113
|
+
item = apply_style(item, "class:completion")
|
114
|
+
if i == index:
|
115
|
+
item = apply_style(item, "class:current")
|
116
|
+
item = [*item, ("", " ")]
|
117
|
+
else:
|
118
|
+
item = [("", " " * col_width)]
|
119
|
+
items.append(item)
|
120
|
+
|
121
|
+
# Construct rows
|
122
|
+
overflow_left = offset > height
|
123
|
+
overflow_right = (len(completions) - offset) - (visible_cols * height) > 0
|
124
|
+
lines: list[StyleAndTextTuples] = (
|
125
|
+
[
|
126
|
+
[("class:overflow", "◀" if i == height // 2 else " ")]
|
127
|
+
for i in range(height)
|
128
|
+
]
|
129
|
+
if overflow_left
|
130
|
+
else [[] for _ in range(height)]
|
131
|
+
)
|
132
|
+
for i, item in enumerate(items):
|
133
|
+
row = i % height
|
134
|
+
col = i // height
|
135
|
+
if col == visible_cols:
|
136
|
+
item = [
|
137
|
+
*truncate(
|
138
|
+
item,
|
139
|
+
width - overflow_left - overflow_right - col_width * col,
|
140
|
+
placeholder="",
|
141
|
+
),
|
142
|
+
("class:overflow", "▶" if row == height // 2 else " "),
|
143
|
+
]
|
144
|
+
lines[row].extend(item)
|
145
|
+
|
146
|
+
def get_line(i: int) -> StyleAndTextTuples:
|
147
|
+
return lines[i]
|
148
|
+
|
149
|
+
return UIContent(
|
150
|
+
get_line=get_line,
|
151
|
+
cursor_position=Point(x=0, y=0), # y=index or 0),
|
152
|
+
line_count=len(lines),
|
153
|
+
)
|
154
|
+
|
155
|
+
def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
|
156
|
+
"""Handle mouse events.
|
157
|
+
|
158
|
+
When `NotImplemented` is returned, it means that the given event is not
|
159
|
+
handled by the `UIControl` itself. The `Window` or key bindings can
|
160
|
+
decide to handle this event as scrolling or changing focus.
|
161
|
+
|
162
|
+
:param mouse_event: `MouseEvent` instance.
|
163
|
+
"""
|
164
|
+
return NotImplemented
|
165
|
+
|
166
|
+
def move_cursor_down(self) -> None:
|
167
|
+
"""Request to move the cursor down.
|
168
|
+
|
169
|
+
This happens when scrolling down and the cursor is completely at the top.
|
170
|
+
"""
|
171
|
+
|
172
|
+
def move_cursor_up(self) -> None:
|
173
|
+
"""Request to move the cursor up."""
|
174
|
+
|
175
|
+
def get_key_bindings(self) -> KeyBindingsBase | None:
|
176
|
+
"""Key bindings that are specific for this user control.
|
177
|
+
|
178
|
+
Return a :class:`.KeyBindings` object if some key bindings are
|
179
|
+
specified, or `None` otherwise.
|
180
|
+
"""
|
181
|
+
|
182
|
+
|
183
|
+
class SelectedCompletionMetaControl(UIControl):
|
184
|
+
"""Control that shows the meta information of the selected completion."""
|
185
|
+
|
186
|
+
def preferred_width(self, max_available_width: int) -> int | None:
|
187
|
+
"""Report the width of the active meta text."""
|
188
|
+
if (
|
189
|
+
(state := get_app().current_buffer.complete_state)
|
190
|
+
and (current_completion := state.current_completion)
|
191
|
+
and (text := current_completion.display_meta_text)
|
192
|
+
):
|
193
|
+
return get_cwidth(text) + 2
|
194
|
+
return 0
|
195
|
+
|
196
|
+
def preferred_height(
|
197
|
+
self,
|
198
|
+
width: int,
|
199
|
+
max_available_height: int,
|
200
|
+
wrap_lines: bool,
|
201
|
+
get_line_prefix: GetLinePrefixCallable | None,
|
202
|
+
) -> int | None:
|
203
|
+
"""Maintain a single line."""
|
204
|
+
return 1
|
205
|
+
|
206
|
+
def create_content(self, width: int, height: int) -> UIContent:
|
207
|
+
"""Format the current completion meta text."""
|
208
|
+
ft: StyleAndTextTuples = []
|
209
|
+
state = get_app().current_buffer.complete_state
|
210
|
+
if (
|
211
|
+
(state := get_app().current_buffer.complete_state)
|
212
|
+
and (current_completion := state.current_completion)
|
213
|
+
and (meta := current_completion.display_meta)
|
214
|
+
):
|
215
|
+
ft = apply_style([("", " "), *meta, ("", " ")], style="class:meta")
|
216
|
+
|
217
|
+
def get_line(i: int) -> StyleAndTextTuples:
|
218
|
+
return ft
|
219
|
+
|
220
|
+
return UIContent(get_line=get_line, line_count=1 if ft else 0)
|
221
|
+
|
222
|
+
|
223
|
+
class ToolbarCompletionsMenu(ConditionalContainer):
|
224
|
+
"""A completion menu widget for toolbars."""
|
225
|
+
|
226
|
+
def __init__(self) -> None:
|
227
|
+
"""Create a pre-populated conditional container."""
|
228
|
+
super().__init__(
|
229
|
+
content=HSplit(
|
230
|
+
[
|
231
|
+
ConditionalContainer(
|
232
|
+
Window(
|
233
|
+
content=SelectedCompletionMetaControl(),
|
234
|
+
height=1,
|
235
|
+
dont_extend_width=True,
|
236
|
+
),
|
237
|
+
filter=Condition(
|
238
|
+
lambda: bool(
|
239
|
+
(
|
240
|
+
complete_state
|
241
|
+
:= get_app().current_buffer.complete_state
|
242
|
+
)
|
243
|
+
and (completion := complete_state.current_completion)
|
244
|
+
and completion.display_meta
|
245
|
+
)
|
246
|
+
),
|
247
|
+
),
|
248
|
+
Window(
|
249
|
+
content=ToolbarCompletionMenuControl(),
|
250
|
+
height=Dimension(min=1, max=8),
|
251
|
+
dont_extend_height=True,
|
252
|
+
dont_extend_width=False,
|
253
|
+
),
|
254
|
+
],
|
255
|
+
style="class:toolbar,menu",
|
256
|
+
),
|
257
|
+
filter=has_toolbar & has_completions & ~is_done,
|
258
|
+
)
|