euporie 2.3.2__py3-none-any.whl → 2.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. euporie/console/__main__.py +3 -1
  2. euporie/console/app.py +6 -4
  3. euporie/console/tabs/console.py +34 -9
  4. euporie/core/__init__.py +6 -1
  5. euporie/core/__main__.py +1 -1
  6. euporie/core/app.py +79 -109
  7. euporie/core/border.py +44 -14
  8. euporie/core/comm/base.py +5 -4
  9. euporie/core/comm/ipywidgets.py +11 -11
  10. euporie/core/comm/registry.py +12 -6
  11. euporie/core/commands.py +30 -23
  12. euporie/core/completion.py +1 -4
  13. euporie/core/config.py +15 -5
  14. euporie/core/convert/{base.py → core.py} +117 -53
  15. euporie/core/convert/formats/ansi.py +46 -25
  16. euporie/core/convert/formats/base64.py +3 -3
  17. euporie/core/convert/formats/common.py +38 -13
  18. euporie/core/convert/formats/formatted_text.py +54 -12
  19. euporie/core/convert/formats/html.py +5 -5
  20. euporie/core/convert/formats/jpeg.py +1 -1
  21. euporie/core/convert/formats/markdown.py +4 -4
  22. euporie/core/convert/formats/pdf.py +1 -1
  23. euporie/core/convert/formats/pil.py +5 -3
  24. euporie/core/convert/formats/png.py +7 -6
  25. euporie/core/convert/formats/rich.py +4 -3
  26. euporie/core/convert/formats/sixel.py +5 -5
  27. euporie/core/convert/utils.py +1 -1
  28. euporie/core/current.py +11 -5
  29. euporie/core/formatted_text/ansi.py +4 -8
  30. euporie/core/formatted_text/html.py +1630 -856
  31. euporie/core/formatted_text/markdown.py +177 -166
  32. euporie/core/formatted_text/table.py +20 -14
  33. euporie/core/formatted_text/utils.py +21 -10
  34. euporie/core/io.py +14 -14
  35. euporie/core/kernel.py +48 -37
  36. euporie/core/key_binding/bindings/micro.py +5 -1
  37. euporie/core/key_binding/bindings/mouse.py +2 -2
  38. euporie/core/keys.py +3 -0
  39. euporie/core/launch.py +5 -2
  40. euporie/core/lexers.py +13 -2
  41. euporie/core/log.py +135 -139
  42. euporie/core/margins.py +32 -14
  43. euporie/core/path.py +273 -0
  44. euporie/core/processors.py +35 -0
  45. euporie/core/renderer.py +21 -5
  46. euporie/core/style.py +34 -19
  47. euporie/core/tabs/base.py +101 -17
  48. euporie/core/tabs/notebook.py +72 -30
  49. euporie/core/terminal.py +56 -48
  50. euporie/core/utils.py +12 -16
  51. euporie/core/widgets/cell.py +6 -5
  52. euporie/core/widgets/cell_outputs.py +2 -2
  53. euporie/core/widgets/decor.py +74 -82
  54. euporie/core/widgets/dialog.py +132 -28
  55. euporie/core/widgets/display.py +76 -24
  56. euporie/core/widgets/file_browser.py +87 -31
  57. euporie/core/widgets/formatted_text_area.py +1 -3
  58. euporie/core/widgets/forms.py +79 -40
  59. euporie/core/widgets/inputs.py +23 -13
  60. euporie/core/widgets/layout.py +4 -3
  61. euporie/core/widgets/menu.py +368 -216
  62. euporie/core/widgets/page.py +99 -58
  63. euporie/core/widgets/pager.py +1 -1
  64. euporie/core/widgets/palette.py +30 -27
  65. euporie/core/widgets/search_bar.py +38 -25
  66. euporie/core/widgets/status_bar.py +103 -5
  67. euporie/data/desktop/euporie-console.desktop +7 -0
  68. euporie/data/desktop/euporie-notebook.desktop +7 -0
  69. euporie/hub/__main__.py +3 -1
  70. euporie/hub/app.py +9 -7
  71. euporie/notebook/__main__.py +3 -1
  72. euporie/notebook/app.py +7 -30
  73. euporie/notebook/tabs/__init__.py +7 -3
  74. euporie/notebook/tabs/display.py +18 -9
  75. euporie/notebook/tabs/edit.py +106 -23
  76. euporie/notebook/tabs/json.py +73 -0
  77. euporie/notebook/tabs/log.py +18 -8
  78. euporie/notebook/tabs/notebook.py +60 -41
  79. euporie/preview/__main__.py +3 -1
  80. euporie/preview/app.py +2 -1
  81. euporie/preview/tabs/notebook.py +23 -10
  82. euporie/web/tabs/web.py +149 -0
  83. euporie/web/widgets/webview.py +563 -0
  84. euporie-2.4.1.data/data/share/applications/euporie-console.desktop +7 -0
  85. euporie-2.4.1.data/data/share/applications/euporie-notebook.desktop +7 -0
  86. {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/METADATA +6 -5
  87. euporie-2.4.1.dist-info/RECORD +129 -0
  88. {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/WHEEL +1 -1
  89. euporie/core/url.py +0 -64
  90. euporie-2.3.2.dist-info/RECORD +0 -122
  91. {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/entry_points.txt +0 -0
  92. {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,8 @@ import logging
6
6
  from functools import partial
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- from prompt_toolkit.filters import Condition, to_filter
9
+ from prompt_toolkit.data_structures import Point
10
+ from prompt_toolkit.filters import Condition, has_completions, is_done, to_filter
10
11
  from prompt_toolkit.formatted_text.base import to_formatted_text
11
12
  from prompt_toolkit.formatted_text.utils import (
12
13
  fragment_list_to_text,
@@ -18,9 +19,16 @@ from prompt_toolkit.layout.containers import (
18
19
  ConditionalContainer,
19
20
  Container,
20
21
  Float,
22
+ HSplit,
23
+ ScrollOffsets,
24
+ VSplit,
21
25
  Window,
22
26
  )
23
- from prompt_toolkit.layout.controls import FormattedTextControl
27
+ from prompt_toolkit.layout.controls import FormattedTextControl, UIContent
28
+ from prompt_toolkit.layout.dimension import Dimension
29
+ from prompt_toolkit.layout.menus import (
30
+ CompletionsMenuControl as PtkCompletionsMenuControl,
31
+ )
24
32
  from prompt_toolkit.layout.utils import explode_text_fragments
25
33
  from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
26
34
  from prompt_toolkit.utils import get_cwidth
@@ -38,11 +46,13 @@ if TYPE_CHECKING:
38
46
  OneStyleAndTextTuple,
39
47
  StyleAndTextTuples,
40
48
  )
49
+ from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
41
50
  from prompt_toolkit.key_binding.key_processor import KeyPressEvent
42
51
 
43
52
  from euporie.core.app import BaseApp
44
53
  from euporie.core.border import GridStyle
45
54
  from euporie.core.commands import Command
55
+ from euporie.core.widgets.status_bar import StatusBarFields
46
56
 
47
57
 
48
58
  log = logging.getLogger(__name__)
@@ -254,203 +264,6 @@ class MenuItem:
254
264
  class MenuBar:
255
265
  """A container to hold the menubar and main application body."""
256
266
 
257
- def _get_menu(self, level: int) -> MenuItem:
258
- menu = self.menu_items[self.selected_menu[0]]
259
-
260
- for i, index in enumerate(self.selected_menu[1:]):
261
- if i < level:
262
- try:
263
- menu = menu.children[index]
264
- except IndexError:
265
- return MenuItem("debug")
266
-
267
- return menu
268
-
269
- def _get_menu_fragments(self) -> StyleAndTextTuples:
270
- focused = get_app().layout.has_focus(self.window)
271
-
272
- # This is called during the rendering. When we discover that this
273
- # widget doesn't have the focus anymore. Reset menu state.
274
- if not focused:
275
- self.selected_menu = [0]
276
-
277
- def mouse_handler(index: int, mouse_event: MouseEvent) -> None:
278
- hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
279
- if mouse_event.event_type == MouseEventType.MOUSE_DOWN or hover and focused:
280
- # Toggle focus.
281
- app = get_app()
282
- if not hover:
283
- if app.layout.has_focus(self.window):
284
- if self.selected_menu == [index]:
285
- app.layout.focus_last()
286
- else:
287
- app.layout.focus(self.window)
288
- self.selected_menu = [index]
289
-
290
- results: StyleAndTextTuples = []
291
- used_keys = set()
292
-
293
- for i, item in enumerate(self.menu_items):
294
- # Add shortcut key hints
295
- key = to_plain_text(item.formatted_text)[0].lower()
296
- ft: StyleAndTextTuples
297
- if key not in used_keys:
298
- ft = explode_text_fragments(item.formatted_text)
299
- ft = [(f"underline {ft[0][0]}", ft[0][1]), *ft[1:]]
300
- used_keys |= {key}
301
- else:
302
- ft = item.formatted_text
303
-
304
- mh = partial(mouse_handler, i)
305
- selected = i == self.selected_menu[0] and focused
306
- style = "class:selection" if selected else ""
307
- first_style = f"{style} [SetMenuPosition]" if selected else style
308
-
309
- results.extend(
310
- [
311
- (first_style, " ", mh),
312
- *[(f"{style} {style_}", text, mh) for style_, text, *_ in ft],
313
- (style, " ", mh),
314
- ]
315
- )
316
-
317
- return results
318
-
319
- def _submenu(self, level: int = 0) -> Window:
320
- def get_text_fragments() -> StyleAndTextTuples:
321
- result: StyleAndTextTuples = []
322
- if level < len(self.selected_menu):
323
- menu = self._get_menu(level)
324
-
325
- if menu.children:
326
- result.extend(
327
- [
328
- ("class:menu,border", self.grid.TOP_LEFT),
329
- ("class:menu,border", self.grid.TOP_MID * menu.width),
330
- ("class:menu,border", self.grid.TOP_RIGHT),
331
- ("", "\n"),
332
- ]
333
- )
334
-
335
- try:
336
- selected_item = self.selected_menu[level + 1]
337
- except IndexError:
338
- selected_item = -1
339
-
340
- def one_item(
341
- i: int, item: MenuItem
342
- ) -> Iterable[OneStyleAndTextTuple]:
343
- assert isinstance(item, MenuItem)
344
- assert isinstance(menu, MenuItem)
345
-
346
- def mouse_handler(mouse_event: MouseEvent) -> None:
347
- if item.disabled:
348
- # The arrow keys can't interact with menu items that
349
- # are disabled. The mouse shouldn't be able to either.
350
- return
351
- hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
352
- if (
353
- mouse_event.event_type == MouseEventType.MOUSE_UP
354
- or hover
355
- ):
356
- app = get_app()
357
- if not hover and item.handler:
358
- app.layout.focus_last()
359
- item.handler()
360
- else:
361
- self.selected_menu = self.selected_menu[
362
- : level + 1
363
- ] + [i]
364
-
365
- if item.separator:
366
- # Show a connected line with no mouse handler
367
- yield (
368
- "class:menu,border",
369
- self.grid.SPLIT_LEFT
370
- + (self.grid.SPLIT_MID * menu.width)
371
- + self.grid.SPLIT_RIGHT,
372
- )
373
-
374
- else:
375
- # Show the right edge
376
- style = ""
377
- # Set the style if disabled
378
- if item.disabled:
379
- style += "class:menu,disabled"
380
- # Set the style and cursor if selected
381
- if i == selected_item:
382
- style += "class:menu,selection"
383
- yield (f"{style} class:menu,border", self.grid.MID_LEFT)
384
- if i == selected_item:
385
- yield ("[SetCursorPosition]", "")
386
- # Construct the menu item contents
387
- prefix_padding = " " * (
388
- 0
389
- if menu.collapse_prefix
390
- else menu.prefix_width
391
- - fragment_list_width(item.prefix)
392
- )
393
- suffix_padding = " " * (
394
- menu.width
395
- - fragment_list_width(item.prefix)
396
- - len(prefix_padding)
397
- - fragment_list_width(item.formatted_text)
398
- - (
399
- fragment_list_width(item.suffix)
400
- if menu.collapse_suffix
401
- else menu.suffix_width
402
- )
403
- )
404
- text_padding = " " * (
405
- menu.width
406
- - fragment_list_width(item.prefix)
407
- - len(prefix_padding)
408
- - fragment_list_width(item.formatted_text)
409
- - fragment_list_width(item.suffix)
410
- - len(suffix_padding)
411
- )
412
- menu_formatted_text: StyleAndTextTuples = to_formatted_text(
413
- [
414
- *item.prefix,
415
- ("", prefix_padding),
416
- *item.formatted_text,
417
- ("", text_padding),
418
- ("", suffix_padding),
419
- *item.suffix,
420
- ],
421
- style=style,
422
- )
423
- # Apply mouse handler to all fragments
424
- menu_formatted_text = [
425
- (fragment[0], fragment[1], mouse_handler)
426
- for fragment in menu_formatted_text
427
- ]
428
- # Show the menu item contents
429
- yield from menu_formatted_text
430
- # Position the sub-menu
431
- if i == selected_item:
432
- yield ("[SetMenuPosition]", "")
433
- # Show the right edge
434
- yield (f"{style} class:menu,border", self.grid.MID_RIGHT)
435
-
436
- yield ("", "\n")
437
-
438
- for i, item in enumerate(menu.children):
439
- if not item.hidden():
440
- result.extend(one_item(i, item))
441
-
442
- result.extend(
443
- [
444
- ("class:menu,border", self.grid.BOTTOM_LEFT),
445
- ("class:menu,border", self.grid.BOTTOM_MID * menu.width),
446
- ("class:menu,border", self.grid.BOTTOM_RIGHT),
447
- ]
448
- )
449
-
450
- return result
451
-
452
- return Window(FormattedTextControl(get_text_fragments), style="class:menu")
453
-
454
267
  def __init__(
455
268
  self,
456
269
  app: BaseApp,
@@ -495,7 +308,10 @@ class MenuBar:
495
308
 
496
309
  @kb.add("down", filter=in_main_menu)
497
310
  def _down(event: KeyPressEvent) -> None:
498
- self.selected_menu.append(0)
311
+ menu = self._get_menu(len(self.selected_menu) - 2)
312
+ indices = [i for i, item in enumerate(menu.children) if not item.disabled]
313
+ if indices:
314
+ self.selected_menu.append(indices[0])
499
315
 
500
316
  @kb.add("c-c", filter=in_main_menu)
501
317
  @kb.add("c-g", filter=in_main_menu)
@@ -540,7 +356,7 @@ class MenuBar:
540
356
  previous_indexes = [
541
357
  i
542
358
  for i, item in enumerate(menu.children)
543
- if i < index and not item.disabled
359
+ if i < index and not item.disabled and not item.hidden()
544
360
  ]
545
361
 
546
362
  if previous_indexes:
@@ -558,7 +374,7 @@ class MenuBar:
558
374
  next_indexes = [
559
375
  i
560
376
  for i, item in enumerate(menu.children)
561
- if i > index and not item.disabled
377
+ if i > index and not item.disabled and not item.hidden()
562
378
  ]
563
379
 
564
380
  if next_indexes:
@@ -607,9 +423,7 @@ class MenuBar:
607
423
  show_cursor=False,
608
424
  )
609
425
  self.window: Window = Window(
610
- height=1,
611
- content=self.control,
612
- style="class:menu,bar",
426
+ height=1, content=self.control, style="class:menu,bar"
613
427
  )
614
428
 
615
429
  submenu = self._submenu(0)
@@ -628,31 +442,242 @@ class MenuBar:
628
442
  ),
629
443
  )
630
444
  self.app.menus["menu-2"] = Float(
631
- attach_to_window=submenu,
445
+ attach_to_window=submenu.get_children()[1],
632
446
  xcursor=True,
633
447
  ycursor=True,
634
448
  allow_cover_cursor=True,
635
449
  content=ConditionalContainer(
636
450
  content=Shadow(body=submenu2),
637
- filter=has_focus & Condition(lambda: len(self.selected_menu) >= 1),
451
+ filter=has_focus
452
+ & Condition(lambda: len(self.selected_menu) > 1)
453
+ & Condition(lambda: bool(self._get_menu(1).children)),
638
454
  ),
639
455
  )
640
456
  self.app.menus["menu-3"] = Float(
641
- attach_to_window=submenu2,
457
+ attach_to_window=submenu2.get_children()[1],
642
458
  xcursor=True,
643
459
  ycursor=True,
644
460
  allow_cover_cursor=True,
645
461
  content=ConditionalContainer(
646
462
  content=Shadow(body=submenu3),
647
- filter=has_focus & Condition(lambda: len(self.selected_menu) >= 2),
463
+ filter=has_focus
464
+ & Condition(lambda: len(self.selected_menu) > 2)
465
+ & Condition(lambda: bool(self._get_menu(2).children)),
648
466
  ),
649
467
  )
650
468
 
651
- get_app().container_statuses[self.window] = self.statusbar_fields
469
+ def _get_menu(self, level: int) -> MenuItem:
470
+ menu = self.menu_items[self.selected_menu[0]]
471
+ for i, index in enumerate(self.selected_menu[1:]):
472
+ if i < level:
473
+ try:
474
+ menu = menu.children[index]
475
+ except IndexError:
476
+ return MenuItem("debug")
477
+ return menu
478
+
479
+ def _get_menu_fragments(self) -> StyleAndTextTuples:
480
+ focused = get_app().layout.has_focus(self.window)
652
481
 
653
- def statusbar_fields(
654
- self,
655
- ) -> tuple[list[AnyFormattedText], list[AnyFormattedText]]:
482
+ # This is called during the rendering. When we discover that this
483
+ # widget doesn't have the focus anymore. Reset menu state.
484
+ if not focused:
485
+ self.selected_menu = [0]
486
+
487
+ def mouse_handler(index: int, mouse_event: MouseEvent) -> NotImplementedOrNone:
488
+ hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
489
+ if mouse_event.event_type == MouseEventType.MOUSE_DOWN or hover and focused:
490
+ # Toggle focus.
491
+ app = get_app()
492
+ if not hover:
493
+ if app.layout.has_focus(self.window):
494
+ if self.selected_menu == [index]:
495
+ app.layout.focus_last()
496
+ else:
497
+ app.layout.focus(self.window)
498
+ self.selected_menu = [index]
499
+ return None
500
+ return NotImplemented
501
+
502
+ results: StyleAndTextTuples = []
503
+ used_keys = set()
504
+
505
+ for i, item in enumerate(self.menu_items):
506
+ # Add shortcut key hints
507
+ key = to_plain_text(item.formatted_text)[0].lower()
508
+ ft: StyleAndTextTuples
509
+ if key not in used_keys:
510
+ ft = explode_text_fragments(item.formatted_text)
511
+ ft = [(f"underline {ft[0][0]}", ft[0][1]), *ft[1:]]
512
+ used_keys |= {key}
513
+ else:
514
+ ft = item.formatted_text
515
+
516
+ mh = partial(mouse_handler, i)
517
+ selected = i == self.selected_menu[0] and focused
518
+ style = "class:selection" if selected else ""
519
+ first_style = f"{style} [SetMenuPosition]" if selected else style
520
+
521
+ results.extend(
522
+ [
523
+ (first_style, " ", mh),
524
+ *[(f"{style} {style_}", text, mh) for style_, text, *_ in ft],
525
+ (style, " ", mh),
526
+ ]
527
+ )
528
+
529
+ return results
530
+
531
+ def _submenu(self, level: int = 0) -> Container:
532
+ grid = self.grid
533
+
534
+ def get_text_fragments() -> StyleAndTextTuples:
535
+ result: StyleAndTextTuples = []
536
+ if level < len(self.selected_menu):
537
+ menu = self._get_menu(level)
538
+
539
+ if menu.children:
540
+ try:
541
+ selected_item = self.selected_menu[level + 1]
542
+ except IndexError:
543
+ selected_item = -1
544
+
545
+ def one_item(
546
+ i: int, item: MenuItem
547
+ ) -> Iterable[OneStyleAndTextTuple]:
548
+ assert isinstance(item, MenuItem)
549
+ assert isinstance(menu, MenuItem)
550
+
551
+ def mouse_handler(mouse_event: MouseEvent) -> None:
552
+ if item.disabled:
553
+ # The arrow keys can't interact with menu items that
554
+ # are disabled. The mouse shouldn't be able to either.
555
+ return
556
+ hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
557
+ if (
558
+ mouse_event.event_type == MouseEventType.MOUSE_UP
559
+ or hover
560
+ ):
561
+ app = get_app()
562
+ if not hover and item.handler:
563
+ app.layout.focus_last()
564
+ item.handler()
565
+ else:
566
+ self.selected_menu = self.selected_menu[
567
+ : level + 1
568
+ ] + [i]
569
+
570
+ if item.separator:
571
+ # Show a connected line with no mouse handler
572
+ yield (
573
+ "class:menu,border",
574
+ grid.SPLIT_LEFT
575
+ + (grid.SPLIT_MID * menu.width)
576
+ + grid.SPLIT_RIGHT,
577
+ )
578
+
579
+ else:
580
+ # Show the right edge
581
+ style = ""
582
+ # Set the style if disabled
583
+ if item.disabled:
584
+ style += "class:menu,disabled"
585
+ # Set the style and cursor if selected
586
+ if i == selected_item:
587
+ style += "class:menu,selection"
588
+ yield (f"{style} class:menu,border", grid.MID_LEFT)
589
+ if i == selected_item:
590
+ yield ("[SetCursorPosition]", "")
591
+ # Construct the menu item contents
592
+ prefix_padding = " " * (
593
+ 0
594
+ if menu.collapse_prefix
595
+ else menu.prefix_width
596
+ - fragment_list_width(item.prefix)
597
+ )
598
+ suffix_padding = " " * (
599
+ menu.width
600
+ - fragment_list_width(item.prefix)
601
+ - len(prefix_padding)
602
+ - fragment_list_width(item.formatted_text)
603
+ - (
604
+ fragment_list_width(item.suffix)
605
+ if menu.collapse_suffix
606
+ else menu.suffix_width
607
+ )
608
+ )
609
+ text_padding = " " * (
610
+ menu.width
611
+ - fragment_list_width(item.prefix)
612
+ - len(prefix_padding)
613
+ - fragment_list_width(item.formatted_text)
614
+ - fragment_list_width(item.suffix)
615
+ - len(suffix_padding)
616
+ )
617
+ menu_formatted_text: StyleAndTextTuples = to_formatted_text(
618
+ [
619
+ *item.prefix,
620
+ ("", prefix_padding),
621
+ *item.formatted_text,
622
+ ("", text_padding),
623
+ ("", suffix_padding),
624
+ *item.suffix,
625
+ ],
626
+ style=style,
627
+ )
628
+ # Apply mouse handler to all fragments
629
+ menu_formatted_text = [
630
+ (fragment[0], fragment[1], mouse_handler)
631
+ for fragment in menu_formatted_text
632
+ ]
633
+ # Show the menu item contents
634
+ yield from menu_formatted_text
635
+ # Position the sub-menu
636
+ if i == selected_item:
637
+ yield ("[SetMenuPosition]", "")
638
+ # Show the right edge
639
+ yield (f"{style} class:menu,border", grid.MID_RIGHT)
640
+
641
+ for i, item in enumerate(menu.children):
642
+ if not item.hidden():
643
+ result.extend(one_item(i, item))
644
+ if i < len(menu.children) - 1:
645
+ result.append(("", "\n"))
646
+
647
+ return result
648
+
649
+ return HSplit(
650
+ [
651
+ VSplit(
652
+ [
653
+ Window(char=grid.TOP_LEFT, width=1, height=1),
654
+ Window(char=grid.TOP_MID, height=1),
655
+ Window(char=grid.TOP_RIGHT, width=1, height=1),
656
+ ],
657
+ style="class:border",
658
+ ),
659
+ Window(
660
+ FormattedTextControl(get_text_fragments),
661
+ scroll_offsets=ScrollOffsets(top=1, bottom=1),
662
+ ),
663
+ VSplit(
664
+ [
665
+ Window(char=grid.BOTTOM_LEFT, width=1, height=1),
666
+ Window(char=grid.BOTTOM_MID, height=1),
667
+ # Window(char="🭑🭆", height=1),
668
+ Window(char=grid.BOTTOM_RIGHT, width=1, height=1),
669
+ ],
670
+ style="class:border",
671
+ ),
672
+ ],
673
+ style="class:menu",
674
+ )
675
+
676
+ def __pt_container__(self) -> Container:
677
+ """Return the menu bar container's content."""
678
+ return self.window
679
+
680
+ def __pt_status__(self) -> StatusBarFields:
656
681
  """Return the description of the currently selected menu item."""
657
682
  selected_item = self._get_menu(len(self.selected_menu) - 1)
658
683
  if isinstance(selected_item, MenuItem):
@@ -660,6 +685,133 @@ class MenuBar:
660
685
  else:
661
686
  return (["", ""], [])
662
687
 
663
- def __pt_container__(self) -> Container:
664
- """Return the menu bar container's content."""
665
- return self.window
688
+
689
+ class CompletionsMenuControl(PtkCompletionsMenuControl):
690
+ """A custom completions menu control."""
691
+
692
+ def create_content(self, width: int, height: int) -> UIContent:
693
+ """Create a UIContent object for this control."""
694
+ complete_state = get_app().current_buffer.complete_state
695
+ if complete_state:
696
+ completions = complete_state.completions
697
+ index = complete_state.complete_index # Can be None!
698
+
699
+ # Calculate width of completions menu.
700
+ menu_width = self._get_menu_width(width, complete_state)
701
+ menu_meta_width = self._get_menu_meta_width(
702
+ width - menu_width, complete_state
703
+ )
704
+ total_width = menu_width + menu_meta_width
705
+
706
+ grid = OuterHalfGrid
707
+
708
+ def get_line(i: int) -> StyleAndTextTuples:
709
+ c = completions[i]
710
+ selected_item = i == index
711
+ output: StyleAndTextTuples = []
712
+
713
+ style = "class:menu"
714
+ if selected_item:
715
+ style += ",selection"
716
+
717
+ output.append((f"{style},border", grid.MID_LEFT))
718
+ if selected_item:
719
+ output.append(("[SetCursorPosition]", ""))
720
+ # Construct the menu item contents
721
+ padding = " " * (
722
+ total_width
723
+ - fragment_list_width(c.display)
724
+ - fragment_list_width(c.display_meta)
725
+ - 2
726
+ )
727
+ output.extend(
728
+ to_formatted_text(
729
+ [
730
+ *c.display,
731
+ ("", padding),
732
+ *to_formatted_text(
733
+ c.display_meta, style=f"{style} {c.style}"
734
+ ),
735
+ ],
736
+ style=style,
737
+ )
738
+ )
739
+ output.append((f"{style},border", grid.MID_RIGHT))
740
+
741
+ # Apply mouse handler
742
+ output = [
743
+ (fragment[0], fragment[1], self.mouse_handler)
744
+ for fragment in output
745
+ ]
746
+
747
+ return output
748
+
749
+ return UIContent(
750
+ get_line=get_line,
751
+ cursor_position=Point(x=0, y=index or 0),
752
+ line_count=len(completions),
753
+ )
754
+
755
+ return UIContent()
756
+
757
+ def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
758
+ """Handle mouse events: clicking and scrolling."""
759
+ if mouse_event.event_type == MouseEventType.MOUSE_MOVE:
760
+ # Set completion
761
+ complete_state = get_app().current_buffer.complete_state
762
+ if complete_state:
763
+ complete_state.complete_index = mouse_event.position.y
764
+ return None
765
+
766
+ return super().mouse_handler(mouse_event)
767
+
768
+
769
+ class CompletionsMenu(ConditionalContainer):
770
+ """A custom completions menu."""
771
+
772
+ def __init__(
773
+ self,
774
+ max_height: int | None = 16,
775
+ scroll_offset: int | Callable[[], int] = 1,
776
+ extra_filter: FilterOrBool = True,
777
+ z_index: int = 10**8,
778
+ ) -> None:
779
+ """Create a completions menu with borders."""
780
+ extra_filter = to_filter(extra_filter)
781
+ grid = OuterHalfGrid
782
+ super().__init__(
783
+ content=HSplit(
784
+ [
785
+ VSplit(
786
+ [
787
+ Window(char=grid.TOP_LEFT, width=1, height=1),
788
+ Window(char=grid.TOP_MID, height=1),
789
+ Window(char=grid.TOP_RIGHT, width=1, height=1),
790
+ ],
791
+ style="class:border",
792
+ ),
793
+ Window(
794
+ content=CompletionsMenuControl(),
795
+ width=Dimension(min=8),
796
+ height=Dimension(min=1, max=max_height),
797
+ scroll_offsets=ScrollOffsets(
798
+ top=scroll_offset, bottom=scroll_offset
799
+ ),
800
+ dont_extend_width=True,
801
+ z_index=z_index,
802
+ ),
803
+ VSplit(
804
+ [
805
+ Window(char=grid.BOTTOM_LEFT, width=1, height=1),
806
+ Window(char=grid.BOTTOM_MID, height=1),
807
+ Window(char=grid.BOTTOM_RIGHT, width=1, height=1),
808
+ ],
809
+ style="class:border",
810
+ ),
811
+ ],
812
+ style="class:menu",
813
+ ),
814
+ # Show when there are completions but not at the point we are
815
+ # returning the input.
816
+ filter=has_completions & ~is_done & extra_filter,
817
+ )