Skip to content

Commit 5a1e9cb

Browse files
Added input_selection shortcut.
1 parent 3374ae9 commit 5a1e9cb

File tree

3 files changed

+274
-29
lines changed

3 files changed

+274
-29
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
from prompt_toolkit.formatted_text import HTML
4+
from prompt_toolkit.shortcuts.input_selection import select_input
5+
from prompt_toolkit.styles import Style
6+
7+
8+
def main() -> None:
9+
style = Style.from_dict(
10+
{
11+
"input-selection": "fg:#ff0000",
12+
"number": "fg:#884444 bold",
13+
"selected-option": "underline",
14+
"frame.border": "#884444",
15+
}
16+
)
17+
18+
result = select_input(
19+
message=HTML("<u>Please select a dish</u>:"),
20+
options=[
21+
("pizza", "Pizza with mushrooms"),
22+
(
23+
"salad",
24+
HTML("<ansigreen>Salad</ansigreen> with <ansired>tomatoes</ansired>"),
25+
),
26+
("sushi", "Sushi"),
27+
],
28+
default=1,
29+
style=style,
30+
#show_frame=True,
31+
)
32+
print(result)
33+
34+
35+
if __name__ == "__main__":
36+
main()
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
from __future__ import annotations
2+
3+
from typing import Generic, Sequence, TypeVar
4+
5+
from prompt_toolkit.application import Application
6+
from prompt_toolkit.filters import Condition, FilterOrBool, to_filter
7+
from prompt_toolkit.formatted_text import AnyFormattedText
8+
from prompt_toolkit.key_binding.key_bindings import KeyBindings
9+
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
10+
from prompt_toolkit.layout import AnyContainer, HSplit, Layout
11+
from prompt_toolkit.styles import BaseStyle
12+
from prompt_toolkit.utils import suspend_to_background_supported
13+
from prompt_toolkit.widgets import Box, Frame, Label, RadioList
14+
15+
_T = TypeVar("_T")
16+
E = KeyPressEvent
17+
18+
19+
class InputSelection(Generic[_T]):
20+
def __init__(
21+
self,
22+
*,
23+
message: AnyFormattedText,
24+
options: Sequence[tuple[_T, AnyFormattedText]],
25+
default: _T | None = None,
26+
mouse_support: bool = True,
27+
style: BaseStyle | None = None,
28+
symbol: str = ">",
29+
show_frame: bool = False,
30+
enable_suspend: FilterOrBool = False,
31+
) -> None:
32+
self.message = message
33+
self.default = default
34+
self.options = options
35+
self.mouse_support = mouse_support
36+
self.style = style
37+
self.symbol = symbol
38+
self.show_frame = show_frame
39+
self.enable_suspend = enable_suspend
40+
41+
def _create_application(self) -> Application[_T]:
42+
radio_list = RadioList(
43+
values=self.options,
44+
default=self.default,
45+
select_on_focus=True,
46+
open_character="",
47+
select_character=self.symbol,
48+
close_character="",
49+
show_cursor=False,
50+
show_numbers=True,
51+
container_style="class:input-selection",
52+
default_style="class:option",
53+
selected_style="",
54+
checked_style="class:selected-option",
55+
number_style="class:number",
56+
)
57+
container: AnyContainer = HSplit(
58+
[
59+
Box(
60+
Label(text=self.message, dont_extend_height=True),
61+
padding_top=0,
62+
padding_left=1,
63+
padding_right=1,
64+
padding_bottom=0,
65+
),
66+
Box(
67+
radio_list,
68+
padding_top=0,
69+
padding_left=3,
70+
padding_right=1,
71+
padding_bottom=0,
72+
),
73+
]
74+
)
75+
if self.show_frame:
76+
container = Frame(container)
77+
layout = Layout(container, radio_list)
78+
79+
kb = KeyBindings()
80+
81+
@kb.add("enter", eager=True)
82+
def _accept_input(event: E) -> None:
83+
"Accept input when enter has been pressed."
84+
event.app.exit(result=radio_list.current_value)
85+
86+
suspend_supported = Condition(suspend_to_background_supported)
87+
88+
@Condition
89+
def enable_suspend() -> bool:
90+
return to_filter(self.enable_suspend)()
91+
92+
@kb.add("c-z", filter=suspend_supported & enable_suspend)
93+
def _suspend(event: E) -> None:
94+
"""
95+
Suspend process to background.
96+
"""
97+
event.app.suspend_to_background()
98+
99+
return Application(
100+
layout=layout,
101+
full_screen=False,
102+
mouse_support=self.mouse_support,
103+
key_bindings=kb,
104+
style=self.style,
105+
)
106+
107+
def prompt(self) -> _T:
108+
return self._create_application().run()
109+
110+
async def prompt_async(self) -> _T:
111+
return await self._create_application().run_async()
112+
113+
114+
def select_input(
115+
message: AnyFormattedText,
116+
options: Sequence[tuple[_T, AnyFormattedText]],
117+
default: _T | None = None,
118+
mouse_support: bool = True,
119+
style: BaseStyle | None = None,
120+
symbol: str = ">",
121+
show_frame: bool = False,
122+
enable_suspend: FilterOrBool = False,
123+
) -> _T:
124+
return InputSelection(
125+
message=message,
126+
options=options,
127+
default=default,
128+
mouse_support=mouse_support,
129+
show_frame=show_frame,
130+
symbol=symbol,
131+
style=style,
132+
enable_suspend=enable_suspend,
133+
).prompt()

src/prompt_toolkit/widgets/base.py

Lines changed: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -694,24 +694,41 @@ class _DialogList(Generic[_T]):
694694
Common code for `RadioList` and `CheckboxList`.
695695
"""
696696

697-
open_character: str = ""
698-
close_character: str = ""
699-
container_style: str = ""
700-
default_style: str = ""
701-
selected_style: str = ""
702-
checked_style: str = ""
703-
multiple_selection: bool = False
704-
show_scrollbar: bool = True
705-
706697
def __init__(
707698
self,
708699
values: Sequence[tuple[_T, AnyFormattedText]],
709700
default_values: Sequence[_T] | None = None,
701+
select_on_focus: bool = False,
702+
open_character: str = "",
703+
select_character: str = "*",
704+
close_character: str = "",
705+
container_style: str = "",
706+
default_style: str = "",
707+
number_style: str = "",
708+
selected_style: str = "",
709+
checked_style: str = "",
710+
multiple_selection: bool = False,
711+
show_scrollbar: bool = True,
712+
show_cursor: bool = True,
713+
show_numbers: bool = False,
710714
) -> None:
711715
assert len(values) > 0
712716
default_values = default_values or []
713717

714718
self.values = values
719+
self.show_numbers = show_numbers
720+
721+
self.open_character = open_character
722+
self.select_character = select_character
723+
self.close_character = close_character
724+
self.container_style = container_style
725+
self.default_style = default_style
726+
self.number_style = number_style
727+
self.selected_style = selected_style
728+
self.checked_style = checked_style
729+
self.multiple_selection = multiple_selection
730+
self.show_scrollbar = show_scrollbar
731+
715732
# current_values will be used in multiple_selection,
716733
# current_value will be used otherwise.
717734
keys: list[_T] = [value for (value, _) in values]
@@ -736,10 +753,14 @@ def __init__(
736753
@kb.add("up")
737754
def _up(event: E) -> None:
738755
self._selected_index = max(0, self._selected_index - 1)
756+
if select_on_focus:
757+
self._handle_enter()
739758

740759
@kb.add("down")
741760
def _down(event: E) -> None:
742761
self._selected_index = min(len(self.values) - 1, self._selected_index + 1)
762+
if select_on_focus:
763+
self._handle_enter()
743764

744765
@kb.add("pageup")
745766
def _pageup(event: E) -> None:
@@ -774,9 +795,22 @@ def _find(event: E) -> None:
774795
self._selected_index = self.values.index(value)
775796
return
776797

798+
numbers_visible = Condition(lambda: self.show_numbers)
799+
800+
for i in range(1, 10):
801+
802+
@kb.add(str(i), filter=numbers_visible)
803+
def _select_i(event: E, index: int = i) -> None:
804+
self._selected_index = min(len(self.values) - 1, index - 1)
805+
if select_on_focus:
806+
self._handle_enter()
807+
777808
# Control and window.
778809
self.control = FormattedTextControl(
779-
self._get_text_fragments, key_bindings=kb, focusable=True
810+
self._get_text_fragments,
811+
key_bindings=kb,
812+
focusable=True,
813+
show_cursor=show_cursor,
780814
)
781815

782816
self.window = Window(
@@ -831,13 +865,19 @@ def mouse_handler(mouse_event: MouseEvent) -> None:
831865
result.append(("[SetCursorPosition]", ""))
832866

833867
if checked:
834-
result.append((style, "*"))
868+
result.append((style, self.select_character))
835869
else:
836870
result.append((style, " "))
837871

838872
result.append((style, self.close_character))
839-
result.append((self.default_style, " "))
840-
result.extend(to_formatted_text(value[1], style=self.default_style))
873+
result.append((f"{style} {self.default_style}", " "))
874+
875+
if self.show_numbers:
876+
result.append((f"{style} {self.number_style}", f"{i + 1:2d}. "))
877+
878+
result.extend(
879+
to_formatted_text(value[1], style=f"{style} {self.default_style}")
880+
)
841881
result.append(("", "\n"))
842882

843883
# Add mouse handler to all fragments.
@@ -858,25 +898,44 @@ class RadioList(_DialogList[_T]):
858898
:param values: List of (value, label) tuples.
859899
"""
860900

861-
open_character = "("
862-
close_character = ")"
863-
container_style = "class:radio-list"
864-
default_style = "class:radio"
865-
selected_style = "class:radio-selected"
866-
checked_style = "class:radio-checked"
867-
multiple_selection = False
868-
869901
def __init__(
870902
self,
871903
values: Sequence[tuple[_T, AnyFormattedText]],
872904
default: _T | None = None,
905+
show_numbers: bool = False,
906+
select_on_focus: bool = False,
907+
open_character: str = "(",
908+
select_character: str = "*",
909+
close_character: str = ")",
910+
container_style: str = "class:radio-list",
911+
default_style: str = "class:radio",
912+
selected_style: str = "class:radio-selected",
913+
checked_style: str = "class:radio-checked",
914+
number_style: str = "class:radio-number",
915+
multiple_selection: bool = False,
916+
show_cursor: bool = True,
873917
) -> None:
874918
if default is None:
875919
default_values = None
876920
else:
877921
default_values = [default]
878922

879-
super().__init__(values, default_values=default_values)
923+
super().__init__(
924+
values,
925+
default_values=default_values,
926+
select_on_focus=select_on_focus,
927+
show_numbers=show_numbers,
928+
open_character=open_character,
929+
select_character=select_character,
930+
close_character=close_character,
931+
container_style=container_style,
932+
default_style=default_style,
933+
selected_style=selected_style,
934+
checked_style=checked_style,
935+
number_style=number_style,
936+
multiple_selection=False,
937+
show_cursor=show_cursor,
938+
)
880939

881940

882941
class CheckboxList(_DialogList[_T]):
@@ -886,13 +945,30 @@ class CheckboxList(_DialogList[_T]):
886945
:param values: List of (value, label) tuples.
887946
"""
888947

889-
open_character = "["
890-
close_character = "]"
891-
container_style = "class:checkbox-list"
892-
default_style = "class:checkbox"
893-
selected_style = "class:checkbox-selected"
894-
checked_style = "class:checkbox-checked"
895-
multiple_selection = True
948+
def __init__(
949+
self,
950+
values: Sequence[tuple[_T, AnyFormattedText]],
951+
default_values: Sequence[_T] | None = None,
952+
open_character: str = "[",
953+
select_character: str = "*",
954+
close_character: str = "]",
955+
container_style: str = "class:checkbox-list",
956+
default_style: str = "class:checkbox",
957+
selected_style: str = "class:checkbox-selected",
958+
checked_style: str = "class:checkbox-checked",
959+
) -> None:
960+
super().__init__(
961+
values,
962+
default_values=default_values,
963+
open_character=open_character,
964+
select_character=select_character,
965+
close_character=close_character,
966+
container_style=container_style,
967+
default_style=default_style,
968+
selected_style=selected_style,
969+
checked_style=checked_style,
970+
multiple_selection=True,
971+
)
896972

897973

898974
class Checkbox(CheckboxList[str]):

0 commit comments

Comments
 (0)