Complete rewrite and removed legacy bash version

This commit is contained in:
2023-09-17 20:26:11 -05:00
parent a7da5d9ed8
commit a7c8e630fa
163 changed files with 27028 additions and 1105 deletions

View File

@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function
import os
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.styles import style_from_dict
from libs.prompt_toolkit.validation import Validator, ValidationError
from .utils import print_json, format_json
__version__ = '1.0.2'
def here(p):
return os.path.abspath(os.path.join(os.path.dirname(__file__), p))
class PromptParameterException(ValueError):
def __init__(self, message, errors=None):
# Call the base class constructor with the parameters it needs
super(PromptParameterException, self).__init__(
'You must provide a `%s` value' % message, errors)
from .prompt import prompt
from .separator import Separator
from .prompts.common import default_style

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""
provide colorized output
"""
from __future__ import print_function, unicode_literals
import sys
from libs.prompt_toolkit.shortcuts import print_tokens, style_from_dict, Token
def _print_token_factory(col):
"""Internal helper to provide color names."""
def _helper(msg):
style = style_from_dict({
Token.Color: col,
})
tokens = [
(Token.Color, msg)
]
print_tokens(tokens, style=style)
def _helper_no_terminal(msg):
# workaround if we have no terminal
print(msg)
if sys.stdout.isatty():
return _helper
else:
return _helper_no_terminal
# used this for color source:
# http://unix.stackexchange.com/questions/105568/how-can-i-list-the-available-color-names
yellow = _print_token_factory('#dfaf00')
blue = _print_token_factory('#0087ff')
gray = _print_token_factory('#6c6c6c')
# TODO
#black
#red
#green
#magenta
#cyan
#white

View File

@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function
from libs.prompt_toolkit.shortcuts import run_application
from . import PromptParameterException, prompts
from .prompts import list, confirm, input, password, checkbox, rawlist, expand, editor
def prompt(questions, answers=None, **kwargs):
if isinstance(questions, dict):
questions = [questions]
answers = answers or {}
patch_stdout = kwargs.pop('patch_stdout', False)
return_asyncio_coroutine = kwargs.pop('return_asyncio_coroutine', False)
true_color = kwargs.pop('true_color', False)
refresh_interval = kwargs.pop('refresh_interval', 0)
eventloop = kwargs.pop('eventloop', None)
kbi_msg = kwargs.pop('keyboard_interrupt_msg', 'Cancelled by user')
for question in questions:
# import the question
if 'type' not in question:
raise PromptParameterException('type')
if 'name' not in question:
raise PromptParameterException('name')
if 'message' not in question:
raise PromptParameterException('message')
try:
choices = question.get('choices')
if choices is not None and callable(choices):
question['choices'] = choices(answers)
_kwargs = {}
_kwargs.update(kwargs)
_kwargs.update(question)
type = _kwargs.pop('type')
name = _kwargs.pop('name')
message = _kwargs.pop('message')
when = _kwargs.pop('when', None)
filter = _kwargs.pop('filter', None)
if when:
# at least a little sanity check!
if callable(question['when']):
try:
if not question['when'](answers):
continue
except Exception as e:
raise ValueError(
'Problem in \'when\' check of %s question: %s' %
(name, e))
else:
raise ValueError('\'when\' needs to be function that ' \
'accepts a dict argument')
if filter:
# at least a little sanity check!
if not callable(question['filter']):
raise ValueError('\'filter\' needs to be function that ' \
'accepts an argument')
if callable(question.get('default')):
_kwargs['default'] = question['default'](answers)
application = getattr(prompts, type).question(message, **_kwargs)
answer = run_application(
application,
patch_stdout=patch_stdout,
return_asyncio_coroutine=return_asyncio_coroutine,
true_color=true_color,
refresh_interval=refresh_interval,
eventloop=eventloop)
if answer is not None:
if filter:
try:
answer = question['filter'](answer)
except Exception as e:
raise ValueError(
'Problem processing \'filter\' of %s question: %s' %
(name, e))
answers[name] = answer
except AttributeError as e:
print(e)
raise ValueError('No question type \'%s\'' % type)
except KeyboardInterrupt:
print('')
print(kbi_msg)
print('')
return {}
return answers
# TODO:
# Bottom Bar - inquirer.ui.BottomBar

View File

View File

@@ -0,0 +1,233 @@
# -*- 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
)

View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
"""
common prompt functionality
"""
import sys
from libs.prompt_toolkit.validation import Validator, ValidationError
from libs.prompt_toolkit.styles import style_from_dict
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.mouse_events import MouseEventTypes
PY3 = sys.version_info[0] >= 3
if PY3:
basestring = str
def if_mousedown(handler):
def handle_if_mouse_down(cli, mouse_event):
if mouse_event.event_type == MouseEventTypes.MOUSE_DOWN:
return handler(cli, mouse_event)
else:
return NotImplemented
return handle_if_mouse_down
# TODO probably better to use base.Condition
def setup_validator(kwargs):
# this is an internal helper not meant for public consumption!
# note this works on a dictionary
validate_prompt = kwargs.pop('validate', None)
if validate_prompt:
if issubclass(validate_prompt, Validator):
kwargs['validator'] = validate_prompt()
elif callable(validate_prompt):
class _InputValidator(Validator):
def validate(self, document):
#print('validation!!')
verdict = validate_prompt(document.text)
if isinstance(verdict, basestring):
raise ValidationError(
message=verdict,
cursor_position=len(document.text))
elif verdict is not True:
raise ValidationError(
message='invalid input',
cursor_position=len(document.text))
kwargs['validator'] = _InputValidator()
return kwargs['validator']
def setup_simple_validator(kwargs):
# this is an internal helper not meant for public consumption!
# note this works on a dictionary
# this validates the answer not a buffer
# TODO
# not sure yet how to deal with the validation result:
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/430
validate = kwargs.pop('validate', None)
if validate is None:
def _always(answer):
return True
return _always
elif not callable(validate):
raise ValueError('Here a simple validate function is expected, no class')
def _validator(answer):
verdict = validate(answer)
if isinstance(verdict, basestring):
raise ValidationError(
message=verdict
)
elif verdict is not True:
raise ValidationError(
message='invalid input'
)
return _validator
# FIXME style defaults on detail level
default_style = style_from_dict({
Token.Separator: '#6C6C6C',
Token.QuestionMark: '#5F819D',
Token.Selected: '', # default
Token.Pointer: '#FF9D00 bold', # AWS orange
Token.Instruction: '', # default
Token.Answer: '#FF9D00 bold', # AWS orange
Token.Question: 'bold',
})

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
"""
confirm 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, HSplit
from libs.prompt_toolkit.layout.controls import TokenListControl
from libs.prompt_toolkit.layout.dimension import LayoutDimension as D
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.shortcuts import create_prompt_application
from libs.prompt_toolkit.styles import style_from_dict
# custom control based on TokenListControl
def question(message, **kwargs):
# TODO need ENTER confirmation
default = kwargs.pop('default', True)
# TODO style defaults on detail level
style = kwargs.pop('style', style_from_dict({
Token.QuestionMark: '#5F819D',
#Token.Selected: '#FF9D00', # AWS orange
Token.Instruction: '', # default
Token.Answer: '#FF9D00 bold', # AWS orange
Token.Question: 'bold',
}))
status = {'answer': None}
qmark = kwargs.pop('qmark', '?')
def get_prompt_tokens(cli):
tokens = []
tokens.append((Token.QuestionMark, qmark))
tokens.append((Token.Question, ' %s ' % message))
if isinstance(status['answer'], bool):
tokens.append((Token.Answer, ' Yes' if status['answer'] else ' No'))
else:
if default:
instruction = ' (Y/n)'
else:
instruction = ' (y/N)'
tokens.append((Token.Instruction, instruction))
return tokens
# 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()
@manager.registry.add_binding('n')
@manager.registry.add_binding('N')
def key_n(event):
status['answer'] = False
event.cli.set_return_value(False)
@manager.registry.add_binding('y')
@manager.registry.add_binding('Y')
def key_y(event):
status['answer'] = True
event.cli.set_return_value(True)
@manager.registry.add_binding(Keys.Enter, eager=True)
def set_answer(event):
status['answer'] = default
event.cli.set_return_value(default)
return create_prompt_application(
get_prompt_tokens=get_prompt_tokens,
key_bindings_registry=manager.registry,
mouse_support=False,
style=style,
erase_when_done=False,
)

View File

@@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-
"""
`editor` type question
"""
from __future__ import print_function, unicode_literals
import os
import sys
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.shortcuts import create_prompt_application
from libs.prompt_toolkit.validation import Validator, ValidationError
from libs.prompt_toolkit.layout.lexers import SimpleLexer
from .common import default_style
# use std prompt-toolkit control
WIN = sys.platform.startswith('win')
class EditorArgumentsError(Exception):
pass
class Editor(object):
def __init__(self, editor=None, env=None, require_save=True, extension='.txt'):
self.editor = editor
self.env = env
self.require_save = require_save
self.extension = extension
def get_editor(self):
if self.editor is not None and self.editor.lower() != "default":
return self.editor
for key in 'VISUAL', 'EDITOR':
rv = os.environ.get(key)
if rv:
return rv
if WIN:
return 'notepad'
for editor in 'vim', 'nano':
if os.system('which %s >/dev/null 2>&1' % editor) == 0:
return editor
return 'vi'
def edit_file(self, filename):
import subprocess
editor = self.get_editor()
if self.env:
environ = os.environ.copy()
environ.update(self.env)
else:
environ = None
try:
c = subprocess.Popen('%s "%s"' % (editor, filename),
env=environ, shell=True)
exit_code = c.wait()
if exit_code != 0:
raise Exception('%s: Editing failed!' % editor)
except OSError as e:
raise Exception('%s: Editing failed: %s' % (editor, e))
def edit(self, text):
import tempfile
text = text or ''
if text and not text.endswith('\n'):
text += '\n'
fd, name = tempfile.mkstemp(prefix='editor-', suffix=self.extension)
try:
if WIN:
encoding = 'utf-8-sig'
text = text.replace('\n', '\r\n')
else:
encoding = 'utf-8'
text = text.encode(encoding)
f = os.fdopen(fd, 'wb')
f.write(text)
f.close()
timestamp = os.path.getmtime(name)
self.edit_file(name)
if self.require_save \
and os.path.getmtime(name) == timestamp:
return None
f = open(name, 'rb')
try:
rv = f.read()
finally:
f.close()
return rv.decode('utf-8-sig').replace('\r\n', '\n')
finally:
os.unlink(name)
def edit(text=None, editor=None, env=None, require_save=True,
extension='.txt', filename=None):
r"""Edits the given text in the defined editor. If an editor is given
(should be the full path to the executable but the regular operating
system search path is used for finding the executable) it overrides
the detected editor. Optionally, some environment variables can be
used. If the editor is closed without changes, `None` is returned. In
case a file is edited directly the return value is always `None` and
`require_save` and `extension` are ignored.
If the editor cannot be opened a :exc:`UsageError` is raised.
Note for Windows: to simplify cross-platform usage, the newlines are
automatically converted from POSIX to Windows and vice versa. As such,
the message here will have ``\n`` as newline markers.
:param text: the text to edit.
:param editor: optionally the editor to use. Defaults to automatic
detection.
:param env: environment variables to forward to the editor.
:param require_save: if this is true, then not saving in the editor
will make the return value become `None`.
:param extension: the extension to tell the editor about. This defaults
to `.txt` but changing this might change syntax
highlighting.
:param filename: if provided it will edit this file instead of the
provided text contents. It will not use a temporary
file as an indirection in that case.
"""
editor = Editor(editor=editor, env=env, require_save=require_save,
extension=extension)
if filename is None:
return editor.edit(text)
editor.edit_file(filename)
def question(message, **kwargs):
default = kwargs.pop('default', '')
eargs = kwargs.pop('eargs', {})
validate_prompt = kwargs.pop('validate', None)
if validate_prompt:
if issubclass(validate_prompt, Validator):
kwargs['validator'] = validate_prompt()
elif callable(validate_prompt):
class _InputValidator(Validator):
def validate(self, document):
verdict = validate_prompt(document.text)
if not verdict == True:
if verdict == False:
verdict = 'invalid input'
raise ValidationError(
message=verdict,
cursor_position=len(document.text))
kwargs['validator'] = _InputValidator()
for k, v in eargs.items():
if v == "" or v == " ":
raise EditorArgumentsError(
"Args '{}' value should not be empty".format(k)
)
editor = eargs.get("editor", None)
ext = eargs.get("ext", ".txt")
env = eargs.get("env", None)
text = default
filename = eargs.get("filename", None)
multiline = True if not editor else False
save = eargs.get("save", None)
if editor:
_text = edit(
editor=editor,
extension=ext,
text=text,
env=env,
filename=filename,
require_save=save
)
if filename:
default = filename
else:
default = _text
# TODO style defaults on detail level
kwargs['style'] = kwargs.pop('style', default_style)
qmark = kwargs.pop('qmark', '?')
def _get_prompt_tokens(cli):
return [
(Token.QuestionMark, qmark),
(Token.Question, ' %s ' % message)
]
return create_prompt_application(
get_prompt_tokens=_get_prompt_tokens,
lexer=SimpleLexer(Token.Answer),
default=default,
multiline=multiline,
**kwargs
)

View File

@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
"""
`expand` type question
"""
from __future__ import print_function, unicode_literals
import sys
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, 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 default_style
from .common import if_mousedown
PY3 = sys.version_info[0] >= 3
if PY3:
basestring = str
# custom control based on TokenListControl
class InquirerControl(TokenListControl):
def __init__(self, choices, default=None, **kwargs):
self.pointer_index = 0
self.answered = False
self._init_choices(choices, default)
self._help_active = False # help is activated via 'h' key
super(InquirerControl, self).__init__(self._get_choice_tokens,
**kwargs)
def _init_choices(self, choices, default=None):
# helper to convert from question format to internal format
self.choices = [] # list (key, name, value)
if not default:
default = 'h'
for i, c in enumerate(choices):
if isinstance(c, Separator):
self.choices.append(c)
else:
if isinstance(c, basestring):
self.choices.append((key, c, c))
else:
key = c.get('key')
name = c.get('name')
value = c.get('value', name)
if default and default == key:
self.pointer_index = i
key = key.upper() # default key is in uppercase
self.choices.append((key, name, value))
# append the help choice
key = 'h'
if not default:
self.pointer_index = len(self.choices)
key = key.upper() # default key is in uppercase
self.choices.append((key, 'Help, list all options', None))
@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:
key = line[0]
line = line[1]
pointed_at = (index == self.pointer_index)
@if_mousedown
def select_item(cli, mouse_event):
# bind option with this index to mouse event
self.pointer_index = index
if pointed_at:
tokens.append((T.Selected, ' %s) %s' % (key, line),
select_item))
else:
tokens.append((T, ' %s) %s' % (key, line),
select_item))
tokens.append((T, '\n'))
if self._help_active:
# prepare the select choices
for i, choice in enumerate(self.choices):
_append(i, choice)
tokens.append((T, ' Answer: %s' %
self.choices[self.pointer_index][0]))
else:
tokens.append((T.Pointer, '>> '))
tokens.append((T, self.choices[self.pointer_index][1]))
return tokens
def get_selected_value(self):
# get value not label
return self.choices[self.pointer_index][2]
def question(message, **kwargs):
# TODO extract common parts for list, checkbox, rawlist, expand
# TODO up, down navigation
if not 'choices' in kwargs:
raise PromptParameterException('choices')
choices = kwargs.pop('choices', None)
default = kwargs.pop('default', None)
qmark = kwargs.pop('qmark', '?')
# TODO style defaults on detail level
style = kwargs.pop('style', default_style)
ic = InquirerControl(choices, default)
def get_prompt_tokens(cli):
tokens = []
T = Token
tokens.append((T.QuestionMark, qmark))
tokens.append((T.Question, ' %s ' % message))
if not ic.answered:
tokens.append((T.Instruction, ' (%s)' % ''.join(
[k[0] for k in ic.choices if not isinstance(k, Separator)])))
else:
tokens.append((T.Answer, ' %s' % ic.get_selected_value()))
return tokens
#@Condition
#def is_help_active(cli):
# return ic._help_active
# assemble layout
layout = HSplit([
Window(height=D.exact(1),
content=TokenListControl(get_prompt_tokens)
),
ConditionalContainer(
Window(ic),
#filter=is_help_active & ~IsDone() # ~ bitwise inverse
filter=~IsDone() # ~ bitwise inverse
)
])
# 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()
# add key bindings for choices
for i, c in enumerate(ic.choices):
if not isinstance(c, Separator):
def _reg_binding(i, keys):
# trick out late evaluation with a "function factory":
# http://stackoverflow.com/questions/3431676/creating-functions-in-a-loop
@manager.registry.add_binding(keys, eager=True)
def select_choice(event):
ic.pointer_index = i
if c[0] not in ['h', 'H']:
_reg_binding(i, c[0])
if c[0].isupper():
_reg_binding(i, c[0].lower())
@manager.registry.add_binding('H', eager=True)
@manager.registry.add_binding('h', eager=True)
def help_choice(event):
ic._help_active = True
@manager.registry.add_binding(Keys.Enter, eager=True)
def set_answer(event):
ic.answered = True
event.cli.set_return_value(ic.get_selected_value())
return Application(
layout=layout,
key_bindings_registry=manager.registry,
mouse_support=True,
style=style
)

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
"""
`input` type question
"""
from __future__ import print_function, unicode_literals
import inspect
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.shortcuts import create_prompt_application
from libs.prompt_toolkit.validation import Validator, ValidationError
from libs.prompt_toolkit.layout.lexers import SimpleLexer
from .common import default_style
# use std prompt-toolkit control
def question(message, **kwargs):
default = kwargs.pop('default', '')
validate_prompt = kwargs.pop('validate', None)
if validate_prompt:
if inspect.isclass(validate_prompt) and issubclass(validate_prompt, Validator):
kwargs['validator'] = validate_prompt()
elif callable(validate_prompt):
class _InputValidator(Validator):
def validate(self, document):
verdict = validate_prompt(document.text)
if not verdict == True:
if verdict == False:
verdict = 'invalid input'
raise ValidationError(
message=verdict,
cursor_position=len(document.text))
kwargs['validator'] = _InputValidator()
# TODO style defaults on detail level
kwargs['style'] = kwargs.pop('style', default_style)
qmark = kwargs.pop('qmark', '?')
def _get_prompt_tokens(cli):
return [
(Token.QuestionMark, qmark),
(Token.Question, ' %s ' % message)
]
return create_prompt_application(
get_prompt_tokens=_get_prompt_tokens,
lexer=SimpleLexer(Token.Answer),
default=default,
**kwargs
)

View File

@@ -0,0 +1,184 @@
# -*- coding: utf-8 -*-
"""
`list` type question
"""
from __future__ import print_function
from __future__ import unicode_literals
import sys
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 if_mousedown, default_style
# custom control based on TokenListControl
# docu here:
# https://github.com/jonathanslenders/python-prompt-toolkit/issues/281
# https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/examples/full-screen-layout.py
# https://github.com/jonathanslenders/python-prompt-toolkit/blob/master/docs/pages/full_screen_apps.rst
PY3 = sys.version_info[0] >= 3
if PY3:
basestring = str
class InquirerControl(TokenListControl):
def __init__(self, choices, **kwargs):
self.selected_option_index = 0
self.answered = False
self.choices = choices
self._init_choices(choices)
super(InquirerControl, self).__init__(self._get_choice_tokens,
**kwargs)
def _init_choices(self, choices, default=None):
# helper to convert from question format to internal format
self.choices = [] # list (name, value, disabled)
searching_first_choice = True
for i, c in enumerate(choices):
if isinstance(c, Separator):
self.choices.append((c, None, None))
else:
if isinstance(c, basestring):
self.choices.append((c, c, None))
else:
name = c.get('name')
value = c.get('value', name)
disabled = c.get('disabled', None)
self.choices.append((name, value, disabled))
if searching_first_choice:
self.selected_option_index = i # found the first choice
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, choice):
selected = (index == self.selected_option_index)
@if_mousedown
def select_item(cli, mouse_event):
# bind option with this index to mouse event
self.selected_option_index = index
self.answered = True
cli.set_return_value(None)
tokens.append((T.Pointer if selected else T, ' \u276f ' if selected
else ' '))
if selected:
tokens.append((Token.SetCursorPosition, ''))
if choice[2]: # disabled
tokens.append((T.Selected if selected else T,
'- %s (%s)' % (choice[0], choice[2])))
else:
try:
tokens.append((T.Selected if selected else T, str(choice[0]),
select_item))
except:
tokens.append((T.Selected if selected else T, choice[0],
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_selection(self):
return self.choices[self.selected_option_index]
def question(message, **kwargs):
# TODO disabled, dict choices
if not 'choices' in kwargs:
raise PromptParameterException('choices')
choices = kwargs.pop('choices', None)
default = kwargs.pop('default', 0) # TODO
qmark = kwargs.pop('qmark', '?')
# TODO style defaults on detail level
style = kwargs.pop('style', default_style)
ic = InquirerControl(choices)
def get_prompt_tokens(cli):
tokens = []
tokens.append((Token.QuestionMark, qmark))
tokens.append((Token.Question, ' %s ' % message))
if ic.answered:
tokens.append((Token.Answer, ' ' + ic.get_selection()[0]))
else:
tokens.append((Token.Instruction, ' (Use arrow keys)'))
return tokens
# assemble layout
layout = HSplit([
Window(height=D.exact(1),
content=TokenListControl(get_prompt_tokens)
),
ConditionalContainer(
Window(ic),
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(Keys.Down, eager=True)
def move_cursor_down(event):
def _next():
ic.selected_option_index = (
(ic.selected_option_index + 1) % ic.choice_count)
_next()
while isinstance(ic.choices[ic.selected_option_index][0], Separator) or\
ic.choices[ic.selected_option_index][2]:
_next()
@manager.registry.add_binding(Keys.Up, eager=True)
def move_cursor_up(event):
def _prev():
ic.selected_option_index = (
(ic.selected_option_index - 1) % ic.choice_count)
_prev()
while isinstance(ic.choices[ic.selected_option_index][0], Separator) or \
ic.choices[ic.selected_option_index][2]:
_prev()
@manager.registry.add_binding(Keys.Enter, eager=True)
def set_answer(event):
ic.answered = True
event.cli.set_return_value(ic.get_selection()[1])
return Application(
layout=layout,
key_bindings_registry=manager.registry,
mouse_support=True,
style=style
)

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
"""
`password` type question
"""
from __future__ import print_function, unicode_literals
from . import input
# use std prompt-toolkit control
def question(message, **kwargs):
kwargs['is_password'] = True
return input.question(message, **kwargs)

View File

@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
"""
`rawlist` type question
"""
from __future__ import print_function, unicode_literals
import sys
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, 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 default_style
from .common import if_mousedown
PY3 = sys.version_info[0] >= 3
if PY3:
basestring = str
# custom control based on TokenListControl
class InquirerControl(TokenListControl):
def __init__(self, choices, **kwargs):
self.pointer_index = 0
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 (key, name, value)
searching_first_choice = True
key = 1 # used for numeric keys
for i, c in enumerate(choices):
if isinstance(c, Separator):
self.choices.append(c)
else:
if isinstance(c, basestring):
self.choices.append((key, c, c))
key += 1
if searching_first_choice:
self.pointer_index = i # found the first choice
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:
key = line[0]
line = line[1]
pointed_at = (index == self.pointer_index)
@if_mousedown
def select_item(cli, mouse_event):
# bind option with this index to mouse event
self.pointer_index = index
if pointed_at:
tokens.append((T.Selected, ' %d) %s' % (key, line),
select_item))
else:
tokens.append((T, ' %d) %s' % (key, line),
select_item))
tokens.append((T, '\n'))
# prepare the select choices
for i, choice in enumerate(self.choices):
_append(i, choice)
tokens.append((T, ' Answer: %d' % self.choices[self.pointer_index][0]))
return tokens
def get_selected_value(self):
# get value not label
return self.choices[self.pointer_index][2]
def question(message, **kwargs):
# TODO extract common parts for list, checkbox, rawlist, expand
if not 'choices' in kwargs:
raise PromptParameterException('choices')
# this does not implement default, use checked...
# TODO
#if 'default' in kwargs:
# raise ValueError('rawlist does not implement \'default\' '
# 'use \'checked\':True\' in choice!')
qmark = kwargs.pop('qmark', '?')
choices = kwargs.pop('choices', None)
if len(choices) > 9:
raise ValueError('rawlist supports only a maximum of 9 choices!')
# TODO style defaults on detail level
style = kwargs.pop('style', default_style)
ic = InquirerControl(choices)
def get_prompt_tokens(cli):
tokens = []
T = Token
tokens.append((T.QuestionMark, qmark))
tokens.append((T.Question, ' %s ' % message))
if ic.answered:
tokens.append((T.Answer, ' %s' % ic.get_selected_value()))
return tokens
# assemble layout
layout = HSplit([
Window(height=D.exact(1),
content=TokenListControl(get_prompt_tokens)
),
ConditionalContainer(
Window(ic),
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()
# add key bindings for choices
for i, c in enumerate(ic.choices):
if not isinstance(c, Separator):
def _reg_binding(i, keys):
# trick out late evaluation with a "function factory":
# http://stackoverflow.com/questions/3431676/creating-functions-in-a-loop
@manager.registry.add_binding(keys, eager=True)
def select_choice(event):
ic.pointer_index = i
_reg_binding(i, '%d' % c[0])
@manager.registry.add_binding(Keys.Enter, eager=True)
def set_answer(event):
ic.answered = True
event.cli.set_return_value(ic.get_selected_value())
return Application(
layout=layout,
key_bindings_registry=manager.registry,
mouse_support=True,
style=style
)

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
"""
Used to space/separate choices group
"""
class Separator(object):
line = '-' * 15
def __init__(self, line=None):
if line:
self.line = line
def __str__(self):
return self.line

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import json
import sys
from pprint import pprint
from pygments import highlight, lexers, formatters
__version__ = '0.1.2'
PY3 = sys.version_info[0] >= 3
def format_json(data):
return json.dumps(data, sort_keys=True, indent=4)
def colorize_json(data):
if PY3:
if isinstance(data, bytes):
data = data.decode('UTF-8')
else:
if not isinstance(data, unicode):
data = unicode(data, 'UTF-8')
colorful_json = highlight(data,
lexers.JsonLexer(),
formatters.TerminalFormatter())
return colorful_json
def print_json(data):
#colorful_json = highlight(unicode(format_json(data), 'UTF-8'),
# lexers.JsonLexer(),
# formatters.TerminalFormatter())
pprint(colorize_json(format_json(data)))