Open skywind3000 opened 4 years ago
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.
Is it possible for full screen apps to use only a portion of current screen ?? Modern CLI tools use this feature to provide better experience:
How does the completion feature work? Can we implement a non-fullscreen app like this?
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.