euporie 2.8.0__py3-none-any.whl → 2.8.5__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 (129) hide show
  1. euporie/console/_commands.py +143 -0
  2. euporie/console/_settings.py +58 -0
  3. euporie/console/app.py +25 -71
  4. euporie/console/tabs/console.py +267 -147
  5. euporie/core/__init__.py +1 -9
  6. euporie/core/__main__.py +31 -5
  7. euporie/core/_settings.py +104 -0
  8. euporie/core/app/__init__.py +3 -0
  9. euporie/core/app/_commands.py +70 -0
  10. euporie/core/app/_settings.py +427 -0
  11. euporie/core/{app.py → app/app.py} +214 -572
  12. euporie/core/app/base.py +51 -0
  13. euporie/core/{current.py → app/current.py} +13 -4
  14. euporie/core/app/cursor.py +35 -0
  15. euporie/core/app/dummy.py +12 -0
  16. euporie/core/app/launch.py +28 -0
  17. euporie/core/bars/__init__.py +11 -0
  18. euporie/core/bars/command.py +182 -0
  19. euporie/core/bars/menu.py +258 -0
  20. euporie/core/{widgets → bars}/search.py +154 -57
  21. euporie/core/{widgets → bars}/status.py +9 -26
  22. euporie/core/clipboard.py +19 -80
  23. euporie/core/comm/base.py +8 -6
  24. euporie/core/comm/ipywidgets.py +21 -12
  25. euporie/core/comm/registry.py +2 -1
  26. euporie/core/commands.py +11 -5
  27. euporie/core/completion.py +3 -2
  28. euporie/core/config.py +368 -341
  29. euporie/core/convert/__init__.py +0 -30
  30. euporie/core/convert/datum.py +131 -60
  31. euporie/core/convert/formats/__init__.py +31 -0
  32. euporie/core/convert/formats/ansi.py +46 -30
  33. euporie/core/convert/formats/common.py +11 -23
  34. euporie/core/convert/formats/html.py +45 -40
  35. euporie/core/convert/formats/pil.py +1 -1
  36. euporie/core/convert/formats/png.py +3 -5
  37. euporie/core/convert/formats/sixel.py +3 -3
  38. euporie/core/convert/registry.py +11 -8
  39. euporie/core/convert/utils.py +50 -23
  40. euporie/core/diagnostics.py +2 -2
  41. euporie/core/filters.py +72 -82
  42. euporie/core/format.py +13 -2
  43. euporie/core/ft/ansi.py +1 -1
  44. euporie/core/ft/html.py +36 -36
  45. euporie/core/ft/table.py +1 -3
  46. euporie/core/ft/utils.py +4 -1
  47. euporie/core/graphics.py +216 -124
  48. euporie/core/history.py +2 -2
  49. euporie/core/inspection.py +3 -2
  50. euporie/core/io.py +207 -28
  51. euporie/core/kernel/__init__.py +1 -0
  52. euporie/core/{kernel.py → kernel/client.py} +100 -139
  53. euporie/core/kernel/manager.py +114 -0
  54. euporie/core/key_binding/bindings/__init__.py +2 -8
  55. euporie/core/key_binding/bindings/basic.py +47 -7
  56. euporie/core/key_binding/bindings/completion.py +3 -8
  57. euporie/core/key_binding/bindings/micro.py +5 -7
  58. euporie/core/key_binding/bindings/mouse.py +26 -24
  59. euporie/core/key_binding/bindings/terminal.py +193 -0
  60. euporie/core/key_binding/bindings/vi.py +46 -0
  61. euporie/core/key_binding/key_processor.py +43 -2
  62. euporie/core/key_binding/registry.py +2 -0
  63. euporie/core/key_binding/utils.py +22 -2
  64. euporie/core/keys.py +7156 -92
  65. euporie/core/layout/cache.py +35 -25
  66. euporie/core/layout/containers.py +280 -74
  67. euporie/core/layout/decor.py +5 -5
  68. euporie/core/layout/mouse.py +1 -1
  69. euporie/core/layout/print.py +16 -3
  70. euporie/core/layout/scroll.py +26 -28
  71. euporie/core/log.py +75 -60
  72. euporie/core/lsp.py +118 -24
  73. euporie/core/margins.py +60 -31
  74. euporie/core/path.py +2 -1
  75. euporie/core/renderer.py +58 -17
  76. euporie/core/style.py +60 -40
  77. euporie/core/suggest.py +103 -85
  78. euporie/core/tabs/__init__.py +34 -0
  79. euporie/core/tabs/_settings.py +113 -0
  80. euporie/core/tabs/base.py +11 -435
  81. euporie/core/tabs/kernel.py +420 -0
  82. euporie/core/tabs/notebook.py +20 -54
  83. euporie/core/utils.py +98 -6
  84. euporie/core/validation.py +1 -1
  85. euporie/core/widgets/_settings.py +188 -0
  86. euporie/core/widgets/cell.py +90 -158
  87. euporie/core/widgets/cell_outputs.py +26 -37
  88. euporie/core/widgets/decor.py +11 -41
  89. euporie/core/widgets/dialog.py +55 -44
  90. euporie/core/widgets/display.py +27 -24
  91. euporie/core/widgets/file_browser.py +5 -26
  92. euporie/core/widgets/forms.py +16 -12
  93. euporie/core/widgets/inputs.py +37 -81
  94. euporie/core/widgets/layout.py +7 -6
  95. euporie/core/widgets/logo.py +49 -0
  96. euporie/core/widgets/menu.py +13 -11
  97. euporie/core/widgets/pager.py +9 -11
  98. euporie/core/widgets/palette.py +6 -6
  99. euporie/hub/app.py +52 -31
  100. euporie/notebook/_commands.py +24 -0
  101. euporie/notebook/_settings.py +107 -0
  102. euporie/notebook/app.py +109 -210
  103. euporie/notebook/filters.py +1 -1
  104. euporie/notebook/tabs/__init__.py +46 -7
  105. euporie/notebook/tabs/_commands.py +714 -0
  106. euporie/notebook/tabs/_settings.py +32 -0
  107. euporie/notebook/tabs/display.py +2 -2
  108. euporie/notebook/tabs/edit.py +12 -7
  109. euporie/notebook/tabs/json.py +3 -3
  110. euporie/notebook/tabs/log.py +1 -18
  111. euporie/notebook/tabs/notebook.py +21 -674
  112. euporie/notebook/widgets/_commands.py +11 -0
  113. euporie/notebook/widgets/_settings.py +19 -0
  114. euporie/notebook/widgets/side_bar.py +14 -34
  115. euporie/preview/_settings.py +104 -0
  116. euporie/preview/app.py +8 -30
  117. euporie/preview/tabs/notebook.py +15 -86
  118. euporie/web/tabs/web.py +4 -6
  119. euporie/web/widgets/webview.py +5 -12
  120. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/METADATA +11 -15
  121. euporie-2.8.5.dist-info/RECORD +172 -0
  122. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/WHEEL +1 -1
  123. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/entry_points.txt +2 -2
  124. {euporie-2.8.0.dist-info → euporie-2.8.5.dist-info}/licenses/LICENSE +1 -1
  125. euporie/core/launch.py +0 -59
  126. euporie/core/terminal.py +0 -527
  127. euporie-2.8.0.dist-info/RECORD +0 -146
  128. {euporie-2.8.0.data → euporie-2.8.5.data}/data/share/applications/euporie-console.desktop +0 -0
  129. {euporie-2.8.0.data → euporie-2.8.5.data}/data/share/applications/euporie-notebook.desktop +0 -0
@@ -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
- # Use a baseapp as our "DummyApplication"
22
- return BaseApp()
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,11 @@
1
+ """Defines various tool bars displayed at the bottom of apps."""
2
+
3
+ # Define toolbar buffer names
4
+
5
+ SEARCH_BAR_BUFFER = "SEARCH_BAR_BUFFER"
6
+ COMMAND_BAR_BUFFER = "COMMAND_BAR_BUFFER"
7
+
8
+ BAR_BUFFERS = {
9
+ SEARCH_BAR_BUFFER,
10
+ COMMAND_BAR_BUFFER,
11
+ }
@@ -0,0 +1,182 @@
1
+ """Define the global command toolbar."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from prompt_toolkit.buffer import Buffer
9
+ from prompt_toolkit.completion.base import Completer, Completion
10
+ from prompt_toolkit.filters import (
11
+ buffer_has_focus,
12
+ has_focus,
13
+ )
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
+ class CommandCompleter(Completer):
43
+ """Completer of commands."""
44
+
45
+ def get_completions(
46
+ self, document: Document, complete_event: CompleteEvent
47
+ ) -> Iterable[Completion]:
48
+ """Complete registered commands."""
49
+ prefix = document.text
50
+ found_so_far: set[Command] = set()
51
+ for alias, command in commands.items():
52
+ if alias.startswith(prefix) and command not in found_so_far:
53
+ yield Completion(
54
+ command.name,
55
+ start_position=-len(prefix),
56
+ display=command.name,
57
+ display_meta=command.description,
58
+ )
59
+ found_so_far.add(command)
60
+
61
+
62
+ class CommandBar:
63
+ """Command mode toolbar.
64
+
65
+ A modal editor like toolbar to allow entry of commands.
66
+ """
67
+
68
+ def __init__(self) -> None:
69
+ """Create a new command bar instance."""
70
+ self.buffer = Buffer(
71
+ completer=CommandCompleter(),
72
+ complete_while_typing=True,
73
+ name=COMMAND_BAR_BUFFER,
74
+ multiline=False,
75
+ accept_handler=self._accept,
76
+ validator=Validator.from_callable(
77
+ validate_func=self._validate,
78
+ error_message="Command not recognised",
79
+ move_cursor_to_end=True,
80
+ ),
81
+ )
82
+ self.control = BufferControl(
83
+ buffer=self.buffer,
84
+ lexer=SimpleLexer(style="class:toolbar.text"),
85
+ input_processors=[
86
+ BeforeInput(":", style="class:toolbar.title"),
87
+ HighlightSelectionProcessor(),
88
+ ],
89
+ include_default_input_processors=False,
90
+ key_bindings=load_registered_bindings(
91
+ "euporie.core.bars.command:CommandBar",
92
+ config=get_app().config,
93
+ ),
94
+ )
95
+ self.window = Window(
96
+ self.control,
97
+ height=1,
98
+ style="class:toolbar",
99
+ )
100
+
101
+ self.container = ConditionalContainer(
102
+ content=self.window,
103
+ filter=has_focus(self.buffer),
104
+ )
105
+
106
+ def _validate(self, text: str) -> bool:
107
+ """Verify that a valid command has been entered."""
108
+ cmd, _, args = text.partition(" ")
109
+ try:
110
+ get_cmd(cmd)
111
+ except KeyError:
112
+ return False
113
+ else:
114
+ return True
115
+
116
+ def _accept(self, buffer: Buffer) -> bool:
117
+ """Return value determines if the text is kept."""
118
+ # TODO - lookup and run command with args
119
+ get_app().layout.focus_last()
120
+ text = buffer.text.strip()
121
+ cmd, _, args = text.partition(" ")
122
+ get_cmd(cmd).run(args)
123
+ return False
124
+
125
+ def __pt_container__(self) -> Container:
126
+ """Magic method for widget container."""
127
+ return self.container
128
+
129
+ register_bindings(
130
+ {
131
+ "euporie.core.app.app:BaseApp": {
132
+ "activate-command-bar": ":",
133
+ "activate-command-bar-alt": "A-:",
134
+ "activate-command-bar-shell": "!",
135
+ "activate-command-bar-shell-alt": "A-!",
136
+ },
137
+ "euporie.core.bars.command:CommandBar": {
138
+ "deactivate-command-bar": "escape",
139
+ },
140
+ }
141
+ )
142
+
143
+ @staticmethod
144
+ @add_cmd(name="activate-command-bar-alt", hidden=True)
145
+ @add_cmd(filter=~buffer_has_focus)
146
+ def _activate_command_bar(event: KeyPressEvent) -> None:
147
+ """Enter command mode."""
148
+ event.app.layout.focus(COMMAND_BAR_BUFFER)
149
+
150
+ @staticmethod
151
+ @add_cmd(filter=~buffer_has_focus)
152
+ @add_cmd(name="activate-command-bar-shell-alt", hidden=True)
153
+ def _activate_command_bar_shell(event: KeyPressEvent) -> None:
154
+ """Enter command mode."""
155
+ layout = event.app.layout
156
+ layout.focus(COMMAND_BAR_BUFFER)
157
+ if isinstance(control := layout.current_control, BufferControl):
158
+ buffer = control.buffer
159
+ buffer.text = "shell "
160
+ buffer.cursor_position = 6
161
+
162
+ @staticmethod
163
+ @add_cmd(hidden=True)
164
+ def _deactivate_command_bar(event: KeyPressEvent) -> None:
165
+ """Exit command mode."""
166
+ layout = event.app.layout
167
+ layout.focus(COMMAND_BAR_BUFFER)
168
+ if isinstance(control := layout.current_control, BufferControl):
169
+ buffer = control.buffer
170
+ buffer.reset()
171
+ event.app.layout.focus_previous()
172
+
173
+ @staticmethod
174
+ @add_cmd(aliases=["shell"])
175
+ async def _run_shell_command(event: KeyPressEvent) -> None:
176
+ """Run system command."""
177
+ app = event.app
178
+ if event._arg:
179
+ await app.run_system_command(
180
+ event._arg,
181
+ display_before_text=[("bold", "$ "), ("", f"{event._arg}\n")],
182
+ )
@@ -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
+ )