Skip to content

Commit ebfb985

Browse files
Added input_selection shortcut.
1 parent af26eec commit ebfb985

File tree

7 files changed

+379
-30
lines changed

7 files changed

+379
-30
lines changed

examples/input_selection/color.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
style=style,
29+
)
30+
print(result)
31+
32+
33+
if __name__ == "__main__":
34+
main()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from __future__ import annotations
2+
3+
from prompt_toolkit.shortcuts.input_selection import select_input
4+
5+
6+
def main() -> None:
7+
result = select_input(
8+
message="Please select an option:",
9+
options=[(i, f"Option {i}") for i in range(1, 100)],
10+
)
11+
print(result)
12+
13+
14+
if __name__ == "__main__":
15+
main()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
from prompt_toolkit.shortcuts.input_selection import select_input
4+
5+
6+
def main() -> None:
7+
result = select_input(
8+
message="Please select a dish:",
9+
options=[
10+
("pizza", "Pizza with mushrooms"),
11+
("salad", "Salad with tomatoes"),
12+
("sushi", "Sushi"),
13+
],
14+
)
15+
print(result)
16+
17+
18+
if __name__ == "__main__":
19+
main()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from __future__ import annotations
2+
3+
from prompt_toolkit.filters import is_done
4+
from prompt_toolkit.formatted_text import HTML
5+
from prompt_toolkit.shortcuts.input_selection import select_input
6+
from prompt_toolkit.styles import Style
7+
8+
9+
def main() -> None:
10+
style = Style.from_dict(
11+
{
12+
"frame.border": "#884444",
13+
# Mark selected option in bold, when accepted:
14+
"accepted selected-option": "bold",
15+
}
16+
)
17+
18+
result = select_input(
19+
message=HTML("<u>Please select a dish</u>:"),
20+
options=[
21+
("pizza", "Pizza with mushrooms"),
22+
("salad", "Salad with tomatoes"),
23+
("sushi", "Sushi"),
24+
],
25+
style=style,
26+
show_frame=~is_done,
27+
)
28+
print(result)
29+
30+
31+
if __name__ == "__main__":
32+
main()
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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, ConditionalContainer, 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+
__all__ = [
16+
"InputSelection",
17+
"select_input",
18+
]
19+
20+
_T = TypeVar("_T")
21+
E = KeyPressEvent
22+
23+
24+
class InputSelection(Generic[_T]):
25+
def __init__(
26+
self,
27+
*,
28+
message: AnyFormattedText,
29+
options: Sequence[tuple[_T, AnyFormattedText]],
30+
default: _T | None = None,
31+
mouse_support: bool = True,
32+
style: BaseStyle | None = None,
33+
symbol: str = ">",
34+
show_frame: FilterOrBool = False,
35+
enable_suspend: FilterOrBool = False,
36+
enable_abort: FilterOrBool = True,
37+
interrupt_exception: type[BaseException] = KeyboardInterrupt,
38+
) -> None:
39+
self.message = message
40+
self.default = default
41+
self.options = options
42+
self.mouse_support = mouse_support
43+
self.style = style
44+
self.symbol = symbol
45+
self.show_frame = show_frame
46+
self.enable_suspend = enable_suspend
47+
self.interrupt_exception = interrupt_exception
48+
self.enable_abort = enable_abort
49+
50+
def _create_application(self) -> Application[_T]:
51+
radio_list = RadioList(
52+
values=self.options,
53+
default=self.default,
54+
select_on_focus=True,
55+
open_character="",
56+
select_character=self.symbol,
57+
close_character="",
58+
show_cursor=False,
59+
show_numbers=True,
60+
container_style="class:input-selection",
61+
default_style="class:option",
62+
selected_style="",
63+
checked_style="class:selected-option",
64+
number_style="class:number",
65+
show_scrollbar=False,
66+
)
67+
container: AnyContainer = HSplit(
68+
[
69+
Box(
70+
Label(text=self.message, dont_extend_height=True),
71+
padding_top=0,
72+
padding_left=1,
73+
padding_right=1,
74+
padding_bottom=0,
75+
),
76+
Box(
77+
radio_list,
78+
padding_top=0,
79+
padding_left=3,
80+
padding_right=1,
81+
padding_bottom=0,
82+
),
83+
]
84+
)
85+
86+
@Condition
87+
def show_frame_filter() -> bool:
88+
return to_filter(self.show_frame)()
89+
90+
container = HSplit(
91+
[
92+
ConditionalContainer(container, filter=~show_frame_filter),
93+
ConditionalContainer(Frame(container), filter=show_frame_filter),
94+
],
95+
)
96+
97+
layout = Layout(container, radio_list)
98+
99+
kb = KeyBindings()
100+
101+
@kb.add("enter", eager=True)
102+
def _accept_input(event: E) -> None:
103+
"Accept input when enter has been pressed."
104+
event.app.exit(result=radio_list.current_value, style="class:accepted")
105+
106+
@Condition
107+
def enable_abort() -> bool:
108+
return to_filter(self.enable_abort)()
109+
110+
@kb.add("c-c", filter=enable_abort)
111+
@kb.add("<sigint>", filter=enable_abort)
112+
def _keyboard_interrupt(event: E) -> None:
113+
"Abort when Control-C has been pressed."
114+
event.app.exit(exception=self.interrupt_exception(), style="class:aborting")
115+
116+
suspend_supported = Condition(suspend_to_background_supported)
117+
118+
@Condition
119+
def enable_suspend() -> bool:
120+
return to_filter(self.enable_suspend)()
121+
122+
@kb.add("c-z", filter=suspend_supported & enable_suspend)
123+
def _suspend(event: E) -> None:
124+
"""
125+
Suspend process to background.
126+
"""
127+
event.app.suspend_to_background()
128+
129+
return Application(
130+
layout=layout,
131+
full_screen=False,
132+
mouse_support=self.mouse_support,
133+
key_bindings=kb,
134+
style=self.style,
135+
)
136+
137+
def prompt(self) -> _T:
138+
return self._create_application().run()
139+
140+
async def prompt_async(self) -> _T:
141+
return await self._create_application().run_async()
142+
143+
144+
def select_input(
145+
message: AnyFormattedText,
146+
options: Sequence[tuple[_T, AnyFormattedText]],
147+
default: _T | None = None,
148+
mouse_support: bool = True,
149+
style: BaseStyle | None = None,
150+
symbol: str = ">",
151+
show_frame: bool = False,
152+
enable_suspend: FilterOrBool = False,
153+
enable_abort: FilterOrBool = True,
154+
interrupt_exception: type[BaseException] = KeyboardInterrupt,
155+
) -> _T:
156+
return InputSelection[_T](
157+
message=message,
158+
options=options,
159+
default=default,
160+
mouse_support=mouse_support,
161+
style=style,
162+
symbol=symbol,
163+
show_frame=show_frame,
164+
enable_suspend=enable_suspend,
165+
enable_abort=enable_abort,
166+
interrupt_exception=interrupt_exception,
167+
).prompt()

src/prompt_toolkit/shortcuts/prompt.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,9 @@ def _create_default_buffer(self) -> Buffer:
502502
def accept(buff: Buffer) -> bool:
503503
"""Accept the content of the default buffer. This is called when
504504
the validation succeeds."""
505-
cast(Application[str], get_app()).exit(result=buff.document.text)
505+
cast(Application[str], get_app()).exit(
506+
result=buff.document.text, style="class:accepted"
507+
)
506508
return True # Keep text, we call 'reset' later on.
507509

508510
return Buffer(

0 commit comments

Comments
 (0)