-
Notifications
You must be signed in to change notification settings - Fork 725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feature-request] Command line input form and MultiSelect Prompt. #1071
Comments
Hi @skywind3000, This functionality can be built on top of prompt_toolkit. It does require a new custom layout (see the pages about full screen applications) and custom key bindings. It should not be a lot of work, but it's not yet available out of the box. |
https://github.com/CITGuru/PyInquirer seems to include most of these controls. |
Thanks for Yajo and PyInquirer. I create a simple prompt for this feature, maybe we can add it into exmaples from typing import List, Optional, Tuple, Union
from prompt_toolkit.application import Application, get_app
from prompt_toolkit.filters import IsDone
from prompt_toolkit.formatted_text import AnyFormattedText
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.layout.containers import ConditionalContainer, HSplit
from prompt_toolkit.mouse_events import MouseEventType
from prompt_toolkit.styles import Style
OptionValue = Optional[AnyFormattedText]
Option = Union[
AnyFormattedText, # name value is same
Tuple[AnyFormattedText, OptionValue] # (name, value)
]
IndexedOption = Tuple[
int, # index
AnyFormattedText, # name
OptionValue
]
class SelectionControl(FormattedTextControl):
def __init__(
self,
options: List[Option],
**kwargs
) -> None:
self.options = self._index_options(options)
self.answered = False
self.selected_option_index = 0
super().__init__(**kwargs)
@property
def selected_option(self) -> IndexedOption:
return self.options[self.selected_option_index]
@property
def options_count(self) -> int:
return len(self.options)
def _index_options(self, options) -> List[IndexedOption]:
"""
Convert Option to IndexedOption
"""
indexed_options = []
for idx, opt in enumerate(options):
if isinstance(opt, str):
indexed_options.append((idx, opt, opt))
if isinstance(opt, tuple):
if len(opt) != 2:
raise ValueError(f'invalid tuple option: {opt}.')
indexed_options.append((idx, *opt))
return indexed_options
def _select_option(self, index):
def handler(mouse_event):
if mouse_event.event_type != MouseEventType.MOUSE_DOWN:
raise NotImplemented
# bind option with this index to mouse event
self.selected_option_index = index
self.answered = True
get_app().exit(result=self.selected_option)
return handler
def format_option(
self,
option: IndexedOption,
*,
selected_style_class: str = '',
selected_prefix_char: str = '>',
indent: int = 1
):
option_prefix: AnyFormattedText = ' ' * indent
idx, name, value = option
if self.selected_option_index == idx:
option_prefix = selected_prefix_char + option_prefix
return selected_style_class, f'{option_prefix}{name}\n', self._select_option(idx)
option_prefix += ' '
return '', f'{option_prefix}{name}\n', self._select_option(idx)
class SelectionPrompt:
def __init__(
self,
message: AnyFormattedText = "",
*,
options: List[Option] = None
) -> None:
self.message = message
self.options = options
self.control = None
self.layout = None
self.key_bindings = None
self.app = None
def _create_layout(self) -> Layout:
"""
Create `Layout` for this prompt.
"""
def get_option_text():
return [
self.control.format_option(
opt, selected_style_class='class:reverse'
) for opt in self.control.options
]
layout = HSplit([
Window(
height=Dimension.exact(1),
content=FormattedTextControl(
lambda: self.message + '\n',
show_cursor=False
),
),
Window(
height=Dimension.exact(self.control.options_count),
content=FormattedTextControl(get_option_text)
),
ConditionalContainer(
Window(self.control),
filter=~IsDone()
)
])
return Layout(layout)
def _create_key_bindings(self) -> KeyBindings:
"""
Create `KeyBindings` for this prompt
"""
control = self.control
kb = KeyBindings()
@kb.add('c-q', eager=True)
@kb.add('c-c', eager=True)
def _(event):
raise KeyboardInterrupt()
@kb.add('down', eager=True)
def move_cursor_down(event):
control.selected_option_index = (control.selected_option_index + 1) % control.options_count
@kb.add('up', eager=True)
def move_cursor_up(event):
control.selected_option_index = (control.selected_option_index - 1) % control.options_count
@kb.add('enter', eager=True)
def set_answer(event):
control.answered = True
_, _, selected_option_value = control.selected_option
event.app.exit(result=selected_option_value)
return kb
def _create_application(self) -> Application:
"""
Create `Application` for this prompt.
"""
style = Style.from_dict(
{
"status": "reverse",
}
)
app = Application(
layout=self.layout,
key_bindings=self.key_bindings,
style=style,
full_screen=False
)
return app
def prompt(
self,
message: Optional[AnyFormattedText] = None,
*,
options: List[Option],
):
# all arguments are overwritten the init arguments in SelectionPrompt.
if message is not None:
self.message = message
if options is not None:
self.options = options
if self.app is None:
self.control = SelectionControl(self.options)
self.layout = self._create_layout()
self.key_bindings = self._create_key_bindings()
self.app = self._create_application()
return self.app.run()
if __name__ == '__main__':
p = SelectionPrompt()
v = p.prompt('choose one', options=['v1', 'v2'])
print(f'you choose: {v}') |
Some cool features from enquier that I really wish to achieve them in prompt-toolkit, because I don't write javascript / nodejs stuff.
Form Prompt
Prompt that allows the user to enter and submit multiple values on a single terminal screen.
MultiSelect Prompt
Prompt that allows the user to select multiple items from a list of options.
The text was updated successfully, but these errors were encountered: