Shellmen/src/libs/PyInquirer/prompts/checkbox.py

234 lines
8.4 KiB
Python

# -*- coding: utf-8 -*-
"""
`checkbox` type question
"""
from __future__ import print_function, unicode_literals
from libs.prompt_toolkit.application import Application
from libs.prompt_toolkit.key_binding.manager import KeyBindingManager
from libs.prompt_toolkit.keys import Keys
from libs.prompt_toolkit.layout.containers import Window
from libs.prompt_toolkit.filters import IsDone
from libs.prompt_toolkit.layout.controls import TokenListControl
from libs.prompt_toolkit.layout.containers import ConditionalContainer, \
ScrollOffsets, HSplit
from libs.prompt_toolkit.layout.dimension import LayoutDimension as D
from libs.prompt_toolkit.token import Token
from .. import PromptParameterException
from ..separator import Separator
from .common import setup_simple_validator, default_style, if_mousedown
# custom control based on TokenListControl
class InquirerControl(TokenListControl):
def __init__(self, choices, **kwargs):
self.pointer_index = 0
self.selected_options = [] # list of names
self.answered = False
self._init_choices(choices)
super(InquirerControl, self).__init__(self._get_choice_tokens,
**kwargs)
def _init_choices(self, choices):
# helper to convert from question format to internal format
self.choices = [] # list (name, value)
searching_first_choice = True
for i, c in enumerate(choices):
if isinstance(c, Separator):
self.choices.append(c)
else:
name = c['name']
value = c.get('value', name)
disabled = c.get('disabled', None)
if 'checked' in c and c['checked'] and not disabled:
self.selected_options.append(c['name'])
self.choices.append((name, value, disabled))
if searching_first_choice and not disabled: # find the first (available) choice
self.pointer_index = i
searching_first_choice = False
@property
def choice_count(self):
return len(self.choices)
def _get_choice_tokens(self, cli):
tokens = []
T = Token
def append(index, line):
if isinstance(line, Separator):
tokens.append((T.Separator, ' %s\n' % line))
else:
line_name = line[0]
line_value = line[1]
selected = (line_value in self.selected_options) # use value to check if option has been selected
pointed_at = (index == self.pointer_index)
@if_mousedown
def select_item(cli, mouse_event):
# bind option with this index to mouse event
if line_value in self.selected_options:
self.selected_options.remove(line_value)
else:
self.selected_options.append(line_value)
if pointed_at:
tokens.append((T.Pointer, ' \u276f', select_item)) # ' >'
else:
tokens.append((T, ' ', select_item))
# 'o ' - FISHEYE
if choice[2]: # disabled
tokens.append((T, '- %s (%s)' % (choice[0], choice[2])))
else:
if selected:
tokens.append((T.Selected, '\u25cf ', select_item))
else:
tokens.append((T, '\u25cb ', select_item))
if pointed_at:
tokens.append((Token.SetCursorPosition, ''))
tokens.append((T, line_name, select_item))
tokens.append((T, '\n'))
# prepare the select choices
for i, choice in enumerate(self.choices):
append(i, choice)
tokens.pop() # Remove last newline.
return tokens
def get_selected_values(self):
# get values not labels
return [c[1] for c in self.choices if not isinstance(c, Separator) and
c[1] in self.selected_options]
@property
def line_count(self):
return len(self.choices)
def question(message, **kwargs):
# TODO add bottom-bar (Move up and down to reveal more choices)
# TODO extract common parts for list, checkbox, rawlist, expand
# TODO validate
if not 'choices' in kwargs:
raise PromptParameterException('choices')
# this does not implement default, use checked...
if 'default' in kwargs:
raise ValueError('Checkbox does not implement \'default\' '
'use \'checked\':True\' in choice!')
choices = kwargs.pop('choices', None)
validator = setup_simple_validator(kwargs)
# TODO style defaults on detail level
style = kwargs.pop('style', default_style)
ic = InquirerControl(choices)
qmark = kwargs.pop('qmark', '?')
def get_prompt_tokens(cli):
tokens = []
tokens.append((Token.QuestionMark, qmark))
tokens.append((Token.Question, ' %s ' % message))
if ic.answered:
nbr_selected = len(ic.selected_options)
if nbr_selected == 0:
tokens.append((Token.Answer, ' done'))
elif nbr_selected == 1:
tokens.append((Token.Answer, ' [%s]' % ic.selected_options[0]))
else:
tokens.append((Token.Answer,
' done (%d selections)' % nbr_selected))
else:
tokens.append((Token.Instruction,
' (<up>, <down> to move, <space> to select, <a> '
'to toggle, <i> to invert)'))
return tokens
# assemble layout
layout = HSplit([
Window(height=D.exact(1),
content=TokenListControl(get_prompt_tokens, align_center=False)
),
ConditionalContainer(
Window(
ic,
width=D.exact(43),
height=D(min=3),
scroll_offsets=ScrollOffsets(top=1, bottom=1)
),
filter=~IsDone()
)
])
# key bindings
manager = KeyBindingManager.for_prompt()
@manager.registry.add_binding(Keys.ControlQ, eager=True)
@manager.registry.add_binding(Keys.ControlC, eager=True)
def _(event):
raise KeyboardInterrupt()
# event.cli.set_return_value(None)
@manager.registry.add_binding(' ', eager=True)
def toggle(event):
pointed_choice = ic.choices[ic.pointer_index][1] # value
if pointed_choice in ic.selected_options:
ic.selected_options.remove(pointed_choice)
else:
ic.selected_options.append(pointed_choice)
@manager.registry.add_binding('i', eager=True)
def invert(event):
inverted_selection = [c[1] for c in ic.choices if
not isinstance(c, Separator) and
c[1] not in ic.selected_options and
not c[2]]
ic.selected_options = inverted_selection
@manager.registry.add_binding('a', eager=True)
def all(event):
all_selected = True # all choices have been selected
for c in ic.choices:
if not isinstance(c, Separator) and c[1] not in ic.selected_options and not c[2]:
# add missing ones
ic.selected_options.append(c[1])
all_selected = False
if all_selected:
ic.selected_options = []
@manager.registry.add_binding(Keys.Down, eager=True)
def move_cursor_down(event):
def _next():
ic.pointer_index = ((ic.pointer_index + 1) % ic.line_count)
_next()
while isinstance(ic.choices[ic.pointer_index], Separator) or \
ic.choices[ic.pointer_index][2]:
_next()
@manager.registry.add_binding(Keys.Up, eager=True)
def move_cursor_up(event):
def _prev():
ic.pointer_index = ((ic.pointer_index - 1) % ic.line_count)
_prev()
while isinstance(ic.choices[ic.pointer_index], Separator) or \
ic.choices[ic.pointer_index][2]:
_prev()
@manager.registry.add_binding(Keys.Enter, eager=True)
def set_answer(event):
ic.answered = True
# TODO use validator
event.cli.set_return_value(ic.get_selected_values())
return Application(
layout=layout,
key_bindings_registry=manager.registry,
mouse_support=True,
style=style
)