Skip to content

Commit 52cd964

Browse files
Added input_selection shortcut.
1 parent af26eec commit 52cd964

File tree

8 files changed

+408
-32
lines changed

8 files changed

+408
-32
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()

src/prompt_toolkit/layout/containers.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2603,8 +2603,18 @@ class ConditionalContainer(Container):
26032603
:param filter: :class:`.Filter` instance.
26042604
"""
26052605

2606-
def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None:
2606+
def __init__(
2607+
self,
2608+
content: AnyContainer,
2609+
filter: FilterOrBool,
2610+
alternative_content: AnyContainer | None = None,
2611+
) -> None:
26072612
self.content = to_container(content)
2613+
self.alternative_content = (
2614+
to_container(alternative_content)
2615+
if alternative_content is not None
2616+
else None
2617+
)
26082618
self.filter = to_filter(filter)
26092619

26102620
def __repr__(self) -> str:
@@ -2616,12 +2626,18 @@ def reset(self) -> None:
26162626
def preferred_width(self, max_available_width: int) -> Dimension:
26172627
if self.filter():
26182628
return self.content.preferred_width(max_available_width)
2629+
elif self.alternative_content is not None:
2630+
return self.alternative_content.preferred_width(max_available_width)
26192631
else:
26202632
return Dimension.zero()
26212633

26222634
def preferred_height(self, width: int, max_available_height: int) -> Dimension:
26232635
if self.filter():
26242636
return self.content.preferred_height(width, max_available_height)
2637+
elif self.alternative_content is not None:
2638+
return self.alternative_content.preferred_height(
2639+
width, max_available_height
2640+
)
26252641
else:
26262642
return Dimension.zero()
26272643

@@ -2638,9 +2654,21 @@ def write_to_screen(
26382654
return self.content.write_to_screen(
26392655
screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
26402656
)
2657+
elif self.alternative_content is not None:
2658+
return self.alternative_content.write_to_screen(
2659+
screen,
2660+
mouse_handlers,
2661+
write_position,
2662+
parent_style,
2663+
erase_bg,
2664+
z_index,
2665+
)
26412666

26422667
def get_children(self) -> list[Container]:
2643-
return [self.content]
2668+
result = [self.content]
2669+
if self.alternative_content is not None:
2670+
result.append(self.alternative_content)
2671+
return result
26442672

26452673

26462674
class DynamicContainer(Container):
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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 = ConditionalContainer(
91+
Frame(container),
92+
alternative_content=container,
93+
filter=show_frame_filter,
94+
)
95+
96+
layout = Layout(container, radio_list)
97+
98+
kb = KeyBindings()
99+
100+
@kb.add("enter", eager=True)
101+
def _accept_input(event: E) -> None:
102+
"Accept input when enter has been pressed."
103+
event.app.exit(result=radio_list.current_value, style="class:accepted")
104+
105+
@Condition
106+
def enable_abort() -> bool:
107+
return to_filter(self.enable_abort)()
108+
109+
@kb.add("c-c", filter=enable_abort)
110+
@kb.add("<sigint>", filter=enable_abort)
111+
def _keyboard_interrupt(event: E) -> None:
112+
"Abort when Control-C has been pressed."
113+
event.app.exit(exception=self.interrupt_exception(), style="class:aborting")
114+
115+
suspend_supported = Condition(suspend_to_background_supported)
116+
117+
@Condition
118+
def enable_suspend() -> bool:
119+
return to_filter(self.enable_suspend)()
120+
121+
@kb.add("c-z", filter=suspend_supported & enable_suspend)
122+
def _suspend(event: E) -> None:
123+
"""
124+
Suspend process to background.
125+
"""
126+
event.app.suspend_to_background()
127+
128+
return Application(
129+
layout=layout,
130+
full_screen=False,
131+
mouse_support=self.mouse_support,
132+
key_bindings=kb,
133+
style=self.style,
134+
)
135+
136+
def prompt(self) -> _T:
137+
return self._create_application().run()
138+
139+
async def prompt_async(self) -> _T:
140+
return await self._create_application().run_async()
141+
142+
143+
def select_input(
144+
message: AnyFormattedText,
145+
options: Sequence[tuple[_T, AnyFormattedText]],
146+
default: _T | None = None,
147+
mouse_support: bool = True,
148+
style: BaseStyle | None = None,
149+
symbol: str = ">",
150+
show_frame: bool = False,
151+
enable_suspend: FilterOrBool = False,
152+
enable_abort: FilterOrBool = True,
153+
interrupt_exception: type[BaseException] = KeyboardInterrupt,
154+
) -> _T:
155+
return InputSelection[_T](
156+
message=message,
157+
options=options,
158+
default=default,
159+
mouse_support=mouse_support,
160+
style=style,
161+
symbol=symbol,
162+
show_frame=show_frame,
163+
enable_suspend=enable_suspend,
164+
enable_abort=enable_abort,
165+
interrupt_exception=interrupt_exception,
166+
).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)