euporie 2.8.6__py3-none-any.whl → 2.8.8__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 (65) hide show
  1. euporie/console/app.py +2 -0
  2. euporie/console/tabs/console.py +27 -17
  3. euporie/core/__init__.py +2 -2
  4. euporie/core/app/_commands.py +4 -21
  5. euporie/core/app/app.py +13 -7
  6. euporie/core/bars/command.py +9 -6
  7. euporie/core/bars/search.py +43 -2
  8. euporie/core/border.py +7 -2
  9. euporie/core/comm/base.py +2 -2
  10. euporie/core/comm/ipywidgets.py +3 -3
  11. euporie/core/commands.py +44 -8
  12. euporie/core/completion.py +14 -6
  13. euporie/core/convert/datum.py +7 -7
  14. euporie/core/data_structures.py +20 -1
  15. euporie/core/filters.py +8 -0
  16. euporie/core/ft/html.py +47 -40
  17. euporie/core/graphics.py +11 -3
  18. euporie/core/history.py +15 -5
  19. euporie/core/inspection.py +16 -9
  20. euporie/core/io.py +1 -1
  21. euporie/core/kernel/__init__.py +53 -1
  22. euporie/core/kernel/base.py +571 -0
  23. euporie/core/kernel/{client.py → jupyter.py} +173 -430
  24. euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
  25. euporie/core/kernel/local.py +694 -0
  26. euporie/core/key_binding/bindings/basic.py +6 -3
  27. euporie/core/keys.py +26 -25
  28. euporie/core/layout/cache.py +31 -7
  29. euporie/core/layout/containers.py +88 -13
  30. euporie/core/layout/scroll.py +45 -148
  31. euporie/core/log.py +1 -1
  32. euporie/core/style.py +2 -1
  33. euporie/core/suggest.py +155 -74
  34. euporie/core/tabs/__init__.py +10 -0
  35. euporie/core/tabs/_commands.py +76 -0
  36. euporie/core/tabs/_settings.py +16 -0
  37. euporie/core/tabs/base.py +22 -8
  38. euporie/core/tabs/kernel.py +81 -35
  39. euporie/core/tabs/notebook.py +14 -22
  40. euporie/core/utils.py +1 -1
  41. euporie/core/validation.py +8 -8
  42. euporie/core/widgets/_settings.py +19 -2
  43. euporie/core/widgets/cell.py +31 -31
  44. euporie/core/widgets/cell_outputs.py +10 -1
  45. euporie/core/widgets/dialog.py +30 -75
  46. euporie/core/widgets/forms.py +71 -59
  47. euporie/core/widgets/inputs.py +7 -4
  48. euporie/core/widgets/layout.py +281 -93
  49. euporie/core/widgets/menu.py +55 -15
  50. euporie/core/widgets/palette.py +3 -1
  51. euporie/core/widgets/tree.py +86 -76
  52. euporie/notebook/app.py +35 -16
  53. euporie/notebook/tabs/edit.py +4 -4
  54. euporie/notebook/tabs/json.py +6 -2
  55. euporie/notebook/tabs/notebook.py +26 -8
  56. euporie/preview/tabs/notebook.py +17 -13
  57. euporie/web/tabs/web.py +22 -3
  58. euporie/web/widgets/webview.py +3 -0
  59. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/METADATA +1 -1
  60. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/RECORD +65 -62
  61. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/entry_points.txt +1 -1
  62. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/licenses/LICENSE +1 -1
  63. {euporie-2.8.6.data → euporie-2.8.8.data}/data/share/applications/euporie-console.desktop +0 -0
  64. {euporie-2.8.6.data → euporie-2.8.8.data}/data/share/applications/euporie-notebook.desktop +0 -0
  65. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/WHEEL +0 -0
@@ -4,14 +4,15 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  from abc import ABCMeta, abstractmethod
7
- from functools import partial
8
- from typing import TYPE_CHECKING, NamedTuple, cast
7
+ from functools import lru_cache, partial
8
+ from typing import TYPE_CHECKING, ClassVar, NamedTuple, cast
9
9
 
10
10
  from prompt_toolkit.application.current import get_app
11
11
  from prompt_toolkit.cache import SimpleCache
12
12
  from prompt_toolkit.filters import Condition, to_filter
13
13
  from prompt_toolkit.formatted_text.base import to_formatted_text
14
14
  from prompt_toolkit.formatted_text.utils import fragment_list_width
15
+ from prompt_toolkit.key_binding.key_bindings import KeyBindings
15
16
  from prompt_toolkit.layout.containers import (
16
17
  ConditionalContainer,
17
18
  DynamicContainer,
@@ -25,6 +26,7 @@ from prompt_toolkit.layout.controls import (
25
26
  )
26
27
  from prompt_toolkit.layout.dimension import Dimension as D
27
28
  from prompt_toolkit.layout.dimension import to_dimension
29
+ from prompt_toolkit.layout.utils import explode_text_fragments
28
30
  from prompt_toolkit.mouse_events import MouseButton, MouseEventType
29
31
  from prompt_toolkit.utils import Event
30
32
 
@@ -39,11 +41,16 @@ if TYPE_CHECKING:
39
41
  from typing import Any, Callable
40
42
 
41
43
  from prompt_toolkit.filters import FilterOrBool
42
- from prompt_toolkit.formatted_text.base import AnyFormattedText, StyleAndTextTuples
44
+ from prompt_toolkit.formatted_text.base import (
45
+ AnyFormattedText,
46
+ OneStyleAndTextTuple,
47
+ StyleAndTextTuples,
48
+ )
43
49
  from prompt_toolkit.key_binding.key_bindings import (
44
50
  KeyBindingsBase,
45
51
  NotImplementedOrNone,
46
52
  )
53
+ from prompt_toolkit.key_binding.key_processor import KeyPressEvent
47
54
  from prompt_toolkit.layout.containers import AnyContainer, Container, _Split
48
55
  from prompt_toolkit.layout.dimension import AnyDimension
49
56
  from prompt_toolkit.mouse_events import MouseEvent
@@ -199,30 +206,35 @@ class ReferencedSplit:
199
206
 
200
207
 
201
208
  class TabBarTab(NamedTuple):
202
- """A named tuple represting a tab and it's callbacks."""
209
+ """A class representing a tab and its callbacks."""
203
210
 
204
211
  title: AnyFormattedText
205
- on_activate: Callable
206
- on_deactivate: Callable | None = None
207
- on_close: Callable | None = None
212
+ on_activate: Callable[[], NotImplementedOrNone]
213
+ on_deactivate: Callable[[], NotImplementedOrNone] | None = None
214
+ on_close: Callable[[], NotImplementedOrNone] | None = None
215
+ closeable: bool = False
216
+
217
+ def __hash__(self) -> int:
218
+ """Hash the Tab based on current title value."""
219
+ return hash(tuple(to_formatted_text(self.title))) * hash(
220
+ (self.on_activate, self.on_deactivate, self.on_close, self.closeable)
221
+ )
208
222
 
209
223
 
210
224
  class TabBarControl(UIControl):
211
225
  """A control which shows a tab bar."""
212
226
 
213
- char_bottom = ""
214
- char_left = ""
215
- char_right = ""
216
- char_top = "▁"
217
- char_close = "✖"
227
+ char_scroll_left = ""
228
+ char_scroll_right = ""
229
+ char_close: ClassVar[str] = ""
218
230
 
219
231
  def __init__(
220
232
  self,
221
233
  tabs: Sequence[TabBarTab] | Callable[[], Sequence[TabBarTab]],
222
234
  active: int | Callable[[], int],
223
235
  spacing: int = 1,
224
- closeable: bool = False,
225
236
  max_title_width: int = 30,
237
+ grid: GridStyle = OutsetGrid,
226
238
  ) -> None:
227
239
  """Create a new tab bar instance.
228
240
 
@@ -231,19 +243,22 @@ class TabBarControl(UIControl):
231
243
  when the tab is activated.
232
244
  active: The index of the currently active tab
233
245
  spacing: The number of characters between the tabs
234
- closeable: Whether to show close buttons the the tabs
235
246
  max_title_width: The maximum width of the title to display
247
+ grid: The grid style to use for drawing borders
236
248
 
237
249
  """
238
250
  self._tabs = tabs
239
251
  self.spacing = spacing
240
- self.closeable = closeable
241
252
  self.max_title_width = max_title_width
242
253
  self._active = active
243
-
244
- self.mouse_handlers: dict[int, Callable[..., Any] | None] = {}
245
-
246
- self._title_cache: SimpleCache = SimpleCache(maxsize=1)
254
+ self._last_active: int | None = None
255
+ self.scroll = -1
256
+ self.grid = grid
257
+
258
+ self.mouse_handlers: dict[int, Callable[[], NotImplementedOrNone] | None] = {}
259
+ self.tab_widths: list[int] = []
260
+ # Caches
261
+ self.render_tab = lru_cache(self._render_tab)
247
262
  self._content_cache: SimpleCache = SimpleCache(maxsize=50)
248
263
 
249
264
  @property
@@ -262,16 +277,43 @@ class TabBarControl(UIControl):
262
277
  @property
263
278
  def active(self) -> int:
264
279
  """Return the index of the active tab."""
265
- if callable(self._active):
266
- return self._active()
267
- else:
268
- return self._active
280
+ current_active = self._active() if callable(self._active) else self._active
281
+
282
+ # Check if active tab has changed
283
+ if self._last_active != current_active:
284
+ # Handle tab switching
285
+ if self._last_active is not None and 0 <= self._last_active < len(
286
+ self.tabs
287
+ ):
288
+ old_tab = self.tabs[self._last_active]
289
+ if callable(on_deactivate := old_tab.on_deactivate):
290
+ on_deactivate()
291
+
292
+ # Call on_activate for new tab
293
+ if current_active is not None and 0 <= current_active < len(self.tabs):
294
+ new_tab = self.tabs[current_active]
295
+ if callable(on_activate := new_tab.on_activate):
296
+ on_activate()
297
+
298
+ # Ensure active tab is visible
299
+ self.scroll_to(current_active)
300
+
301
+ # Update last known active value
302
+ self._last_active = current_active
303
+
304
+ return current_active
269
305
 
270
306
  @active.setter
271
307
  def active(self, active: int | Callable[[], int]) -> None:
272
308
  """Set the currently active tab."""
309
+ # Store new active value
273
310
  self._active = active
274
311
 
312
+ # If it's a direct value (not callable), handle tab switching immediately
313
+ if not callable(active):
314
+ # Force property getter to handle the change
315
+ _ = self.active
316
+
275
317
  def preferred_width(self, max_available_width: int) -> int | None:
276
318
  """Return the preferred width of the tab-bar control, the maximum available."""
277
319
  return max_available_width
@@ -292,87 +334,221 @@ class TabBarControl(UIControl):
292
334
 
293
335
  def create_content(self, width: int, height: int) -> UIContent:
294
336
  """Generate the formatted text fragments which make the controls output."""
337
+ self.available_width = width
295
338
 
296
- def get_content() -> UIContent:
297
- fragment_lines = self.render(width)
339
+ def get_content() -> tuple[
340
+ UIContent, dict[int, Callable[[], NotImplementedOrNone] | None]
341
+ ]:
342
+ *fragment_lines, mouse_handlers = self.render(width)
298
343
 
299
344
  return UIContent(
300
345
  get_line=lambda i: fragment_lines[i],
301
346
  line_count=len(fragment_lines),
302
347
  show_cursor=False,
303
- )
348
+ ), mouse_handlers
349
+
350
+ key = (hash(tuple(self.tabs)), width, self.active, self.scroll)
351
+ ui_content, self.mouse_handlers = self._content_cache.get(key, get_content)
352
+ return ui_content
353
+
354
+ def scroll_to(self, active: int) -> None:
355
+ """Adjust scroll position to ensure the active tab is visible."""
356
+ # Calculate position of active tab
357
+ pos = self.spacing # Initial spacing
358
+ for i in range(len(self.tabs)):
359
+ if i == active:
360
+ # Found active tab - check if it's visible
361
+ tab_width = self.tab_widths[i] if i < len(self.tab_widths) else 0
362
+ # Scroll left if tab start is before visible area
363
+ if pos < self.scroll:
364
+ self.scroll = pos - self.spacing - 1
365
+ # Scroll right if tab end is after visible area
366
+ elif pos + tab_width > self.scroll + self.available_width:
367
+ self.scroll = pos + tab_width - self.available_width + 2
368
+ break
369
+
370
+ # Add tab width and spacing
371
+ pos += (
372
+ self.tab_widths[i] if i < len(self.tab_widths) else 0
373
+ ) + self.spacing
374
+
375
+ def _render_tab(
376
+ self,
377
+ title: tuple[OneStyleAndTextTuple, ...],
378
+ on_activate: Callable[[], NotImplementedOrNone],
379
+ on_deactivate: Callable[[], NotImplementedOrNone] | None,
380
+ on_close: Callable[[], NotImplementedOrNone] | None,
381
+ closeable: bool,
382
+ active: bool,
383
+ max_title_width: int,
384
+ grid: GridStyle,
385
+ ) -> tuple[
386
+ StyleAndTextTuples,
387
+ StyleAndTextTuples,
388
+ list[Callable[[], NotImplementedOrNone] | None],
389
+ ]:
390
+ """Render the tab as formatted text.
304
391
 
305
- key = (hash(tuple(self.tabs)), width, self.closeable, self.active)
306
- return self._content_cache.get(key, get_content)
392
+ Args:
393
+ title: The formatted text fragments making up the tab's title
394
+ on_activate: Callback function to run when the tab is activated
395
+ on_deactivate: Optional callback function to run when the tab is deactivated
396
+ on_close: Optional callback function to run when the tab is closed
397
+ closeable: Whether the tab can be closed
398
+ active: Whether this tab is currently active
399
+ max_title_width: Maximum width to display the tab title
400
+ grid: The grid style to use for drawing borders
307
401
 
308
- def render(self, width: int) -> list[StyleAndTextTuples]:
309
- """Render the tab-bar as linest of formatted text."""
310
- top_line: StyleAndTextTuples = []
311
- tab_line: StyleAndTextTuples = []
312
- i = 0
402
+ Returns:
403
+ Tuple containing:
404
+ - Top line formatted text fragments
405
+ - Bottom line formatted text fragments
406
+ - List of mouse handler callbacks for each character position
407
+ """
408
+ title_ft = truncate(list(title), max_title_width)
409
+ title_width = fragment_list_width(title_ft)
410
+ style = "class:active" if active else "class:inactive"
313
411
 
314
- # Initial spacing
315
- for _ in range(self.spacing):
316
- top_line += [("", " ")]
317
- tab_line += [("class:border,bottom", self.char_bottom)]
318
- i += self.spacing
319
-
320
- for j, tab in enumerate(self.tabs):
321
- title_ft = truncate(to_formatted_text(tab.title), self.max_title_width)
322
- title_width = fragment_list_width(title_ft)
323
- style = "class:active" if self.active == j else "class:inactive"
324
-
325
- # Add top edge over title
326
- top_line += [
327
- (f"{style} class:tab,border,top", self.char_top * (title_width + 2))
328
- ]
412
+ top_line: StyleAndTextTuples = explode_text_fragments([])
413
+ tab_line: StyleAndTextTuples = explode_text_fragments([])
414
+ mouse_handlers: list[Callable[[], NotImplementedOrNone] | None] = []
415
+
416
+ # Add top edge over title
417
+ top_line.append(
418
+ (f"{style} class:tab,border,top", grid.TOP_MID * (title_width + 2))
419
+ )
329
420
 
330
- # Left edge
331
- tab_line += [(f"{style} class:tab,border,left", self.char_left)]
332
- self.mouse_handlers[i] = tab.on_activate
333
- i += 1
421
+ # Left edge
422
+ tab_line.append((f"{style} class:tab,border,left", grid.MID_LEFT))
423
+ mouse_handlers.append(on_activate)
334
424
 
335
- # Title
336
- tab_line += [
425
+ # Title
426
+ tab_line.extend(
427
+ [
337
428
  (f"{style} class:tab,title {frag_style}", text)
338
429
  for frag_style, text, *_ in title_ft
339
430
  ]
340
- for _ in range(title_width):
341
- self.mouse_handlers[i] = tab.on_activate
342
- i += 1
343
-
344
- # Close button
345
- if self.closeable:
346
- top_line += [(f"{style} class:tab,border,top", self.char_top * 2)]
347
- self.mouse_handlers[i] = tab.on_activate
348
- i += 1
349
- tab_line += [
431
+ )
432
+ for _ in range(title_width):
433
+ mouse_handlers.append(on_activate)
434
+
435
+ # Close button
436
+ if closeable:
437
+ top_line.append((f"{style} class:tab,border,top", grid.TOP_MID * 2))
438
+ mouse_handlers.append(on_activate)
439
+ tab_line.extend(
440
+ [
350
441
  (f"{style} class:tab", " "),
351
442
  (f"{style} class:tab,close", self.char_close),
352
443
  ]
353
- self.mouse_handlers[i] = tab.on_close
354
- i += 1
355
-
356
- # Right edge
357
- tab_line += [(f"{style} class:tab,border,right", self.char_right)]
358
- self.mouse_handlers[i] = tab.on_activate
359
- i += 1
360
-
361
- # Spacing
362
- for _ in range(self.spacing):
363
- top_line += [("", " ")]
364
- tab_line += [("class:border,bottom", self.char_bottom)]
365
- i += 1
366
-
367
- # Add border to fill width
368
- tab_line += [
369
- (
370
- "class:border,bottom",
371
- self.char_bottom * (width - fragment_list_width(tab_line)),
372
444
  )
445
+ mouse_handlers.append(on_close)
446
+
447
+ # Right edge
448
+ tab_line.append((f"{style} class:tab,border,right", grid.MID_RIGHT))
449
+ mouse_handlers.append(on_activate)
450
+
451
+ return (top_line, tab_line, mouse_handlers)
452
+
453
+ def render(
454
+ self, width: int
455
+ ) -> tuple[
456
+ StyleAndTextTuples,
457
+ StyleAndTextTuples,
458
+ dict[int, Callable[[], NotImplementedOrNone] | None],
459
+ ]:
460
+ """Render the tab-bar as lines of formatted text."""
461
+ top_line: StyleAndTextTuples = []
462
+ tab_line: StyleAndTextTuples = []
463
+ mouse_handlers: dict[int, Callable[[], NotImplementedOrNone] | None] = {}
464
+ pos = 0
465
+ full = 0
466
+
467
+ renderings = [
468
+ self.render_tab(
469
+ title=tuple(to_formatted_text(tab.title)),
470
+ max_title_width=self.max_title_width,
471
+ on_activate=tab.on_activate,
472
+ on_deactivate=tab.on_deactivate,
473
+ on_close=tab.on_close,
474
+ closeable=tab.closeable,
475
+ active=(self.active == j),
476
+ grid=self.grid,
477
+ )
478
+ for j, tab in enumerate(self.tabs)
373
479
  ]
480
+ self.tab_widths = [len(x[0]) for x in renderings]
481
+
482
+ # Do initial scroll if first render
483
+ if self.scroll == -1:
484
+ self.scroll_to(self._active() if callable(self._active) else self._active)
485
+
486
+ # Apply scroll limits
487
+ self.scroll = max(
488
+ 0,
489
+ min(
490
+ self.scroll,
491
+ self.spacing * (len(self.tabs) + 1)
492
+ + sum(len(x[1]) for x in renderings)
493
+ - width,
494
+ ),
495
+ )
496
+ scroll = self.scroll
374
497
 
375
- return [top_line, tab_line]
498
+ # Initial spacing
499
+ for _ in range(self.spacing):
500
+ if full >= scroll:
501
+ top_line += [("", " ")]
502
+ tab_line += [("class:border,bottom", self.grid.TOP_MID)]
503
+ pos += 1
504
+ full += 1
505
+
506
+ for rendering in renderings:
507
+ # Add the rendered tab content
508
+ for tab_top, tab_bottom, handler in zip(*rendering):
509
+ if full >= scroll:
510
+ top_line.append(tab_top)
511
+ tab_line.append(tab_bottom)
512
+ mouse_handlers[pos] = handler
513
+ pos += 1
514
+ full += 1
515
+ if pos == width:
516
+ break
517
+
518
+ if pos == width:
519
+ break
520
+
521
+ # Inter-tab spacing
522
+ if rendering is not renderings[-1]:
523
+ if full >= scroll:
524
+ for _ in range(self.spacing):
525
+ top_line += [("", " ")]
526
+ tab_line += [("class:border,bottom", self.grid.TOP_MID)]
527
+ if pos == width:
528
+ break
529
+ pos += 1
530
+ full += 1
531
+
532
+ if pos == width:
533
+ break
534
+
535
+ # Add scroll indicators
536
+ if scroll > 0:
537
+ top_line[0] = ("", " ")
538
+ tab_line[0] = ("class:overflow", self.char_scroll_left)
539
+ if pos >= width:
540
+ top_line[-1] = ("", " ")
541
+ tab_line[-1] = ("class:overflow", self.char_scroll_right)
542
+ else:
543
+ # Otherwise add border to fill width
544
+ tab_line += [
545
+ (
546
+ "class:border,bottom",
547
+ self.grid.TOP_MID * (width - pos + 1),
548
+ )
549
+ ]
550
+
551
+ return top_line, tab_line, mouse_handlers
376
552
 
377
553
  def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
378
554
  """Handle mouse events."""
@@ -386,31 +562,27 @@ class TabBarControl(UIControl):
386
562
  # Activate the tab
387
563
  handler()
388
564
  return None
389
- elif mouse_event.button == MouseButton.MIDDLE and self.closeable:
565
+ elif mouse_event.button == MouseButton.MIDDLE:
390
566
  if callable(handler := self.mouse_handlers.get(col)):
391
567
  # Activate tab
392
568
  handler()
393
569
  # Close the now active tab
394
570
  tabs = self.tabs
395
- on_close = tabs[self.active]
396
- if callable(on_close):
397
- on_close()
571
+ tab = tabs[self.active]
572
+ if tab.closeable and callable(tab.on_close):
573
+ tab.on_close()
398
574
  return None
399
575
 
400
576
  tabs = self.tabs
401
577
  if mouse_event.event_type == MouseEventType.SCROLL_UP:
402
578
  index = max(self.active - 1, 0)
403
579
  if index != self.active:
404
- if callable(deactivate := tabs[self.active].on_deactivate):
405
- deactivate()
406
580
  if callable(activate := tabs[index].on_activate):
407
581
  activate()
408
582
  return None
409
583
  elif mouse_event.event_type == MouseEventType.SCROLL_DOWN:
410
584
  index = min(self.active + 1, len(tabs) - 1)
411
585
  if index != self.active:
412
- if callable(deactivate := tabs[self.active].on_deactivate):
413
- deactivate()
414
586
  if callable(activate := tabs[index].on_activate):
415
587
  activate()
416
588
  return None
@@ -474,7 +646,7 @@ class StackedSplit(metaclass=ABCMeta):
474
646
  value: The index of the tab to make active
475
647
  """
476
648
  if value is not None:
477
- value = max(0, min(value, len(self.children)))
649
+ value = max(0, min(value, len(self.children) - 1))
478
650
  if value != self._active:
479
651
  self._active = value
480
652
  self.refresh()
@@ -537,6 +709,21 @@ class TabbedSplit(StackedSplit):
537
709
  """Initialize a new tabbed container."""
538
710
  self.border = border
539
711
  self.show_borders = show_borders or DiBool(False, True, True, True)
712
+
713
+ kb = KeyBindings()
714
+
715
+ @kb.add("left")
716
+ def _prev(event: KeyPressEvent) -> None:
717
+ """Previous tab."""
718
+ self.active = (self.active or 0) - 1
719
+
720
+ @kb.add("right")
721
+ def _next(event: KeyPressEvent) -> None:
722
+ """Next tab."""
723
+ self.active = (self.active or 0) + 1
724
+
725
+ self.key_bindings = kb
726
+
540
727
  super().__init__(
541
728
  children=children,
542
729
  titles=titles,
@@ -579,6 +766,7 @@ class TabbedSplit(StackedSplit):
579
766
  style="class:tabbed-split",
580
767
  width=self.width,
581
768
  height=self.height,
769
+ key_bindings=self.key_bindings,
582
770
  )
583
771
 
584
772
  def refresh(self) -> None:
@@ -365,14 +365,16 @@ class MenuBar:
365
365
  menu = self._get_menu(len(self.selected_menu) - 2)
366
366
  index = self.selected_menu[-1]
367
367
 
368
- previous_indexes = [
369
- i
370
- for i, item in enumerate(menu.children)
371
- if i < index and not item.disabled and not item.hidden()
372
- ]
373
-
374
- if previous_indexes:
375
- self.selected_menu[-1] = previous_indexes[-1]
368
+ previous_index = next(
369
+ (
370
+ i
371
+ for i, item in reversed(list(enumerate(menu.children)))
372
+ if i < index and not item.disabled and not item.hidden()
373
+ ),
374
+ None,
375
+ )
376
+ if previous_index is not None:
377
+ self.selected_menu[-1] = previous_index
376
378
  elif len(self.selected_menu) == 2:
377
379
  # Return to main menu.
378
380
  self.selected_menu.pop()
@@ -384,14 +386,17 @@ class MenuBar:
384
386
  menu = self._get_menu(len(self.selected_menu) - 2)
385
387
  index = self.selected_menu[-1]
386
388
 
387
- next_indexes = [
388
- i
389
- for i, item in enumerate(menu.children)
390
- if i > index and not item.disabled and not item.hidden()
391
- ]
389
+ next_index = next(
390
+ (
391
+ i
392
+ for i, item in enumerate(menu.children)
393
+ if i > index and not item.disabled and not item.hidden()
394
+ ),
395
+ None,
396
+ )
392
397
 
393
- if next_indexes:
394
- self.selected_menu[-1] = next_indexes[0]
398
+ if next_index is not None:
399
+ self.selected_menu[-1] = next_index
395
400
  self.refocus()
396
401
 
397
402
  @kb.add("enter")
@@ -621,6 +626,41 @@ class MenuBar:
621
626
  len(self.selected_menu) - 1
622
627
  ]
623
628
  )
629
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
630
+ menu = self._get_menu(len(self.selected_menu) - 2)
631
+ index = self.selected_menu[-1]
632
+
633
+ previous_index = next(
634
+ (
635
+ i
636
+ for i, item in reversed(
637
+ list(enumerate(menu.children))
638
+ )
639
+ if i < index
640
+ and not item.disabled
641
+ and not item.hidden()
642
+ ),
643
+ None,
644
+ )
645
+
646
+ if previous_index is not None:
647
+ self.selected_menu[-1] = previous_index
648
+ elif mouse_event.event_type == MouseEventType.SCROLL_DOWN:
649
+ menu = self._get_menu(len(self.selected_menu) - 2)
650
+ index = self.selected_menu[-1]
651
+
652
+ next_index = next(
653
+ (
654
+ i
655
+ for i, item in enumerate(menu.children)
656
+ if i > index
657
+ and not item.disabled
658
+ and not item.hidden()
659
+ ),
660
+ None,
661
+ )
662
+ if next_index is not None:
663
+ self.selected_menu[-1] = next_index
624
664
 
625
665
  if item.separator:
626
666
  # Show a connected line with no mouse handler
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, NamedTuple
10
10
  from prompt_toolkit.data_structures import Point
11
11
  from prompt_toolkit.filters import Condition
12
12
  from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
13
+ from prompt_toolkit.key_binding.vi_state import InputMode
13
14
  from prompt_toolkit.layout.containers import ScrollOffsets
14
15
  from prompt_toolkit.layout.controls import UIContent, UIControl
15
16
  from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
@@ -167,7 +168,6 @@ class CommandPalette(Dialog):
167
168
  # self.kb = KeyBindings()
168
169
  self.kb.add("s-tab")(focus_previous)
169
170
  self.kb.add("tab")(focus_next)
170
- self.kb.add("escape")(self.hide)
171
171
  self.kb.add("up", filter=Condition(lambda: bool(self.matches)))(
172
172
  partial(self.select, -1)
173
173
  )
@@ -222,6 +222,8 @@ class CommandPalette(Dialog):
222
222
  """Reset the dialog ready for display."""
223
223
  self.text_area.buffer.text = ""
224
224
  self.to_focus = self.text_area
225
+ app = get_app()
226
+ app.vi_state.input_mode = InputMode.INSERT
225
227
 
226
228
  def select(self, n: int, event: KeyPressEvent | None = None) -> None:
227
229
  """Change the index of the selected command.