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)))

View File

@@ -0,0 +1,22 @@
"""
libs.prompt_toolkit
==============
Author: Jonathan Slenders
Description: libs.prompt_toolkit is a Library for building powerful interactive
command lines in Python. It can be a replacement for GNU
readline, but it can be much more than that.
See the examples directory to learn about the usage.
Probably, to get started, you meight also want to have a look at
`libs.prompt_toolkit.shortcuts.prompt`.
"""
from .interface import CommandLineInterface
from .application import AbortAction, Application
from .shortcuts import prompt, prompt_async
# Don't forget to update in `docs/conf.py`!
__version__ = '1.0.14'

View File

@@ -0,0 +1,192 @@
from __future__ import unicode_literals
from .buffer import Buffer, AcceptAction
from .buffer_mapping import BufferMapping
from .clipboard import Clipboard, InMemoryClipboard
from .enums import DEFAULT_BUFFER, EditingMode
from .filters import CLIFilter, to_cli_filter
from .key_binding.bindings.basic import load_basic_bindings
from .key_binding.bindings.emacs import load_emacs_bindings
from .key_binding.bindings.vi import load_vi_bindings
from .key_binding.registry import BaseRegistry
from .key_binding.defaults import load_key_bindings
from .layout import Window
from .layout.containers import Container
from .layout.controls import BufferControl
from .styles import DEFAULT_STYLE, Style
import six
__all__ = (
'AbortAction',
'Application',
)
class AbortAction(object):
"""
Actions to take on an Exit or Abort exception.
"""
RETRY = 'retry'
RAISE_EXCEPTION = 'raise-exception'
RETURN_NONE = 'return-none'
_all = (RETRY, RAISE_EXCEPTION, RETURN_NONE)
class Application(object):
"""
Application class to be passed to a
:class:`~libs.prompt_toolkit.interface.CommandLineInterface`.
This contains all customizable logic that is not I/O dependent.
(So, what is independent of event loops, input and output.)
This way, such an :class:`.Application` can run easily on several
:class:`~libs.prompt_toolkit.interface.CommandLineInterface` instances, each
with a different I/O backends. that runs for instance over telnet, SSH or
any other I/O backend.
:param layout: A :class:`~libs.prompt_toolkit.layout.containers.Container` instance.
:param buffer: A :class:`~libs.prompt_toolkit.buffer.Buffer` instance for the default buffer.
:param initial_focussed_buffer: Name of the buffer that is focussed during start-up.
:param key_bindings_registry:
:class:`~libs.prompt_toolkit.key_binding.registry.BaseRegistry` instance for
the key bindings.
:param clipboard: :class:`~libs.prompt_toolkit.clipboard.base.Clipboard` to use.
:param on_abort: What to do when Control-C is pressed.
:param on_exit: What to do when Control-D is pressed.
:param use_alternate_screen: When True, run the application on the alternate screen buffer.
:param get_title: Callable that returns the current title to be displayed in the terminal.
:param erase_when_done: (bool) Clear the application output when it finishes.
:param reverse_vi_search_direction: Normally, in Vi mode, a '/' searches
forward and a '?' searches backward. In readline mode, this is usually
reversed.
Filters:
:param mouse_support: (:class:`~libs.prompt_toolkit.filters.CLIFilter` or
boolean). When True, enable mouse support.
:param paste_mode: :class:`~libs.prompt_toolkit.filters.CLIFilter` or boolean.
:param ignore_case: :class:`~libs.prompt_toolkit.filters.CLIFilter` or boolean.
:param editing_mode: :class:`~libs.prompt_toolkit.enums.EditingMode`.
Callbacks (all of these should accept a
:class:`~libs.prompt_toolkit.interface.CommandLineInterface` object as input.)
:param on_input_timeout: Called when there is no input for x seconds.
(Fired when any eventloop.onInputTimeout is fired.)
:param on_start: Called when reading input starts.
:param on_stop: Called when reading input ends.
:param on_reset: Called during reset.
:param on_buffer_changed: Called when the content of a buffer has been changed.
:param on_initialize: Called after the
:class:`~libs.prompt_toolkit.interface.CommandLineInterface` initializes.
:param on_render: Called right after rendering.
:param on_invalidate: Called when the UI has been invalidated.
"""
def __init__(self, layout=None, buffer=None, buffers=None,
initial_focussed_buffer=DEFAULT_BUFFER,
style=None,
key_bindings_registry=None, clipboard=None,
on_abort=AbortAction.RAISE_EXCEPTION, on_exit=AbortAction.RAISE_EXCEPTION,
use_alternate_screen=False, mouse_support=False,
get_title=None,
paste_mode=False, ignore_case=False, editing_mode=EditingMode.EMACS,
erase_when_done=False,
reverse_vi_search_direction=False,
on_input_timeout=None, on_start=None, on_stop=None,
on_reset=None, on_initialize=None, on_buffer_changed=None,
on_render=None, on_invalidate=None):
paste_mode = to_cli_filter(paste_mode)
ignore_case = to_cli_filter(ignore_case)
mouse_support = to_cli_filter(mouse_support)
reverse_vi_search_direction = to_cli_filter(reverse_vi_search_direction)
assert layout is None or isinstance(layout, Container)
assert buffer is None or isinstance(buffer, Buffer)
assert buffers is None or isinstance(buffers, (dict, BufferMapping))
assert key_bindings_registry is None or isinstance(key_bindings_registry, BaseRegistry)
assert clipboard is None or isinstance(clipboard, Clipboard)
assert on_abort in AbortAction._all
assert on_exit in AbortAction._all
assert isinstance(use_alternate_screen, bool)
assert get_title is None or callable(get_title)
assert isinstance(paste_mode, CLIFilter)
assert isinstance(ignore_case, CLIFilter)
assert isinstance(editing_mode, six.string_types)
assert on_input_timeout is None or callable(on_input_timeout)
assert style is None or isinstance(style, Style)
assert isinstance(erase_when_done, bool)
assert on_start is None or callable(on_start)
assert on_stop is None or callable(on_stop)
assert on_reset is None or callable(on_reset)
assert on_buffer_changed is None or callable(on_buffer_changed)
assert on_initialize is None or callable(on_initialize)
assert on_render is None or callable(on_render)
assert on_invalidate is None or callable(on_invalidate)
self.layout = layout or Window(BufferControl())
# Make sure that the 'buffers' dictionary is a BufferMapping.
# NOTE: If no buffer is given, we create a default Buffer, with IGNORE as
# default accept_action. This is what makes sense for most users
# creating full screen applications. Doing nothing is the obvious
# default. Those creating a REPL would use the shortcuts module that
# passes in RETURN_DOCUMENT.
self.buffer = buffer or Buffer(accept_action=AcceptAction.IGNORE)
if not buffers or not isinstance(buffers, BufferMapping):
self.buffers = BufferMapping(buffers, initial=initial_focussed_buffer)
else:
self.buffers = buffers
if buffer:
self.buffers[DEFAULT_BUFFER] = buffer
self.initial_focussed_buffer = initial_focussed_buffer
self.style = style or DEFAULT_STYLE
if key_bindings_registry is None:
key_bindings_registry = load_key_bindings()
if get_title is None:
get_title = lambda: None
self.key_bindings_registry = key_bindings_registry
self.clipboard = clipboard or InMemoryClipboard()
self.on_abort = on_abort
self.on_exit = on_exit
self.use_alternate_screen = use_alternate_screen
self.mouse_support = mouse_support
self.get_title = get_title
self.paste_mode = paste_mode
self.ignore_case = ignore_case
self.editing_mode = editing_mode
self.erase_when_done = erase_when_done
self.reverse_vi_search_direction = reverse_vi_search_direction
def dummy_handler(cli):
" Dummy event handler. "
self.on_input_timeout = on_input_timeout or dummy_handler
self.on_start = on_start or dummy_handler
self.on_stop = on_stop or dummy_handler
self.on_reset = on_reset or dummy_handler
self.on_initialize = on_initialize or dummy_handler
self.on_buffer_changed = on_buffer_changed or dummy_handler
self.on_render = on_render or dummy_handler
self.on_invalidate = on_invalidate or dummy_handler
# List of 'extra' functions to execute before a CommandLineInterface.run.
# Note: It's important to keep this here, and not in the
# CommandLineInterface itself. shortcuts.run_application creates
# a new Application instance everytime. (Which is correct, it
# could be that we want to detach from one IO backend and attach
# the UI on a different backend.) But important is to keep as
# much state as possible between runs.
self.pre_run_callables = []

View File

@@ -0,0 +1,88 @@
"""
`Fish-style <http://fishshell.com/>`_ like auto-suggestion.
While a user types input in a certain buffer, suggestions are generated
(asynchronously.) Usually, they are displayed after the input. When the cursor
presses the right arrow and the cursor is at the end of the input, the
suggestion will be inserted.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
from .filters import to_cli_filter
__all__ = (
'Suggestion',
'AutoSuggest',
'AutoSuggestFromHistory',
'ConditionalAutoSuggest',
)
class Suggestion(object):
"""
Suggestion returned by an auto-suggest algorithm.
:param text: The suggestion text.
"""
def __init__(self, text):
self.text = text
def __repr__(self):
return 'Suggestion(%s)' % self.text
class AutoSuggest(with_metaclass(ABCMeta, object)):
"""
Base class for auto suggestion implementations.
"""
@abstractmethod
def get_suggestion(self, cli, buffer, document):
"""
Return `None` or a :class:`.Suggestion` instance.
We receive both ``buffer`` and ``document``. The reason is that auto
suggestions are retrieved asynchronously. (Like completions.) The
buffer text could be changed in the meantime, but ``document`` contains
the buffer document like it was at the start of the auto suggestion
call. So, from here, don't access ``buffer.text``, but use
``document.text`` instead.
:param buffer: The :class:`~libs.prompt_toolkit.buffer.Buffer` instance.
:param document: The :class:`~libs.prompt_toolkit.document.Document` instance.
"""
class AutoSuggestFromHistory(AutoSuggest):
"""
Give suggestions based on the lines in the history.
"""
def get_suggestion(self, cli, buffer, document):
history = buffer.history
# Consider only the last line for the suggestion.
text = document.text.rsplit('\n', 1)[-1]
# Only create a suggestion when this is not an empty line.
if text.strip():
# Find first matching line in history.
for string in reversed(list(history)):
for line in reversed(string.splitlines()):
if line.startswith(text):
return Suggestion(line[len(text):])
class ConditionalAutoSuggest(AutoSuggest):
"""
Auto suggest that can be turned on and of according to a certain condition.
"""
def __init__(self, auto_suggest, filter):
assert isinstance(auto_suggest, AutoSuggest)
self.auto_suggest = auto_suggest
self.filter = to_cli_filter(filter)
def get_suggestion(self, cli, buffer, document):
if self.filter(cli):
return self.auto_suggest.get_suggestion(cli, buffer, document)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
"""
The BufferMapping contains all the buffers for a command line interface, and it
keeps track of which buffer gets the focus.
"""
from __future__ import unicode_literals
from .enums import DEFAULT_BUFFER, SEARCH_BUFFER, SYSTEM_BUFFER, DUMMY_BUFFER
from .buffer import Buffer, AcceptAction
from .history import InMemoryHistory
import six
__all__ = (
'BufferMapping',
)
class BufferMapping(dict):
"""
Dictionary that maps the name of the buffers to the
:class:`~libs.prompt_toolkit.buffer.Buffer` instances.
This mapping also keeps track of which buffer currently has the focus.
(Some methods receive a 'cli' parameter. This is useful for applications
where this `BufferMapping` is shared between several applications.)
"""
def __init__(self, buffers=None, initial=DEFAULT_BUFFER):
assert buffers is None or isinstance(buffers, dict)
# Start with an empty dict.
super(BufferMapping, self).__init__()
# Add default buffers.
self.update({
# For the 'search' and 'system' buffers, 'returnable' is False, in
# order to block normal Enter/ControlC behaviour.
DEFAULT_BUFFER: Buffer(accept_action=AcceptAction.RETURN_DOCUMENT),
SEARCH_BUFFER: Buffer(history=InMemoryHistory(), accept_action=AcceptAction.IGNORE),
SYSTEM_BUFFER: Buffer(history=InMemoryHistory(), accept_action=AcceptAction.IGNORE),
DUMMY_BUFFER: Buffer(read_only=True),
})
# Add received buffers.
if buffers is not None:
self.update(buffers)
# Focus stack.
self.focus_stack = [initial or DEFAULT_BUFFER]
def current(self, cli):
"""
The active :class:`.Buffer`.
"""
return self[self.focus_stack[-1]]
def current_name(self, cli):
"""
The name of the active :class:`.Buffer`.
"""
return self.focus_stack[-1]
def previous(self, cli):
"""
Return the previously focussed :class:`.Buffer` or `None`.
"""
if len(self.focus_stack) > 1:
try:
return self[self.focus_stack[-2]]
except KeyError:
pass
def focus(self, cli, buffer_name):
"""
Focus the buffer with the given name.
"""
assert isinstance(buffer_name, six.text_type)
self.focus_stack = [buffer_name]
def push_focus(self, cli, buffer_name):
"""
Push buffer on the focus stack.
"""
assert isinstance(buffer_name, six.text_type)
self.focus_stack.append(buffer_name)
def pop_focus(self, cli):
"""
Pop buffer from the focus stack.
"""
if len(self.focus_stack) > 1:
self.focus_stack.pop()
else:
raise IndexError('Cannot pop last item from the focus stack.')

View File

@@ -0,0 +1,111 @@
from __future__ import unicode_literals
from collections import deque
from functools import wraps
__all__ = (
'SimpleCache',
'FastDictCache',
'memoized',
)
class SimpleCache(object):
"""
Very simple cache that discards the oldest item when the cache size is
exceeded.
:param maxsize: Maximum size of the cache. (Don't make it too big.)
"""
def __init__(self, maxsize=8):
assert isinstance(maxsize, int) and maxsize > 0
self._data = {}
self._keys = deque()
self.maxsize = maxsize
def get(self, key, getter_func):
"""
Get object from the cache.
If not found, call `getter_func` to resolve it, and put that on the top
of the cache instead.
"""
# Look in cache first.
try:
return self._data[key]
except KeyError:
# Not found? Get it.
value = getter_func()
self._data[key] = value
self._keys.append(key)
# Remove the oldest key when the size is exceeded.
if len(self._data) > self.maxsize:
key_to_remove = self._keys.popleft()
if key_to_remove in self._data:
del self._data[key_to_remove]
return value
def clear(self):
" Clear cache. "
self._data = {}
self._keys = deque()
class FastDictCache(dict):
"""
Fast, lightweight cache which keeps at most `size` items.
It will discard the oldest items in the cache first.
The cache is a dictionary, which doesn't keep track of access counts.
It is perfect to cache little immutable objects which are not expensive to
create, but where a dictionary lookup is still much faster than an object
instantiation.
:param get_value: Callable that's called in case of a missing key.
"""
# NOTE: This cache is used to cache `libs.prompt_toolkit.layout.screen.Char` and
# `libs.prompt_toolkit.Document`. Make sure to keep this really lightweight.
# Accessing the cache should stay faster than instantiating new
# objects.
# (Dictionary lookups are really fast.)
# SimpleCache is still required for cases where the cache key is not
# the same as the arguments given to the function that creates the
# value.)
def __init__(self, get_value=None, size=1000000):
assert callable(get_value)
assert isinstance(size, int) and size > 0
self._keys = deque()
self.get_value = get_value
self.size = size
def __missing__(self, key):
# Remove the oldest key when the size is exceeded.
if len(self) > self.size:
key_to_remove = self._keys.popleft()
if key_to_remove in self:
del self[key_to_remove]
result = self.get_value(*key)
self[key] = result
self._keys.append(key)
return result
def memoized(maxsize=1024):
"""
Momoization decorator for immutable classes and pure functions.
"""
cache = SimpleCache(maxsize=maxsize)
def decorator(obj):
@wraps(obj)
def new_callable(*a, **kw):
def create_new():
return obj(*a, **kw)
key = (a, tuple(kw.items()))
return cache.get(key, create_new)
return new_callable
return decorator

View File

@@ -0,0 +1,8 @@
from .base import Clipboard, ClipboardData
from .in_memory import InMemoryClipboard
# We are not importing `PyperclipClipboard` here, because it would require the
# `pyperclip` module to be present.
#from .pyperclip import PyperclipClipboard

View File

@@ -0,0 +1,62 @@
"""
Clipboard for command line interface.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
import six
from libs.prompt_toolkit.selection import SelectionType
__all__ = (
'Clipboard',
'ClipboardData',
)
class ClipboardData(object):
"""
Text on the clipboard.
:param text: string
:param type: :class:`~prompt_toolkit.selection.SelectionType`
"""
def __init__(self, text='', type=SelectionType.CHARACTERS):
assert isinstance(text, six.string_types)
assert type in (SelectionType.CHARACTERS, SelectionType.LINES, SelectionType.BLOCK)
self.text = text
self.type = type
class Clipboard(with_metaclass(ABCMeta, object)):
"""
Abstract baseclass for clipboards.
(An implementation can be in memory, it can share the X11 or Windows
keyboard, or can be persistent.)
"""
@abstractmethod
def set_data(self, data):
"""
Set data to the clipboard.
:param data: :class:`~.ClipboardData` instance.
"""
def set_text(self, text): # Not abstract.
"""
Shortcut for setting plain text on clipboard.
"""
assert isinstance(text, six.string_types)
self.set_data(ClipboardData(text))
def rotate(self):
"""
For Emacs mode, rotate the kill ring.
"""
@abstractmethod
def get_data(self):
"""
Return clipboard data.
"""

View File

@@ -0,0 +1,42 @@
from .base import Clipboard, ClipboardData
from collections import deque
__all__ = (
'InMemoryClipboard',
)
class InMemoryClipboard(Clipboard):
"""
Default clipboard implementation.
Just keep the data in memory.
This implements a kill-ring, for Emacs mode.
"""
def __init__(self, data=None, max_size=60):
assert data is None or isinstance(data, ClipboardData)
assert max_size >= 1
self.max_size = max_size
self._ring = deque()
if data is not None:
self.set_data(data)
def set_data(self, data):
assert isinstance(data, ClipboardData)
self._ring.appendleft(data)
while len(self._ring) > self.max_size:
self._ring.pop()
def get_data(self):
if self._ring:
return self._ring[0]
else:
return ClipboardData()
def rotate(self):
if self._ring:
# Add the very first item at the end.
self._ring.append(self._ring.popleft())

View File

@@ -0,0 +1,39 @@
from __future__ import absolute_import, unicode_literals
import pyperclip
from libs.prompt_toolkit.selection import SelectionType
from .base import Clipboard, ClipboardData
__all__ = (
'PyperclipClipboard',
)
class PyperclipClipboard(Clipboard):
"""
Clipboard that synchronizes with the Windows/Mac/Linux system clipboard,
using the pyperclip module.
"""
def __init__(self):
self._data = None
def set_data(self, data):
assert isinstance(data, ClipboardData)
self._data = data
pyperclip.copy(data.text)
def get_data(self):
text = pyperclip.paste()
# When the clipboard data is equal to what we copied last time, reuse
# the `ClipboardData` instance. That way we're sure to keep the same
# `SelectionType`.
if self._data and self._data.text == text:
return self._data
# Pyperclip returned something else. Create a new `ClipboardData`
# instance.
else:
return ClipboardData(
text=text,
type=SelectionType.LINES if '\n' in text else SelectionType.LINES)

View File

@@ -0,0 +1,170 @@
"""
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
__all__ = (
'Completion',
'Completer',
'CompleteEvent',
'get_common_complete_suffix',
)
class Completion(object):
"""
:param text: The new string that will be inserted into the document.
:param start_position: Position relative to the cursor_position where the
new text will start. The text will be inserted between the
start_position and the original cursor position.
:param display: (optional string) If the completion has to be displayed
differently in the completion menu.
:param display_meta: (Optional string) Meta information about the
completion, e.g. the path or source where it's coming from.
:param get_display_meta: Lazy `display_meta`. Retrieve meta information
only when meta is displayed.
"""
def __init__(self, text, start_position=0, display=None, display_meta=None,
get_display_meta=None):
self.text = text
self.start_position = start_position
self._display_meta = display_meta
self._get_display_meta = get_display_meta
if display is None:
self.display = text
else:
self.display = display
assert self.start_position <= 0
def __repr__(self):
if self.display == self.text:
return '%s(text=%r, start_position=%r)' % (
self.__class__.__name__, self.text, self.start_position)
else:
return '%s(text=%r, start_position=%r, display=%r)' % (
self.__class__.__name__, self.text, self.start_position,
self.display)
def __eq__(self, other):
return (
self.text == other.text and
self.start_position == other.start_position and
self.display == other.display and
self.display_meta == other.display_meta)
def __hash__(self):
return hash((self.text, self.start_position, self.display, self.display_meta))
@property
def display_meta(self):
# Return meta-text. (This is lazy when using "get_display_meta".)
if self._display_meta is not None:
return self._display_meta
elif self._get_display_meta:
self._display_meta = self._get_display_meta()
return self._display_meta
else:
return ''
def new_completion_from_position(self, position):
"""
(Only for internal use!)
Get a new completion by splitting this one. Used by
`CommandLineInterface` when it needs to have a list of new completions
after inserting the common prefix.
"""
assert isinstance(position, int) and position - self.start_position >= 0
return Completion(
text=self.text[position - self.start_position:],
display=self.display,
display_meta=self._display_meta,
get_display_meta=self._get_display_meta)
class CompleteEvent(object):
"""
Event that called the completer.
:param text_inserted: When True, it means that completions are requested
because of a text insert. (`Buffer.complete_while_typing`.)
:param completion_requested: When True, it means that the user explicitely
pressed the `Tab` key in order to view the completions.
These two flags can be used for instance to implemented a completer that
shows some completions when ``Tab`` has been pressed, but not
automatically when the user presses a space. (Because of
`complete_while_typing`.)
"""
def __init__(self, text_inserted=False, completion_requested=False):
assert not (text_inserted and completion_requested)
#: Automatic completion while typing.
self.text_inserted = text_inserted
#: Used explicitely requested completion by pressing 'tab'.
self.completion_requested = completion_requested
def __repr__(self):
return '%s(text_inserted=%r, completion_requested=%r)' % (
self.__class__.__name__, self.text_inserted, self.completion_requested)
class Completer(with_metaclass(ABCMeta, object)):
"""
Base class for completer implementations.
"""
@abstractmethod
def get_completions(self, document, complete_event):
"""
Yield :class:`.Completion` instances.
:param document: :class:`~libs.prompt_toolkit.document.Document` instance.
:param complete_event: :class:`.CompleteEvent` instance.
"""
while False:
yield
def get_common_complete_suffix(document, completions):
"""
Return the common prefix for all completions.
"""
# Take only completions that don't change the text before the cursor.
def doesnt_change_before_cursor(completion):
end = completion.text[:-completion.start_position]
return document.text_before_cursor.endswith(end)
completions2 = [c for c in completions if doesnt_change_before_cursor(c)]
# When there is at least one completion that changes the text before the
# cursor, don't return any common part.
if len(completions2) != len(completions):
return ''
# Return the common prefix.
def get_suffix(completion):
return completion.text[-completion.start_position:]
return _commonprefix([get_suffix(c) for c in completions2])
def _commonprefix(strings):
# Similar to os.path.commonprefix
if not strings:
return ''
else:
s1 = min(strings)
s2 = max(strings)
for i, c in enumerate(s1):
if c != s2[i]:
return s1[:i]
return s1

View File

@@ -0,0 +1,5 @@
from __future__ import unicode_literals
from .filesystem import PathCompleter
from .base import WordCompleter
from .system import SystemCompleter

View File

@@ -0,0 +1,61 @@
from __future__ import unicode_literals
from six import string_types
from prompt_toolkit.completion import Completer, Completion
__all__ = (
'WordCompleter',
)
class WordCompleter(Completer):
"""
Simple autocompletion on a list of words.
:param words: List of words.
:param ignore_case: If True, case-insensitive completion.
:param meta_dict: Optional dict mapping words to their meta-information.
:param WORD: When True, use WORD characters.
:param sentence: When True, don't complete by comparing the word before the
cursor, but by comparing all the text before the cursor. In this case,
the list of words is just a list of strings, where each string can
contain spaces. (Can not be used together with the WORD option.)
:param match_middle: When True, match not only the start, but also in the
middle of the word.
"""
def __init__(self, words, ignore_case=False, meta_dict=None, WORD=False,
sentence=False, match_middle=False):
assert not (WORD and sentence)
self.words = list(words)
self.ignore_case = ignore_case
self.meta_dict = meta_dict or {}
self.WORD = WORD
self.sentence = sentence
self.match_middle = match_middle
assert all(isinstance(w, string_types) for w in self.words)
def get_completions(self, document, complete_event):
# Get word/text before cursor.
if self.sentence:
word_before_cursor = document.text_before_cursor
else:
word_before_cursor = document.get_word_before_cursor(WORD=self.WORD)
if self.ignore_case:
word_before_cursor = word_before_cursor.lower()
def word_matches(word):
""" True when the word before the cursor matches. """
if self.ignore_case:
word = word.lower()
if self.match_middle:
return word_before_cursor in word
else:
return word.startswith(word_before_cursor)
for a in self.words:
if word_matches(a):
display_meta = self.meta_dict.get(a, '')
yield Completion(a, -len(word_before_cursor), display_meta=display_meta)

View File

@@ -0,0 +1,105 @@
from __future__ import unicode_literals
from prompt_toolkit.completion import Completer, Completion
import os
__all__ = (
'PathCompleter',
'ExecutableCompleter',
)
class PathCompleter(Completer):
"""
Complete for Path variables.
:param get_paths: Callable which returns a list of directories to look into
when the user enters a relative path.
:param file_filter: Callable which takes a filename and returns whether
this file should show up in the completion. ``None``
when no filtering has to be done.
:param min_input_len: Don't do autocompletion when the input string is shorter.
"""
def __init__(self, only_directories=False, get_paths=None, file_filter=None,
min_input_len=0, expanduser=False):
assert get_paths is None or callable(get_paths)
assert file_filter is None or callable(file_filter)
assert isinstance(min_input_len, int)
assert isinstance(expanduser, bool)
self.only_directories = only_directories
self.get_paths = get_paths or (lambda: ['.'])
self.file_filter = file_filter or (lambda _: True)
self.min_input_len = min_input_len
self.expanduser = expanduser
def get_completions(self, document, complete_event):
text = document.text_before_cursor
# Complete only when we have at least the minimal input length,
# otherwise, we can too many results and autocompletion will become too
# heavy.
if len(text) < self.min_input_len:
return
try:
# Do tilde expansion.
if self.expanduser:
text = os.path.expanduser(text)
# Directories where to look.
dirname = os.path.dirname(text)
if dirname:
directories = [os.path.dirname(os.path.join(p, text))
for p in self.get_paths()]
else:
directories = self.get_paths()
# Start of current file.
prefix = os.path.basename(text)
# Get all filenames.
filenames = []
for directory in directories:
# Look for matches in this directory.
if os.path.isdir(directory):
for filename in os.listdir(directory):
if filename.startswith(prefix):
filenames.append((directory, filename))
# Sort
filenames = sorted(filenames, key=lambda k: k[1])
# Yield them.
for directory, filename in filenames:
completion = filename[len(prefix):]
full_name = os.path.join(directory, filename)
if os.path.isdir(full_name):
# For directories, add a slash to the filename.
# (We don't add them to the `completion`. Users can type it
# to trigger the autocompletion themself.)
filename += '/'
elif self.only_directories:
continue
if not self.file_filter(full_name):
continue
yield Completion(completion, 0, display=filename)
except OSError:
pass
class ExecutableCompleter(PathCompleter):
"""
Complete only excutable files in the current path.
"""
def __init__(self):
PathCompleter.__init__(
self,
only_directories=False,
min_input_len=1,
get_paths=lambda: os.environ.get('PATH', '').split(os.pathsep),
file_filter=lambda name: os.access(name, os.X_OK),
expanduser=True),

View File

@@ -0,0 +1,56 @@
from __future__ import unicode_literals
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
from prompt_toolkit.contrib.regular_languages.compiler import compile
from .filesystem import PathCompleter, ExecutableCompleter
__all__ = (
'SystemCompleter',
)
class SystemCompleter(GrammarCompleter):
"""
Completer for system commands.
"""
def __init__(self):
# Compile grammar.
g = compile(
r"""
# First we have an executable.
(?P<executable>[^\s]+)
# Ignore literals in between.
(
\s+
("[^"]*" | '[^']*' | [^'"]+ )
)*
\s+
# Filename as parameters.
(
(?P<filename>[^\s]+) |
"(?P<double_quoted_filename>[^\s]+)" |
'(?P<single_quoted_filename>[^\s]+)'
)
""",
escape_funcs={
'double_quoted_filename': (lambda string: string.replace('"', '\\"')),
'single_quoted_filename': (lambda string: string.replace("'", "\\'")),
},
unescape_funcs={
'double_quoted_filename': (lambda string: string.replace('\\"', '"')), # XXX: not enterily correct.
'single_quoted_filename': (lambda string: string.replace("\\'", "'")),
})
# Create GrammarCompleter
super(SystemCompleter, self).__init__(
g,
{
'executable': ExecutableCompleter(),
'filename': PathCompleter(only_directories=False, expanduser=True),
'double_quoted_filename': PathCompleter(only_directories=False, expanduser=True),
'single_quoted_filename': PathCompleter(only_directories=False, expanduser=True),
})

View File

@@ -0,0 +1,76 @@
r"""
Tool for expressing the grammar of an input as a regular language.
==================================================================
The grammar for the input of many simple command line interfaces can be
expressed by a regular language. Examples are PDB (the Python debugger); a
simple (bash-like) shell with "pwd", "cd", "cat" and "ls" commands; arguments
that you can pass to an executable; etc. It is possible to use regular
expressions for validation and parsing of such a grammar. (More about regular
languages: http://en.wikipedia.org/wiki/Regular_language)
Example
-------
Let's take the pwd/cd/cat/ls example. We want to have a shell that accepts
these three commands. "cd" is followed by a quoted directory name and "cat" is
followed by a quoted file name. (We allow quotes inside the filename when
they're escaped with a backslash.) We could define the grammar using the
following regular expression::
grammar = \s* (
pwd |
ls |
(cd \s+ " ([^"]|\.)+ ") |
(cat \s+ " ([^"]|\.)+ ")
) \s*
What can we do with this grammar?
---------------------------------
- Syntax highlighting: We could use this for instance to give file names
different colour.
- Parse the result: .. We can extract the file names and commands by using a
regular expression with named groups.
- Input validation: .. Don't accept anything that does not match this grammar.
When combined with a parser, we can also recursively do
filename validation (and accept only existing files.)
- Autocompletion: .... Each part of the grammar can have its own autocompleter.
"cat" has to be completed using file names, while "cd"
has to be completed using directory names.
How does it work?
-----------------
As a user of this library, you have to define the grammar of the input as a
regular expression. The parts of this grammar where autocompletion, validation
or any other processing is required need to be marked using a regex named
group. Like ``(?P<varname>...)`` for instance.
When the input is processed for validation (for instance), the regex will
execute, the named group is captured, and the validator associated with this
named group will test the captured string.
There is one tricky bit:
Ofter we operate on incomplete input (this is by definition the case for
autocompletion) and we have to decide for the cursor position in which
possible state the grammar it could be and in which way variables could be
matched up to that point.
To solve this problem, the compiler takes the original regular expression and
translates it into a set of other regular expressions which each match prefixes
of strings that would match the first expression. (We translate it into
multiple expression, because we want to have each possible state the regex
could be in -- in case there are several or-clauses with each different
completers.)
TODO: some examples of:
- How to create a highlighter from this grammar.
- How to create a validator from this grammar.
- How to create an autocompleter from this grammar.
- How to create a parser from this grammar.
"""
from .compiler import compile

View File

@@ -0,0 +1,408 @@
r"""
Compiler for a regular grammar.
Example usage::
# Create and compile grammar.
p = compile('add \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)')
# Match input string.
m = p.match('add 23 432')
# Get variables.
m.variables().get('var1') # Returns "23"
m.variables().get('var2') # Returns "432"
Partial matches are possible::
# Create and compile grammar.
p = compile('''
# Operators with two arguments.
((?P<operator1>[^\s]+) \s+ (?P<var1>[^\s]+) \s+ (?P<var2>[^\s]+)) |
# Operators with only one arguments.
((?P<operator2>[^\s]+) \s+ (?P<var1>[^\s]+))
''')
# Match partial input string.
m = p.match_prefix('add 23')
# Get variables. (Notice that both operator1 and operator2 contain the
# value "add".) This is because our input is incomplete, and we don't know
# yet in which rule of the regex we we'll end up. It could also be that
# `operator1` and `operator2` have a different autocompleter and we want to
# call all possible autocompleters that would result in valid input.)
m.variables().get('var1') # Returns "23"
m.variables().get('operator1') # Returns "add"
m.variables().get('operator2') # Returns "add"
"""
from __future__ import unicode_literals
import re
from six.moves import range
from .regex_parser import Any, Sequence, Regex, Variable, Repeat, Lookahead
from .regex_parser import parse_regex, tokenize_regex
__all__ = (
'compile',
)
# Name of the named group in the regex, matching trailing input.
# (Trailing input is when the input contains characters after the end of the
# expression has been matched.)
_INVALID_TRAILING_INPUT = 'invalid_trailing'
class _CompiledGrammar(object):
"""
Compiles a grammar. This will take the parse tree of a regular expression
and compile the grammar.
:param root_node: :class~`.regex_parser.Node` instance.
:param escape_funcs: `dict` mapping variable names to escape callables.
:param unescape_funcs: `dict` mapping variable names to unescape callables.
"""
def __init__(self, root_node, escape_funcs=None, unescape_funcs=None):
self.root_node = root_node
self.escape_funcs = escape_funcs or {}
self.unescape_funcs = unescape_funcs or {}
#: Dictionary that will map the redex names to Node instances.
self._group_names_to_nodes = {} # Maps regex group names to varnames.
counter = [0]
def create_group_func(node):
name = 'n%s' % counter[0]
self._group_names_to_nodes[name] = node.varname
counter[0] += 1
return name
# Compile regex strings.
self._re_pattern = '^%s$' % self._transform(root_node, create_group_func)
self._re_prefix_patterns = list(self._transform_prefix(root_node, create_group_func))
# Compile the regex itself.
flags = re.DOTALL # Note that we don't need re.MULTILINE! (^ and $
# still represent the start and end of input text.)
self._re = re.compile(self._re_pattern, flags)
self._re_prefix = [re.compile(t, flags) for t in self._re_prefix_patterns]
# We compile one more set of regexes, similar to `_re_prefix`, but accept any trailing
# input. This will ensure that we can still highlight the input correctly, even when the
# input contains some additional characters at the end that don't match the grammar.)
self._re_prefix_with_trailing_input = [
re.compile(r'(?:%s)(?P<%s>.*?)$' % (t.rstrip('$'), _INVALID_TRAILING_INPUT), flags)
for t in self._re_prefix_patterns]
def escape(self, varname, value):
"""
Escape `value` to fit in the place of this variable into the grammar.
"""
f = self.escape_funcs.get(varname)
return f(value) if f else value
def unescape(self, varname, value):
"""
Unescape `value`.
"""
f = self.unescape_funcs.get(varname)
return f(value) if f else value
@classmethod
def _transform(cls, root_node, create_group_func):
"""
Turn a :class:`Node` object into a regular expression.
:param root_node: The :class:`Node` instance for which we generate the grammar.
:param create_group_func: A callable which takes a `Node` and returns the next
free name for this node.
"""
def transform(node):
# Turn `Any` into an OR.
if isinstance(node, Any):
return '(?:%s)' % '|'.join(transform(c) for c in node.children)
# Concatenate a `Sequence`
elif isinstance(node, Sequence):
return ''.join(transform(c) for c in node.children)
# For Regex and Lookahead nodes, just insert them literally.
elif isinstance(node, Regex):
return node.regex
elif isinstance(node, Lookahead):
before = ('(?!' if node.negative else '(=')
return before + transform(node.childnode) + ')'
# A `Variable` wraps the children into a named group.
elif isinstance(node, Variable):
return '(?P<%s>%s)' % (create_group_func(node), transform(node.childnode))
# `Repeat`.
elif isinstance(node, Repeat):
return '(?:%s){%i,%s}%s' % (
transform(node.childnode), node.min_repeat,
('' if node.max_repeat is None else str(node.max_repeat)),
('' if node.greedy else '?')
)
else:
raise TypeError('Got %r' % (node, ))
return transform(root_node)
@classmethod
def _transform_prefix(cls, root_node, create_group_func):
"""
Yield all the regular expressions matching a prefix of the grammar
defined by the `Node` instance.
This can yield multiple expressions, because in the case of on OR
operation in the grammar, we can have another outcome depending on
which clause would appear first. E.g. "(A|B)C" is not the same as
"(B|A)C" because the regex engine is lazy and takes the first match.
However, because we the current input is actually a prefix of the
grammar which meight not yet contain the data for "C", we need to know
both intermediate states, in order to call the appropriate
autocompletion for both cases.
:param root_node: The :class:`Node` instance for which we generate the grammar.
:param create_group_func: A callable which takes a `Node` and returns the next
free name for this node.
"""
def transform(node):
# Generate regexes for all permutations of this OR. Each node
# should be in front once.
if isinstance(node, Any):
for c in node.children:
for r in transform(c):
yield '(?:%s)?' % r
# For a sequence. We can either have a match for the sequence
# of all the children, or for an exact match of the first X
# children, followed by a partial match of the next children.
elif isinstance(node, Sequence):
for i in range(len(node.children)):
a = [cls._transform(c, create_group_func) for c in node.children[:i]]
for c in transform(node.children[i]):
yield '(?:%s)' % (''.join(a) + c)
elif isinstance(node, Regex):
yield '(?:%s)?' % node.regex
elif isinstance(node, Lookahead):
if node.negative:
yield '(?!%s)' % cls._transform(node.childnode, create_group_func)
else:
# Not sure what the correct semantics are in this case.
# (Probably it's not worth implementing this.)
raise Exception('Positive lookahead not yet supported.')
elif isinstance(node, Variable):
# (Note that we should not append a '?' here. the 'transform'
# method will already recursively do that.)
for c in transform(node.childnode):
yield '(?P<%s>%s)' % (create_group_func(node), c)
elif isinstance(node, Repeat):
# If we have a repetition of 8 times. That would mean that the
# current input could have for instance 7 times a complete
# match, followed by a partial match.
prefix = cls._transform(node.childnode, create_group_func)
for c in transform(node.childnode):
if node.max_repeat:
repeat_sign = '{,%i}' % (node.max_repeat - 1)
else:
repeat_sign = '*'
yield '(?:%s)%s%s(?:%s)?' % (
prefix,
repeat_sign,
('' if node.greedy else '?'),
c)
else:
raise TypeError('Got %r' % node)
for r in transform(root_node):
yield '^%s$' % r
def match(self, string):
"""
Match the string with the grammar.
Returns a :class:`Match` instance or `None` when the input doesn't match the grammar.
:param string: The input string.
"""
m = self._re.match(string)
if m:
return Match(string, [(self._re, m)], self._group_names_to_nodes, self.unescape_funcs)
def match_prefix(self, string):
"""
Do a partial match of the string with the grammar. The returned
:class:`Match` instance can contain multiple representations of the
match. This will never return `None`. If it doesn't match at all, the "trailing input"
part will capture all of the input.
:param string: The input string.
"""
# First try to match using `_re_prefix`. If nothing is found, use the patterns that
# also accept trailing characters.
for patterns in [self._re_prefix, self._re_prefix_with_trailing_input]:
matches = [(r, r.match(string)) for r in patterns]
matches = [(r, m) for r, m in matches if m]
if matches != []:
return Match(string, matches, self._group_names_to_nodes, self.unescape_funcs)
class Match(object):
"""
:param string: The input string.
:param re_matches: List of (compiled_re_pattern, re_match) tuples.
:param group_names_to_nodes: Dictionary mapping all the re group names to the matching Node instances.
"""
def __init__(self, string, re_matches, group_names_to_nodes, unescape_funcs):
self.string = string
self._re_matches = re_matches
self._group_names_to_nodes = group_names_to_nodes
self._unescape_funcs = unescape_funcs
def _nodes_to_regs(self):
"""
Return a list of (varname, reg) tuples.
"""
def get_tuples():
for r, re_match in self._re_matches:
for group_name, group_index in r.groupindex.items():
if group_name != _INVALID_TRAILING_INPUT:
reg = re_match.regs[group_index]
node = self._group_names_to_nodes[group_name]
yield (node, reg)
return list(get_tuples())
def _nodes_to_values(self):
"""
Returns list of list of (Node, string_value) tuples.
"""
def is_none(slice):
return slice[0] == -1 and slice[1] == -1
def get(slice):
return self.string[slice[0]:slice[1]]
return [(varname, get(slice), slice) for varname, slice in self._nodes_to_regs() if not is_none(slice)]
def _unescape(self, varname, value):
unwrapper = self._unescape_funcs.get(varname)
return unwrapper(value) if unwrapper else value
def variables(self):
"""
Returns :class:`Variables` instance.
"""
return Variables([(k, self._unescape(k, v), sl) for k, v, sl in self._nodes_to_values()])
def trailing_input(self):
"""
Get the `MatchVariable` instance, representing trailing input, if there is any.
"Trailing input" is input at the end that does not match the grammar anymore, but
when this is removed from the end of the input, the input would be a valid string.
"""
slices = []
# Find all regex group for the name _INVALID_TRAILING_INPUT.
for r, re_match in self._re_matches:
for group_name, group_index in r.groupindex.items():
if group_name == _INVALID_TRAILING_INPUT:
slices.append(re_match.regs[group_index])
# Take the smallest part. (Smaller trailing text means that a larger input has
# been matched, so that is better.)
if slices:
slice = [max(i[0] for i in slices), max(i[1] for i in slices)]
value = self.string[slice[0]:slice[1]]
return MatchVariable('<trailing_input>', value, slice)
def end_nodes(self):
"""
Yields `MatchVariable` instances for all the nodes having their end
position at the end of the input string.
"""
for varname, reg in self._nodes_to_regs():
# If this part goes until the end of the input string.
if reg[1] == len(self.string):
value = self._unescape(varname, self.string[reg[0]: reg[1]])
yield MatchVariable(varname, value, (reg[0], reg[1]))
class Variables(object):
def __init__(self, tuples):
#: List of (varname, value, slice) tuples.
self._tuples = tuples
def __repr__(self):
return '%s(%s)' % (
self.__class__.__name__, ', '.join('%s=%r' % (k, v) for k, v, _ in self._tuples))
def get(self, key, default=None):
items = self.getall(key)
return items[0] if items else default
def getall(self, key):
return [v for k, v, _ in self._tuples if k == key]
def __getitem__(self, key):
return self.get(key)
def __iter__(self):
"""
Yield `MatchVariable` instances.
"""
for varname, value, slice in self._tuples:
yield MatchVariable(varname, value, slice)
class MatchVariable(object):
"""
Represents a match of a variable in the grammar.
:param varname: (string) Name of the variable.
:param value: (string) Value of this variable.
:param slice: (start, stop) tuple, indicating the position of this variable
in the input string.
"""
def __init__(self, varname, value, slice):
self.varname = varname
self.value = value
self.slice = slice
self.start = self.slice[0]
self.stop = self.slice[1]
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.varname, self.value)
def compile(expression, escape_funcs=None, unescape_funcs=None):
"""
Compile grammar (given as regex string), returning a `CompiledGrammar`
instance.
"""
return _compile_from_parse_tree(
parse_regex(tokenize_regex(expression)),
escape_funcs=escape_funcs,
unescape_funcs=unescape_funcs)
def _compile_from_parse_tree(root_node, *a, **kw):
"""
Compile grammar (given as parse tree), returning a `CompiledGrammar`
instance.
"""
return _CompiledGrammar(root_node, *a, **kw)

View File

@@ -0,0 +1,84 @@
"""
Completer for a regular grammar.
"""
from __future__ import unicode_literals
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document
from .compiler import _CompiledGrammar
__all__ = (
'GrammarCompleter',
)
class GrammarCompleter(Completer):
"""
Completer which can be used for autocompletion according to variables in
the grammar. Each variable can have a different autocompleter.
:param compiled_grammar: `GrammarCompleter` instance.
:param completers: `dict` mapping variable names of the grammar to the
`Completer` instances to be used for each variable.
"""
def __init__(self, compiled_grammar, completers):
assert isinstance(compiled_grammar, _CompiledGrammar)
assert isinstance(completers, dict)
self.compiled_grammar = compiled_grammar
self.completers = completers
def get_completions(self, document, complete_event):
m = self.compiled_grammar.match_prefix(document.text_before_cursor)
if m:
completions = self._remove_duplicates(
self._get_completions_for_match(m, complete_event))
for c in completions:
yield c
def _get_completions_for_match(self, match, complete_event):
"""
Yield all the possible completions for this input string.
(The completer assumes that the cursor position was at the end of the
input string.)
"""
for match_variable in match.end_nodes():
varname = match_variable.varname
start = match_variable.start
completer = self.completers.get(varname)
if completer:
text = match_variable.value
# Unwrap text.
unwrapped_text = self.compiled_grammar.unescape(varname, text)
# Create a document, for the completions API (text/cursor_position)
document = Document(unwrapped_text, len(unwrapped_text))
# Call completer
for completion in completer.get_completions(document, complete_event):
new_text = unwrapped_text[:len(text) + completion.start_position] + completion.text
# Wrap again.
yield Completion(
text=self.compiled_grammar.escape(varname, new_text),
start_position=start - len(match.string),
display=completion.display,
display_meta=completion.display_meta)
def _remove_duplicates(self, items):
"""
Remove duplicates, while keeping the order.
(Sometimes we have duplicates, because the there several matches of the
same grammar, each yielding similar completions.)
"""
result = []
for i in items:
if i not in result:
result.append(i)
return result

View File

@@ -0,0 +1,90 @@
"""
`GrammarLexer` is compatible with Pygments lexers and can be used to highlight
the input using a regular grammar with token annotations.
"""
from __future__ import unicode_literals
from prompt_toolkit.document import Document
from prompt_toolkit.layout.lexers import Lexer
from prompt_toolkit.layout.utils import split_lines
from prompt_toolkit.token import Token
from .compiler import _CompiledGrammar
from six.moves import range
__all__ = (
'GrammarLexer',
)
class GrammarLexer(Lexer):
"""
Lexer which can be used for highlighting of tokens according to variables in the grammar.
(It does not actual lexing of the string, but it exposes an API, compatible
with the Pygments lexer class.)
:param compiled_grammar: Grammar as returned by the `compile()` function.
:param lexers: Dictionary mapping variable names of the regular grammar to
the lexers that should be used for this part. (This can
call other lexers recursively.) If you wish a part of the
grammar to just get one token, use a
`prompt_toolkit.layout.lexers.SimpleLexer`.
"""
def __init__(self, compiled_grammar, default_token=None, lexers=None):
assert isinstance(compiled_grammar, _CompiledGrammar)
assert default_token is None or isinstance(default_token, tuple)
assert lexers is None or all(isinstance(v, Lexer) for k, v in lexers.items())
assert lexers is None or isinstance(lexers, dict)
self.compiled_grammar = compiled_grammar
self.default_token = default_token or Token
self.lexers = lexers or {}
def _get_tokens(self, cli, text):
m = self.compiled_grammar.match_prefix(text)
if m:
characters = [[self.default_token, c] for c in text]
for v in m.variables():
# If we have a `Lexer` instance for this part of the input.
# Tokenize recursively and apply tokens.
lexer = self.lexers.get(v.varname)
if lexer:
document = Document(text[v.start:v.stop])
lexer_tokens_for_line = lexer.lex_document(cli, document)
lexer_tokens = []
for i in range(len(document.lines)):
lexer_tokens.extend(lexer_tokens_for_line(i))
lexer_tokens.append((Token, '\n'))
if lexer_tokens:
lexer_tokens.pop()
i = v.start
for t, s in lexer_tokens:
for c in s:
if characters[i][0] == self.default_token:
characters[i][0] = t
i += 1
# Highlight trailing input.
trailing_input = m.trailing_input()
if trailing_input:
for i in range(trailing_input.start, trailing_input.stop):
characters[i][0] = Token.TrailingInput
return characters
else:
return [(Token, text)]
def lex_document(self, cli, document):
lines = list(split_lines(self._get_tokens(cli, document.text)))
def get_line(lineno):
try:
return lines[lineno]
except IndexError:
return []
return get_line

View File

@@ -0,0 +1,262 @@
"""
Parser for parsing a regular expression.
Take a string representing a regular expression and return the root node of its
parse tree.
usage::
root_node = parse_regex('(hello|world)')
Remarks:
- The regex parser processes multiline, it ignores all whitespace and supports
multiple named groups with the same name and #-style comments.
Limitations:
- Lookahead is not supported.
"""
from __future__ import unicode_literals
import re
__all__ = (
'Repeat',
'Variable',
'Regex',
'Lookahead',
'tokenize_regex',
'parse_regex',
)
class Node(object):
"""
Base class for all the grammar nodes.
(You don't initialize this one.)
"""
def __add__(self, other_node):
return Sequence([self, other_node])
def __or__(self, other_node):
return Any([self, other_node])
class Any(Node):
"""
Union operation (OR operation) between several grammars. You don't
initialize this yourself, but it's a result of a "Grammar1 | Grammar2"
operation.
"""
def __init__(self, children):
self.children = children
def __or__(self, other_node):
return Any(self.children + [other_node])
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.children)
class Sequence(Node):
"""
Concatenation operation of several grammars. You don't initialize this
yourself, but it's a result of a "Grammar1 + Grammar2" operation.
"""
def __init__(self, children):
self.children = children
def __add__(self, other_node):
return Sequence(self.children + [other_node])
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.children)
class Regex(Node):
"""
Regular expression.
"""
def __init__(self, regex):
re.compile(regex) # Validate
self.regex = regex
def __repr__(self):
return '%s(/%s/)' % (self.__class__.__name__, self.regex)
class Lookahead(Node):
"""
Lookahead expression.
"""
def __init__(self, childnode, negative=False):
self.childnode = childnode
self.negative = negative
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.childnode)
class Variable(Node):
"""
Mark a variable in the regular grammar. This will be translated into a
named group. Each variable can have his own completer, validator, etc..
:param childnode: The grammar which is wrapped inside this variable.
:param varname: String.
"""
def __init__(self, childnode, varname=None):
self.childnode = childnode
self.varname = varname
def __repr__(self):
return '%s(childnode=%r, varname=%r)' % (
self.__class__.__name__, self.childnode, self.varname)
class Repeat(Node):
def __init__(self, childnode, min_repeat=0, max_repeat=None, greedy=True):
self.childnode = childnode
self.min_repeat = min_repeat
self.max_repeat = max_repeat
self.greedy = greedy
def __repr__(self):
return '%s(childnode=%r)' % (self.__class__.__name__, self.childnode)
def tokenize_regex(input):
"""
Takes a string, representing a regular expression as input, and tokenizes
it.
:param input: string, representing a regular expression.
:returns: List of tokens.
"""
# Regular expression for tokenizing other regular expressions.
p = re.compile(r'''^(
\(\?P\<[a-zA-Z0-9_-]+\> | # Start of named group.
\(\?#[^)]*\) | # Comment
\(\?= | # Start of lookahead assertion
\(\?! | # Start of negative lookahead assertion
\(\?<= | # If preceded by.
\(\?< | # If not preceded by.
\(?: | # Start of group. (non capturing.)
\( | # Start of group.
\(?[iLmsux] | # Flags.
\(?P=[a-zA-Z]+\) | # Back reference to named group
\) | # End of group.
\{[^{}]*\} | # Repetition
\*\? | \+\? | \?\?\ | # Non greedy repetition.
\* | \+ | \? | # Repetition
\#.*\n | # Comment
\\. |
# Character group.
\[
( [^\]\\] | \\.)*
\] |
[^(){}] |
.
)''', re.VERBOSE)
tokens = []
while input:
m = p.match(input)
if m:
token, input = input[:m.end()], input[m.end():]
if not token.isspace():
tokens.append(token)
else:
raise Exception('Could not tokenize input regex.')
return tokens
def parse_regex(regex_tokens):
"""
Takes a list of tokens from the tokenizer, and returns a parse tree.
"""
# We add a closing brace because that represents the final pop of the stack.
tokens = [')'] + regex_tokens[::-1]
def wrap(lst):
""" Turn list into sequence when it contains several items. """
if len(lst) == 1:
return lst[0]
else:
return Sequence(lst)
def _parse():
or_list = []
result = []
def wrapped_result():
if or_list == []:
return wrap(result)
else:
or_list.append(result)
return Any([wrap(i) for i in or_list])
while tokens:
t = tokens.pop()
if t.startswith('(?P<'):
variable = Variable(_parse(), varname=t[4:-1])
result.append(variable)
elif t in ('*', '*?'):
greedy = (t == '*')
result[-1] = Repeat(result[-1], greedy=greedy)
elif t in ('+', '+?'):
greedy = (t == '+')
result[-1] = Repeat(result[-1], min_repeat=1, greedy=greedy)
elif t in ('?', '??'):
if result == []:
raise Exception('Nothing to repeat.' + repr(tokens))
else:
greedy = (t == '?')
result[-1] = Repeat(result[-1], min_repeat=0, max_repeat=1, greedy=greedy)
elif t == '|':
or_list.append(result)
result = []
elif t in ('(', '(?:'):
result.append(_parse())
elif t == '(?!':
result.append(Lookahead(_parse(), negative=True))
elif t == '(?=':
result.append(Lookahead(_parse(), negative=False))
elif t == ')':
return wrapped_result()
elif t.startswith('#'):
pass
elif t.startswith('{'):
# TODO: implement!
raise Exception('{}-style repitition not yet supported' % t)
elif t.startswith('(?'):
raise Exception('%r not supported' % t)
elif t.isspace():
pass
else:
result.append(Regex(t))
raise Exception("Expecting ')' token")
result = _parse()
if len(tokens) != 0:
raise Exception("Unmatched parantheses.")
else:
return result

View File

@@ -0,0 +1,57 @@
"""
Validator for a regular langage.
"""
from __future__ import unicode_literals
from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit.document import Document
from .compiler import _CompiledGrammar
__all__ = (
'GrammarValidator',
)
class GrammarValidator(Validator):
"""
Validator which can be used for validation according to variables in
the grammar. Each variable can have its own validator.
:param compiled_grammar: `GrammarCompleter` instance.
:param validators: `dict` mapping variable names of the grammar to the
`Validator` instances to be used for each variable.
"""
def __init__(self, compiled_grammar, validators):
assert isinstance(compiled_grammar, _CompiledGrammar)
assert isinstance(validators, dict)
self.compiled_grammar = compiled_grammar
self.validators = validators
def validate(self, document):
# Parse input document.
# We use `match`, not `match_prefix`, because for validation, we want
# the actual, unambiguous interpretation of the input.
m = self.compiled_grammar.match(document.text)
if m:
for v in m.variables():
validator = self.validators.get(v.varname)
if validator:
# Unescape text.
unwrapped_text = self.compiled_grammar.unescape(v.varname, v.value)
# Create a document, for the completions API (text/cursor_position)
inner_document = Document(unwrapped_text, len(unwrapped_text))
try:
validator.validate(inner_document)
except ValidationError as e:
raise ValidationError(
cursor_position=v.start + e.cursor_position,
message=e.message)
else:
raise ValidationError(cursor_position=len(document.text),
message='Invalid command')

View File

@@ -0,0 +1,2 @@
from .server import *
from .application import *

View File

@@ -0,0 +1,32 @@
"""
Interface for Telnet applications.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
__all__ = (
'TelnetApplication',
)
class TelnetApplication(with_metaclass(ABCMeta, object)):
"""
The interface which has to be implemented for any telnet application.
An instance of this class has to be passed to `TelnetServer`.
"""
@abstractmethod
def client_connected(self, telnet_connection):
"""
Called when a new client was connected.
Probably you want to call `telnet_connection.set_cli` here to set a
the CommandLineInterface instance to be used.
Hint: Use the following shortcut: `prompt_toolkit.shortcuts.create_cli`
"""
@abstractmethod
def client_leaving(self, telnet_connection):
"""
Called when a client quits.
"""

View File

@@ -0,0 +1,11 @@
"""
Python logger for the telnet server.
"""
from __future__ import unicode_literals
import logging
logger = logging.getLogger(__package__)
__all__ = (
'logger',
)

View File

@@ -0,0 +1,181 @@
"""
Parser for the Telnet protocol. (Not a complete implementation of the telnet
specification, but sufficient for a command line interface.)
Inspired by `Twisted.conch.telnet`.
"""
from __future__ import unicode_literals
import struct
from six import int2byte, binary_type, iterbytes
from .log import logger
__all__ = (
'TelnetProtocolParser',
)
# Telnet constants.
NOP = int2byte(0)
SGA = int2byte(3)
IAC = int2byte(255)
DO = int2byte(253)
DONT = int2byte(254)
LINEMODE = int2byte(34)
SB = int2byte(250)
WILL = int2byte(251)
WONT = int2byte(252)
MODE = int2byte(1)
SE = int2byte(240)
ECHO = int2byte(1)
NAWS = int2byte(31)
LINEMODE = int2byte(34)
SUPPRESS_GO_AHEAD = int2byte(3)
DM = int2byte(242)
BRK = int2byte(243)
IP = int2byte(244)
AO = int2byte(245)
AYT = int2byte(246)
EC = int2byte(247)
EL = int2byte(248)
GA = int2byte(249)
class TelnetProtocolParser(object):
"""
Parser for the Telnet protocol.
Usage::
def data_received(data):
print(data)
def size_received(rows, columns):
print(rows, columns)
p = TelnetProtocolParser(data_received, size_received)
p.feed(binary_data)
"""
def __init__(self, data_received_callback, size_received_callback):
self.data_received_callback = data_received_callback
self.size_received_callback = size_received_callback
self._parser = self._parse_coroutine()
self._parser.send(None)
def received_data(self, data):
self.data_received_callback(data)
def do_received(self, data):
""" Received telnet DO command. """
logger.info('DO %r', data)
def dont_received(self, data):
""" Received telnet DONT command. """
logger.info('DONT %r', data)
def will_received(self, data):
""" Received telnet WILL command. """
logger.info('WILL %r', data)
def wont_received(self, data):
""" Received telnet WONT command. """
logger.info('WONT %r', data)
def command_received(self, command, data):
if command == DO:
self.do_received(data)
elif command == DONT:
self.dont_received(data)
elif command == WILL:
self.will_received(data)
elif command == WONT:
self.wont_received(data)
else:
logger.info('command received %r %r', command, data)
def naws(self, data):
"""
Received NAWS. (Window dimensions.)
"""
if len(data) == 4:
# NOTE: the first parameter of struct.unpack should be
# a 'str' object. Both on Py2/py3. This crashes on OSX
# otherwise.
columns, rows = struct.unpack(str('!HH'), data)
self.size_received_callback(rows, columns)
else:
logger.warning('Wrong number of NAWS bytes')
def negotiate(self, data):
"""
Got negotiate data.
"""
command, payload = data[0:1], data[1:]
assert isinstance(command, bytes)
if command == NAWS:
self.naws(payload)
else:
logger.info('Negotiate (%r got bytes)', len(data))
def _parse_coroutine(self):
"""
Parser state machine.
Every 'yield' expression returns the next byte.
"""
while True:
d = yield
if d == int2byte(0):
pass # NOP
# Go to state escaped.
elif d == IAC:
d2 = yield
if d2 == IAC:
self.received_data(d2)
# Handle simple commands.
elif d2 in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA):
self.command_received(d2, None)
# Handle IAC-[DO/DONT/WILL/WONT] commands.
elif d2 in (DO, DONT, WILL, WONT):
d3 = yield
self.command_received(d2, d3)
# Subnegotiation
elif d2 == SB:
# Consume everything until next IAC-SE
data = []
while True:
d3 = yield
if d3 == IAC:
d4 = yield
if d4 == SE:
break
else:
data.append(d4)
else:
data.append(d3)
self.negotiate(b''.join(data))
else:
self.received_data(d)
def feed(self, data):
"""
Feed data to the parser.
"""
assert isinstance(data, binary_type)
for b in iterbytes(data):
self._parser.send(int2byte(b))

View File

@@ -0,0 +1,407 @@
"""
Telnet server.
Example usage::
class MyTelnetApplication(TelnetApplication):
def client_connected(self, telnet_connection):
# Set CLI with simple prompt.
telnet_connection.set_application(
telnet_connection.create_prompt_application(...))
def handle_command(self, telnet_connection, document):
# When the client enters a command, just reply.
telnet_connection.send('You said: %r\n\n' % document.text)
...
a = MyTelnetApplication()
TelnetServer(application=a, host='127.0.0.1', port=23).run()
"""
from __future__ import unicode_literals
import socket
import select
import threading
import os
import fcntl
from six import int2byte, text_type, binary_type
from codecs import getincrementaldecoder
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.eventloop.base import EventLoop
from prompt_toolkit.interface import CommandLineInterface, Application
from prompt_toolkit.layout.screen import Size
from prompt_toolkit.shortcuts import create_prompt_application
from prompt_toolkit.terminal.vt100_input import InputStream
from prompt_toolkit.terminal.vt100_output import Vt100_Output
from .log import logger
from .protocol import IAC, DO, LINEMODE, SB, MODE, SE, WILL, ECHO, NAWS, SUPPRESS_GO_AHEAD
from .protocol import TelnetProtocolParser
from .application import TelnetApplication
__all__ = (
'TelnetServer',
)
def _initialize_telnet(connection):
logger.info('Initializing telnet connection')
# Iac Do Linemode
connection.send(IAC + DO + LINEMODE)
# Suppress Go Ahead. (This seems important for Putty to do correct echoing.)
# This will allow bi-directional operation.
connection.send(IAC + WILL + SUPPRESS_GO_AHEAD)
# Iac sb
connection.send(IAC + SB + LINEMODE + MODE + int2byte(0) + IAC + SE)
# IAC Will Echo
connection.send(IAC + WILL + ECHO)
# Negotiate window size
connection.send(IAC + DO + NAWS)
class _ConnectionStdout(object):
"""
Wrapper around socket which provides `write` and `flush` methods for the
Vt100_Output output.
"""
def __init__(self, connection, encoding):
self._encoding = encoding
self._connection = connection
self._buffer = []
def write(self, data):
assert isinstance(data, text_type)
self._buffer.append(data.encode(self._encoding))
self.flush()
def flush(self):
try:
self._connection.send(b''.join(self._buffer))
except socket.error as e:
logger.error("Couldn't send data over socket: %s" % e)
self._buffer = []
class TelnetConnection(object):
"""
Class that represents one Telnet connection.
"""
def __init__(self, conn, addr, application, server, encoding):
assert isinstance(addr, tuple) # (addr, port) tuple
assert isinstance(application, TelnetApplication)
assert isinstance(server, TelnetServer)
assert isinstance(encoding, text_type) # e.g. 'utf-8'
self.conn = conn
self.addr = addr
self.application = application
self.closed = False
self.handling_command = True
self.server = server
self.encoding = encoding
self.callback = None # Function that handles the CLI result.
# Create "Output" object.
self.size = Size(rows=40, columns=79)
# Initialize.
_initialize_telnet(conn)
# Create output.
def get_size():
return self.size
self.stdout = _ConnectionStdout(conn, encoding=encoding)
self.vt100_output = Vt100_Output(self.stdout, get_size, write_binary=False)
# Create an eventloop (adaptor) for the CommandLineInterface.
self.eventloop = _TelnetEventLoopInterface(server)
# Set default CommandLineInterface.
self.set_application(create_prompt_application())
# Call client_connected
application.client_connected(self)
# Draw for the first time.
self.handling_command = False
self.cli._redraw()
def set_application(self, app, callback=None):
"""
Set ``CommandLineInterface`` instance for this connection.
(This can be replaced any time.)
:param cli: CommandLineInterface instance.
:param callback: Callable that takes the result of the CLI.
"""
assert isinstance(app, Application)
assert callback is None or callable(callback)
self.cli = CommandLineInterface(
application=app,
eventloop=self.eventloop,
output=self.vt100_output)
self.callback = callback
# Create a parser, and parser callbacks.
cb = self.cli.create_eventloop_callbacks()
inputstream = InputStream(cb.feed_key)
# Input decoder for stdin. (Required when working with multibyte
# characters, like chinese input.)
stdin_decoder_cls = getincrementaldecoder(self.encoding)
stdin_decoder = [stdin_decoder_cls()] # nonlocal
# Tell the CLI that it's running. We don't start it through the run()
# call, but will still want _redraw() to work.
self.cli._is_running = True
def data_received(data):
""" TelnetProtocolParser 'data_received' callback """
assert isinstance(data, binary_type)
try:
result = stdin_decoder[0].decode(data)
inputstream.feed(result)
except UnicodeDecodeError:
stdin_decoder[0] = stdin_decoder_cls()
return ''
def size_received(rows, columns):
""" TelnetProtocolParser 'size_received' callback """
self.size = Size(rows=rows, columns=columns)
cb.terminal_size_changed()
self.parser = TelnetProtocolParser(data_received, size_received)
def feed(self, data):
"""
Handler for incoming data. (Called by TelnetServer.)
"""
assert isinstance(data, binary_type)
self.parser.feed(data)
# Render again.
self.cli._redraw()
# When a return value has been set (enter was pressed), handle command.
if self.cli.is_returning:
try:
return_value = self.cli.return_value()
except (EOFError, KeyboardInterrupt) as e:
# Control-D or Control-C was pressed.
logger.info('%s, closing connection.', type(e).__name__)
self.close()
return
# Handle CLI command
self._handle_command(return_value)
def _handle_command(self, command):
"""
Handle command. This will run in a separate thread, in order not
to block the event loop.
"""
logger.info('Handle command %r', command)
def in_executor():
self.handling_command = True
try:
if self.callback is not None:
self.callback(self, command)
finally:
self.server.call_from_executor(done)
def done():
self.handling_command = False
# Reset state and draw again. (If the connection is still open --
# the application could have called TelnetConnection.close()
if not self.closed:
self.cli.reset()
self.cli.buffers[DEFAULT_BUFFER].reset()
self.cli.renderer.request_absolute_cursor_position()
self.vt100_output.flush()
self.cli._redraw()
self.server.run_in_executor(in_executor)
def erase_screen(self):
"""
Erase output screen.
"""
self.vt100_output.erase_screen()
self.vt100_output.cursor_goto(0, 0)
self.vt100_output.flush()
def send(self, data):
"""
Send text to the client.
"""
assert isinstance(data, text_type)
# When data is send back to the client, we should replace the line
# endings. (We didn't allocate a real pseudo terminal, and the telnet
# connection is raw, so we are responsible for inserting \r.)
self.stdout.write(data.replace('\n', '\r\n'))
self.stdout.flush()
def close(self):
"""
Close the connection.
"""
self.application.client_leaving(self)
self.conn.close()
self.closed = True
class _TelnetEventLoopInterface(EventLoop):
"""
Eventloop object to be assigned to `CommandLineInterface`.
"""
def __init__(self, server):
self._server = server
def close(self):
" Ignore. "
def stop(self):
" Ignore. "
def run_in_executor(self, callback):
self._server.run_in_executor(callback)
def call_from_executor(self, callback, _max_postpone_until=None):
self._server.call_from_executor(callback)
def add_reader(self, fd, callback):
raise NotImplementedError
def remove_reader(self, fd):
raise NotImplementedError
class TelnetServer(object):
"""
Telnet server implementation.
"""
def __init__(self, host='127.0.0.1', port=23, application=None, encoding='utf-8'):
assert isinstance(host, text_type)
assert isinstance(port, int)
assert isinstance(application, TelnetApplication)
assert isinstance(encoding, text_type)
self.host = host
self.port = port
self.application = application
self.encoding = encoding
self.connections = set()
self._calls_from_executor = []
# Create a pipe for inter thread communication.
self._schedule_pipe = os.pipe()
fcntl.fcntl(self._schedule_pipe[0], fcntl.F_SETFL, os.O_NONBLOCK)
@classmethod
def create_socket(cls, host, port):
# Create and bind socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((host, port))
s.listen(4)
return s
def run_in_executor(self, callback):
threading.Thread(target=callback).start()
def call_from_executor(self, callback):
self._calls_from_executor.append(callback)
if self._schedule_pipe:
os.write(self._schedule_pipe[1], b'x')
def _process_callbacks(self):
"""
Process callbacks from `call_from_executor` in eventloop.
"""
# Flush all the pipe content.
os.read(self._schedule_pipe[0], 1024)
# Process calls from executor.
calls_from_executor, self._calls_from_executor = self._calls_from_executor, []
for c in calls_from_executor:
c()
def run(self):
"""
Run the eventloop for the telnet server.
"""
listen_socket = self.create_socket(self.host, self.port)
logger.info('Listening for telnet connections on %s port %r', self.host, self.port)
try:
while True:
# Removed closed connections.
self.connections = set([c for c in self.connections if not c.closed])
# Ignore connections handling commands.
connections = set([c for c in self.connections if not c.handling_command])
# Wait for next event.
read_list = (
[listen_socket, self._schedule_pipe[0]] +
[c.conn for c in connections])
read, _, _ = select.select(read_list, [], [])
for s in read:
# When the socket itself is ready, accept a new connection.
if s == listen_socket:
self._accept(listen_socket)
# If we receive something on our "call_from_executor" pipe, process
# these callbacks in a thread safe way.
elif s == self._schedule_pipe[0]:
self._process_callbacks()
# Handle incoming data on socket.
else:
self._handle_incoming_data(s)
finally:
listen_socket.close()
def _accept(self, listen_socket):
"""
Accept new incoming connection.
"""
conn, addr = listen_socket.accept()
connection = TelnetConnection(conn, addr, self.application, self, encoding=self.encoding)
self.connections.add(connection)
logger.info('New connection %r %r', *addr)
def _handle_incoming_data(self, conn):
"""
Handle incoming data on socket.
"""
connection = [c for c in self.connections if c.conn == conn][0]
data = conn.recv(1024)
if data:
connection.feed(data)
else:
self.connections.remove(connection)

View File

@@ -0,0 +1,34 @@
from __future__ import unicode_literals
from prompt_toolkit.validation import Validator, ValidationError
from six import string_types
class SentenceValidator(Validator):
"""
Validate input only when it appears in this list of sentences.
:param sentences: List of sentences.
:param ignore_case: If True, case-insensitive comparisons.
"""
def __init__(self, sentences, ignore_case=False, error_message='Invalid input', move_cursor_to_end=False):
assert all(isinstance(s, string_types) for s in sentences)
assert isinstance(ignore_case, bool)
assert isinstance(error_message, string_types)
self.sentences = list(sentences)
self.ignore_case = ignore_case
self.error_message = error_message
self.move_cursor_to_end = move_cursor_to_end
if ignore_case:
self.sentences = set([s.lower() for s in self.sentences])
def validate(self, document):
if document.text not in self.sentences:
if self.move_cursor_to_end:
index = len(document.text)
else:
index = 0
raise ValidationError(cursor_position=index,
message=self.error_message)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
from __future__ import unicode_literals
class IncrementalSearchDirection(object):
FORWARD = 'FORWARD'
BACKWARD = 'BACKWARD'
class EditingMode(object):
# The set of key bindings that is active.
VI = 'VI'
EMACS = 'EMACS'
#: Name of the search buffer.
SEARCH_BUFFER = 'SEARCH_BUFFER'
#: Name of the default buffer.
DEFAULT_BUFFER = 'DEFAULT_BUFFER'
#: Name of the system buffer.
SYSTEM_BUFFER = 'SYSTEM_BUFFER'
# Dummy buffer. This is the buffer returned by
# `CommandLineInterface.current_buffer` when the top of the `FocusStack` is
# `None`. This could be the case when there is some widget has the focus and no
# actual text editing is possible. This buffer should also never be displayed.
# (It will never contain any actual text.)
DUMMY_BUFFER = 'DUMMY_BUFFER'

View File

@@ -0,0 +1,46 @@
"""
Eventloop for integration with Python3 asyncio.
Note that we can't use "yield from", because the package should be installable
under Python 2.6 as well, and it should contain syntactically valid Python 2.6
code.
"""
from __future__ import unicode_literals
__all__ = (
'AsyncioTimeout',
)
class AsyncioTimeout(object):
"""
Call the `timeout` function when the timeout expires.
Every call of the `reset` method, resets the timeout and starts a new
timer.
"""
def __init__(self, timeout, callback, loop):
self.timeout = timeout
self.callback = callback
self.loop = loop
self.counter = 0
self.running = True
def reset(self):
"""
Reset the timeout. Starts a new timer.
"""
self.counter += 1
local_counter = self.counter
def timer_timeout():
if self.counter == local_counter and self.running:
self.callback()
self.loop.call_later(self.timeout, timer_timeout)
def stop(self):
"""
Ignore timeout. Don't call the callback anymore.
"""
self.running = False

View File

@@ -0,0 +1,113 @@
"""
Posix asyncio event loop.
"""
from __future__ import unicode_literals
from ..terminal.vt100_input import InputStream
from .asyncio_base import AsyncioTimeout
from .base import EventLoop, INPUT_TIMEOUT
from .callbacks import EventLoopCallbacks
from .posix_utils import PosixStdinReader
import asyncio
import signal
__all__ = (
'PosixAsyncioEventLoop',
)
class PosixAsyncioEventLoop(EventLoop):
def __init__(self, loop=None):
self.loop = loop or asyncio.get_event_loop()
self.closed = False
self._stopped_f = asyncio.Future(loop=self.loop)
@asyncio.coroutine
def run_as_coroutine(self, stdin, callbacks):
"""
The input 'event loop'.
"""
assert isinstance(callbacks, EventLoopCallbacks)
# Create reader class.
stdin_reader = PosixStdinReader(stdin.fileno())
if self.closed:
raise Exception('Event loop already closed.')
inputstream = InputStream(callbacks.feed_key)
try:
# Create a new Future every time.
self._stopped_f = asyncio.Future(loop=self.loop)
# Handle input timouts
def timeout_handler():
"""
When no input has been received for INPUT_TIMEOUT seconds,
flush the input stream and fire the timeout event.
"""
inputstream.flush()
callbacks.input_timeout()
timeout = AsyncioTimeout(INPUT_TIMEOUT, timeout_handler, self.loop)
# Catch sigwinch
def received_winch():
self.call_from_executor(callbacks.terminal_size_changed)
self.loop.add_signal_handler(signal.SIGWINCH, received_winch)
# Read input data.
def stdin_ready():
data = stdin_reader.read()
inputstream.feed(data)
timeout.reset()
# Quit when the input stream was closed.
if stdin_reader.closed:
self.stop()
self.loop.add_reader(stdin.fileno(), stdin_ready)
# Block this coroutine until stop() has been called.
for f in self._stopped_f:
yield f
finally:
# Clean up.
self.loop.remove_reader(stdin.fileno())
self.loop.remove_signal_handler(signal.SIGWINCH)
# Don't trigger any timeout events anymore.
timeout.stop()
def stop(self):
# Trigger the 'Stop' future.
self._stopped_f.set_result(True)
def close(self):
# Note: we should not close the asyncio loop itself, because that one
# was not created here.
self.closed = True
def run_in_executor(self, callback):
self.loop.run_in_executor(None, callback)
def call_from_executor(self, callback, _max_postpone_until=None):
"""
Call this function in the main event loop.
Similar to Twisted's ``callFromThread``.
"""
self.loop.call_soon_threadsafe(callback)
def add_reader(self, fd, callback):
" Start watching the file descriptor for read availability. "
self.loop.add_reader(fd, callback)
def remove_reader(self, fd):
" Stop watching the file descriptor for read availability. "
self.loop.remove_reader(fd)

View File

@@ -0,0 +1,83 @@
"""
Win32 asyncio event loop.
Windows notes:
- Somehow it doesn't seem to work with the 'ProactorEventLoop'.
"""
from __future__ import unicode_literals
from .base import EventLoop, INPUT_TIMEOUT
from ..terminal.win32_input import ConsoleInputReader
from .callbacks import EventLoopCallbacks
from .asyncio_base import AsyncioTimeout
import asyncio
__all__ = (
'Win32AsyncioEventLoop',
)
class Win32AsyncioEventLoop(EventLoop):
def __init__(self, loop=None):
self._console_input_reader = ConsoleInputReader()
self.running = False
self.closed = False
self.loop = loop or asyncio.get_event_loop()
@asyncio.coroutine
def run_as_coroutine(self, stdin, callbacks):
"""
The input 'event loop'.
"""
# Note: We cannot use "yield from", because this package also
# installs on Python 2.
assert isinstance(callbacks, EventLoopCallbacks)
if self.closed:
raise Exception('Event loop already closed.')
timeout = AsyncioTimeout(INPUT_TIMEOUT, callbacks.input_timeout, self.loop)
self.running = True
try:
while self.running:
timeout.reset()
# Get keys
try:
g = iter(self.loop.run_in_executor(None, self._console_input_reader.read))
while True:
yield next(g)
except StopIteration as e:
keys = e.args[0]
# Feed keys to input processor.
for k in keys:
callbacks.feed_key(k)
finally:
timeout.stop()
def stop(self):
self.running = False
def close(self):
# Note: we should not close the asyncio loop itself, because that one
# was not created here.
self.closed = True
self._console_input_reader.close()
def run_in_executor(self, callback):
self.loop.run_in_executor(None, callback)
def call_from_executor(self, callback, _max_postpone_until=None):
self.loop.call_soon_threadsafe(callback)
def add_reader(self, fd, callback):
" Start watching the file descriptor for read availability. "
self.loop.add_reader(fd, callback)
def remove_reader(self, fd):
" Stop watching the file descriptor for read availability. "
self.loop.remove_reader(fd)

View File

@@ -0,0 +1,85 @@
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
__all__ = (
'EventLoop',
'INPUT_TIMEOUT',
)
#: When to trigger the `onInputTimeout` event.
INPUT_TIMEOUT = .5
class EventLoop(with_metaclass(ABCMeta, object)):
"""
Eventloop interface.
"""
def run(self, stdin, callbacks):
"""
Run the eventloop until stop() is called. Report all
input/timeout/terminal-resize events to the callbacks.
:param stdin: :class:`~libs.prompt_toolkit.input.Input` instance.
:param callbacks: :class:`~libs.prompt_toolkit.eventloop.callbacks.EventLoopCallbacks` instance.
"""
raise NotImplementedError("This eventloop doesn't implement synchronous 'run()'.")
def run_as_coroutine(self, stdin, callbacks):
"""
Similar to `run`, but this is a coroutine. (For asyncio integration.)
"""
raise NotImplementedError("This eventloop doesn't implement 'run_as_coroutine()'.")
@abstractmethod
def stop(self):
"""
Stop the `run` call. (Normally called by
:class:`~libs.prompt_toolkit.interface.CommandLineInterface`, when a result
is available, or Abort/Quit has been called.)
"""
@abstractmethod
def close(self):
"""
Clean up of resources. Eventloop cannot be reused a second time after
this call.
"""
@abstractmethod
def add_reader(self, fd, callback):
"""
Start watching the file descriptor for read availability and then call
the callback.
"""
@abstractmethod
def remove_reader(self, fd):
"""
Stop watching the file descriptor for read availability.
"""
@abstractmethod
def run_in_executor(self, callback):
"""
Run a long running function in a background thread. (This is
recommended for code that could block the event loop.)
Similar to Twisted's ``deferToThread``.
"""
@abstractmethod
def call_from_executor(self, callback, _max_postpone_until=None):
"""
Call this function in the main event loop. Similar to Twisted's
``callFromThread``.
:param _max_postpone_until: `None` or `time.time` value. For interal
use. If the eventloop is saturated, consider this task to be low
priority and postpone maximum until this timestamp. (For instance,
repaint is done using low priority.)
Note: In the past, this used to be a datetime.datetime instance,
but apparently, executing `time.time` is more efficient: it
does fewer system calls. (It doesn't read /etc/localtime.)
"""

View File

@@ -0,0 +1,29 @@
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
__all__ = (
'EventLoopCallbacks',
)
class EventLoopCallbacks(with_metaclass(ABCMeta, object)):
"""
This is the glue between the :class:`~libs.prompt_toolkit.eventloop.base.EventLoop`
and :class:`~libs.prompt_toolkit.interface.CommandLineInterface`.
:meth:`~libs.prompt_toolkit.eventloop.base.EventLoop.run` takes an
:class:`.EventLoopCallbacks` instance and operates on that one, driving the
interface.
"""
@abstractmethod
def terminal_size_changed(self):
pass
@abstractmethod
def input_timeout(self):
pass
@abstractmethod
def feed_key(self, key):
pass

View File

@@ -0,0 +1,107 @@
"""
Similar to `PyOS_InputHook` of the Python API. Some eventloops can have an
inputhook to allow easy integration with other event loops.
When the eventloop of prompt-toolkit is idle, it can call such a hook. This
hook can call another eventloop that runs for a short while, for instance to
keep a graphical user interface responsive.
It's the responsibility of this hook to exit when there is input ready.
There are two ways to detect when input is ready:
- Call the `input_is_ready` method periodically. Quit when this returns `True`.
- Add the `fileno` as a watch to the external eventloop. Quit when file descriptor
becomes readable. (But don't read from it.)
Note that this is not the same as checking for `sys.stdin.fileno()`. The
eventloop of prompt-toolkit allows thread-based executors, for example for
asynchronous autocompletion. When the completion for instance is ready, we
also want prompt-toolkit to gain control again in order to display that.
An alternative to using input hooks, is to create a custom `EventLoop` class that
controls everything.
"""
from __future__ import unicode_literals
import os
import threading
from libs.prompt_toolkit.utils import is_windows
from .select import select_fds
__all__ = (
'InputHookContext',
)
class InputHookContext(object):
"""
Given as a parameter to the inputhook.
"""
def __init__(self, inputhook):
assert callable(inputhook)
self.inputhook = inputhook
self._input_is_ready = None
self._r, self._w = os.pipe()
def input_is_ready(self):
"""
Return True when the input is ready.
"""
return self._input_is_ready(wait=False)
def fileno(self):
"""
File descriptor that will become ready when the event loop needs to go on.
"""
return self._r
def call_inputhook(self, input_is_ready_func):
"""
Call the inputhook. (Called by a prompt-toolkit eventloop.)
"""
self._input_is_ready = input_is_ready_func
# Start thread that activates this pipe when there is input to process.
def thread():
input_is_ready_func(wait=True)
os.write(self._w, b'x')
threading.Thread(target=thread).start()
# Call inputhook.
self.inputhook(self)
# Flush the read end of the pipe.
try:
# Before calling 'os.read', call select.select. This is required
# when the gevent monkey patch has been applied. 'os.read' is never
# monkey patched and won't be cooperative, so that would block all
# other select() calls otherwise.
# See: http://www.gevent.org/gevent.os.html
# Note: On Windows, this is apparently not an issue.
# However, if we would ever want to add a select call, it
# should use `windll.kernel32.WaitForMultipleObjects`,
# because `select.select` can't wait for a pipe on Windows.
if not is_windows():
select_fds([self._r], timeout=None)
os.read(self._r, 1024)
except OSError:
# This happens when the window resizes and a SIGWINCH was received.
# We get 'Error: [Errno 4] Interrupted system call'
# Just ignore.
pass
self._input_is_ready = None
def close(self):
"""
Clean up resources.
"""
if self._r:
os.close(self._r)
os.close(self._w)
self._r = self._w = None

View File

@@ -0,0 +1,311 @@
from __future__ import unicode_literals
import fcntl
import os
import random
import signal
import threading
import time
from libs.prompt_toolkit.terminal.vt100_input import InputStream
from libs.prompt_toolkit.utils import DummyContext, in_main_thread
from libs.prompt_toolkit.input import Input
from .base import EventLoop, INPUT_TIMEOUT
from .callbacks import EventLoopCallbacks
from .inputhook import InputHookContext
from .posix_utils import PosixStdinReader
from .utils import TimeIt
from .select import AutoSelector, Selector, fd_to_int
__all__ = (
'PosixEventLoop',
)
_now = time.time
class PosixEventLoop(EventLoop):
"""
Event loop for posix systems (Linux, Mac os X).
"""
def __init__(self, inputhook=None, selector=AutoSelector):
assert inputhook is None or callable(inputhook)
assert issubclass(selector, Selector)
self.running = False
self.closed = False
self._running = False
self._callbacks = None
self._calls_from_executor = []
self._read_fds = {} # Maps fd to handler.
self.selector = selector()
# Create a pipe for inter thread communication.
self._schedule_pipe = os.pipe()
fcntl.fcntl(self._schedule_pipe[0], fcntl.F_SETFL, os.O_NONBLOCK)
# Create inputhook context.
self._inputhook_context = InputHookContext(inputhook) if inputhook else None
def run(self, stdin, callbacks):
"""
The input 'event loop'.
"""
assert isinstance(stdin, Input)
assert isinstance(callbacks, EventLoopCallbacks)
assert not self._running
if self.closed:
raise Exception('Event loop already closed.')
self._running = True
self._callbacks = callbacks
inputstream = InputStream(callbacks.feed_key)
current_timeout = [INPUT_TIMEOUT] # Nonlocal
# Create reader class.
stdin_reader = PosixStdinReader(stdin.fileno())
# Only attach SIGWINCH signal handler in main thread.
# (It's not possible to attach signal handlers in other threads. In
# that case we should rely on a the main thread to call this manually
# instead.)
if in_main_thread():
ctx = call_on_sigwinch(self.received_winch)
else:
ctx = DummyContext()
def read_from_stdin():
" Read user input. "
# Feed input text.
data = stdin_reader.read()
inputstream.feed(data)
# Set timeout again.
current_timeout[0] = INPUT_TIMEOUT
# Quit when the input stream was closed.
if stdin_reader.closed:
self.stop()
self.add_reader(stdin, read_from_stdin)
self.add_reader(self._schedule_pipe[0], None)
with ctx:
while self._running:
# Call inputhook.
if self._inputhook_context:
with TimeIt() as inputhook_timer:
def ready(wait):
" True when there is input ready. The inputhook should return control. "
return self._ready_for_reading(current_timeout[0] if wait else 0) != []
self._inputhook_context.call_inputhook(ready)
inputhook_duration = inputhook_timer.duration
else:
inputhook_duration = 0
# Calculate remaining timeout. (The inputhook consumed some of the time.)
if current_timeout[0] is None:
remaining_timeout = None
else:
remaining_timeout = max(0, current_timeout[0] - inputhook_duration)
# Wait until input is ready.
fds = self._ready_for_reading(remaining_timeout)
# When any of the FDs are ready. Call the appropriate callback.
if fds:
# Create lists of high/low priority tasks. The main reason
# for this is to allow painting the UI to happen as soon as
# possible, but when there are many events happening, we
# don't want to call the UI renderer 1000x per second. If
# the eventloop is completely saturated with many CPU
# intensive tasks (like processing input/output), we say
# that drawing the UI can be postponed a little, to make
# CPU available. This will be a low priority task in that
# case.
tasks = []
low_priority_tasks = []
now = None # Lazy load time. (Fewer system calls.)
for fd in fds:
# For the 'call_from_executor' fd, put each pending
# item on either the high or low priority queue.
if fd == self._schedule_pipe[0]:
for c, max_postpone_until in self._calls_from_executor:
if max_postpone_until is None:
# Execute now.
tasks.append(c)
else:
# Execute soon, if `max_postpone_until` is in the future.
now = now or _now()
if max_postpone_until < now:
tasks.append(c)
else:
low_priority_tasks.append((c, max_postpone_until))
self._calls_from_executor = []
# Flush all the pipe content.
os.read(self._schedule_pipe[0], 1024)
else:
handler = self._read_fds.get(fd)
if handler:
tasks.append(handler)
# Handle everything in random order. (To avoid starvation.)
random.shuffle(tasks)
random.shuffle(low_priority_tasks)
# When there are high priority tasks, run all these.
# Schedule low priority tasks for the next iteration.
if tasks:
for t in tasks:
t()
# Postpone low priority tasks.
for t, max_postpone_until in low_priority_tasks:
self.call_from_executor(t, _max_postpone_until=max_postpone_until)
else:
# Currently there are only low priority tasks -> run them right now.
for t, _ in low_priority_tasks:
t()
else:
# Flush all pending keys on a timeout. (This is most
# important to flush the vt100 'Escape' key early when
# nothing else follows.)
inputstream.flush()
# Fire input timeout event.
callbacks.input_timeout()
current_timeout[0] = None
self.remove_reader(stdin)
self.remove_reader(self._schedule_pipe[0])
self._callbacks = None
def _ready_for_reading(self, timeout=None):
"""
Return the file descriptors that are ready for reading.
"""
fds = self.selector.select(timeout)
return fds
def received_winch(self):
"""
Notify the event loop that SIGWINCH has been received
"""
# Process signal asynchronously, because this handler can write to the
# output, and doing this inside the signal handler causes easily
# reentrant calls, giving runtime errors..
# Furthur, this has to be thread safe. When the CommandLineInterface
# runs not in the main thread, this function still has to be called
# from the main thread. (The only place where we can install signal
# handlers.)
def process_winch():
if self._callbacks:
self._callbacks.terminal_size_changed()
self.call_from_executor(process_winch)
def run_in_executor(self, callback):
"""
Run a long running function in a background thread.
(This is recommended for code that could block the event loop.)
Similar to Twisted's ``deferToThread``.
"""
# Wait until the main thread is idle.
# We start the thread by using `call_from_executor`. The event loop
# favours processing input over `calls_from_executor`, so the thread
# will not start until there is no more input to process and the main
# thread becomes idle for an instant. This is good, because Python
# threading favours CPU over I/O -- an autocompletion thread in the
# background would cause a significantly slow down of the main thread.
# It is mostly noticable when pasting large portions of text while
# having real time autocompletion while typing on.
def start_executor():
threading.Thread(target=callback).start()
self.call_from_executor(start_executor)
def call_from_executor(self, callback, _max_postpone_until=None):
"""
Call this function in the main event loop.
Similar to Twisted's ``callFromThread``.
:param _max_postpone_until: `None` or `time.time` value. For interal
use. If the eventloop is saturated, consider this task to be low
priority and postpone maximum until this timestamp. (For instance,
repaint is done using low priority.)
"""
assert _max_postpone_until is None or isinstance(_max_postpone_until, float)
self._calls_from_executor.append((callback, _max_postpone_until))
if self._schedule_pipe:
try:
os.write(self._schedule_pipe[1], b'x')
except (AttributeError, IndexError, OSError):
# Handle race condition. We're in a different thread.
# - `_schedule_pipe` could have become None in the meantime.
# - We catch `OSError` (actually BrokenPipeError), because the
# main thread could have closed the pipe already.
pass
def stop(self):
"""
Stop the event loop.
"""
self._running = False
def close(self):
self.closed = True
# Close pipes.
schedule_pipe = self._schedule_pipe
self._schedule_pipe = None
if schedule_pipe:
os.close(schedule_pipe[0])
os.close(schedule_pipe[1])
if self._inputhook_context:
self._inputhook_context.close()
def add_reader(self, fd, callback):
" Add read file descriptor to the event loop. "
fd = fd_to_int(fd)
self._read_fds[fd] = callback
self.selector.register(fd)
def remove_reader(self, fd):
" Remove read file descriptor from the event loop. "
fd = fd_to_int(fd)
if fd in self._read_fds:
del self._read_fds[fd]
self.selector.unregister(fd)
class call_on_sigwinch(object):
"""
Context manager which Installs a SIGWINCH callback.
(This signal occurs when the terminal size changes.)
"""
def __init__(self, callback):
self.callback = callback
self.previous_callback = None
def __enter__(self):
self.previous_callback = signal.signal(signal.SIGWINCH, lambda *a: self.callback())
def __exit__(self, *a, **kw):
if self.previous_callback is None:
# Normally, `signal.signal` should never return `None`.
# For some reason it happens here:
# https://github.com/jonathanslenders/python-prompt-toolkit/pull/174
signal.signal(signal.SIGWINCH, 0)
else:
signal.signal(signal.SIGWINCH, self.previous_callback)

View File

@@ -0,0 +1,82 @@
from __future__ import unicode_literals
from codecs import getincrementaldecoder
import os
import six
__all__ = (
'PosixStdinReader',
)
class PosixStdinReader(object):
"""
Wrapper around stdin which reads (nonblocking) the next available 1024
bytes and decodes it.
Note that you can't be sure that the input file is closed if the ``read``
function returns an empty string. When ``errors=ignore`` is passed,
``read`` can return an empty string if all malformed input was replaced by
an empty string. (We can't block here and wait for more input.) So, because
of that, check the ``closed`` attribute, to be sure that the file has been
closed.
:param stdin_fd: File descriptor from which we read.
:param errors: Can be 'ignore', 'strict' or 'replace'.
On Python3, this can be 'surrogateescape', which is the default.
'surrogateescape' is preferred, because this allows us to transfer
unrecognised bytes to the key bindings. Some terminals, like lxterminal
and Guake, use the 'Mxx' notation to send mouse events, where each 'x'
can be any possible byte.
"""
# By default, we want to 'ignore' errors here. The input stream can be full
# of junk. One occurrence of this that I had was when using iTerm2 on OS X,
# with "Option as Meta" checked (You should choose "Option as +Esc".)
def __init__(self, stdin_fd,
errors=('ignore' if six.PY2 else 'surrogateescape')):
assert isinstance(stdin_fd, int)
self.stdin_fd = stdin_fd
self.errors = errors
# Create incremental decoder for decoding stdin.
# We can not just do `os.read(stdin.fileno(), 1024).decode('utf-8')`, because
# it could be that we are in the middle of a utf-8 byte sequence.
self._stdin_decoder_cls = getincrementaldecoder('utf-8')
self._stdin_decoder = self._stdin_decoder_cls(errors=errors)
#: True when there is nothing anymore to read.
self.closed = False
def read(self, count=1024):
# By default we choose a rather small chunk size, because reading
# big amounts of input at once, causes the event loop to process
# all these key bindings also at once without going back to the
# loop. This will make the application feel unresponsive.
"""
Read the input and return it as a string.
Return the text. Note that this can return an empty string, even when
the input stream was not yet closed. This means that something went
wrong during the decoding.
"""
if self.closed:
return b''
# Note: the following works better than wrapping `self.stdin` like
# `codecs.getreader('utf-8')(stdin)` and doing `read(1)`.
# Somehow that causes some latency when the escape
# character is pressed. (Especially on combination with the `select`.)
try:
data = os.read(self.stdin_fd, count)
# Nothing more to read, stream is closed.
if data == b'':
self.closed = True
return ''
except OSError:
# In case of SIGWINCH
data = b''
return self._stdin_decoder.decode(data)

View File

@@ -0,0 +1,216 @@
"""
Selectors for the Posix event loop.
"""
from __future__ import unicode_literals, absolute_import
import sys
import abc
import errno
import select
import six
__all__ = (
'AutoSelector',
'PollSelector',
'SelectSelector',
'Selector',
'fd_to_int',
)
def fd_to_int(fd):
assert isinstance(fd, int) or hasattr(fd, 'fileno')
if isinstance(fd, int):
return fd
else:
return fd.fileno()
class Selector(six.with_metaclass(abc.ABCMeta, object)):
@abc.abstractmethod
def register(self, fd):
assert isinstance(fd, int)
@abc.abstractmethod
def unregister(self, fd):
assert isinstance(fd, int)
@abc.abstractmethod
def select(self, timeout):
pass
@abc.abstractmethod
def close(self):
pass
class AutoSelector(Selector):
def __init__(self):
self._fds = []
self._select_selector = SelectSelector()
self._selectors = [self._select_selector]
# When 'select.poll' exists, create a PollSelector.
if hasattr(select, 'poll'):
self._poll_selector = PollSelector()
self._selectors.append(self._poll_selector)
else:
self._poll_selector = None
# Use of the 'select' module, that was introduced in Python3.4. We don't
# use it before 3.5 however, because this is the point where this module
# retries interrupted system calls.
if sys.version_info >= (3, 5):
self._py3_selector = Python3Selector()
self._selectors.append(self._py3_selector)
else:
self._py3_selector = None
def register(self, fd):
assert isinstance(fd, int)
self._fds.append(fd)
for sel in self._selectors:
sel.register(fd)
def unregister(self, fd):
assert isinstance(fd, int)
self._fds.remove(fd)
for sel in self._selectors:
sel.unregister(fd)
def select(self, timeout):
# Try Python 3 selector first.
if self._py3_selector:
try:
return self._py3_selector.select(timeout)
except PermissionError:
# We had a situation (in pypager) where epoll raised a
# PermissionError when a local file descriptor was registered,
# however poll and select worked fine. So, in that case, just
# try using select below.
pass
try:
# Prefer 'select.select', if we don't have much file descriptors.
# This is more universal.
return self._select_selector.select(timeout)
except ValueError:
# When we have more than 1024 open file descriptors, we'll always
# get a "ValueError: filedescriptor out of range in select()" for
# 'select'. In this case, try, using 'poll' instead.
if self._poll_selector is not None:
return self._poll_selector.select(timeout)
else:
raise
def close(self):
for sel in self._selectors:
sel.close()
class Python3Selector(Selector):
"""
Use of the Python3 'selectors' module.
NOTE: Only use on Python 3.5 or newer!
"""
def __init__(self):
assert sys.version_info >= (3, 5)
import selectors # Inline import: Python3 only!
self._sel = selectors.DefaultSelector()
def register(self, fd):
assert isinstance(fd, int)
import selectors # Inline import: Python3 only!
self._sel.register(fd, selectors.EVENT_READ, None)
def unregister(self, fd):
assert isinstance(fd, int)
self._sel.unregister(fd)
def select(self, timeout):
events = self._sel.select(timeout=timeout)
return [key.fileobj for key, mask in events]
def close(self):
self._sel.close()
class PollSelector(Selector):
def __init__(self):
self._poll = select.poll()
def register(self, fd):
assert isinstance(fd, int)
self._poll.register(fd, select.POLLIN)
def unregister(self, fd):
assert isinstance(fd, int)
def select(self, timeout):
tuples = self._poll.poll(timeout) # Returns (fd, event) tuples.
return [t[0] for t in tuples]
def close(self):
pass # XXX
class SelectSelector(Selector):
"""
Wrapper around select.select.
When the SIGWINCH signal is handled, other system calls, like select
are aborted in Python. This wrapper will retry the system call.
"""
def __init__(self):
self._fds = []
def register(self, fd):
self._fds.append(fd)
def unregister(self, fd):
self._fds.remove(fd)
def select(self, timeout):
while True:
try:
return select.select(self._fds, [], [], timeout)[0]
except select.error as e:
# Retry select call when EINTR
if e.args and e.args[0] == errno.EINTR:
continue
else:
raise
def close(self):
pass
def select_fds(read_fds, timeout, selector=AutoSelector):
"""
Wait for a list of file descriptors (`read_fds`) to become ready for
reading. This chooses the most appropriate select-tool for use in
prompt-toolkit.
"""
# Map to ensure that we return the objects that were passed in originally.
# Whether they are a fd integer or an object that has a fileno().
# (The 'poll' implementation for instance, returns always integers.)
fd_map = dict((fd_to_int(fd), fd) for fd in read_fds)
# Wait, using selector.
sel = selector()
try:
for fd in read_fds:
sel.register(fd)
result = sel.select(timeout)
if result is not None:
return [fd_map[fd_to_int(fd)] for fd in result]
finally:
sel.close()

View File

@@ -0,0 +1,23 @@
from __future__ import unicode_literals
import time
__all__ = (
'TimeIt',
)
class TimeIt(object):
"""
Context manager that times the duration of the code body.
The `duration` attribute will contain the execution time in seconds.
"""
def __init__(self):
self.duration = None
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
self.end = time.time()
self.duration = self.end - self.start

View File

@@ -0,0 +1,187 @@
"""
Win32 event loop.
Windows notes:
- Somehow it doesn't seem to work with the 'ProactorEventLoop'.
"""
from __future__ import unicode_literals
from ..terminal.win32_input import ConsoleInputReader
from ..win32_types import SECURITY_ATTRIBUTES
from .base import EventLoop, INPUT_TIMEOUT
from .inputhook import InputHookContext
from .utils import TimeIt
from ctypes import windll, pointer
from ctypes.wintypes import DWORD, BOOL, HANDLE
import msvcrt
import threading
__all__ = (
'Win32EventLoop',
)
WAIT_TIMEOUT = 0x00000102
INPUT_TIMEOUT_MS = int(1000 * INPUT_TIMEOUT)
class Win32EventLoop(EventLoop):
"""
Event loop for Windows systems.
:param recognize_paste: When True, try to discover paste actions and turn
the event into a BracketedPaste.
"""
def __init__(self, inputhook=None, recognize_paste=True):
assert inputhook is None or callable(inputhook)
self._event = _create_event()
self._console_input_reader = ConsoleInputReader(recognize_paste=recognize_paste)
self._calls_from_executor = []
self.closed = False
self._running = False
# Additional readers.
self._read_fds = {} # Maps fd to handler.
# Create inputhook context.
self._inputhook_context = InputHookContext(inputhook) if inputhook else None
def run(self, stdin, callbacks):
if self.closed:
raise Exception('Event loop already closed.')
current_timeout = INPUT_TIMEOUT_MS
self._running = True
while self._running:
# Call inputhook.
with TimeIt() as inputhook_timer:
if self._inputhook_context:
def ready(wait):
" True when there is input ready. The inputhook should return control. "
return bool(self._ready_for_reading(current_timeout if wait else 0))
self._inputhook_context.call_inputhook(ready)
# Calculate remaining timeout. (The inputhook consumed some of the time.)
if current_timeout == -1:
remaining_timeout = -1
else:
remaining_timeout = max(0, current_timeout - int(1000 * inputhook_timer.duration))
# Wait for the next event.
handle = self._ready_for_reading(remaining_timeout)
if handle == self._console_input_reader.handle:
# When stdin is ready, read input and reset timeout timer.
keys = self._console_input_reader.read()
for k in keys:
callbacks.feed_key(k)
current_timeout = INPUT_TIMEOUT_MS
elif handle == self._event:
# When the Windows Event has been trigger, process the messages in the queue.
windll.kernel32.ResetEvent(self._event)
self._process_queued_calls_from_executor()
elif handle in self._read_fds:
callback = self._read_fds[handle]
callback()
else:
# Fire input timeout event.
callbacks.input_timeout()
current_timeout = -1
def _ready_for_reading(self, timeout=None):
"""
Return the handle that is ready for reading or `None` on timeout.
"""
handles = [self._event, self._console_input_reader.handle]
handles.extend(self._read_fds.keys())
return _wait_for_handles(handles, timeout)
def stop(self):
self._running = False
def close(self):
self.closed = True
# Clean up Event object.
windll.kernel32.CloseHandle(self._event)
if self._inputhook_context:
self._inputhook_context.close()
self._console_input_reader.close()
def run_in_executor(self, callback):
"""
Run a long running function in a background thread.
(This is recommended for code that could block the event loop.)
Similar to Twisted's ``deferToThread``.
"""
# Wait until the main thread is idle for an instant before starting the
# executor. (Like in eventloop/posix.py, we start the executor using
# `call_from_executor`.)
def start_executor():
threading.Thread(target=callback).start()
self.call_from_executor(start_executor)
def call_from_executor(self, callback, _max_postpone_until=None):
"""
Call this function in the main event loop.
Similar to Twisted's ``callFromThread``.
"""
# Append to list of pending callbacks.
self._calls_from_executor.append(callback)
# Set Windows event.
windll.kernel32.SetEvent(self._event)
def _process_queued_calls_from_executor(self):
# Process calls from executor.
calls_from_executor, self._calls_from_executor = self._calls_from_executor, []
for c in calls_from_executor:
c()
def add_reader(self, fd, callback):
" Start watching the file descriptor for read availability. "
h = msvcrt.get_osfhandle(fd)
self._read_fds[h] = callback
def remove_reader(self, fd):
" Stop watching the file descriptor for read availability. "
h = msvcrt.get_osfhandle(fd)
if h in self._read_fds:
del self._read_fds[h]
def _wait_for_handles(handles, timeout=-1):
"""
Waits for multiple handles. (Similar to 'select') Returns the handle which is ready.
Returns `None` on timeout.
http://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx
"""
arrtype = HANDLE * len(handles)
handle_array = arrtype(*handles)
ret = windll.kernel32.WaitForMultipleObjects(
len(handle_array), handle_array, BOOL(False), DWORD(timeout))
if ret == WAIT_TIMEOUT:
return None
else:
h = handle_array[ret]
return h
def _create_event():
"""
Creates a Win32 unnamed Event .
http://msdn.microsoft.com/en-us/library/windows/desktop/ms682396(v=vs.85).aspx
"""
return windll.kernel32.CreateEventA(pointer(SECURITY_ATTRIBUTES()), BOOL(True), BOOL(False), None)

View File

@@ -0,0 +1,36 @@
"""
Filters decide whether something is active or not (they decide about a boolean
state). This is used to enable/disable features, like key bindings, parts of
the layout and other stuff. For instance, we could have a `HasSearch` filter
attached to some part of the layout, in order to show that part of the user
interface only while the user is searching.
Filters are made to avoid having to attach callbacks to all event in order to
propagate state. However, they are lazy, they don't automatically propagate the
state of what they are observing. Only when a filter is called (it's actually a
callable), it will calculate its value. So, its not really reactive
programming, but it's made to fit for this framework.
One class of filters observe a `CommandLineInterface` instance. However, they
are not attached to such an instance. (We have to pass this instance to the
filter when calling it.) The reason for this is to allow declarative
programming: for key bindings, we can attach a filter to a key binding without
knowing yet which `CommandLineInterface` instance it will observe in the end.
Examples are `HasSearch` or `IsExiting`.
Another class of filters doesn't take anything as input. And a third class of
filters are universal, for instance `Always` and `Never`.
It is impossible to mix the first and the second class, because that would mean
mixing filters with a different signature.
Filters can be chained using ``&`` and ``|`` operations, and inverted using the
``~`` operator, for instance::
filter = HasFocus('default') & ~ HasSelection()
"""
from __future__ import unicode_literals
from .base import *
from .cli import *
from .types import *
from .utils import *

View File

@@ -0,0 +1,234 @@
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
from libs.prompt_toolkit.utils import test_callable_args
__all__ = (
'Filter',
'Never',
'Always',
'Condition',
)
class Filter(with_metaclass(ABCMeta, object)):
"""
Filter to activate/deactivate a feature, depending on a condition.
The return value of ``__call__`` will tell if the feature should be active.
"""
@abstractmethod
def __call__(self, *a, **kw):
"""
The actual call to evaluate the filter.
"""
return True
def __and__(self, other):
"""
Chaining of filters using the & operator.
"""
return _and_cache[self, other]
def __or__(self, other):
"""
Chaining of filters using the | operator.
"""
return _or_cache[self, other]
def __invert__(self):
"""
Inverting of filters using the ~ operator.
"""
return _invert_cache[self]
def __bool__(self):
"""
By purpose, we don't allow bool(...) operations directly on a filter,
because because the meaning is ambigue.
Executing a filter has to be done always by calling it. Providing
defaults for `None` values should be done through an `is None` check
instead of for instance ``filter1 or Always()``.
"""
raise TypeError
__nonzero__ = __bool__ # For Python 2.
def test_args(self, *args):
"""
Test whether this filter can be called with the following argument list.
"""
return test_callable_args(self.__call__, args)
class _AndCache(dict):
"""
Cache for And operation between filters.
(Filter classes are stateless, so we can reuse them.)
Note: This could be a memory leak if we keep creating filters at runtime.
If that is True, the filters should be weakreffed (not the tuple of
filters), and tuples should be removed when one of these filters is
removed. In practise however, there is a finite amount of filters.
"""
def __missing__(self, filters):
a, b = filters
assert isinstance(b, Filter), 'Expecting filter, got %r' % b
if isinstance(b, Always) or isinstance(a, Never):
return a
elif isinstance(b, Never) or isinstance(a, Always):
return b
result = _AndList(filters)
self[filters] = result
return result
class _OrCache(dict):
""" Cache for Or operation between filters. """
def __missing__(self, filters):
a, b = filters
assert isinstance(b, Filter), 'Expecting filter, got %r' % b
if isinstance(b, Always) or isinstance(a, Never):
return b
elif isinstance(b, Never) or isinstance(a, Always):
return a
result = _OrList(filters)
self[filters] = result
return result
class _InvertCache(dict):
""" Cache for inversion operator. """
def __missing__(self, filter):
result = _Invert(filter)
self[filter] = result
return result
_and_cache = _AndCache()
_or_cache = _OrCache()
_invert_cache = _InvertCache()
class _AndList(Filter):
"""
Result of &-operation between several filters.
"""
def __init__(self, filters):
all_filters = []
for f in filters:
if isinstance(f, _AndList): # Turn nested _AndLists into one.
all_filters.extend(f.filters)
else:
all_filters.append(f)
self.filters = all_filters
def test_args(self, *args):
return all(f.test_args(*args) for f in self.filters)
def __call__(self, *a, **kw):
return all(f(*a, **kw) for f in self.filters)
def __repr__(self):
return '&'.join(repr(f) for f in self.filters)
class _OrList(Filter):
"""
Result of |-operation between several filters.
"""
def __init__(self, filters):
all_filters = []
for f in filters:
if isinstance(f, _OrList): # Turn nested _OrLists into one.
all_filters.extend(f.filters)
else:
all_filters.append(f)
self.filters = all_filters
def test_args(self, *args):
return all(f.test_args(*args) for f in self.filters)
def __call__(self, *a, **kw):
return any(f(*a, **kw) for f in self.filters)
def __repr__(self):
return '|'.join(repr(f) for f in self.filters)
class _Invert(Filter):
"""
Negation of another filter.
"""
def __init__(self, filter):
self.filter = filter
def __call__(self, *a, **kw):
return not self.filter(*a, **kw)
def __repr__(self):
return '~%r' % self.filter
def test_args(self, *args):
return self.filter.test_args(*args)
class Always(Filter):
"""
Always enable feature.
"""
def __call__(self, *a, **kw):
return True
def __invert__(self):
return Never()
class Never(Filter):
"""
Never enable feature.
"""
def __call__(self, *a, **kw):
return False
def __invert__(self):
return Always()
class Condition(Filter):
"""
Turn any callable (which takes a cli and returns a boolean) into a Filter.
This can be used as a decorator::
@Condition
def feature_is_active(cli): # `feature_is_active` becomes a Filter.
return True
:param func: Callable which takes either a
:class:`~prompt_toolkit.interface.CommandLineInterface` or nothing and
returns a boolean. (Depending on what it takes, this will become a
:class:`.Filter` or :class:`~prompt_toolkit.filters.CLIFilter`.)
"""
def __init__(self, func):
assert callable(func)
self.func = func
def __call__(self, *a, **kw):
return self.func(*a, **kw)
def __repr__(self):
return 'Condition(%r)' % self.func
def test_args(self, *a):
return test_callable_args(self.func, a)

View File

@@ -0,0 +1,395 @@
"""
Filters that accept a `CommandLineInterface` as argument.
"""
from __future__ import unicode_literals
from .base import Filter
from libs.prompt_toolkit.enums import EditingMode
from libs.prompt_toolkit.key_binding.vi_state import InputMode as ViInputMode
from libs.prompt_toolkit.cache import memoized
__all__ = (
'HasArg',
'HasCompletions',
'HasFocus',
'InFocusStack',
'HasSearch',
'HasSelection',
'HasValidationError',
'IsAborting',
'IsDone',
'IsMultiline',
'IsReadOnly',
'IsReturning',
'RendererHeightIsKnown',
'InEditingMode',
# Vi modes.
'ViMode',
'ViNavigationMode',
'ViInsertMode',
'ViInsertMultipleMode',
'ViReplaceMode',
'ViSelectionMode',
'ViWaitingForTextObjectMode',
'ViDigraphMode',
# Emacs modes.
'EmacsMode',
'EmacsInsertMode',
'EmacsSelectionMode',
)
@memoized()
class HasFocus(Filter):
"""
Enable when this buffer has the focus.
"""
def __init__(self, buffer_name):
self._buffer_name = buffer_name
@property
def buffer_name(self):
" The given buffer name. (Read-only) "
return self._buffer_name
def __call__(self, cli):
return cli.current_buffer_name == self.buffer_name
def __repr__(self):
return 'HasFocus(%r)' % self.buffer_name
@memoized()
class InFocusStack(Filter):
"""
Enable when this buffer appears on the focus stack.
"""
def __init__(self, buffer_name):
self._buffer_name = buffer_name
@property
def buffer_name(self):
" The given buffer name. (Read-only) "
return self._buffer_name
def __call__(self, cli):
return self.buffer_name in cli.buffers.focus_stack
def __repr__(self):
return 'InFocusStack(%r)' % self.buffer_name
@memoized()
class HasSelection(Filter):
"""
Enable when the current buffer has a selection.
"""
def __call__(self, cli):
return bool(cli.current_buffer.selection_state)
def __repr__(self):
return 'HasSelection()'
@memoized()
class HasCompletions(Filter):
"""
Enable when the current buffer has completions.
"""
def __call__(self, cli):
return cli.current_buffer.complete_state is not None
def __repr__(self):
return 'HasCompletions()'
@memoized()
class IsMultiline(Filter):
"""
Enable in multiline mode.
"""
def __call__(self, cli):
return cli.current_buffer.is_multiline()
def __repr__(self):
return 'IsMultiline()'
@memoized()
class IsReadOnly(Filter):
"""
True when the current buffer is read only.
"""
def __call__(self, cli):
return cli.current_buffer.read_only()
def __repr__(self):
return 'IsReadOnly()'
@memoized()
class HasValidationError(Filter):
"""
Current buffer has validation error.
"""
def __call__(self, cli):
return cli.current_buffer.validation_error is not None
def __repr__(self):
return 'HasValidationError()'
@memoized()
class HasArg(Filter):
"""
Enable when the input processor has an 'arg'.
"""
def __call__(self, cli):
return cli.input_processor.arg is not None
def __repr__(self):
return 'HasArg()'
@memoized()
class HasSearch(Filter):
"""
Incremental search is active.
"""
def __call__(self, cli):
return cli.is_searching
def __repr__(self):
return 'HasSearch()'
@memoized()
class IsReturning(Filter):
"""
When a return value has been set.
"""
def __call__(self, cli):
return cli.is_returning
def __repr__(self):
return 'IsReturning()'
@memoized()
class IsAborting(Filter):
"""
True when aborting. (E.g. Control-C pressed.)
"""
def __call__(self, cli):
return cli.is_aborting
def __repr__(self):
return 'IsAborting()'
@memoized()
class IsExiting(Filter):
"""
True when exiting. (E.g. Control-D pressed.)
"""
def __call__(self, cli):
return cli.is_exiting
def __repr__(self):
return 'IsExiting()'
@memoized()
class IsDone(Filter):
"""
True when the CLI is returning, aborting or exiting.
"""
def __call__(self, cli):
return cli.is_done
def __repr__(self):
return 'IsDone()'
@memoized()
class RendererHeightIsKnown(Filter):
"""
Only True when the renderer knows it's real height.
(On VT100 terminals, we have to wait for a CPR response, before we can be
sure of the available height between the cursor position and the bottom of
the terminal. And usually it's nicer to wait with drawing bottom toolbars
until we receive the height, in order to avoid flickering -- first drawing
somewhere in the middle, and then again at the bottom.)
"""
def __call__(self, cli):
return cli.renderer.height_is_known
def __repr__(self):
return 'RendererHeightIsKnown()'
@memoized()
class InEditingMode(Filter):
"""
Check whether a given editing mode is active. (Vi or Emacs.)
"""
def __init__(self, editing_mode):
self._editing_mode = editing_mode
@property
def editing_mode(self):
" The given editing mode. (Read-only) "
return self._editing_mode
def __call__(self, cli):
return cli.editing_mode == self.editing_mode
def __repr__(self):
return 'InEditingMode(%r)' % (self.editing_mode, )
@memoized()
class ViMode(Filter):
def __call__(self, cli):
return cli.editing_mode == EditingMode.VI
def __repr__(self):
return 'ViMode()'
@memoized()
class ViNavigationMode(Filter):
"""
Active when the set for Vi navigation key bindings are active.
"""
def __call__(self, cli):
if (cli.editing_mode != EditingMode.VI
or cli.vi_state.operator_func
or cli.vi_state.waiting_for_digraph
or cli.current_buffer.selection_state):
return False
return (cli.vi_state.input_mode == ViInputMode.NAVIGATION or
cli.current_buffer.read_only())
def __repr__(self):
return 'ViNavigationMode()'
@memoized()
class ViInsertMode(Filter):
def __call__(self, cli):
if (cli.editing_mode != EditingMode.VI
or cli.vi_state.operator_func
or cli.vi_state.waiting_for_digraph
or cli.current_buffer.selection_state
or cli.current_buffer.read_only()):
return False
return cli.vi_state.input_mode == ViInputMode.INSERT
def __repr__(self):
return 'ViInputMode()'
@memoized()
class ViInsertMultipleMode(Filter):
def __call__(self, cli):
if (cli.editing_mode != EditingMode.VI
or cli.vi_state.operator_func
or cli.vi_state.waiting_for_digraph
or cli.current_buffer.selection_state
or cli.current_buffer.read_only()):
return False
return cli.vi_state.input_mode == ViInputMode.INSERT_MULTIPLE
def __repr__(self):
return 'ViInsertMultipleMode()'
@memoized()
class ViReplaceMode(Filter):
def __call__(self, cli):
if (cli.editing_mode != EditingMode.VI
or cli.vi_state.operator_func
or cli.vi_state.waiting_for_digraph
or cli.current_buffer.selection_state
or cli.current_buffer.read_only()):
return False
return cli.vi_state.input_mode == ViInputMode.REPLACE
def __repr__(self):
return 'ViReplaceMode()'
@memoized()
class ViSelectionMode(Filter):
def __call__(self, cli):
if cli.editing_mode != EditingMode.VI:
return False
return bool(cli.current_buffer.selection_state)
def __repr__(self):
return 'ViSelectionMode()'
@memoized()
class ViWaitingForTextObjectMode(Filter):
def __call__(self, cli):
if cli.editing_mode != EditingMode.VI:
return False
return cli.vi_state.operator_func is not None
def __repr__(self):
return 'ViWaitingForTextObjectMode()'
@memoized()
class ViDigraphMode(Filter):
def __call__(self, cli):
if cli.editing_mode != EditingMode.VI:
return False
return cli.vi_state.waiting_for_digraph
def __repr__(self):
return 'ViDigraphMode()'
@memoized()
class EmacsMode(Filter):
" When the Emacs bindings are active. "
def __call__(self, cli):
return cli.editing_mode == EditingMode.EMACS
def __repr__(self):
return 'EmacsMode()'
@memoized()
class EmacsInsertMode(Filter):
def __call__(self, cli):
if (cli.editing_mode != EditingMode.EMACS
or cli.current_buffer.selection_state
or cli.current_buffer.read_only()):
return False
return True
def __repr__(self):
return 'EmacsInsertMode()'
@memoized()
class EmacsSelectionMode(Filter):
def __call__(self, cli):
return (cli.editing_mode == EditingMode.EMACS
and cli.current_buffer.selection_state)
def __repr__(self):
return 'EmacsSelectionMode()'

View File

@@ -0,0 +1,55 @@
from __future__ import unicode_literals
from six import with_metaclass
from collections import defaultdict
import weakref
__all__ = (
'CLIFilter',
'SimpleFilter',
)
# Cache for _FilterTypeMeta. (Don't test the same __instancecheck__ twice as
# long as the object lives. -- We do this a lot and calling 'test_args' is
# expensive.)
_instance_check_cache = defaultdict(weakref.WeakKeyDictionary)
class _FilterTypeMeta(type):
def __instancecheck__(cls, instance):
cache = _instance_check_cache[tuple(cls.arguments_list)]
def get():
" The actual test. "
if not hasattr(instance, 'test_args'):
return False
return instance.test_args(*cls.arguments_list)
try:
return cache[instance]
except KeyError:
result = get()
cache[instance] = result
return result
class _FilterType(with_metaclass(_FilterTypeMeta)):
def __new__(cls):
raise NotImplementedError('This class should not be initiated.')
class CLIFilter(_FilterType):
"""
Abstract base class for filters that accept a
:class:`~prompt_toolkit.interface.CommandLineInterface` argument. It cannot
be instantiated, it's only to be used for instance assertions, e.g.::
isinstance(my_filter, CliFilter)
"""
arguments_list = ['cli']
class SimpleFilter(_FilterType):
"""
Abstract base class for filters that don't accept any arguments.
"""
arguments_list = []

View File

@@ -0,0 +1,39 @@
from __future__ import unicode_literals
from .base import Always, Never
from .types import SimpleFilter, CLIFilter
__all__ = (
'to_cli_filter',
'to_simple_filter',
)
_always = Always()
_never = Never()
def to_simple_filter(bool_or_filter):
"""
Accept both booleans and CLIFilters as input and
turn it into a SimpleFilter.
"""
if not isinstance(bool_or_filter, (bool, SimpleFilter)):
raise TypeError('Expecting a bool or a SimpleFilter instance. Got %r' % bool_or_filter)
return {
True: _always,
False: _never,
}.get(bool_or_filter, bool_or_filter)
def to_cli_filter(bool_or_filter):
"""
Accept both booleans and CLIFilters as input and
turn it into a CLIFilter.
"""
if not isinstance(bool_or_filter, (bool, CLIFilter)):
raise TypeError('Expecting a bool or a CLIFilter instance. Got %r' % bool_or_filter)
return {
True: _always,
False: _never,
}.get(bool_or_filter, bool_or_filter)

View File

@@ -0,0 +1,120 @@
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
import datetime
import os
__all__ = (
'FileHistory',
'History',
'InMemoryHistory',
)
class History(with_metaclass(ABCMeta, object)):
"""
Base ``History`` interface.
"""
@abstractmethod
def append(self, string):
" Append string to history. "
@abstractmethod
def __getitem__(self, key):
" Return one item of the history. It should be accessible like a `list`. "
@abstractmethod
def __iter__(self):
" Iterate through all the items of the history. Cronologically. "
@abstractmethod
def __len__(self):
" Return the length of the history. "
def __bool__(self):
"""
Never evaluate to False, even when the history is empty.
(Python calls __len__ if __bool__ is not implemented.)
This is mainly to allow lazy evaluation::
x = history or InMemoryHistory()
"""
return True
__nonzero__ = __bool__ # For Python 2.
class InMemoryHistory(History):
"""
:class:`.History` class that keeps a list of all strings in memory.
"""
def __init__(self):
self.strings = []
def append(self, string):
self.strings.append(string)
def __getitem__(self, key):
return self.strings[key]
def __iter__(self):
return iter(self.strings)
def __len__(self):
return len(self.strings)
class FileHistory(History):
"""
:class:`.History` class that stores all strings in a file.
"""
def __init__(self, filename):
self.strings = []
self.filename = filename
self._load()
def _load(self):
lines = []
def add():
if lines:
# Join and drop trailing newline.
string = ''.join(lines)[:-1]
self.strings.append(string)
if os.path.exists(self.filename):
with open(self.filename, 'rb') as f:
for line in f:
line = line.decode('utf-8')
if line.startswith('+'):
lines.append(line[1:])
else:
add()
lines = []
add()
def append(self, string):
self.strings.append(string)
# Save to file.
with open(self.filename, 'ab') as f:
def write(t):
f.write(t.encode('utf-8'))
write('\n# %s\n' % datetime.datetime.now())
for line in string.split('\n'):
write('+%s\n' % line)
def __getitem__(self, key):
return self.strings[key]
def __iter__(self):
return iter(self.strings)
def __len__(self):
return len(self.strings)

View File

@@ -0,0 +1,135 @@
"""
Abstraction of CLI Input.
"""
from __future__ import unicode_literals
from .utils import DummyContext, is_windows
from abc import ABCMeta, abstractmethod
from six import with_metaclass
import io
import os
import sys
if is_windows():
from .terminal.win32_input import raw_mode, cooked_mode
else:
from .terminal.vt100_input import raw_mode, cooked_mode
__all__ = (
'Input',
'StdinInput',
'PipeInput',
)
class Input(with_metaclass(ABCMeta, object)):
"""
Abstraction for any input.
An instance of this class can be given to the constructor of a
:class:`~libs.prompt_toolkit.interface.CommandLineInterface` and will also be
passed to the :class:`~libs.prompt_toolkit.eventloop.base.EventLoop`.
"""
@abstractmethod
def fileno(self):
"""
Fileno for putting this in an event loop.
"""
@abstractmethod
def read(self):
"""
Return text from the input.
"""
@abstractmethod
def raw_mode(self):
"""
Context manager that turns the input into raw mode.
"""
@abstractmethod
def cooked_mode(self):
"""
Context manager that turns the input into cooked mode.
"""
class StdinInput(Input):
"""
Simple wrapper around stdin.
"""
def __init__(self, stdin=None):
self.stdin = stdin or sys.stdin
# The input object should be a TTY.
assert self.stdin.isatty()
# Test whether the given input object has a file descriptor.
# (Idle reports stdin to be a TTY, but fileno() is not implemented.)
try:
# This should not raise, but can return 0.
self.stdin.fileno()
except io.UnsupportedOperation:
if 'idlelib.run' in sys.modules:
raise io.UnsupportedOperation(
'Stdin is not a terminal. Running from Idle is not supported.')
else:
raise io.UnsupportedOperation('Stdin is not a terminal.')
def __repr__(self):
return 'StdinInput(stdin=%r)' % (self.stdin,)
def raw_mode(self):
return raw_mode(self.stdin.fileno())
def cooked_mode(self):
return cooked_mode(self.stdin.fileno())
def fileno(self):
return self.stdin.fileno()
def read(self):
return self.stdin.read()
class PipeInput(Input):
"""
Input that is send through a pipe.
This is useful if we want to send the input programatically into the
interface, but still use the eventloop.
Usage::
input = PipeInput()
input.send('inputdata')
"""
def __init__(self):
self._r, self._w = os.pipe()
def fileno(self):
return self._r
def read(self):
return os.read(self._r)
def send_text(self, data):
" Send text to the input. "
os.write(self._w, data.encode('utf-8'))
# Deprecated alias for `send_text`.
send = send_text
def raw_mode(self):
return DummyContext()
def cooked_mode(self):
return DummyContext()
def close(self):
" Close pipe fds. "
os.close(self._r)
os.close(self._w)
self._r = None
self._w = None

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
from __future__ import unicode_literals

View File

@@ -0,0 +1,407 @@
# pylint: disable=function-redefined
from __future__ import unicode_literals
from libs.prompt_toolkit.enums import DEFAULT_BUFFER
from libs.prompt_toolkit.filters import HasSelection, Condition, EmacsInsertMode, ViInsertMode
from libs.prompt_toolkit.keys import Keys
from libs.prompt_toolkit.layout.screen import Point
from libs.prompt_toolkit.mouse_events import MouseEventType, MouseEvent
from libs.prompt_toolkit.renderer import HeightIsUnknownError
from libs.prompt_toolkit.utils import suspend_to_background_supported, is_windows
from .named_commands import get_by_name
from ..registry import Registry
__all__ = (
'load_basic_bindings',
'load_abort_and_exit_bindings',
'load_basic_system_bindings',
'load_auto_suggestion_bindings',
)
def if_no_repeat(event):
""" Callable that returns True when the previous event was delivered to
another handler. """
return not event.is_repeat
def load_basic_bindings():
registry = Registry()
insert_mode = ViInsertMode() | EmacsInsertMode()
handle = registry.add_binding
has_selection = HasSelection()
@handle(Keys.ControlA)
@handle(Keys.ControlB)
@handle(Keys.ControlC)
@handle(Keys.ControlD)
@handle(Keys.ControlE)
@handle(Keys.ControlF)
@handle(Keys.ControlG)
@handle(Keys.ControlH)
@handle(Keys.ControlI)
@handle(Keys.ControlJ)
@handle(Keys.ControlK)
@handle(Keys.ControlL)
@handle(Keys.ControlM)
@handle(Keys.ControlN)
@handle(Keys.ControlO)
@handle(Keys.ControlP)
@handle(Keys.ControlQ)
@handle(Keys.ControlR)
@handle(Keys.ControlS)
@handle(Keys.ControlT)
@handle(Keys.ControlU)
@handle(Keys.ControlV)
@handle(Keys.ControlW)
@handle(Keys.ControlX)
@handle(Keys.ControlY)
@handle(Keys.ControlZ)
@handle(Keys.F1)
@handle(Keys.F2)
@handle(Keys.F3)
@handle(Keys.F4)
@handle(Keys.F5)
@handle(Keys.F6)
@handle(Keys.F7)
@handle(Keys.F8)
@handle(Keys.F9)
@handle(Keys.F10)
@handle(Keys.F11)
@handle(Keys.F12)
@handle(Keys.F13)
@handle(Keys.F14)
@handle(Keys.F15)
@handle(Keys.F16)
@handle(Keys.F17)
@handle(Keys.F18)
@handle(Keys.F19)
@handle(Keys.F20)
@handle(Keys.ControlSpace)
@handle(Keys.ControlBackslash)
@handle(Keys.ControlSquareClose)
@handle(Keys.ControlCircumflex)
@handle(Keys.ControlUnderscore)
@handle(Keys.Backspace)
@handle(Keys.Up)
@handle(Keys.Down)
@handle(Keys.Right)
@handle(Keys.Left)
@handle(Keys.ShiftUp)
@handle(Keys.ShiftDown)
@handle(Keys.ShiftRight)
@handle(Keys.ShiftLeft)
@handle(Keys.Home)
@handle(Keys.End)
@handle(Keys.Delete)
@handle(Keys.ShiftDelete)
@handle(Keys.ControlDelete)
@handle(Keys.PageUp)
@handle(Keys.PageDown)
@handle(Keys.BackTab)
@handle(Keys.Tab)
@handle(Keys.ControlLeft)
@handle(Keys.ControlRight)
@handle(Keys.ControlUp)
@handle(Keys.ControlDown)
@handle(Keys.Insert)
@handle(Keys.Ignore)
def _(event):
"""
First, for any of these keys, Don't do anything by default. Also don't
catch them in the 'Any' handler which will insert them as data.
If people want to insert these characters as a literal, they can always
do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi
mode.)
"""
pass
# Readline-style bindings.
handle(Keys.Home)(get_by_name('beginning-of-line'))
handle(Keys.End)(get_by_name('end-of-line'))
handle(Keys.Left)(get_by_name('backward-char'))
handle(Keys.Right)(get_by_name('forward-char'))
handle(Keys.ControlUp)(get_by_name('previous-history'))
handle(Keys.ControlDown)(get_by_name('next-history'))
handle(Keys.ControlL)(get_by_name('clear-screen'))
handle(Keys.ControlK, filter=insert_mode)(get_by_name('kill-line'))
handle(Keys.ControlU, filter=insert_mode)(get_by_name('unix-line-discard'))
handle(Keys.ControlH, filter=insert_mode, save_before=if_no_repeat)(
get_by_name('backward-delete-char'))
handle(Keys.Backspace, filter=insert_mode, save_before=if_no_repeat)(
get_by_name('backward-delete-char'))
handle(Keys.Delete, filter=insert_mode, save_before=if_no_repeat)(
get_by_name('delete-char'))
handle(Keys.ShiftDelete, filter=insert_mode, save_before=if_no_repeat)(
get_by_name('delete-char'))
handle(Keys.Any, filter=insert_mode, save_before=if_no_repeat)(
get_by_name('self-insert'))
handle(Keys.ControlT, filter=insert_mode)(get_by_name('transpose-chars'))
handle(Keys.ControlW, filter=insert_mode)(get_by_name('unix-word-rubout'))
handle(Keys.ControlI, filter=insert_mode)(get_by_name('menu-complete'))
handle(Keys.BackTab, filter=insert_mode)(get_by_name('menu-complete-backward'))
handle(Keys.PageUp, filter= ~has_selection)(get_by_name('previous-history'))
handle(Keys.PageDown, filter= ~has_selection)(get_by_name('next-history'))
# CTRL keys.
text_before_cursor = Condition(lambda cli: cli.current_buffer.text)
handle(Keys.ControlD, filter=text_before_cursor & insert_mode)(get_by_name('delete-char'))
is_multiline = Condition(lambda cli: cli.current_buffer.is_multiline())
is_returnable = Condition(lambda cli: cli.current_buffer.accept_action.is_returnable)
@handle(Keys.ControlJ, filter=is_multiline & insert_mode)
def _(event):
" Newline (in case of multiline input. "
event.current_buffer.newline(copy_margin=not event.cli.in_paste_mode)
@handle(Keys.ControlJ, filter=~is_multiline & is_returnable)
def _(event):
" Enter, accept input. "
buff = event.current_buffer
buff.accept_action.validate_and_handle(event.cli, buff)
# Delete the word before the cursor.
@handle(Keys.Up)
def _(event):
event.current_buffer.auto_up(count=event.arg)
@handle(Keys.Down)
def _(event):
event.current_buffer.auto_down(count=event.arg)
@handle(Keys.Delete, filter=has_selection)
def _(event):
data = event.current_buffer.cut_selection()
event.cli.clipboard.set_data(data)
# Global bindings.
@handle(Keys.ControlZ)
def _(event):
"""
By default, control-Z should literally insert Ctrl-Z.
(Ansi Ctrl-Z, code 26 in MSDOS means End-Of-File.
In a Python REPL for instance, it's possible to type
Control-Z followed by enter to quit.)
When the system bindings are loaded and suspend-to-background is
supported, that will override this binding.
"""
event.current_buffer.insert_text(event.data)
@handle(Keys.CPRResponse, save_before=lambda e: False)
def _(event):
"""
Handle incoming Cursor-Position-Request response.
"""
# The incoming data looks like u'\x1b[35;1R'
# Parse row/col information.
row, col = map(int, event.data[2:-1].split(';'))
# Report absolute cursor position to the renderer.
event.cli.renderer.report_absolute_cursor_row(row)
@handle(Keys.BracketedPaste)
def _(event):
" Pasting from clipboard. "
data = event.data
# Be sure to use \n as line ending.
# Some terminals (Like iTerm2) seem to paste \r\n line endings in a
# bracketed paste. See: https://github.com/ipython/ipython/issues/9737
data = data.replace('\r\n', '\n')
data = data.replace('\r', '\n')
event.current_buffer.insert_text(data)
@handle(Keys.Any, filter=Condition(lambda cli: cli.quoted_insert), eager=True)
def _(event):
"""
Handle quoted insert.
"""
event.current_buffer.insert_text(event.data, overwrite=False)
event.cli.quoted_insert = False
return registry
def load_mouse_bindings():
"""
Key bindings, required for mouse support.
(Mouse events enter through the key binding system.)
"""
registry = Registry()
@registry.add_binding(Keys.Vt100MouseEvent)
def _(event):
"""
Handling of incoming mouse event.
"""
# Typical: "Esc[MaB*"
# Urxvt: "Esc[96;14;13M"
# Xterm SGR: "Esc[<64;85;12M"
# Parse incoming packet.
if event.data[2] == 'M':
# Typical.
mouse_event, x, y = map(ord, event.data[3:])
mouse_event = {
32: MouseEventType.MOUSE_DOWN,
35: MouseEventType.MOUSE_UP,
96: MouseEventType.SCROLL_UP,
97: MouseEventType.SCROLL_DOWN,
}.get(mouse_event)
# Handle situations where `PosixStdinReader` used surrogateescapes.
if x >= 0xdc00: x-= 0xdc00
if y >= 0xdc00: y-= 0xdc00
x -= 32
y -= 32
else:
# Urxvt and Xterm SGR.
# When the '<' is not present, we are not using the Xterm SGR mode,
# but Urxvt instead.
data = event.data[2:]
if data[:1] == '<':
sgr = True
data = data[1:]
else:
sgr = False
# Extract coordinates.
mouse_event, x, y = map(int, data[:-1].split(';'))
m = data[-1]
# Parse event type.
if sgr:
mouse_event = {
(0, 'M'): MouseEventType.MOUSE_DOWN,
(0, 'm'): MouseEventType.MOUSE_UP,
(64, 'M'): MouseEventType.SCROLL_UP,
(65, 'M'): MouseEventType.SCROLL_DOWN,
}.get((mouse_event, m))
else:
mouse_event = {
32: MouseEventType.MOUSE_DOWN,
35: MouseEventType.MOUSE_UP,
96: MouseEventType.SCROLL_UP,
97: MouseEventType.SCROLL_DOWN,
}.get(mouse_event)
x -= 1
y -= 1
# Only handle mouse events when we know the window height.
if event.cli.renderer.height_is_known and mouse_event is not None:
# Take region above the layout into account. The reported
# coordinates are absolute to the visible part of the terminal.
try:
y -= event.cli.renderer.rows_above_layout
except HeightIsUnknownError:
return
# Call the mouse handler from the renderer.
handler = event.cli.renderer.mouse_handlers.mouse_handlers[x,y]
handler(event.cli, MouseEvent(position=Point(x=x, y=y),
event_type=mouse_event))
@registry.add_binding(Keys.WindowsMouseEvent)
def _(event):
"""
Handling of mouse events for Windows.
"""
assert is_windows() # This key binding should only exist for Windows.
# Parse data.
event_type, x, y = event.data.split(';')
x = int(x)
y = int(y)
# Make coordinates absolute to the visible part of the terminal.
screen_buffer_info = event.cli.renderer.output.get_win32_screen_buffer_info()
rows_above_cursor = screen_buffer_info.dwCursorPosition.Y - event.cli.renderer._cursor_pos.y
y -= rows_above_cursor
# Call the mouse event handler.
handler = event.cli.renderer.mouse_handlers.mouse_handlers[x,y]
handler(event.cli, MouseEvent(position=Point(x=x, y=y),
event_type=event_type))
return registry
def load_abort_and_exit_bindings():
"""
Basic bindings for abort (Ctrl-C) and exit (Ctrl-D).
"""
registry = Registry()
handle = registry.add_binding
@handle(Keys.ControlC)
def _(event):
" Abort when Control-C has been pressed. "
event.cli.abort()
@Condition
def ctrl_d_condition(cli):
""" Ctrl-D binding is only active when the default buffer is selected
and empty. """
return (cli.current_buffer_name == DEFAULT_BUFFER and
not cli.current_buffer.text)
handle(Keys.ControlD, filter=ctrl_d_condition)(get_by_name('end-of-file'))
return registry
def load_basic_system_bindings():
"""
Basic system bindings (For both Emacs and Vi mode.)
"""
registry = Registry()
suspend_supported = Condition(
lambda cli: suspend_to_background_supported())
@registry.add_binding(Keys.ControlZ, filter=suspend_supported)
def _(event):
"""
Suspend process to background.
"""
event.cli.suspend_to_background()
return registry
def load_auto_suggestion_bindings():
"""
Key bindings for accepting auto suggestion text.
"""
registry = Registry()
handle = registry.add_binding
suggestion_available = Condition(
lambda cli:
cli.current_buffer.suggestion is not None and
cli.current_buffer.document.is_cursor_at_the_end)
@handle(Keys.ControlF, filter=suggestion_available)
@handle(Keys.ControlE, filter=suggestion_available)
@handle(Keys.Right, filter=suggestion_available)
def _(event):
" Accept suggestion. "
b = event.current_buffer
suggestion = b.suggestion
if suggestion:
b.insert_text(suggestion.text)
return registry

View File

@@ -0,0 +1,161 @@
"""
Key binding handlers for displaying completions.
"""
from __future__ import unicode_literals
from libs.prompt_toolkit.completion import CompleteEvent, get_common_complete_suffix
from libs.prompt_toolkit.utils import get_cwidth
from libs.prompt_toolkit.keys import Keys
from libs.prompt_toolkit.key_binding.registry import Registry
import math
__all__ = (
'generate_completions',
'display_completions_like_readline',
)
def generate_completions(event):
r"""
Tab-completion: where the first tab completes the common suffix and the
second tab lists all the completions.
"""
b = event.current_buffer
# When already navigating through completions, select the next one.
if b.complete_state:
b.complete_next()
else:
event.cli.start_completion(insert_common_part=True, select_first=False)
def display_completions_like_readline(event):
"""
Key binding handler for readline-style tab completion.
This is meant to be as similar as possible to the way how readline displays
completions.
Generate the completions immediately (blocking) and display them above the
prompt in columns.
Usage::
# Call this handler when 'Tab' has been pressed.
registry.add_binding(Keys.ControlI)(display_completions_like_readline)
"""
# Request completions.
b = event.current_buffer
if b.completer is None:
return
complete_event = CompleteEvent(completion_requested=True)
completions = list(b.completer.get_completions(b.document, complete_event))
# Calculate the common suffix.
common_suffix = get_common_complete_suffix(b.document, completions)
# One completion: insert it.
if len(completions) == 1:
b.delete_before_cursor(-completions[0].start_position)
b.insert_text(completions[0].text)
# Multiple completions with common part.
elif common_suffix:
b.insert_text(common_suffix)
# Otherwise: display all completions.
elif completions:
_display_completions_like_readline(event.cli, completions)
def _display_completions_like_readline(cli, completions):
"""
Display the list of completions in columns above the prompt.
This will ask for a confirmation if there are too many completions to fit
on a single page and provide a paginator to walk through them.
"""
from libs.prompt_toolkit.shortcuts import create_confirm_application
assert isinstance(completions, list)
# Get terminal dimensions.
term_size = cli.output.get_size()
term_width = term_size.columns
term_height = term_size.rows
# Calculate amount of required columns/rows for displaying the
# completions. (Keep in mind that completions are displayed
# alphabetically column-wise.)
max_compl_width = min(term_width,
max(get_cwidth(c.text) for c in completions) + 1)
column_count = max(1, term_width // max_compl_width)
completions_per_page = column_count * (term_height - 1)
page_count = int(math.ceil(len(completions) / float(completions_per_page)))
# Note: math.ceil can return float on Python2.
def display(page):
# Display completions.
page_completions = completions[page * completions_per_page:
(page+1) * completions_per_page]
page_row_count = int(math.ceil(len(page_completions) / float(column_count)))
page_columns = [page_completions[i * page_row_count:(i+1) * page_row_count]
for i in range(column_count)]
result = []
for r in range(page_row_count):
for c in range(column_count):
try:
result.append(page_columns[c][r].text.ljust(max_compl_width))
except IndexError:
pass
result.append('\n')
cli.output.write(''.join(result))
cli.output.flush()
# User interaction through an application generator function.
def run():
if len(completions) > completions_per_page:
# Ask confirmation if it doesn't fit on the screen.
message = 'Display all {} possibilities? (y on n) '.format(len(completions))
confirm = yield create_confirm_application(message)
if confirm:
# Display pages.
for page in range(page_count):
display(page)
if page != page_count - 1:
# Display --MORE-- and go to the next page.
show_more = yield _create_more_application()
if not show_more:
return
else:
cli.output.write('\n'); cli.output.flush()
else:
# Display all completions.
display(0)
cli.run_application_generator(run, render_cli_done=True)
def _create_more_application():
"""
Create an `Application` instance that displays the "--MORE--".
"""
from libs.prompt_toolkit.shortcuts import create_prompt_application
registry = Registry()
@registry.add_binding(' ')
@registry.add_binding('y')
@registry.add_binding('Y')
@registry.add_binding(Keys.ControlJ)
@registry.add_binding(Keys.ControlI) # Tab.
def _(event):
event.cli.set_return_value(True)
@registry.add_binding('n')
@registry.add_binding('N')
@registry.add_binding('q')
@registry.add_binding('Q')
@registry.add_binding(Keys.ControlC)
def _(event):
event.cli.set_return_value(False)
return create_prompt_application(
'--MORE--', key_bindings_registry=registry, erase_when_done=True)

View File

@@ -0,0 +1,451 @@
# pylint: disable=function-redefined
from __future__ import unicode_literals
from libs.prompt_toolkit.buffer import SelectionType, indent, unindent
from libs.prompt_toolkit.keys import Keys
from libs.prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER, SYSTEM_BUFFER
from libs.prompt_toolkit.filters import Condition, EmacsMode, HasSelection, EmacsInsertMode, HasFocus, HasArg
from libs.prompt_toolkit.completion import CompleteEvent
from .scroll import scroll_page_up, scroll_page_down
from .named_commands import get_by_name
from ..registry import Registry, ConditionalRegistry
__all__ = (
'load_emacs_bindings',
'load_emacs_search_bindings',
'load_emacs_system_bindings',
'load_extra_emacs_page_navigation_bindings',
)
def load_emacs_bindings():
"""
Some e-macs extensions.
"""
# Overview of Readline emacs commands:
# http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf
registry = ConditionalRegistry(Registry(), EmacsMode())
handle = registry.add_binding
insert_mode = EmacsInsertMode()
has_selection = HasSelection()
@handle(Keys.Escape)
def _(event):
"""
By default, ignore escape key.
(If we don't put this here, and Esc is followed by a key which sequence
is not handled, we'll insert an Escape character in the input stream.
Something we don't want and happens to easily in emacs mode.
Further, people can always use ControlQ to do a quoted insert.)
"""
pass
handle(Keys.ControlA)(get_by_name('beginning-of-line'))
handle(Keys.ControlB)(get_by_name('backward-char'))
handle(Keys.ControlDelete, filter=insert_mode)(get_by_name('kill-word'))
handle(Keys.ControlE)(get_by_name('end-of-line'))
handle(Keys.ControlF)(get_by_name('forward-char'))
handle(Keys.ControlLeft)(get_by_name('backward-word'))
handle(Keys.ControlRight)(get_by_name('forward-word'))
handle(Keys.ControlX, 'r', 'y', filter=insert_mode)(get_by_name('yank'))
handle(Keys.ControlY, filter=insert_mode)(get_by_name('yank'))
handle(Keys.Escape, 'b')(get_by_name('backward-word'))
handle(Keys.Escape, 'c', filter=insert_mode)(get_by_name('capitalize-word'))
handle(Keys.Escape, 'd', filter=insert_mode)(get_by_name('kill-word'))
handle(Keys.Escape, 'f')(get_by_name('forward-word'))
handle(Keys.Escape, 'l', filter=insert_mode)(get_by_name('downcase-word'))
handle(Keys.Escape, 'u', filter=insert_mode)(get_by_name('uppercase-word'))
handle(Keys.Escape, 'y', filter=insert_mode)(get_by_name('yank-pop'))
handle(Keys.Escape, Keys.ControlH, filter=insert_mode)(get_by_name('backward-kill-word'))
handle(Keys.Escape, Keys.Backspace, filter=insert_mode)(get_by_name('backward-kill-word'))
handle(Keys.Escape, '\\', filter=insert_mode)(get_by_name('delete-horizontal-space'))
handle(Keys.ControlUnderscore, save_before=(lambda e: False), filter=insert_mode)(
get_by_name('undo'))
handle(Keys.ControlX, Keys.ControlU, save_before=(lambda e: False), filter=insert_mode)(
get_by_name('undo'))
handle(Keys.Escape, '<', filter= ~has_selection)(get_by_name('beginning-of-history'))
handle(Keys.Escape, '>', filter= ~has_selection)(get_by_name('end-of-history'))
handle(Keys.Escape, '.', filter=insert_mode)(get_by_name('yank-last-arg'))
handle(Keys.Escape, '_', filter=insert_mode)(get_by_name('yank-last-arg'))
handle(Keys.Escape, Keys.ControlY, filter=insert_mode)(get_by_name('yank-nth-arg'))
handle(Keys.Escape, '#', filter=insert_mode)(get_by_name('insert-comment'))
handle(Keys.ControlO)(get_by_name('operate-and-get-next'))
# ControlQ does a quoted insert. Not that for vt100 terminals, you have to
# disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and
# Ctrl-S are captured by the terminal.
handle(Keys.ControlQ, filter= ~has_selection)(get_by_name('quoted-insert'))
handle(Keys.ControlX, '(')(get_by_name('start-kbd-macro'))
handle(Keys.ControlX, ')')(get_by_name('end-kbd-macro'))
handle(Keys.ControlX, 'e')(get_by_name('call-last-kbd-macro'))
@handle(Keys.ControlN)
def _(event):
" Next line. "
event.current_buffer.auto_down()
@handle(Keys.ControlP)
def _(event):
" Previous line. "
event.current_buffer.auto_up(count=event.arg)
def handle_digit(c):
"""
Handle input of arguments.
The first number needs to be preceeded by escape.
"""
@handle(c, filter=HasArg())
@handle(Keys.Escape, c)
def _(event):
event.append_to_arg_count(c)
for c in '0123456789':
handle_digit(c)
@handle(Keys.Escape, '-', filter=~HasArg())
def _(event):
"""
"""
if event._arg is None:
event.append_to_arg_count('-')
@handle('-', filter=Condition(lambda cli: cli.input_processor.arg == '-'))
def _(event):
"""
When '-' is typed again, after exactly '-' has been given as an
argument, ignore this.
"""
event.cli.input_processor.arg = '-'
is_returnable = Condition(
lambda cli: cli.current_buffer.accept_action.is_returnable)
# Meta + Newline: always accept input.
handle(Keys.Escape, Keys.ControlJ, filter=insert_mode & is_returnable)(
get_by_name('accept-line'))
def character_search(buff, char, count):
if count < 0:
match = buff.document.find_backwards(char, in_current_line=True, count=-count)
else:
match = buff.document.find(char, in_current_line=True, count=count)
if match is not None:
buff.cursor_position += match
@handle(Keys.ControlSquareClose, Keys.Any)
def _(event):
" When Ctl-] + a character is pressed. go to that character. "
# Also named 'character-search'
character_search(event.current_buffer, event.data, event.arg)
@handle(Keys.Escape, Keys.ControlSquareClose, Keys.Any)
def _(event):
" Like Ctl-], but backwards. "
# Also named 'character-search-backward'
character_search(event.current_buffer, event.data, -event.arg)
@handle(Keys.Escape, 'a')
def _(event):
" Previous sentence. "
# TODO:
@handle(Keys.Escape, 'e')
def _(event):
" Move to end of sentence. "
# TODO:
@handle(Keys.Escape, 't', filter=insert_mode)
def _(event):
"""
Swap the last two words before the cursor.
"""
# TODO
@handle(Keys.Escape, '*', filter=insert_mode)
def _(event):
"""
`meta-*`: Insert all possible completions of the preceding text.
"""
buff = event.current_buffer
# List all completions.
complete_event = CompleteEvent(text_inserted=False, completion_requested=True)
completions = list(buff.completer.get_completions(buff.document, complete_event))
# Insert them.
text_to_insert = ' '.join(c.text for c in completions)
buff.insert_text(text_to_insert)
@handle(Keys.ControlX, Keys.ControlX)
def _(event):
"""
Move cursor back and forth between the start and end of the current
line.
"""
buffer = event.current_buffer
if buffer.document.is_cursor_at_the_end_of_line:
buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=False)
else:
buffer.cursor_position += buffer.document.get_end_of_line_position()
@handle(Keys.ControlSpace)
def _(event):
"""
Start of the selection (if the current buffer is not empty).
"""
# Take the current cursor position as the start of this selection.
buff = event.current_buffer
if buff.text:
buff.start_selection(selection_type=SelectionType.CHARACTERS)
@handle(Keys.ControlG, filter= ~has_selection)
def _(event):
"""
Control + G: Cancel completion menu and validation state.
"""
event.current_buffer.complete_state = None
event.current_buffer.validation_error = None
@handle(Keys.ControlG, filter=has_selection)
def _(event):
"""
Cancel selection.
"""
event.current_buffer.exit_selection()
@handle(Keys.ControlW, filter=has_selection)
@handle(Keys.ControlX, 'r', 'k', filter=has_selection)
def _(event):
"""
Cut selected text.
"""
data = event.current_buffer.cut_selection()
event.cli.clipboard.set_data(data)
@handle(Keys.Escape, 'w', filter=has_selection)
def _(event):
"""
Copy selected text.
"""
data = event.current_buffer.copy_selection()
event.cli.clipboard.set_data(data)
@handle(Keys.Escape, Keys.Left)
def _(event):
"""
Cursor to start of previous word.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.find_previous_word_beginning(count=event.arg) or 0
@handle(Keys.Escape, Keys.Right)
def _(event):
"""
Cursor to start of next word.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.find_next_word_beginning(count=event.arg) or \
buffer.document.get_end_of_document_position()
@handle(Keys.Escape, '/', filter=insert_mode)
def _(event):
"""
M-/: Complete.
"""
b = event.current_buffer
if b.complete_state:
b.complete_next()
else:
event.cli.start_completion(select_first=True)
@handle(Keys.ControlC, '>', filter=has_selection)
def _(event):
"""
Indent selected text.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True)
from_, to = buffer.document.selection_range()
from_, _ = buffer.document.translate_index_to_position(from_)
to, _ = buffer.document.translate_index_to_position(to)
indent(buffer, from_, to + 1, count=event.arg)
@handle(Keys.ControlC, '<', filter=has_selection)
def _(event):
"""
Unindent selected text.
"""
buffer = event.current_buffer
from_, to = buffer.document.selection_range()
from_, _ = buffer.document.translate_index_to_position(from_)
to, _ = buffer.document.translate_index_to_position(to)
unindent(buffer, from_, to + 1, count=event.arg)
return registry
def load_emacs_open_in_editor_bindings():
"""
Pressing C-X C-E will open the buffer in an external editor.
"""
registry = Registry()
registry.add_binding(Keys.ControlX, Keys.ControlE,
filter=EmacsMode() & ~HasSelection())(
get_by_name('edit-and-execute-command'))
return registry
def load_emacs_system_bindings():
registry = ConditionalRegistry(Registry(), EmacsMode())
handle = registry.add_binding
has_focus = HasFocus(SYSTEM_BUFFER)
@handle(Keys.Escape, '!', filter= ~has_focus)
def _(event):
"""
M-'!' opens the system prompt.
"""
event.cli.push_focus(SYSTEM_BUFFER)
@handle(Keys.Escape, filter=has_focus)
@handle(Keys.ControlG, filter=has_focus)
@handle(Keys.ControlC, filter=has_focus)
def _(event):
"""
Cancel system prompt.
"""
event.cli.buffers[SYSTEM_BUFFER].reset()
event.cli.pop_focus()
@handle(Keys.ControlJ, filter=has_focus)
def _(event):
"""
Run system command.
"""
system_line = event.cli.buffers[SYSTEM_BUFFER]
event.cli.run_system_command(system_line.text)
system_line.reset(append_to_history=True)
# Focus previous buffer again.
event.cli.pop_focus()
return registry
def load_emacs_search_bindings(get_search_state=None):
registry = ConditionalRegistry(Registry(), EmacsMode())
handle = registry.add_binding
has_focus = HasFocus(SEARCH_BUFFER)
assert get_search_state is None or callable(get_search_state)
if not get_search_state:
def get_search_state(cli): return cli.search_state
@handle(Keys.ControlG, filter=has_focus)
@handle(Keys.ControlC, filter=has_focus)
# NOTE: the reason for not also binding Escape to this one, is that we want
# Alt+Enter to accept input directly in incremental search mode.
def _(event):
"""
Abort an incremental search and restore the original line.
"""
search_buffer = event.cli.buffers[SEARCH_BUFFER]
search_buffer.reset()
event.cli.pop_focus()
@handle(Keys.ControlJ, filter=has_focus)
def _(event):
"""
When enter pressed in isearch, quit isearch mode. (Multiline
isearch would be too complicated.)
"""
input_buffer = event.cli.buffers.previous(event.cli)
search_buffer = event.cli.buffers[SEARCH_BUFFER]
# Update search state.
if search_buffer.text:
get_search_state(event.cli).text = search_buffer.text
# Apply search.
input_buffer.apply_search(get_search_state(event.cli), include_current_position=True)
# Add query to history of search line.
search_buffer.append_to_history()
search_buffer.reset()
# Focus previous document again.
event.cli.pop_focus()
@handle(Keys.ControlR, filter= ~has_focus)
def _(event):
get_search_state(event.cli).direction = IncrementalSearchDirection.BACKWARD
event.cli.push_focus(SEARCH_BUFFER)
@handle(Keys.ControlS, filter= ~has_focus)
def _(event):
get_search_state(event.cli).direction = IncrementalSearchDirection.FORWARD
event.cli.push_focus(SEARCH_BUFFER)
def incremental_search(cli, direction, count=1):
" Apply search, but keep search buffer focussed. "
# Update search_state.
search_state = get_search_state(cli)
direction_changed = search_state.direction != direction
search_state.text = cli.buffers[SEARCH_BUFFER].text
search_state.direction = direction
# Apply search to current buffer.
if not direction_changed:
input_buffer = cli.buffers.previous(cli)
input_buffer.apply_search(search_state,
include_current_position=False, count=count)
@handle(Keys.ControlR, filter=has_focus)
@handle(Keys.Up, filter=has_focus)
def _(event):
incremental_search(event.cli, IncrementalSearchDirection.BACKWARD, count=event.arg)
@handle(Keys.ControlS, filter=has_focus)
@handle(Keys.Down, filter=has_focus)
def _(event):
incremental_search(event.cli, IncrementalSearchDirection.FORWARD, count=event.arg)
return registry
def load_extra_emacs_page_navigation_bindings():
"""
Key bindings, for scrolling up and down through pages.
This are separate bindings, because GNU readline doesn't have them.
"""
registry = ConditionalRegistry(Registry(), EmacsMode())
handle = registry.add_binding
handle(Keys.ControlV)(scroll_page_down)
handle(Keys.PageDown)(scroll_page_down)
handle(Keys.Escape, 'v')(scroll_page_up)
handle(Keys.PageUp)(scroll_page_up)
return registry

View File

@@ -0,0 +1,578 @@
"""
Key bindings which are also known by GNU readline by the given names.
See: http://www.delorie.com/gnu/docs/readline/rlman_13.html
"""
from __future__ import unicode_literals
from libs.prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER
from libs.prompt_toolkit.selection import PasteMode
from six.moves import range
import six
from .completion import generate_completions, display_completions_like_readline
from libs.prompt_toolkit.document import Document
from libs.prompt_toolkit.enums import EditingMode
from libs.prompt_toolkit.key_binding.input_processor import KeyPress
from libs.prompt_toolkit.keys import Keys
__all__ = (
'get_by_name',
)
# Registry that maps the Readline command names to their handlers.
_readline_commands = {}
def register(name):
"""
Store handler in the `_readline_commands` dictionary.
"""
assert isinstance(name, six.text_type)
def decorator(handler):
assert callable(handler)
_readline_commands[name] = handler
return handler
return decorator
def get_by_name(name):
"""
Return the handler for the (Readline) command with the given name.
"""
try:
return _readline_commands[name]
except KeyError:
raise KeyError('Unknown readline command: %r' % name)
#
# Commands for moving
# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html
#
@register('beginning-of-line')
def beginning_of_line(event):
" Move to the start of the current line. "
buff = event.current_buffer
buff.cursor_position += buff.document.get_start_of_line_position(after_whitespace=False)
@register('end-of-line')
def end_of_line(event):
" Move to the end of the line. "
buff = event.current_buffer
buff.cursor_position += buff.document.get_end_of_line_position()
@register('forward-char')
def forward_char(event):
" Move forward a character. "
buff = event.current_buffer
buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg)
@register('backward-char')
def backward_char(event):
" Move back a character. "
buff = event.current_buffer
buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg)
@register('forward-word')
def forward_word(event):
"""
Move forward to the end of the next word. Words are composed of letters and
digits.
"""
buff = event.current_buffer
pos = buff.document.find_next_word_ending(count=event.arg)
if pos:
buff.cursor_position += pos
@register('backward-word')
def backward_word(event):
"""
Move back to the start of the current or previous word. Words are composed
of letters and digits.
"""
buff = event.current_buffer
pos = buff.document.find_previous_word_beginning(count=event.arg)
if pos:
buff.cursor_position += pos
@register('clear-screen')
def clear_screen(event):
"""
Clear the screen and redraw everything at the top of the screen.
"""
event.cli.renderer.clear()
@register('redraw-current-line')
def redraw_current_line(event):
"""
Refresh the current line.
(Readline defines this command, but prompt-toolkit doesn't have it.)
"""
pass
#
# Commands for manipulating the history.
# See: http://www.delorie.com/gnu/docs/readline/rlman_15.html
#
@register('accept-line')
def accept_line(event):
" Accept the line regardless of where the cursor is. "
b = event.current_buffer
b.accept_action.validate_and_handle(event.cli, b)
@register('previous-history')
def previous_history(event):
" Move `back` through the history list, fetching the previous command. "
event.current_buffer.history_backward(count=event.arg)
@register('next-history')
def next_history(event):
" Move `forward` through the history list, fetching the next command. "
event.current_buffer.history_forward(count=event.arg)
@register('beginning-of-history')
def beginning_of_history(event):
" Move to the first line in the history. "
event.current_buffer.go_to_history(0)
@register('end-of-history')
def end_of_history(event):
"""
Move to the end of the input history, i.e., the line currently being entered.
"""
event.current_buffer.history_forward(count=10**100)
buff = event.current_buffer
buff.go_to_history(len(buff._working_lines) - 1)
@register('reverse-search-history')
def reverse_search_history(event):
"""
Search backward starting at the current line and moving `up` through
the history as necessary. This is an incremental search.
"""
event.cli.current_search_state.direction = IncrementalSearchDirection.BACKWARD
event.cli.push_focus(SEARCH_BUFFER)
#
# Commands for changing text
#
@register('end-of-file')
def end_of_file(event):
"""
Exit.
"""
event.cli.exit()
@register('delete-char')
def delete_char(event):
" Delete character before the cursor. "
deleted = event.current_buffer.delete(count=event.arg)
if not deleted:
event.cli.output.bell()
@register('backward-delete-char')
def backward_delete_char(event):
" Delete the character behind the cursor. "
if event.arg < 0:
# When a negative argument has been given, this should delete in front
# of the cursor.
deleted = event.current_buffer.delete(count=-event.arg)
else:
deleted = event.current_buffer.delete_before_cursor(count=event.arg)
if not deleted:
event.cli.output.bell()
@register('self-insert')
def self_insert(event):
" Insert yourself. "
event.current_buffer.insert_text(event.data * event.arg)
@register('transpose-chars')
def transpose_chars(event):
"""
Emulate Emacs transpose-char behavior: at the beginning of the buffer,
do nothing. At the end of a line or buffer, swap the characters before
the cursor. Otherwise, move the cursor right, and then swap the
characters before the cursor.
"""
b = event.current_buffer
p = b.cursor_position
if p == 0:
return
elif p == len(b.text) or b.text[p] == '\n':
b.swap_characters_before_cursor()
else:
b.cursor_position += b.document.get_cursor_right_position()
b.swap_characters_before_cursor()
@register('uppercase-word')
def uppercase_word(event):
"""
Uppercase the current (or following) word.
"""
buff = event.current_buffer
for i in range(event.arg):
pos = buff.document.find_next_word_ending()
words = buff.document.text_after_cursor[:pos]
buff.insert_text(words.upper(), overwrite=True)
@register('downcase-word')
def downcase_word(event):
"""
Lowercase the current (or following) word.
"""
buff = event.current_buffer
for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!!
pos = buff.document.find_next_word_ending()
words = buff.document.text_after_cursor[:pos]
buff.insert_text(words.lower(), overwrite=True)
@register('capitalize-word')
def capitalize_word(event):
"""
Capitalize the current (or following) word.
"""
buff = event.current_buffer
for i in range(event.arg):
pos = buff.document.find_next_word_ending()
words = buff.document.text_after_cursor[:pos]
buff.insert_text(words.title(), overwrite=True)
@register('quoted-insert')
def quoted_insert(event):
"""
Add the next character typed to the line verbatim. This is how to insert
key sequences like C-q, for example.
"""
event.cli.quoted_insert = True
#
# Killing and yanking.
#
@register('kill-line')
def kill_line(event):
"""
Kill the text from the cursor to the end of the line.
If we are at the end of the line, this should remove the newline.
(That way, it is possible to delete multiple lines by executing this
command multiple times.)
"""
buff = event.current_buffer
if event.arg < 0:
deleted = buff.delete_before_cursor(count=-buff.document.get_start_of_line_position())
else:
if buff.document.current_char == '\n':
deleted = buff.delete(1)
else:
deleted = buff.delete(count=buff.document.get_end_of_line_position())
event.cli.clipboard.set_text(deleted)
@register('kill-word')
def kill_word(event):
"""
Kill from point to the end of the current word, or if between words, to the
end of the next word. Word boundaries are the same as forward-word.
"""
buff = event.current_buffer
pos = buff.document.find_next_word_ending(count=event.arg)
if pos:
deleted = buff.delete(count=pos)
event.cli.clipboard.set_text(deleted)
@register('unix-word-rubout')
def unix_word_rubout(event, WORD=True):
"""
Kill the word behind point, using whitespace as a word boundary.
Usually bound to ControlW.
"""
buff = event.current_buffer
pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD)
if pos is None:
# Nothing found? delete until the start of the document. (The
# input starts with whitespace and no words were found before the
# cursor.)
pos = - buff.cursor_position
if pos:
deleted = buff.delete_before_cursor(count=-pos)
# If the previous key press was also Control-W, concatenate deleted
# text.
if event.is_repeat:
deleted += event.cli.clipboard.get_data().text
event.cli.clipboard.set_text(deleted)
else:
# Nothing to delete. Bell.
event.cli.output.bell()
@register('backward-kill-word')
def backward_kill_word(event):
"""
Kills the word before point, using "not a letter nor a digit" as a word boundary.
Usually bound to M-Del or M-Backspace.
"""
unix_word_rubout(event, WORD=False)
@register('delete-horizontal-space')
def delete_horizontal_space(event):
" Delete all spaces and tabs around point. "
buff = event.current_buffer
text_before_cursor = buff.document.text_before_cursor
text_after_cursor = buff.document.text_after_cursor
delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip('\t '))
delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip('\t '))
buff.delete_before_cursor(count=delete_before)
buff.delete(count=delete_after)
@register('unix-line-discard')
def unix_line_discard(event):
"""
Kill backward from the cursor to the beginning of the current line.
"""
buff = event.current_buffer
if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0:
buff.delete_before_cursor(count=1)
else:
deleted = buff.delete_before_cursor(count=-buff.document.get_start_of_line_position())
event.cli.clipboard.set_text(deleted)
@register('yank')
def yank(event):
"""
Paste before cursor.
"""
event.current_buffer.paste_clipboard_data(
event.cli.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS)
@register('yank-nth-arg')
def yank_nth_arg(event):
"""
Insert the first argument of the previous command. With an argument, insert
the nth word from the previous command (start counting at 0).
"""
n = (event.arg if event.arg_present else None)
event.current_buffer.yank_nth_arg(n)
@register('yank-last-arg')
def yank_last_arg(event):
"""
Like `yank_nth_arg`, but if no argument has been given, yank the last word
of each line.
"""
n = (event.arg if event.arg_present else None)
event.current_buffer.yank_last_arg(n)
@register('yank-pop')
def yank_pop(event):
"""
Rotate the kill ring, and yank the new top. Only works following yank or
yank-pop.
"""
buff = event.current_buffer
doc_before_paste = buff.document_before_paste
clipboard = event.cli.clipboard
if doc_before_paste is not None:
buff.document = doc_before_paste
clipboard.rotate()
buff.paste_clipboard_data(
clipboard.get_data(), paste_mode=PasteMode.EMACS)
#
# Completion.
#
@register('complete')
def complete(event):
" Attempt to perform completion. "
display_completions_like_readline(event)
@register('menu-complete')
def menu_complete(event):
"""
Generate completions, or go to the next completion. (This is the default
way of completing input in libs.prompt_toolkit.)
"""
generate_completions(event)
@register('menu-complete-backward')
def menu_complete_backward(event):
" Move backward through the list of possible completions. "
event.current_buffer.complete_previous()
#
# Keyboard macros.
#
@register('start-kbd-macro')
def start_kbd_macro(event):
"""
Begin saving the characters typed into the current keyboard macro.
"""
event.cli.input_processor.start_macro()
@register('end-kbd-macro')
def start_kbd_macro(event):
"""
Stop saving the characters typed into the current keyboard macro and save
the definition.
"""
event.cli.input_processor.end_macro()
@register('call-last-kbd-macro')
def start_kbd_macro(event):
"""
Re-execute the last keyboard macro defined, by making the characters in the
macro appear as if typed at the keyboard.
"""
event.cli.input_processor.call_macro()
@register('print-last-kbd-macro')
def print_last_kbd_macro(event):
" Print the last keboard macro. "
# TODO: Make the format suitable for the inputrc file.
def print_macro():
for k in event.cli.input_processor.macro:
print(k)
event.cli.run_in_terminal(print_macro)
#
# Miscellaneous Commands.
#
@register('undo')
def undo(event):
" Incremental undo. "
event.current_buffer.undo()
@register('insert-comment')
def insert_comment(event):
"""
Without numeric argument, comment all lines.
With numeric argument, uncomment all lines.
In any case accept the input.
"""
buff = event.current_buffer
# Transform all lines.
if event.arg != 1:
def change(line):
return line[1:] if line.startswith('#') else line
else:
def change(line):
return '#' + line
buff.document = Document(
text='\n'.join(map(change, buff.text.splitlines())),
cursor_position=0)
# Accept input.
buff.accept_action.validate_and_handle(event.cli, buff)
@register('vi-editing-mode')
def vi_editing_mode(event):
" Switch to Vi editing mode. "
event.cli.editing_mode = EditingMode.VI
@register('emacs-editing-mode')
def emacs_editing_mode(event):
" Switch to Emacs editing mode. "
event.cli.editing_mode = EditingMode.EMACS
@register('prefix-meta')
def prefix_meta(event):
"""
Metafy the next character typed. This is for keyboards without a meta key.
Sometimes people also want to bind other keys to Meta, e.g. 'jj'::
registry.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta)
"""
event.cli.input_processor.feed(KeyPress(Keys.Escape))
@register('operate-and-get-next')
def operate_and_get_next(event):
"""
Accept the current line for execution and fetch the next line relative to
the current line from the history for editing.
"""
buff = event.current_buffer
new_index = buff.working_index + 1
# Accept the current input. (This will also redraw the interface in the
# 'done' state.)
buff.accept_action.validate_and_handle(event.cli, buff)
# Set the new index at the start of the next run.
def set_working_index():
if new_index < len(buff._working_lines):
buff.working_index = new_index
event.cli.pre_run_callables.append(set_working_index)
@register('edit-and-execute-command')
def edit_and_execute(event):
"""
Invoke an editor on the current command line, and accept the result.
"""
buff = event.current_buffer
buff.open_in_editor(event.cli)
buff.accept_action.validate_and_handle(event.cli, buff)

View File

@@ -0,0 +1,185 @@
"""
Key bindings, for scrolling up and down through pages.
This are separate bindings, because GNU readline doesn't have them, but
they are very useful for navigating through long multiline buffers, like in
Vi, Emacs, etc...
"""
from __future__ import unicode_literals
from libs.prompt_toolkit.layout.utils import find_window_for_buffer_name
from six.moves import range
__all__ = (
'scroll_forward',
'scroll_backward',
'scroll_half_page_up',
'scroll_half_page_down',
'scroll_one_line_up',
'scroll_one_line_down',
)
def _current_window_for_event(event):
"""
Return the `Window` for the currently focussed Buffer.
"""
return find_window_for_buffer_name(event.cli, event.cli.current_buffer_name)
def scroll_forward(event, half=False):
"""
Scroll window down.
"""
w = _current_window_for_event(event)
b = event.cli.current_buffer
if w and w.render_info:
info = w.render_info
ui_content = info.ui_content
# Height to scroll.
scroll_height = info.window_height
if half:
scroll_height //= 2
# Calculate how many lines is equivalent to that vertical space.
y = b.document.cursor_position_row + 1
height = 0
while y < ui_content.line_count:
line_height = info.get_height_for_line(y)
if height + line_height < scroll_height:
height += line_height
y += 1
else:
break
b.cursor_position = b.document.translate_row_col_to_index(y, 0)
def scroll_backward(event, half=False):
"""
Scroll window up.
"""
w = _current_window_for_event(event)
b = event.cli.current_buffer
if w and w.render_info:
info = w.render_info
# Height to scroll.
scroll_height = info.window_height
if half:
scroll_height //= 2
# Calculate how many lines is equivalent to that vertical space.
y = max(0, b.document.cursor_position_row - 1)
height = 0
while y > 0:
line_height = info.get_height_for_line(y)
if height + line_height < scroll_height:
height += line_height
y -= 1
else:
break
b.cursor_position = b.document.translate_row_col_to_index(y, 0)
def scroll_half_page_down(event):
"""
Same as ControlF, but only scroll half a page.
"""
scroll_forward(event, half=True)
def scroll_half_page_up(event):
"""
Same as ControlB, but only scroll half a page.
"""
scroll_backward(event, half=True)
def scroll_one_line_down(event):
"""
scroll_offset += 1
"""
w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name)
b = event.cli.current_buffer
if w:
# When the cursor is at the top, move to the next line. (Otherwise, only scroll.)
if w.render_info:
info = w.render_info
if w.vertical_scroll < info.content_height - info.window_height:
if info.cursor_position.y <= info.configured_scroll_offsets.top:
b.cursor_position += b.document.get_cursor_down_position()
w.vertical_scroll += 1
def scroll_one_line_up(event):
"""
scroll_offset -= 1
"""
w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name)
b = event.cli.current_buffer
if w:
# When the cursor is at the bottom, move to the previous line. (Otherwise, only scroll.)
if w.render_info:
info = w.render_info
if w.vertical_scroll > 0:
first_line_height = info.get_height_for_line(info.first_visible_line())
cursor_up = info.cursor_position.y - (info.window_height - 1 - first_line_height -
info.configured_scroll_offsets.bottom)
# Move cursor up, as many steps as the height of the first line.
# TODO: not entirely correct yet, in case of line wrapping and many long lines.
for _ in range(max(0, cursor_up)):
b.cursor_position += b.document.get_cursor_up_position()
# Scroll window
w.vertical_scroll -= 1
def scroll_page_down(event):
"""
Scroll page down. (Prefer the cursor at the top of the page, after scrolling.)
"""
w = _current_window_for_event(event)
b = event.cli.current_buffer
if w and w.render_info:
# Scroll down one page.
line_index = max(w.render_info.last_visible_line(), w.vertical_scroll + 1)
w.vertical_scroll = line_index
b.cursor_position = b.document.translate_row_col_to_index(line_index, 0)
b.cursor_position += b.document.get_start_of_line_position(after_whitespace=True)
def scroll_page_up(event):
"""
Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.)
"""
w = _current_window_for_event(event)
b = event.cli.current_buffer
if w and w.render_info:
# Put cursor at the first visible line. (But make sure that the cursor
# moves at least one line up.)
line_index = max(0, min(w.render_info.first_visible_line(),
b.document.cursor_position_row - 1))
b.cursor_position = b.document.translate_row_col_to_index(line_index, 0)
b.cursor_position += b.document.get_start_of_line_position(after_whitespace=True)
# Set the scroll offset. We can safely set it to zero; the Window will
# make sure that it scrolls at least until the cursor becomes visible.
w.vertical_scroll = 0

View File

@@ -0,0 +1,25 @@
from __future__ import unicode_literals
from libs.prompt_toolkit.filters import CLIFilter, Always
__all__ = (
'create_handle_decorator',
)
def create_handle_decorator(registry, filter=Always()):
"""
Create a key handle decorator, which is compatible with `Registry.handle`,
but will chain the given filter to every key binding.
:param filter: `CLIFilter`
"""
assert isinstance(filter, CLIFilter)
def handle(*keys, **kw):
# Chain the given filter to the filter of this specific binding.
if 'filter' in kw:
kw['filter'] = kw['filter'] & filter
else:
kw['filter'] = filter
return registry.add_binding(*keys, **kw)
return handle

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
"""
Default key bindings.::
registry = load_key_bindings()
app = Application(key_bindings_registry=registry)
"""
from __future__ import unicode_literals
from libs.prompt_toolkit.key_binding.registry import ConditionalRegistry, MergedRegistry
from libs.prompt_toolkit.key_binding.bindings.basic import load_basic_bindings, load_abort_and_exit_bindings, load_basic_system_bindings, load_auto_suggestion_bindings, load_mouse_bindings
from libs.prompt_toolkit.key_binding.bindings.emacs import load_emacs_bindings, load_emacs_system_bindings, load_emacs_search_bindings, load_emacs_open_in_editor_bindings, load_extra_emacs_page_navigation_bindings
from libs.prompt_toolkit.key_binding.bindings.vi import load_vi_bindings, load_vi_system_bindings, load_vi_search_bindings, load_vi_open_in_editor_bindings, load_extra_vi_page_navigation_bindings
from libs.prompt_toolkit.filters import to_cli_filter
__all__ = (
'load_key_bindings',
'load_key_bindings_for_prompt',
)
def load_key_bindings(
get_search_state=None,
enable_abort_and_exit_bindings=False,
enable_system_bindings=False,
enable_search=False,
enable_open_in_editor=False,
enable_extra_page_navigation=False,
enable_auto_suggest_bindings=False):
"""
Create a Registry object that contains the default key bindings.
:param enable_abort_and_exit_bindings: Filter to enable Ctrl-C and Ctrl-D.
:param enable_system_bindings: Filter to enable the system bindings (meta-!
prompt and Control-Z suspension.)
:param enable_search: Filter to enable the search bindings.
:param enable_open_in_editor: Filter to enable open-in-editor.
:param enable_open_in_editor: Filter to enable open-in-editor.
:param enable_extra_page_navigation: Filter for enabling extra page
navigation. (Bindings for up/down scrolling through long pages, like in
Emacs or Vi.)
:param enable_auto_suggest_bindings: Filter to enable fish-style suggestions.
"""
assert get_search_state is None or callable(get_search_state)
# Accept both Filters and booleans as input.
enable_abort_and_exit_bindings = to_cli_filter(enable_abort_and_exit_bindings)
enable_system_bindings = to_cli_filter(enable_system_bindings)
enable_search = to_cli_filter(enable_search)
enable_open_in_editor = to_cli_filter(enable_open_in_editor)
enable_extra_page_navigation = to_cli_filter(enable_extra_page_navigation)
enable_auto_suggest_bindings = to_cli_filter(enable_auto_suggest_bindings)
registry = MergedRegistry([
# Load basic bindings.
load_basic_bindings(),
load_mouse_bindings(),
ConditionalRegistry(load_abort_and_exit_bindings(),
enable_abort_and_exit_bindings),
ConditionalRegistry(load_basic_system_bindings(),
enable_system_bindings),
# Load emacs bindings.
load_emacs_bindings(),
ConditionalRegistry(load_emacs_open_in_editor_bindings(),
enable_open_in_editor),
ConditionalRegistry(load_emacs_search_bindings(get_search_state=get_search_state),
enable_search),
ConditionalRegistry(load_emacs_system_bindings(),
enable_system_bindings),
ConditionalRegistry(load_extra_emacs_page_navigation_bindings(),
enable_extra_page_navigation),
# Load Vi bindings.
load_vi_bindings(get_search_state=get_search_state),
ConditionalRegistry(load_vi_open_in_editor_bindings(),
enable_open_in_editor),
ConditionalRegistry(load_vi_search_bindings(get_search_state=get_search_state),
enable_search),
ConditionalRegistry(load_vi_system_bindings(),
enable_system_bindings),
ConditionalRegistry(load_extra_vi_page_navigation_bindings(),
enable_extra_page_navigation),
# Suggestion bindings.
# (This has to come at the end, because the Vi bindings also have an
# implementation for the "right arrow", but we really want the
# suggestion binding when a suggestion is available.)
ConditionalRegistry(load_auto_suggestion_bindings(),
enable_auto_suggest_bindings),
])
return registry
def load_key_bindings_for_prompt(**kw):
"""
Create a ``Registry`` object with the defaults key bindings for an input
prompt.
This activates the key bindings for abort/exit (Ctrl-C/Ctrl-D),
incremental search and auto suggestions.
(Not for full screen applications.)
"""
kw.setdefault('enable_abort_and_exit_bindings', True)
kw.setdefault('enable_search', True)
kw.setdefault('enable_auto_suggest_bindings', True)
return load_key_bindings(**kw)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,372 @@
# *** encoding: utf-8 ***
"""
An :class:`~.InputProcessor` receives callbacks for the keystrokes parsed from
the input in the :class:`~libs.prompt_toolkit.inputstream.InputStream` instance.
The `InputProcessor` will according to the implemented keybindings call the
correct callbacks when new key presses are feed through `feed`.
"""
from __future__ import unicode_literals
from libs.prompt_toolkit.buffer import EditReadOnlyBuffer
from libs.prompt_toolkit.filters.cli import ViNavigationMode
from libs.prompt_toolkit.keys import Keys, Key
from libs.prompt_toolkit.utils import Event
from .registry import BaseRegistry
from collections import deque
from six.moves import range
import weakref
import six
__all__ = (
'InputProcessor',
'KeyPress',
)
class KeyPress(object):
"""
:param key: A `Keys` instance or text (one character).
:param data: The received string on stdin. (Often vt100 escape codes.)
"""
def __init__(self, key, data=None):
assert isinstance(key, (six.text_type, Key))
assert data is None or isinstance(data, six.text_type)
if data is None:
data = key.name if isinstance(key, Key) else key
self.key = key
self.data = data
def __repr__(self):
return '%s(key=%r, data=%r)' % (
self.__class__.__name__, self.key, self.data)
def __eq__(self, other):
return self.key == other.key and self.data == other.data
class InputProcessor(object):
"""
Statemachine that receives :class:`KeyPress` instances and according to the
key bindings in the given :class:`Registry`, calls the matching handlers.
::
p = InputProcessor(registry)
# Send keys into the processor.
p.feed(KeyPress(Keys.ControlX, '\x18'))
p.feed(KeyPress(Keys.ControlC, '\x03')
# Process all the keys in the queue.
p.process_keys()
# Now the ControlX-ControlC callback will be called if this sequence is
# registered in the registry.
:param registry: `BaseRegistry` instance.
:param cli_ref: weakref to `CommandLineInterface`.
"""
def __init__(self, registry, cli_ref):
assert isinstance(registry, BaseRegistry)
self._registry = registry
self._cli_ref = cli_ref
self.beforeKeyPress = Event(self)
self.afterKeyPress = Event(self)
# The queue of keys not yet send to our _process generator/state machine.
self.input_queue = deque()
# The key buffer that is matched in the generator state machine.
# (This is at at most the amount of keys that make up for one key binding.)
self.key_buffer = []
# Simple macro recording. (Like readline does.)
self.record_macro = False
self.macro = []
self.reset()
def reset(self):
self._previous_key_sequence = []
self._previous_handler = None
self._process_coroutine = self._process()
self._process_coroutine.send(None)
#: Readline argument (for repetition of commands.)
#: https://www.gnu.org/software/bash/manual/html_node/Readline-Arguments.html
self.arg = None
def start_macro(self):
" Start recording macro. "
self.record_macro = True
self.macro = []
def end_macro(self):
" End recording macro. "
self.record_macro = False
def call_macro(self):
for k in self.macro:
self.feed(k)
def _get_matches(self, key_presses):
"""
For a list of :class:`KeyPress` instances. Give the matching handlers
that would handle this.
"""
keys = tuple(k.key for k in key_presses)
cli = self._cli_ref()
# Try match, with mode flag
return [b for b in self._registry.get_bindings_for_keys(keys) if b.filter(cli)]
def _is_prefix_of_longer_match(self, key_presses):
"""
For a list of :class:`KeyPress` instances. Return True if there is any
handler that is bound to a suffix of this keys.
"""
keys = tuple(k.key for k in key_presses)
cli = self._cli_ref()
# Get the filters for all the key bindings that have a longer match.
# Note that we transform it into a `set`, because we don't care about
# the actual bindings and executing it more than once doesn't make
# sense. (Many key bindings share the same filter.)
filters = set(b.filter for b in self._registry.get_bindings_starting_with_keys(keys))
# When any key binding is active, return True.
return any(f(cli) for f in filters)
def _process(self):
"""
Coroutine implementing the key match algorithm. Key strokes are sent
into this generator, and it calls the appropriate handlers.
"""
buffer = self.key_buffer
retry = False
while True:
if retry:
retry = False
else:
buffer.append((yield))
# If we have some key presses, check for matches.
if buffer:
is_prefix_of_longer_match = self._is_prefix_of_longer_match(buffer)
matches = self._get_matches(buffer)
# When eager matches were found, give priority to them and also
# ignore all the longer matches.
eager_matches = [m for m in matches if m.eager(self._cli_ref())]
if eager_matches:
matches = eager_matches
is_prefix_of_longer_match = False
# Exact matches found, call handler.
if not is_prefix_of_longer_match and matches:
self._call_handler(matches[-1], key_sequence=buffer[:])
del buffer[:] # Keep reference.
# No match found.
elif not is_prefix_of_longer_match and not matches:
retry = True
found = False
# Loop over the input, try longest match first and shift.
for i in range(len(buffer), 0, -1):
matches = self._get_matches(buffer[:i])
if matches:
self._call_handler(matches[-1], key_sequence=buffer[:i])
del buffer[:i]
found = True
break
if not found:
del buffer[:1]
def feed(self, key_press):
"""
Add a new :class:`KeyPress` to the input queue.
(Don't forget to call `process_keys` in order to process the queue.)
"""
assert isinstance(key_press, KeyPress)
self.input_queue.append(key_press)
def process_keys(self):
"""
Process all the keys in the `input_queue`.
(To be called after `feed`.)
Note: because of the `feed`/`process_keys` separation, it is
possible to call `feed` from inside a key binding.
This function keeps looping until the queue is empty.
"""
while self.input_queue:
key_press = self.input_queue.popleft()
if key_press.key != Keys.CPRResponse:
self.beforeKeyPress.fire()
self._process_coroutine.send(key_press)
if key_press.key != Keys.CPRResponse:
self.afterKeyPress.fire()
# Invalidate user interface.
cli = self._cli_ref()
if cli:
cli.invalidate()
def _call_handler(self, handler, key_sequence=None):
was_recording = self.record_macro
arg = self.arg
self.arg = None
event = KeyPressEvent(
weakref.ref(self), arg=arg, key_sequence=key_sequence,
previous_key_sequence=self._previous_key_sequence,
is_repeat=(handler == self._previous_handler))
# Save the state of the current buffer.
cli = event.cli # Can be `None` (In unit-tests only.)
if handler.save_before(event) and cli:
cli.current_buffer.save_to_undo_stack()
# Call handler.
try:
handler.call(event)
self._fix_vi_cursor_position(event)
except EditReadOnlyBuffer:
# When a key binding does an attempt to change a buffer which is
# read-only, we can just silently ignore that.
pass
self._previous_key_sequence = key_sequence
self._previous_handler = handler
# Record the key sequence in our macro. (Only if we're in macro mode
# before and after executing the key.)
if self.record_macro and was_recording:
self.macro.extend(key_sequence)
def _fix_vi_cursor_position(self, event):
"""
After every command, make sure that if we are in Vi navigation mode, we
never put the cursor after the last character of a line. (Unless it's
an empty line.)
"""
cli = self._cli_ref()
if cli:
buff = cli.current_buffer
preferred_column = buff.preferred_column
if (ViNavigationMode()(event.cli) and
buff.document.is_cursor_at_the_end_of_line and
len(buff.document.current_line) > 0):
buff.cursor_position -= 1
# Set the preferred_column for arrow up/down again.
# (This was cleared after changing the cursor position.)
buff.preferred_column = preferred_column
class KeyPressEvent(object):
"""
Key press event, delivered to key bindings.
:param input_processor_ref: Weak reference to the `InputProcessor`.
:param arg: Repetition argument.
:param key_sequence: List of `KeyPress` instances.
:param previouskey_sequence: Previous list of `KeyPress` instances.
:param is_repeat: True when the previous event was delivered to the same handler.
"""
def __init__(self, input_processor_ref, arg=None, key_sequence=None,
previous_key_sequence=None, is_repeat=False):
self._input_processor_ref = input_processor_ref
self.key_sequence = key_sequence
self.previous_key_sequence = previous_key_sequence
#: True when the previous key sequence was handled by the same handler.
self.is_repeat = is_repeat
self._arg = arg
def __repr__(self):
return 'KeyPressEvent(arg=%r, key_sequence=%r, is_repeat=%r)' % (
self.arg, self.key_sequence, self.is_repeat)
@property
def data(self):
return self.key_sequence[-1].data
@property
def input_processor(self):
return self._input_processor_ref()
@property
def cli(self):
"""
Command line interface.
"""
return self.input_processor._cli_ref()
@property
def current_buffer(self):
"""
The current buffer.
"""
return self.cli.current_buffer
@property
def arg(self):
"""
Repetition argument.
"""
if self._arg == '-':
return -1
result = int(self._arg or 1)
# Don't exceed a million.
if int(result) >= 1000000:
result = 1
return result
@property
def arg_present(self):
"""
True if repetition argument was explicitly provided.
"""
return self._arg is not None
def append_to_arg_count(self, data):
"""
Add digit to the input argument.
:param data: the typed digit as string
"""
assert data in '-0123456789'
current = self._arg
if data == '-':
assert current is None or current == '-'
result = data
elif current is None:
result = data
else:
result = "%s%s" % (current, data)
self.input_processor.arg = result

View File

@@ -0,0 +1,96 @@
"""
DEPRECATED:
Use `libs.prompt_toolkit.key_binding.defaults.load_key_bindings` instead.
:class:`KeyBindingManager` is a utility (or shortcut) for loading all the key
bindings in a key binding registry, with a logic set of filters to quickly to
quickly change from Vi to Emacs key bindings at runtime.
You don't have to use this, but it's practical.
Usage::
manager = KeyBindingManager()
app = Application(key_bindings_registry=manager.registry)
"""
from __future__ import unicode_literals
from .defaults import load_key_bindings
from libs.prompt_toolkit.filters import to_cli_filter
from libs.prompt_toolkit.key_binding.registry import Registry, ConditionalRegistry, MergedRegistry
__all__ = (
'KeyBindingManager',
)
class KeyBindingManager(object):
"""
Utility for loading all key bindings into memory.
:param registry: Optional `Registry` instance.
:param enable_abort_and_exit_bindings: Filter to enable Ctrl-C and Ctrl-D.
:param enable_system_bindings: Filter to enable the system bindings
(meta-! prompt and Control-Z suspension.)
:param enable_search: Filter to enable the search bindings.
:param enable_open_in_editor: Filter to enable open-in-editor.
:param enable_open_in_editor: Filter to enable open-in-editor.
:param enable_extra_page_navigation: Filter for enabling extra page navigation.
(Bindings for up/down scrolling through long pages, like in Emacs or Vi.)
:param enable_auto_suggest_bindings: Filter to enable fish-style suggestions.
:param enable_vi_mode: Deprecated!
"""
def __init__(self,
registry=None, # XXX: not used anymore.
enable_vi_mode=None, # (`enable_vi_mode` is deprecated.)
enable_all=True, #
get_search_state=None,
enable_abort_and_exit_bindings=False,
enable_system_bindings=False,
enable_search=False,
enable_open_in_editor=False,
enable_extra_page_navigation=False,
enable_auto_suggest_bindings=False):
assert registry is None or isinstance(registry, Registry)
assert get_search_state is None or callable(get_search_state)
enable_all = to_cli_filter(enable_all)
defaults = load_key_bindings(
get_search_state=get_search_state,
enable_abort_and_exit_bindings=enable_abort_and_exit_bindings,
enable_system_bindings=enable_system_bindings,
enable_search=enable_search,
enable_open_in_editor=enable_open_in_editor,
enable_extra_page_navigation=enable_extra_page_navigation,
enable_auto_suggest_bindings=enable_auto_suggest_bindings)
# Note, we wrap this whole thing again in a MergedRegistry, because we
# don't want the `enable_all` settings to apply on items that were
# added to the registry as a whole.
self.registry = MergedRegistry([
ConditionalRegistry(defaults, enable_all)
])
@classmethod
def for_prompt(cls, **kw):
"""
Create a ``KeyBindingManager`` with the defaults for an input prompt.
This activates the key bindings for abort/exit (Ctrl-C/Ctrl-D),
incremental search and auto suggestions.
(Not for full screen applications.)
"""
kw.setdefault('enable_abort_and_exit_bindings', True)
kw.setdefault('enable_search', True)
kw.setdefault('enable_auto_suggest_bindings', True)
return cls(**kw)
def reset(self, cli):
# For backwards compatibility.
pass
def get_vi_state(self, cli):
# Deprecated!
return cli.vi_state

View File

@@ -0,0 +1,350 @@
"""
Key bindings registry.
A `Registry` object is a container that holds a list of key bindings. It has a
very efficient internal data structure for checking which key bindings apply
for a pressed key.
Typical usage::
r = Registry()
@r.add_binding(Keys.ControlX, Keys.ControlC, filter=INSERT)
def handler(event):
# Handle ControlX-ControlC key sequence.
pass
It is also possible to combine multiple registries. We do this in the default
key bindings. There are some registries that contain Emacs bindings, while
others contain the Vi bindings. They are merged together using a
`MergedRegistry`.
We also have a `ConditionalRegistry` object that can enable/disable a group of
key bindings at once.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from libs.prompt_toolkit.cache import SimpleCache
from libs.prompt_toolkit.filters import CLIFilter, to_cli_filter, Never
from libs.prompt_toolkit.keys import Key, Keys
from six import text_type, with_metaclass
__all__ = (
'BaseRegistry',
'Registry',
'ConditionalRegistry',
'MergedRegistry',
)
class _Binding(object):
"""
(Immutable binding class.)
"""
def __init__(self, keys, handler, filter=None, eager=None, save_before=None):
assert isinstance(keys, tuple)
assert callable(handler)
assert isinstance(filter, CLIFilter)
assert isinstance(eager, CLIFilter)
assert callable(save_before)
self.keys = keys
self.handler = handler
self.filter = filter
self.eager = eager
self.save_before = save_before
def call(self, event):
return self.handler(event)
def __repr__(self):
return '%s(keys=%r, handler=%r)' % (
self.__class__.__name__, self.keys, self.handler)
class BaseRegistry(with_metaclass(ABCMeta, object)):
"""
Interface for a Registry.
"""
_version = 0 # For cache invalidation.
@abstractmethod
def get_bindings_for_keys(self, keys):
pass
@abstractmethod
def get_bindings_starting_with_keys(self, keys):
pass
# `add_binding` and `remove_binding` don't have to be part of this
# interface.
class Registry(BaseRegistry):
"""
Key binding registry.
"""
def __init__(self):
self.key_bindings = []
self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
self._version = 0 # For cache invalidation.
def _clear_cache(self):
self._version += 1
self._get_bindings_for_keys_cache.clear()
self._get_bindings_starting_with_keys_cache.clear()
def add_binding(self, *keys, **kwargs):
"""
Decorator for annotating key bindings.
:param filter: :class:`~libs.prompt_toolkit.filters.CLIFilter` to determine
when this key binding is active.
:param eager: :class:`~libs.prompt_toolkit.filters.CLIFilter` or `bool`.
When True, ignore potential longer matches when this key binding is
hit. E.g. when there is an active eager key binding for Ctrl-X,
execute the handler immediately and ignore the key binding for
Ctrl-X Ctrl-E of which it is a prefix.
:param save_before: Callable that takes an `Event` and returns True if
we should save the current buffer, before handling the event.
(That's the default.)
"""
filter = to_cli_filter(kwargs.pop('filter', True))
eager = to_cli_filter(kwargs.pop('eager', False))
save_before = kwargs.pop('save_before', lambda e: True)
to_cli_filter(kwargs.pop('invalidate_ui', True)) # Deprecated! (ignored.)
assert not kwargs
assert keys
assert all(isinstance(k, (Key, text_type)) for k in keys), \
'Key bindings should consist of Key and string (unicode) instances.'
assert callable(save_before)
if isinstance(filter, Never):
# When a filter is Never, it will always stay disabled, so in that case
# don't bother putting it in the registry. It will slow down every key
# press otherwise.
def decorator(func):
return func
else:
def decorator(func):
self.key_bindings.append(
_Binding(keys, func, filter=filter, eager=eager,
save_before=save_before))
self._clear_cache()
return func
return decorator
def remove_binding(self, function):
"""
Remove a key binding.
This expects a function that was given to `add_binding` method as
parameter. Raises `ValueError` when the given function was not
registered before.
"""
assert callable(function)
for b in self.key_bindings:
if b.handler == function:
self.key_bindings.remove(b)
self._clear_cache()
return
# No key binding found for this function. Raise ValueError.
raise ValueError('Binding not found: %r' % (function, ))
def get_bindings_for_keys(self, keys):
"""
Return a list of key bindings that can handle this key.
(This return also inactive bindings, so the `filter` still has to be
called, for checking it.)
:param keys: tuple of keys.
"""
def get():
result = []
for b in self.key_bindings:
if len(keys) == len(b.keys):
match = True
any_count = 0
for i, j in zip(b.keys, keys):
if i != j and i != Keys.Any:
match = False
break
if i == Keys.Any:
any_count += 1
if match:
result.append((any_count, b))
# Place bindings that have more 'Any' occurences in them at the end.
result = sorted(result, key=lambda item: -item[0])
return [item[1] for item in result]
return self._get_bindings_for_keys_cache.get(keys, get)
def get_bindings_starting_with_keys(self, keys):
"""
Return a list of key bindings that handle a key sequence starting with
`keys`. (It does only return bindings for which the sequences are
longer than `keys`. And like `get_bindings_for_keys`, it also includes
inactive bindings.)
:param keys: tuple of keys.
"""
def get():
result = []
for b in self.key_bindings:
if len(keys) < len(b.keys):
match = True
for i, j in zip(b.keys, keys):
if i != j and i != Keys.Any:
match = False
break
if match:
result.append(b)
return result
return self._get_bindings_starting_with_keys_cache.get(keys, get)
class _AddRemoveMixin(BaseRegistry):
"""
Common part for ConditionalRegistry and MergedRegistry.
"""
def __init__(self):
# `Registry` to be synchronized with all the others.
self._registry2 = Registry()
self._last_version = None
# The 'extra' registry. Mostly for backwards compatibility.
self._extra_registry = Registry()
def _update_cache(self):
raise NotImplementedError
# For backwards, compatibility, we allow adding bindings to both
# ConditionalRegistry and MergedRegistry. This is however not the
# recommended way. Better is to create a new registry and merge them
# together using MergedRegistry.
def add_binding(self, *k, **kw):
return self._extra_registry.add_binding(*k, **kw)
def remove_binding(self, *k, **kw):
return self._extra_registry.remove_binding(*k, **kw)
# Proxy methods to self._registry2.
@property
def key_bindings(self):
self._update_cache()
return self._registry2.key_bindings
@property
def _version(self):
self._update_cache()
return self._last_version
def get_bindings_for_keys(self, *a, **kw):
self._update_cache()
return self._registry2.get_bindings_for_keys(*a, **kw)
def get_bindings_starting_with_keys(self, *a, **kw):
self._update_cache()
return self._registry2.get_bindings_starting_with_keys(*a, **kw)
class ConditionalRegistry(_AddRemoveMixin):
"""
Wraps around a `Registry`. Disable/enable all the key bindings according to
the given (additional) filter.::
@Condition
def setting_is_true(cli):
return True # or False
registy = ConditionalRegistry(registry, setting_is_true)
When new key bindings are added to this object. They are also
enable/disabled according to the given `filter`.
:param registries: List of `Registry` objects.
:param filter: `CLIFilter` object.
"""
def __init__(self, registry=None, filter=True):
registry = registry or Registry()
assert isinstance(registry, BaseRegistry)
_AddRemoveMixin.__init__(self)
self.registry = registry
self.filter = to_cli_filter(filter)
def _update_cache(self):
" If the original registry was changed. Update our copy version. "
expected_version = (self.registry._version, self._extra_registry._version)
if self._last_version != expected_version:
registry2 = Registry()
# Copy all bindings from `self.registry`, adding our condition.
for reg in (self.registry, self._extra_registry):
for b in reg.key_bindings:
registry2.key_bindings.append(
_Binding(
keys=b.keys,
handler=b.handler,
filter=self.filter & b.filter,
eager=b.eager,
save_before=b.save_before))
self._registry2 = registry2
self._last_version = expected_version
class MergedRegistry(_AddRemoveMixin):
"""
Merge multiple registries of key bindings into one.
This class acts as a proxy to multiple `Registry` objects, but behaves as
if this is just one bigger `Registry`.
:param registries: List of `Registry` objects.
"""
def __init__(self, registries):
assert all(isinstance(r, BaseRegistry) for r in registries)
_AddRemoveMixin.__init__(self)
self.registries = registries
def _update_cache(self):
"""
If one of the original registries was changed. Update our merged
version.
"""
expected_version = (
tuple(r._version for r in self.registries) +
(self._extra_registry._version, ))
if self._last_version != expected_version:
registry2 = Registry()
for reg in self.registries:
registry2.key_bindings.extend(reg.key_bindings)
# Copy all bindings from `self._extra_registry`.
registry2.key_bindings.extend(self._extra_registry.key_bindings)
self._registry2 = registry2
self._last_version = expected_version

View File

@@ -0,0 +1,61 @@
from __future__ import unicode_literals
__all__ = (
'InputMode',
'CharacterFind',
'ViState',
)
class InputMode(object):
INSERT = 'vi-insert'
INSERT_MULTIPLE = 'vi-insert-multiple'
NAVIGATION = 'vi-navigation'
REPLACE = 'vi-replace'
class CharacterFind(object):
def __init__(self, character, backwards=False):
self.character = character
self.backwards = backwards
class ViState(object):
"""
Mutable class to hold the state of the Vi navigation.
"""
def __init__(self):
#: None or CharacterFind instance. (This is used to repeat the last
#: search in Vi mode, by pressing the 'n' or 'N' in navigation mode.)
self.last_character_find = None
# When an operator is given and we are waiting for text object,
# -- e.g. in the case of 'dw', after the 'd' --, an operator callback
# is set here.
self.operator_func = None
self.operator_arg = None
#: Named registers. Maps register name (e.g. 'a') to
#: :class:`ClipboardData` instances.
self.named_registers = {}
#: The Vi mode we're currently in to.
self.input_mode = InputMode.INSERT
#: Waiting for digraph.
self.waiting_for_digraph = False
self.digraph_symbol1 = None # (None or a symbol.)
#: When true, make ~ act as an operator.
self.tilde_operator = False
def reset(self, mode=InputMode.INSERT):
"""
Reset state, go back to the given mode. INSERT by default.
"""
# Go back to insert mode.
self.input_mode = mode
self.waiting_for_digraph = False
self.operator_func = None
self.operator_arg = None

View File

@@ -0,0 +1,129 @@
from __future__ import unicode_literals
__all__ = (
'Key',
'Keys',
)
class Key(object):
def __init__(self, name):
#: Descriptive way of writing keys in configuration files. e.g. <C-A>
#: for ``Control-A``.
self.name = name
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.name)
class Keys(object):
Escape = Key('<Escape>')
ControlA = Key('<C-A>')
ControlB = Key('<C-B>')
ControlC = Key('<C-C>')
ControlD = Key('<C-D>')
ControlE = Key('<C-E>')
ControlF = Key('<C-F>')
ControlG = Key('<C-G>')
ControlH = Key('<C-H>')
ControlI = Key('<C-I>') # Tab
ControlJ = Key('<C-J>') # Enter
ControlK = Key('<C-K>')
ControlL = Key('<C-L>')
ControlM = Key('<C-M>') # Enter
ControlN = Key('<C-N>')
ControlO = Key('<C-O>')
ControlP = Key('<C-P>')
ControlQ = Key('<C-Q>')
ControlR = Key('<C-R>')
ControlS = Key('<C-S>')
ControlT = Key('<C-T>')
ControlU = Key('<C-U>')
ControlV = Key('<C-V>')
ControlW = Key('<C-W>')
ControlX = Key('<C-X>')
ControlY = Key('<C-Y>')
ControlZ = Key('<C-Z>')
ControlSpace = Key('<C-Space>')
ControlBackslash = Key('<C-Backslash>')
ControlSquareClose = Key('<C-SquareClose>')
ControlCircumflex = Key('<C-Circumflex>')
ControlUnderscore = Key('<C-Underscore>')
ControlLeft = Key('<C-Left>')
ControlRight = Key('<C-Right>')
ControlUp = Key('<C-Up>')
ControlDown = Key('<C-Down>')
Up = Key('<Up>')
Down = Key('<Down>')
Right = Key('<Right>')
Left = Key('<Left>')
ShiftLeft = Key('<ShiftLeft>')
ShiftUp = Key('<ShiftUp>')
ShiftDown = Key('<ShiftDown>')
ShiftRight = Key('<ShiftRight>')
Home = Key('<Home>')
End = Key('<End>')
Delete = Key('<Delete>')
ShiftDelete = Key('<ShiftDelete>')
ControlDelete = Key('<C-Delete>')
PageUp = Key('<PageUp>')
PageDown = Key('<PageDown>')
BackTab = Key('<BackTab>') # shift + tab
Insert = Key('<Insert>')
Backspace = Key('<Backspace>')
# Aliases.
Tab = ControlI
Enter = ControlJ
# XXX: Actually Enter equals ControlM, not ControlJ,
# However, in libs.prompt_toolkit, we made the mistake of translating
# \r into \n during the input, so everyone is now handling the
# enter key by binding ControlJ.
# From now on, it's better to bind `Keys.Enter` everywhere,
# because that's future compatible, and will still work when we
# stop replacing \r by \n.
F1 = Key('<F1>')
F2 = Key('<F2>')
F3 = Key('<F3>')
F4 = Key('<F4>')
F5 = Key('<F5>')
F6 = Key('<F6>')
F7 = Key('<F7>')
F8 = Key('<F8>')
F9 = Key('<F9>')
F10 = Key('<F10>')
F11 = Key('<F11>')
F12 = Key('<F12>')
F13 = Key('<F13>')
F14 = Key('<F14>')
F15 = Key('<F15>')
F16 = Key('<F16>')
F17 = Key('<F17>')
F18 = Key('<F18>')
F19 = Key('<F19>')
F20 = Key('<F20>')
F21 = Key('<F21>')
F22 = Key('<F22>')
F23 = Key('<F23>')
F24 = Key('<F24>')
# Matches any key.
Any = Key('<Any>')
# Special
CPRResponse = Key('<Cursor-Position-Response>')
Vt100MouseEvent = Key('<Vt100-Mouse-Event>')
WindowsMouseEvent = Key('<Windows-Mouse-Event>')
BracketedPaste = Key('<Bracketed-Paste>')
# Key which is ignored. (The key binding for this key should not do
# anything.)
Ignore = Key('<Ignore>')

View File

@@ -0,0 +1,51 @@
"""
Command line layout definitions
-------------------------------
The layout of a command line interface is defined by a Container instance.
There are two main groups of classes here. Containers and controls:
- A container can contain other containers or controls, it can have multiple
children and it decides about the dimensions.
- A control is responsible for rendering the actual content to a screen.
A control can propose some dimensions, but it's the container who decides
about the dimensions -- or when the control consumes more space -- which part
of the control will be visible.
Container classes::
- Container (Abstract base class)
|- HSplit (Horizontal split)
|- VSplit (Vertical split)
|- FloatContainer (Container which can also contain menus and other floats)
`- Window (Container which contains one actual control
Control classes::
- UIControl (Abstract base class)
|- TokenListControl (Renders a simple list of tokens)
|- FillControl (Fills control with one token/character.)
`- BufferControl (Renders an input buffer.)
Usually, you end up wrapping every control inside a `Window` object, because
that's the only way to render it in a layout.
There are some prepared toolbars which are ready to use::
- SystemToolbar (Shows the 'system' input buffer, for entering system commands.)
- ArgToolbar (Shows the input 'arg', for repetition of input commands.)
- SearchToolbar (Shows the 'search' input buffer, for incremental search.)
- CompletionsToolbar (Shows the completions of the current buffer.)
- ValidationToolbar (Shows validation errors of the current buffer.)
And one prepared menu:
- CompletionsMenu
"""
from __future__ import unicode_literals
from .containers import Float, FloatContainer, HSplit, VSplit, Window, ConditionalContainer
from .controls import TokenListControl, FillControl, BufferControl

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,730 @@
"""
User interface Controls for the layout.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from six import with_metaclass
from six.moves import range
from libs.prompt_toolkit.cache import SimpleCache
from libs.prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
from libs.prompt_toolkit.filters import to_cli_filter
from libs.prompt_toolkit.mouse_events import MouseEventType
from libs.prompt_toolkit.search_state import SearchState
from libs.prompt_toolkit.selection import SelectionType
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.utils import get_cwidth
from .lexers import Lexer, SimpleLexer
from .processors import Processor
from .screen import Char, Point
from .utils import token_list_width, split_lines, token_list_to_text
import six
import time
__all__ = (
'BufferControl',
'FillControl',
'TokenListControl',
'UIControl',
'UIContent',
)
class UIControl(with_metaclass(ABCMeta, object)):
"""
Base class for all user interface controls.
"""
def reset(self):
# Default reset. (Doesn't have to be implemented.)
pass
def preferred_width(self, cli, max_available_width):
return None
def preferred_height(self, cli, width, max_available_height, wrap_lines):
return None
def has_focus(self, cli):
"""
Return ``True`` when this user control has the focus.
If so, the cursor will be displayed according to the cursor position
reported by :meth:`.UIControl.create_content`. If the created content
has the property ``show_cursor=False``, the cursor will be hidden from
the output.
"""
return False
@abstractmethod
def create_content(self, cli, width, height):
"""
Generate the content for this user control.
Returns a :class:`.UIContent` instance.
"""
def mouse_handler(self, cli, mouse_event):
"""
Handle mouse events.
When `NotImplemented` is returned, it means that the given event is not
handled by the `UIControl` itself. The `Window` or key bindings can
decide to handle this event as scrolling or changing focus.
:param cli: `CommandLineInterface` instance.
:param mouse_event: `MouseEvent` instance.
"""
return NotImplemented
def move_cursor_down(self, cli):
"""
Request to move the cursor down.
This happens when scrolling down and the cursor is completely at the
top.
"""
def move_cursor_up(self, cli):
"""
Request to move the cursor up.
"""
class UIContent(object):
"""
Content generated by a user control. This content consists of a list of
lines.
:param get_line: Callable that returns the current line. This is a list of
(Token, text) tuples.
:param line_count: The number of lines.
:param cursor_position: a :class:`.Point` for the cursor position.
:param menu_position: a :class:`.Point` for the menu position.
:param show_cursor: Make the cursor visible.
:param default_char: The default :class:`.Char` for filling the background.
"""
def __init__(self, get_line=None, line_count=0,
cursor_position=None, menu_position=None, show_cursor=True,
default_char=None):
assert callable(get_line)
assert isinstance(line_count, six.integer_types)
assert cursor_position is None or isinstance(cursor_position, Point)
assert menu_position is None or isinstance(menu_position, Point)
assert default_char is None or isinstance(default_char, Char)
self.get_line = get_line
self.line_count = line_count
self.cursor_position = cursor_position or Point(0, 0)
self.menu_position = menu_position
self.show_cursor = show_cursor
self.default_char = default_char
# Cache for line heights. Maps (lineno, width) -> height.
self._line_heights = {}
def __getitem__(self, lineno):
" Make it iterable (iterate line by line). "
if lineno < self.line_count:
return self.get_line(lineno)
else:
raise IndexError
def get_height_for_line(self, lineno, width):
"""
Return the height that a given line would need if it is rendered in a
space with the given width.
"""
try:
return self._line_heights[lineno, width]
except KeyError:
text = token_list_to_text(self.get_line(lineno))
result = self.get_height_for_text(text, width)
# Cache and return
self._line_heights[lineno, width] = result
return result
@staticmethod
def get_height_for_text(text, width):
# Get text width for this line.
line_width = get_cwidth(text)
# Calculate height.
try:
quotient, remainder = divmod(line_width, width)
except ZeroDivisionError:
# Return something very big.
# (This can happen, when the Window gets very small.)
return 10 ** 10
else:
if remainder:
quotient += 1 # Like math.ceil.
return max(1, quotient)
class TokenListControl(UIControl):
"""
Control that displays a list of (Token, text) tuples.
(It's mostly optimized for rather small widgets, like toolbars, menus, etc...)
Mouse support:
The list of tokens can also contain tuples of three items, looking like:
(Token, text, handler). When mouse support is enabled and the user
clicks on this token, then the given handler is called. That handler
should accept two inputs: (CommandLineInterface, MouseEvent) and it
should either handle the event or return `NotImplemented` in case we
want the containing Window to handle this event.
:param get_tokens: Callable that takes a `CommandLineInterface` instance
and returns the list of (Token, text) tuples to be displayed right now.
:param default_char: default :class:`.Char` (character and Token) to use
for the background when there is more space available than `get_tokens`
returns.
:param get_default_char: Like `default_char`, but this is a callable that
takes a :class:`libs.prompt_toolkit.interface.CommandLineInterface` and
returns a :class:`.Char` instance.
:param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
this UI control will take the focus. The cursor will be shown in the
upper left corner of this control, unless `get_token` returns a
``Token.SetCursorPosition`` token somewhere in the token list, then the
cursor will be shown there.
"""
def __init__(self, get_tokens, default_char=None, get_default_char=None,
align_right=False, align_center=False, has_focus=False):
assert callable(get_tokens)
assert default_char is None or isinstance(default_char, Char)
assert get_default_char is None or callable(get_default_char)
assert not (default_char and get_default_char)
self.align_right = to_cli_filter(align_right)
self.align_center = to_cli_filter(align_center)
self._has_focus_filter = to_cli_filter(has_focus)
self.get_tokens = get_tokens
# Construct `get_default_char` callable.
if default_char:
get_default_char = lambda _: default_char
elif not get_default_char:
get_default_char = lambda _: Char(' ', Token.Transparent)
self.get_default_char = get_default_char
#: Cache for the content.
self._content_cache = SimpleCache(maxsize=18)
self._token_cache = SimpleCache(maxsize=1)
# Only cache one token list. We don't need the previous item.
# Render info for the mouse support.
self._tokens = None
def reset(self):
self._tokens = None
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.get_tokens)
def _get_tokens_cached(self, cli):
"""
Get tokens, but only retrieve tokens once during one render run.
(This function is called several times during one rendering, because
we also need those for calculating the dimensions.)
"""
return self._token_cache.get(
cli.render_counter, lambda: self.get_tokens(cli))
def has_focus(self, cli):
return self._has_focus_filter(cli)
def preferred_width(self, cli, max_available_width):
"""
Return the preferred width for this control.
That is the width of the longest line.
"""
text = token_list_to_text(self._get_tokens_cached(cli))
line_lengths = [get_cwidth(l) for l in text.split('\n')]
return max(line_lengths)
def preferred_height(self, cli, width, max_available_height, wrap_lines):
content = self.create_content(cli, width, None)
return content.line_count
def create_content(self, cli, width, height):
# Get tokens
tokens_with_mouse_handlers = self._get_tokens_cached(cli)
default_char = self.get_default_char(cli)
# Wrap/align right/center parameters.
right = self.align_right(cli)
center = self.align_center(cli)
def process_line(line):
" Center or right align a single line. "
used_width = token_list_width(line)
padding = width - used_width
if center:
padding = int(padding / 2)
return [(default_char.token, default_char.char * padding)] + line
if right or center:
token_lines_with_mouse_handlers = []
for line in split_lines(tokens_with_mouse_handlers):
token_lines_with_mouse_handlers.append(process_line(line))
else:
token_lines_with_mouse_handlers = list(split_lines(tokens_with_mouse_handlers))
# Strip mouse handlers from tokens.
token_lines = [
[tuple(item[:2]) for item in line]
for line in token_lines_with_mouse_handlers
]
# Keep track of the tokens with mouse handler, for later use in
# `mouse_handler`.
self._tokens = tokens_with_mouse_handlers
# If there is a `Token.SetCursorPosition` in the token list, set the
# cursor position here.
def get_cursor_position():
SetCursorPosition = Token.SetCursorPosition
for y, line in enumerate(token_lines):
x = 0
for token, text in line:
if token == SetCursorPosition:
return Point(x=x, y=y)
x += len(text)
return None
# Create content, or take it from the cache.
key = (default_char.char, default_char.token,
tuple(tokens_with_mouse_handlers), width, right, center)
def get_content():
return UIContent(get_line=lambda i: token_lines[i],
line_count=len(token_lines),
default_char=default_char,
cursor_position=get_cursor_position())
return self._content_cache.get(key, get_content)
@classmethod
def static(cls, tokens):
def get_static_tokens(cli):
return tokens
return cls(get_static_tokens)
def mouse_handler(self, cli, mouse_event):
"""
Handle mouse events.
(When the token list contained mouse handlers and the user clicked on
on any of these, the matching handler is called. This handler can still
return `NotImplemented` in case we want the `Window` to handle this
particular event.)
"""
if self._tokens:
# Read the generator.
tokens_for_line = list(split_lines(self._tokens))
try:
tokens = tokens_for_line[mouse_event.position.y]
except IndexError:
return NotImplemented
else:
# Find position in the token list.
xpos = mouse_event.position.x
# Find mouse handler for this character.
count = 0
for item in tokens:
count += len(item[1])
if count >= xpos:
if len(item) >= 3:
# Handler found. Call it.
# (Handler can return NotImplemented, so return
# that result.)
handler = item[2]
return handler(cli, mouse_event)
else:
break
# Otherwise, don't handle here.
return NotImplemented
class FillControl(UIControl):
"""
Fill whole control with characters with this token.
(Also helpful for debugging.)
:param char: :class:`.Char` instance to use for filling.
:param get_char: A callable that takes a CommandLineInterface and returns a
:class:`.Char` object.
"""
def __init__(self, character=None, token=Token, char=None, get_char=None): # 'character' and 'token' parameters are deprecated.
assert char is None or isinstance(char, Char)
assert get_char is None or callable(get_char)
assert not (char and get_char)
self.char = char
if character:
# Passing (character=' ', token=token) is deprecated.
self.character = character
self.token = token
self.get_char = lambda cli: Char(character, token)
elif get_char:
# When 'get_char' is given.
self.get_char = get_char
else:
# When 'char' is given.
self.char = self.char or Char()
self.get_char = lambda cli: self.char
self.char = char
def __repr__(self):
if self.char:
return '%s(char=%r)' % (self.__class__.__name__, self.char)
else:
return '%s(get_char=%r)' % (self.__class__.__name__, self.get_char)
def reset(self):
pass
def has_focus(self, cli):
return False
def create_content(self, cli, width, height):
def get_line(i):
return []
return UIContent(
get_line=get_line,
line_count=100 ** 100, # Something very big.
default_char=self.get_char(cli))
_ProcessedLine = namedtuple('_ProcessedLine', 'tokens source_to_display display_to_source')
class BufferControl(UIControl):
"""
Control for visualising the content of a `Buffer`.
:param input_processors: list of :class:`~libs.prompt_toolkit.layout.processors.Processor`.
:param lexer: :class:`~libs.prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
:param preview_search: `bool` or `CLIFilter`: Show search while typing.
:param get_search_state: Callable that takes a CommandLineInterface and
returns the SearchState to be used. (If not CommandLineInterface.search_state.)
:param buffer_name: String representing the name of the buffer to display.
:param default_char: :class:`.Char` instance to use to fill the background. This is
transparent by default.
:param focus_on_click: Focus this buffer when it's click, but not yet focussed.
"""
def __init__(self,
buffer_name=DEFAULT_BUFFER,
input_processors=None,
lexer=None,
preview_search=False,
search_buffer_name=SEARCH_BUFFER,
get_search_state=None,
menu_position=None,
default_char=None,
focus_on_click=False):
assert input_processors is None or all(isinstance(i, Processor) for i in input_processors)
assert menu_position is None or callable(menu_position)
assert lexer is None or isinstance(lexer, Lexer)
assert get_search_state is None or callable(get_search_state)
assert default_char is None or isinstance(default_char, Char)
self.preview_search = to_cli_filter(preview_search)
self.get_search_state = get_search_state
self.focus_on_click = to_cli_filter(focus_on_click)
self.input_processors = input_processors or []
self.buffer_name = buffer_name
self.menu_position = menu_position
self.lexer = lexer or SimpleLexer()
self.default_char = default_char or Char(token=Token.Transparent)
self.search_buffer_name = search_buffer_name
#: Cache for the lexer.
#: Often, due to cursor movement, undo/redo and window resizing
#: operations, it happens that a short time, the same document has to be
#: lexed. This is a faily easy way to cache such an expensive operation.
self._token_cache = SimpleCache(maxsize=8)
self._xy_to_cursor_position = None
self._last_click_timestamp = None
self._last_get_processed_line = None
def _buffer(self, cli):
"""
The buffer object that contains the 'main' content.
"""
return cli.buffers[self.buffer_name]
def has_focus(self, cli):
# This control gets the focussed if the actual `Buffer` instance has the
# focus or when any of the `InputProcessor` classes tells us that it
# wants the focus. (E.g. in case of a reverse-search, where the actual
# search buffer may not be displayed, but the "reverse-i-search" text
# should get the focus.)
return cli.current_buffer_name == self.buffer_name or \
any(i.has_focus(cli) for i in self.input_processors)
def preferred_width(self, cli, max_available_width):
"""
This should return the preferred width.
Note: We don't specify a preferred width according to the content,
because it would be too expensive. Calculating the preferred
width can be done by calculating the longest line, but this would
require applying all the processors to each line. This is
unfeasible for a larger document, and doing it for small
documents only would result in inconsistent behaviour.
"""
return None
def preferred_height(self, cli, width, max_available_height, wrap_lines):
# Calculate the content height, if it was drawn on a screen with the
# given width.
height = 0
content = self.create_content(cli, width, None)
# When line wrapping is off, the height should be equal to the amount
# of lines.
if not wrap_lines:
return content.line_count
# When the number of lines exceeds the max_available_height, just
# return max_available_height. No need to calculate anything.
if content.line_count >= max_available_height:
return max_available_height
for i in range(content.line_count):
height += content.get_height_for_line(i, width)
if height >= max_available_height:
return max_available_height
return height
def _get_tokens_for_line_func(self, cli, document):
"""
Create a function that returns the tokens for a given line.
"""
# Cache using `document.text`.
def get_tokens_for_line():
return self.lexer.lex_document(cli, document)
return self._token_cache.get(document.text, get_tokens_for_line)
def _create_get_processed_line_func(self, cli, document):
"""
Create a function that takes a line number of the current document and
returns a _ProcessedLine(processed_tokens, source_to_display, display_to_source)
tuple.
"""
def transform(lineno, tokens):
" Transform the tokens for a given line number. "
source_to_display_functions = []
display_to_source_functions = []
# Get cursor position at this line.
if document.cursor_position_row == lineno:
cursor_column = document.cursor_position_col
else:
cursor_column = None
def source_to_display(i):
""" Translate x position from the buffer to the x position in the
processed token list. """
for f in source_to_display_functions:
i = f(i)
return i
# Apply each processor.
for p in self.input_processors:
transformation = p.apply_transformation(
cli, document, lineno, source_to_display, tokens)
tokens = transformation.tokens
if cursor_column:
cursor_column = transformation.source_to_display(cursor_column)
display_to_source_functions.append(transformation.display_to_source)
source_to_display_functions.append(transformation.source_to_display)
def display_to_source(i):
for f in reversed(display_to_source_functions):
i = f(i)
return i
return _ProcessedLine(tokens, source_to_display, display_to_source)
def create_func():
get_line = self._get_tokens_for_line_func(cli, document)
cache = {}
def get_processed_line(i):
try:
return cache[i]
except KeyError:
processed_line = transform(i, get_line(i))
cache[i] = processed_line
return processed_line
return get_processed_line
return create_func()
def create_content(self, cli, width, height):
"""
Create a UIContent.
"""
buffer = self._buffer(cli)
# Get the document to be shown. If we are currently searching (the
# search buffer has focus, and the preview_search filter is enabled),
# then use the search document, which has possibly a different
# text/cursor position.)
def preview_now():
""" True when we should preview a search. """
return bool(self.preview_search(cli) and
cli.buffers[self.search_buffer_name].text)
if preview_now():
if self.get_search_state:
ss = self.get_search_state(cli)
else:
ss = cli.search_state
document = buffer.document_for_search(SearchState(
text=cli.current_buffer.text,
direction=ss.direction,
ignore_case=ss.ignore_case))
else:
document = buffer.document
get_processed_line = self._create_get_processed_line_func(cli, document)
self._last_get_processed_line = get_processed_line
def translate_rowcol(row, col):
" Return the content column for this coordinate. "
return Point(y=row, x=get_processed_line(row).source_to_display(col))
def get_line(i):
" Return the tokens for a given line number. "
tokens = get_processed_line(i).tokens
# Add a space at the end, because that is a possible cursor
# position. (When inserting after the input.) We should do this on
# all the lines, not just the line containing the cursor. (Because
# otherwise, line wrapping/scrolling could change when moving the
# cursor around.)
tokens = tokens + [(self.default_char.token, ' ')]
return tokens
content = UIContent(
get_line=get_line,
line_count=document.line_count,
cursor_position=translate_rowcol(document.cursor_position_row,
document.cursor_position_col),
default_char=self.default_char)
# If there is an auto completion going on, use that start point for a
# pop-up menu position. (But only when this buffer has the focus --
# there is only one place for a menu, determined by the focussed buffer.)
if cli.current_buffer_name == self.buffer_name:
menu_position = self.menu_position(cli) if self.menu_position else None
if menu_position is not None:
assert isinstance(menu_position, int)
menu_row, menu_col = buffer.document.translate_index_to_position(menu_position)
content.menu_position = translate_rowcol(menu_row, menu_col)
elif buffer.complete_state:
# Position for completion menu.
# Note: We use 'min', because the original cursor position could be
# behind the input string when the actual completion is for
# some reason shorter than the text we had before. (A completion
# can change and shorten the input.)
menu_row, menu_col = buffer.document.translate_index_to_position(
min(buffer.cursor_position,
buffer.complete_state.original_document.cursor_position))
content.menu_position = translate_rowcol(menu_row, menu_col)
else:
content.menu_position = None
return content
def mouse_handler(self, cli, mouse_event):
"""
Mouse handler for this control.
"""
buffer = self._buffer(cli)
position = mouse_event.position
# Focus buffer when clicked.
if self.has_focus(cli):
if self._last_get_processed_line:
processed_line = self._last_get_processed_line(position.y)
# Translate coordinates back to the cursor position of the
# original input.
xpos = processed_line.display_to_source(position.x)
index = buffer.document.translate_row_col_to_index(position.y, xpos)
# Set the cursor position.
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
buffer.exit_selection()
buffer.cursor_position = index
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
# When the cursor was moved to another place, select the text.
# (The >1 is actually a small but acceptable workaround for
# selecting text in Vi navigation mode. In navigation mode,
# the cursor can never be after the text, so the cursor
# will be repositioned automatically.)
if abs(buffer.cursor_position - index) > 1:
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
buffer.cursor_position = index
# Select word around cursor on double click.
# Two MOUSE_UP events in a short timespan are considered a double click.
double_click = self._last_click_timestamp and time.time() - self._last_click_timestamp < .3
self._last_click_timestamp = time.time()
if double_click:
start, end = buffer.document.find_boundaries_of_current_word()
buffer.cursor_position += start
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
buffer.cursor_position += end - start
else:
# Don't handle scroll events here.
return NotImplemented
# Not focussed, but focussing on click events.
else:
if self.focus_on_click(cli) and mouse_event.event_type == MouseEventType.MOUSE_UP:
# Focus happens on mouseup. (If we did this on mousedown, the
# up event will be received at the point where this widget is
# focussed and be handled anyway.)
cli.focus(self.buffer_name)
else:
return NotImplemented
def move_cursor_down(self, cli):
b = self._buffer(cli)
b.cursor_position += b.document.get_cursor_down_position()
def move_cursor_up(self, cli):
b = self._buffer(cli)
b.cursor_position += b.document.get_cursor_up_position()

View File

@@ -0,0 +1,92 @@
"""
Layout dimensions are used to give the minimum, maximum and preferred
dimensions for containers and controls.
"""
from __future__ import unicode_literals
__all__ = (
'LayoutDimension',
'sum_layout_dimensions',
'max_layout_dimensions',
)
class LayoutDimension(object):
"""
Specified dimension (width/height) of a user control or window.
The layout engine tries to honor the preferred size. If that is not
possible, because the terminal is larger or smaller, it tries to keep in
between min and max.
:param min: Minimum size.
:param max: Maximum size.
:param weight: For a VSplit/HSplit, the actual size will be determined
by taking the proportion of weights from all the children.
E.g. When there are two children, one width a weight of 1,
and the other with a weight of 2. The second will always be
twice as big as the first, if the min/max values allow it.
:param preferred: Preferred size.
"""
def __init__(self, min=None, max=None, weight=1, preferred=None):
assert isinstance(weight, int) and weight > 0 # Cannot be a float.
self.min_specified = min is not None
self.max_specified = max is not None
self.preferred_specified = preferred is not None
if min is None:
min = 0 # Smallest possible value.
if max is None: # 0-values are allowed, so use "is None"
max = 1000 ** 10 # Something huge.
if preferred is None:
preferred = min
self.min = min
self.max = max
self.preferred = preferred
self.weight = weight
# Make sure that the 'preferred' size is always in the min..max range.
if self.preferred < self.min:
self.preferred = self.min
if self.preferred > self.max:
self.preferred = self.max
@classmethod
def exact(cls, amount):
"""
Return a :class:`.LayoutDimension` with an exact size. (min, max and
preferred set to ``amount``).
"""
return cls(min=amount, max=amount, preferred=amount)
def __repr__(self):
return 'LayoutDimension(min=%r, max=%r, preferred=%r, weight=%r)' % (
self.min, self.max, self.preferred, self.weight)
def __add__(self, other):
return sum_layout_dimensions([self, other])
def sum_layout_dimensions(dimensions):
"""
Sum a list of :class:`.LayoutDimension` instances.
"""
min = sum([d.min for d in dimensions if d.min is not None])
max = sum([d.max for d in dimensions if d.max is not None])
preferred = sum([d.preferred for d in dimensions])
return LayoutDimension(min=min, max=max, preferred=preferred)
def max_layout_dimensions(dimensions):
"""
Take the maximum of a list of :class:`.LayoutDimension` instances.
"""
min_ = max([d.min for d in dimensions if d.min is not None])
max_ = max([d.max for d in dimensions if d.max is not None])
preferred = max([d.preferred for d in dimensions])
return LayoutDimension(min=min_, max=max_, preferred=preferred)

View File

@@ -0,0 +1,320 @@
"""
Lexer interface and implementation.
Used for syntax highlighting.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
from six.moves import range
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.filters import to_cli_filter
from .utils import split_lines
import re
import six
__all__ = (
'Lexer',
'SimpleLexer',
'PygmentsLexer',
'SyntaxSync',
'SyncFromStart',
'RegexSync',
)
class Lexer(with_metaclass(ABCMeta, object)):
"""
Base class for all lexers.
"""
@abstractmethod
def lex_document(self, cli, document):
"""
Takes a :class:`~libs.prompt_toolkit.document.Document` and returns a
callable that takes a line number and returns the tokens for that line.
"""
class SimpleLexer(Lexer):
"""
Lexer that doesn't do any tokenizing and returns the whole input as one token.
:param token: The `Token` for this lexer.
"""
# `default_token` parameter is deprecated!
def __init__(self, token=Token, default_token=None):
self.token = token
if default_token is not None:
self.token = default_token
def lex_document(self, cli, document):
lines = document.lines
def get_line(lineno):
" Return the tokens for the given line. "
try:
return [(self.token, lines[lineno])]
except IndexError:
return []
return get_line
class SyntaxSync(with_metaclass(ABCMeta, object)):
"""
Syntax synchroniser. This is a tool that finds a start position for the
lexer. This is especially important when editing big documents; we don't
want to start the highlighting by running the lexer from the beginning of
the file. That is very slow when editing.
"""
@abstractmethod
def get_sync_start_position(self, document, lineno):
"""
Return the position from where we can start lexing as a (row, column)
tuple.
:param document: `Document` instance that contains all the lines.
:param lineno: The line that we want to highlight. (We need to return
this line, or an earlier position.)
"""
class SyncFromStart(SyntaxSync):
"""
Always start the syntax highlighting from the beginning.
"""
def get_sync_start_position(self, document, lineno):
return 0, 0
class RegexSync(SyntaxSync):
"""
Synchronize by starting at a line that matches the given regex pattern.
"""
# Never go more than this amount of lines backwards for synchronisation.
# That would be too CPU intensive.
MAX_BACKWARDS = 500
# Start lexing at the start, if we are in the first 'n' lines and no
# synchronisation position was found.
FROM_START_IF_NO_SYNC_POS_FOUND = 100
def __init__(self, pattern):
assert isinstance(pattern, six.text_type)
self._compiled_pattern = re.compile(pattern)
def get_sync_start_position(self, document, lineno):
" Scan backwards, and find a possible position to start. "
pattern = self._compiled_pattern
lines = document.lines
# Scan upwards, until we find a point where we can start the syntax
# synchronisation.
for i in range(lineno, max(-1, lineno - self.MAX_BACKWARDS), -1):
match = pattern.match(lines[i])
if match:
return i, match.start()
# No synchronisation point found. If we aren't that far from the
# beginning, start at the very beginning, otherwise, just try to start
# at the current line.
if lineno < self.FROM_START_IF_NO_SYNC_POS_FOUND:
return 0, 0
else:
return lineno, 0
@classmethod
def from_pygments_lexer_cls(cls, lexer_cls):
"""
Create a :class:`.RegexSync` instance for this Pygments lexer class.
"""
patterns = {
# For Python, start highlighting at any class/def block.
'Python': r'^\s*(class|def)\s+',
'Python 3': r'^\s*(class|def)\s+',
# For HTML, start at any open/close tag definition.
'HTML': r'<[/a-zA-Z]',
# For javascript, start at a function.
'JavaScript': r'\bfunction\b'
# TODO: Add definitions for other languages.
# By default, we start at every possible line.
}
p = patterns.get(lexer_cls.name, '^')
return cls(p)
class PygmentsLexer(Lexer):
"""
Lexer that calls a pygments lexer.
Example::
from pygments.lexers import HtmlLexer
lexer = PygmentsLexer(HtmlLexer)
Note: Don't forget to also load a Pygments compatible style. E.g.::
from libs.prompt_toolkit.styles.from_pygments import style_from_pygments
from pygments.styles import get_style_by_name
style = style_from_pygments(get_style_by_name('monokai'))
:param pygments_lexer_cls: A `Lexer` from Pygments.
:param sync_from_start: Start lexing at the start of the document. This
will always give the best results, but it will be slow for bigger
documents. (When the last part of the document is display, then the
whole document will be lexed by Pygments on every key stroke.) It is
recommended to disable this for inputs that are expected to be more
than 1,000 lines.
:param syntax_sync: `SyntaxSync` object.
"""
# Minimum amount of lines to go backwards when starting the parser.
# This is important when the lines are retrieved in reverse order, or when
# scrolling upwards. (Due to the complexity of calculating the vertical
# scroll offset in the `Window` class, lines are not always retrieved in
# order.)
MIN_LINES_BACKWARDS = 50
# When a parser was started this amount of lines back, read the parser
# until we get the current line. Otherwise, start a new parser.
# (This should probably be bigger than MIN_LINES_BACKWARDS.)
REUSE_GENERATOR_MAX_DISTANCE = 100
def __init__(self, pygments_lexer_cls, sync_from_start=True, syntax_sync=None):
assert syntax_sync is None or isinstance(syntax_sync, SyntaxSync)
self.pygments_lexer_cls = pygments_lexer_cls
self.sync_from_start = to_cli_filter(sync_from_start)
# Instantiate the Pygments lexer.
self.pygments_lexer = pygments_lexer_cls(
stripnl=False,
stripall=False,
ensurenl=False)
# Create syntax sync instance.
self.syntax_sync = syntax_sync or RegexSync.from_pygments_lexer_cls(pygments_lexer_cls)
@classmethod
def from_filename(cls, filename, sync_from_start=True):
"""
Create a `Lexer` from a filename.
"""
# Inline imports: the Pygments dependency is optional!
from pygments.util import ClassNotFound
from pygments.lexers import get_lexer_for_filename
try:
pygments_lexer = get_lexer_for_filename(filename)
except ClassNotFound:
return SimpleLexer()
else:
return cls(pygments_lexer.__class__, sync_from_start=sync_from_start)
def lex_document(self, cli, document):
"""
Create a lexer function that takes a line number and returns the list
of (Token, text) tuples as the Pygments lexer returns for that line.
"""
# Cache of already lexed lines.
cache = {}
# Pygments generators that are currently lexing.
line_generators = {} # Map lexer generator to the line number.
def get_syntax_sync():
" The Syntax synchronisation objcet that we currently use. "
if self.sync_from_start(cli):
return SyncFromStart()
else:
return self.syntax_sync
def find_closest_generator(i):
" Return a generator close to line 'i', or None if none was fonud. "
for generator, lineno in line_generators.items():
if lineno < i and i - lineno < self.REUSE_GENERATOR_MAX_DISTANCE:
return generator
def create_line_generator(start_lineno, column=0):
"""
Create a generator that yields the lexed lines.
Each iteration it yields a (line_number, [(token, text), ...]) tuple.
"""
def get_tokens():
text = '\n'.join(document.lines[start_lineno:])[column:]
# We call `get_tokens_unprocessed`, because `get_tokens` will
# still replace \r\n and \r by \n. (We don't want that,
# Pygments should return exactly the same amount of text, as we
# have given as input.)
for _, t, v in self.pygments_lexer.get_tokens_unprocessed(text):
yield t, v
return enumerate(split_lines(get_tokens()), start_lineno)
def get_generator(i):
"""
Find an already started generator that is close, or create a new one.
"""
# Find closest line generator.
generator = find_closest_generator(i)
if generator:
return generator
# No generator found. Determine starting point for the syntax
# synchronisation first.
# Go at least x lines back. (Make scrolling upwards more
# efficient.)
i = max(0, i - self.MIN_LINES_BACKWARDS)
if i == 0:
row = 0
column = 0
else:
row, column = get_syntax_sync().get_sync_start_position(document, i)
# Find generator close to this point, or otherwise create a new one.
generator = find_closest_generator(i)
if generator:
return generator
else:
generator = create_line_generator(row, column)
# If the column is not 0, ignore the first line. (Which is
# incomplete. This happens when the synchronisation algorithm tells
# us to start parsing in the middle of a line.)
if column:
next(generator)
row += 1
line_generators[generator] = row
return generator
def get_line(i):
" Return the tokens for a given line number. "
try:
return cache[i]
except KeyError:
generator = get_generator(i)
# Exhaust the generator, until we find the requested line.
for num, line in generator:
cache[num] = line
if num == i:
line_generators[generator] = i
# Remove the next item from the cache.
# (It could happen that it's already there, because of
# another generator that started filling these lines,
# but we want to synchronise these lines with the
# current lexer's state.)
if num + 1 in cache:
del cache[num + 1]
return cache[num]
return []
return get_line

View File

@@ -0,0 +1,253 @@
"""
Margin implementations for a :class:`~libs.prompt_toolkit.layout.containers.Window`.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
from six.moves import range
from libs.prompt_toolkit.filters import to_cli_filter
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.utils import get_cwidth
from .utils import token_list_to_text
__all__ = (
'Margin',
'NumberredMargin',
'ScrollbarMargin',
'ConditionalMargin',
'PromptMargin',
)
class Margin(with_metaclass(ABCMeta, object)):
"""
Base interface for a margin.
"""
@abstractmethod
def get_width(self, cli, get_ui_content):
"""
Return the width that this margin is going to consume.
:param cli: :class:`.CommandLineInterface` instance.
:param get_ui_content: Callable that asks the user control to create
a :class:`.UIContent` instance. This can be used for instance to
obtain the number of lines.
"""
return 0
@abstractmethod
def create_margin(self, cli, window_render_info, width, height):
"""
Creates a margin.
This should return a list of (Token, text) tuples.
:param cli: :class:`.CommandLineInterface` instance.
:param window_render_info:
:class:`~libs.prompt_toolkit.layout.containers.WindowRenderInfo`
instance, generated after rendering and copying the visible part of
the :class:`~libs.prompt_toolkit.layout.controls.UIControl` into the
:class:`~libs.prompt_toolkit.layout.containers.Window`.
:param width: The width that's available for this margin. (As reported
by :meth:`.get_width`.)
:param height: The height that's available for this margin. (The height
of the :class:`~libs.prompt_toolkit.layout.containers.Window`.)
"""
return []
class NumberredMargin(Margin):
"""
Margin that displays the line numbers.
:param relative: Number relative to the cursor position. Similar to the Vi
'relativenumber' option.
:param display_tildes: Display tildes after the end of the document, just
like Vi does.
"""
def __init__(self, relative=False, display_tildes=False):
self.relative = to_cli_filter(relative)
self.display_tildes = to_cli_filter(display_tildes)
def get_width(self, cli, get_ui_content):
line_count = get_ui_content().line_count
return max(3, len('%s' % line_count) + 1)
def create_margin(self, cli, window_render_info, width, height):
relative = self.relative(cli)
token = Token.LineNumber
token_current = Token.LineNumber.Current
# Get current line number.
current_lineno = window_render_info.ui_content.cursor_position.y
# Construct margin.
result = []
last_lineno = None
for y, lineno in enumerate(window_render_info.displayed_lines):
# Only display line number if this line is not a continuation of the previous line.
if lineno != last_lineno:
if lineno is None:
pass
elif lineno == current_lineno:
# Current line.
if relative:
# Left align current number in relative mode.
result.append((token_current, '%i' % (lineno + 1)))
else:
result.append((token_current, ('%i ' % (lineno + 1)).rjust(width)))
else:
# Other lines.
if relative:
lineno = abs(lineno - current_lineno) - 1
result.append((token, ('%i ' % (lineno + 1)).rjust(width)))
last_lineno = lineno
result.append((Token, '\n'))
# Fill with tildes.
if self.display_tildes(cli):
while y < window_render_info.window_height:
result.append((Token.Tilde, '~\n'))
y += 1
return result
class ConditionalMargin(Margin):
"""
Wrapper around other :class:`.Margin` classes to show/hide them.
"""
def __init__(self, margin, filter):
assert isinstance(margin, Margin)
self.margin = margin
self.filter = to_cli_filter(filter)
def get_width(self, cli, ui_content):
if self.filter(cli):
return self.margin.get_width(cli, ui_content)
else:
return 0
def create_margin(self, cli, window_render_info, width, height):
if width and self.filter(cli):
return self.margin.create_margin(cli, window_render_info, width, height)
else:
return []
class ScrollbarMargin(Margin):
"""
Margin displaying a scrollbar.
:param display_arrows: Display scroll up/down arrows.
"""
def __init__(self, display_arrows=False):
self.display_arrows = to_cli_filter(display_arrows)
def get_width(self, cli, ui_content):
return 1
def create_margin(self, cli, window_render_info, width, height):
total_height = window_render_info.content_height
display_arrows = self.display_arrows(cli)
window_height = window_render_info.window_height
if display_arrows:
window_height -= 2
try:
items_per_row = float(total_height) / min(total_height, window_height)
except ZeroDivisionError:
return []
else:
def is_scroll_button(row):
" True if we should display a button on this row. "
current_row_middle = int((row + .5) * items_per_row)
return current_row_middle in window_render_info.displayed_lines
# Up arrow.
result = []
if display_arrows:
result.extend([
(Token.Scrollbar.Arrow, '^'),
(Token.Scrollbar, '\n')
])
# Scrollbar body.
for i in range(window_height):
if is_scroll_button(i):
result.append((Token.Scrollbar.Button, ' '))
else:
result.append((Token.Scrollbar, ' '))
result.append((Token, '\n'))
# Down arrow
if display_arrows:
result.append((Token.Scrollbar.Arrow, 'v'))
return result
class PromptMargin(Margin):
"""
Create margin that displays a prompt.
This can display one prompt at the first line, and a continuation prompt
(e.g, just dots) on all the following lines.
:param get_prompt_tokens: Callable that takes a CommandLineInterface as
input and returns a list of (Token, type) tuples to be shown as the
prompt at the first line.
:param get_continuation_tokens: Callable that takes a CommandLineInterface
and a width as input and returns a list of (Token, type) tuples for the
next lines of the input.
:param show_numbers: (bool or :class:`~libs.prompt_toolkit.filters.CLIFilter`)
Display line numbers instead of the continuation prompt.
"""
def __init__(self, get_prompt_tokens, get_continuation_tokens=None,
show_numbers=False):
assert callable(get_prompt_tokens)
assert get_continuation_tokens is None or callable(get_continuation_tokens)
show_numbers = to_cli_filter(show_numbers)
self.get_prompt_tokens = get_prompt_tokens
self.get_continuation_tokens = get_continuation_tokens
self.show_numbers = show_numbers
def get_width(self, cli, ui_content):
" Width to report to the `Window`. "
# Take the width from the first line.
text = token_list_to_text(self.get_prompt_tokens(cli))
return get_cwidth(text)
def create_margin(self, cli, window_render_info, width, height):
# First line.
tokens = self.get_prompt_tokens(cli)[:]
# Next lines. (Show line numbering when numbering is enabled.)
if self.get_continuation_tokens:
# Note: we turn this into a list, to make sure that we fail early
# in case `get_continuation_tokens` returns something else,
# like `None`.
tokens2 = list(self.get_continuation_tokens(cli, width))
else:
tokens2 = []
show_numbers = self.show_numbers(cli)
last_y = None
for y in window_render_info.displayed_lines[1:]:
tokens.append((Token, '\n'))
if show_numbers:
if y != last_y:
tokens.append((Token.LineNumber, ('%i ' % (y + 1)).rjust(width)))
else:
tokens.extend(tokens2)
last_y = y
return tokens

View File

@@ -0,0 +1,496 @@
from __future__ import unicode_literals
from six.moves import zip_longest, range
from libs.prompt_toolkit.filters import HasCompletions, IsDone, Condition, to_cli_filter
from libs.prompt_toolkit.mouse_events import MouseEventType
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.utils import get_cwidth
from .containers import Window, HSplit, ConditionalContainer, ScrollOffsets
from .controls import UIControl, UIContent
from .dimension import LayoutDimension
from .margins import ScrollbarMargin
from .screen import Point, Char
import math
__all__ = (
'CompletionsMenu',
'MultiColumnCompletionsMenu',
)
class CompletionsMenuControl(UIControl):
"""
Helper for drawing the complete menu to the screen.
:param scroll_offset: Number (integer) representing the preferred amount of
completions to be displayed before and after the current one. When this
is a very high number, the current completion will be shown in the
middle most of the time.
"""
# Preferred minimum size of the menu control.
# The CompletionsMenu class defines a width of 8, and there is a scrollbar
# of 1.)
MIN_WIDTH = 7
def __init__(self):
self.token = Token.Menu.Completions
def has_focus(self, cli):
return False
def preferred_width(self, cli, max_available_width):
complete_state = cli.current_buffer.complete_state
if complete_state:
menu_width = self._get_menu_width(500, complete_state)
menu_meta_width = self._get_menu_meta_width(500, complete_state)
return menu_width + menu_meta_width
else:
return 0
def preferred_height(self, cli, width, max_available_height, wrap_lines):
complete_state = cli.current_buffer.complete_state
if complete_state:
return len(complete_state.current_completions)
else:
return 0
def create_content(self, cli, width, height):
"""
Create a UIContent object for this control.
"""
complete_state = cli.current_buffer.complete_state
if complete_state:
completions = complete_state.current_completions
index = complete_state.complete_index # Can be None!
# Calculate width of completions menu.
menu_width = self._get_menu_width(width, complete_state)
menu_meta_width = self._get_menu_meta_width(width - menu_width, complete_state)
show_meta = self._show_meta(complete_state)
def get_line(i):
c = completions[i]
is_current_completion = (i == index)
result = self._get_menu_item_tokens(c, is_current_completion, menu_width)
if show_meta:
result += self._get_menu_item_meta_tokens(c, is_current_completion, menu_meta_width)
return result
return UIContent(get_line=get_line,
cursor_position=Point(x=0, y=index or 0),
line_count=len(completions),
default_char=Char(' ', self.token))
return UIContent()
def _show_meta(self, complete_state):
"""
Return ``True`` if we need to show a column with meta information.
"""
return any(c.display_meta for c in complete_state.current_completions)
def _get_menu_width(self, max_width, complete_state):
"""
Return the width of the main column.
"""
return min(max_width, max(self.MIN_WIDTH, max(get_cwidth(c.display)
for c in complete_state.current_completions) + 2))
def _get_menu_meta_width(self, max_width, complete_state):
"""
Return the width of the meta column.
"""
if self._show_meta(complete_state):
return min(max_width, max(get_cwidth(c.display_meta)
for c in complete_state.current_completions) + 2)
else:
return 0
def _get_menu_item_tokens(self, completion, is_current_completion, width):
if is_current_completion:
token = self.token.Completion.Current
else:
token = self.token.Completion
text, tw = _trim_text(completion.display, width - 2)
padding = ' ' * (width - 2 - tw)
return [(token, ' %s%s ' % (text, padding))]
def _get_menu_item_meta_tokens(self, completion, is_current_completion, width):
if is_current_completion:
token = self.token.Meta.Current
else:
token = self.token.Meta
text, tw = _trim_text(completion.display_meta, width - 2)
padding = ' ' * (width - 2 - tw)
return [(token, ' %s%s ' % (text, padding))]
def mouse_handler(self, cli, mouse_event):
"""
Handle mouse events: clicking and scrolling.
"""
b = cli.current_buffer
if mouse_event.event_type == MouseEventType.MOUSE_UP:
# Select completion.
b.go_to_completion(mouse_event.position.y)
b.complete_state = None
elif mouse_event.event_type == MouseEventType.SCROLL_DOWN:
# Scroll up.
b.complete_next(count=3, disable_wrap_around=True)
elif mouse_event.event_type == MouseEventType.SCROLL_UP:
# Scroll down.
b.complete_previous(count=3, disable_wrap_around=True)
def _trim_text(text, max_width):
"""
Trim the text to `max_width`, append dots when the text is too long.
Returns (text, width) tuple.
"""
width = get_cwidth(text)
# When the text is too wide, trim it.
if width > max_width:
# When there are no double width characters, just use slice operation.
if len(text) == width:
trimmed_text = (text[:max(1, max_width-3)] + '...')[:max_width]
return trimmed_text, len(trimmed_text)
# Otherwise, loop until we have the desired width. (Rather
# inefficient, but ok for now.)
else:
trimmed_text = ''
for c in text:
if get_cwidth(trimmed_text + c) <= max_width - 3:
trimmed_text += c
trimmed_text += '...'
return (trimmed_text, get_cwidth(trimmed_text))
else:
return text, width
class CompletionsMenu(ConditionalContainer):
def __init__(self, max_height=None, scroll_offset=0, extra_filter=True, display_arrows=False):
extra_filter = to_cli_filter(extra_filter)
display_arrows = to_cli_filter(display_arrows)
super(CompletionsMenu, self).__init__(
content=Window(
content=CompletionsMenuControl(),
width=LayoutDimension(min=8),
height=LayoutDimension(min=1, max=max_height),
scroll_offsets=ScrollOffsets(top=scroll_offset, bottom=scroll_offset),
right_margins=[ScrollbarMargin(display_arrows=display_arrows)],
dont_extend_width=True,
),
# Show when there are completions but not at the point we are
# returning the input.
filter=HasCompletions() & ~IsDone() & extra_filter)
class MultiColumnCompletionMenuControl(UIControl):
"""
Completion menu that displays all the completions in several columns.
When there are more completions than space for them to be displayed, an
arrow is shown on the left or right side.
`min_rows` indicates how many rows will be available in any possible case.
When this is langer than one, in will try to use less columns and more
rows until this value is reached.
Be careful passing in a too big value, if less than the given amount of
rows are available, more columns would have been required, but
`preferred_width` doesn't know about that and reports a too small value.
This results in less completions displayed and additional scrolling.
(It's a limitation of how the layout engine currently works: first the
widths are calculated, then the heights.)
:param suggested_max_column_width: The suggested max width of a column.
The column can still be bigger than this, but if there is place for two
columns of this width, we will display two columns. This to avoid that
if there is one very wide completion, that it doesn't significantly
reduce the amount of columns.
"""
_required_margin = 3 # One extra padding on the right + space for arrows.
def __init__(self, min_rows=3, suggested_max_column_width=30):
assert isinstance(min_rows, int) and min_rows >= 1
self.min_rows = min_rows
self.suggested_max_column_width = suggested_max_column_width
self.token = Token.Menu.Completions
self.scroll = 0
# Info of last rendering.
self._rendered_rows = 0
self._rendered_columns = 0
self._total_columns = 0
self._render_pos_to_completion = {}
self._render_left_arrow = False
self._render_right_arrow = False
self._render_width = 0
def reset(self):
self.scroll = 0
def has_focus(self, cli):
return False
def preferred_width(self, cli, max_available_width):
"""
Preferred width: prefer to use at least min_rows, but otherwise as much
as possible horizontally.
"""
complete_state = cli.current_buffer.complete_state
column_width = self._get_column_width(complete_state)
result = int(column_width * math.ceil(len(complete_state.current_completions) / float(self.min_rows)))
# When the desired width is still more than the maximum available,
# reduce by removing columns until we are less than the available
# width.
while result > column_width and result > max_available_width - self._required_margin:
result -= column_width
return result + self._required_margin
def preferred_height(self, cli, width, max_available_height, wrap_lines):
"""
Preferred height: as much as needed in order to display all the completions.
"""
complete_state = cli.current_buffer.complete_state
column_width = self._get_column_width(complete_state)
column_count = max(1, (width - self._required_margin) // column_width)
return int(math.ceil(len(complete_state.current_completions) / float(column_count)))
def create_content(self, cli, width, height):
"""
Create a UIContent object for this menu.
"""
complete_state = cli.current_buffer.complete_state
column_width = self._get_column_width(complete_state)
self._render_pos_to_completion = {}
def grouper(n, iterable, fillvalue=None):
" grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx "
args = [iter(iterable)] * n
return zip_longest(fillvalue=fillvalue, *args)
def is_current_completion(completion):
" Returns True when this completion is the currently selected one. "
return complete_state.complete_index is not None and c == complete_state.current_completion
# Space required outside of the regular columns, for displaying the
# left and right arrow.
HORIZONTAL_MARGIN_REQUIRED = 3
if complete_state:
# There should be at least one column, but it cannot be wider than
# the available width.
column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width)
# However, when the columns tend to be very wide, because there are
# some very wide entries, shrink it anyway.
if column_width > self.suggested_max_column_width:
# `column_width` can still be bigger that `suggested_max_column_width`,
# but if there is place for two columns, we divide by two.
column_width //= (column_width // self.suggested_max_column_width)
visible_columns = max(1, (width - self._required_margin) // column_width)
columns_ = list(grouper(height, complete_state.current_completions))
rows_ = list(zip(*columns_))
# Make sure the current completion is always visible: update scroll offset.
selected_column = (complete_state.complete_index or 0) // height
self.scroll = min(selected_column, max(self.scroll, selected_column - visible_columns + 1))
render_left_arrow = self.scroll > 0
render_right_arrow = self.scroll < len(rows_[0]) - visible_columns
# Write completions to screen.
tokens_for_line = []
for row_index, row in enumerate(rows_):
tokens = []
middle_row = row_index == len(rows_) // 2
# Draw left arrow if we have hidden completions on the left.
if render_left_arrow:
tokens += [(Token.Scrollbar, '<' if middle_row else ' ')]
# Draw row content.
for column_index, c in enumerate(row[self.scroll:][:visible_columns]):
if c is not None:
tokens += self._get_menu_item_tokens(c, is_current_completion(c), column_width)
# Remember render position for mouse click handler.
for x in range(column_width):
self._render_pos_to_completion[(column_index * column_width + x, row_index)] = c
else:
tokens += [(self.token.Completion, ' ' * column_width)]
# Draw trailing padding. (_get_menu_item_tokens only returns padding on the left.)
tokens += [(self.token.Completion, ' ')]
# Draw right arrow if we have hidden completions on the right.
if render_right_arrow:
tokens += [(Token.Scrollbar, '>' if middle_row else ' ')]
# Newline.
tokens_for_line.append(tokens)
else:
tokens = []
self._rendered_rows = height
self._rendered_columns = visible_columns
self._total_columns = len(columns_)
self._render_left_arrow = render_left_arrow
self._render_right_arrow = render_right_arrow
self._render_width = column_width * visible_columns + render_left_arrow + render_right_arrow + 1
def get_line(i):
return tokens_for_line[i]
return UIContent(get_line=get_line, line_count=len(rows_))
def _get_column_width(self, complete_state):
"""
Return the width of each column.
"""
return max(get_cwidth(c.display) for c in complete_state.current_completions) + 1
def _get_menu_item_tokens(self, completion, is_current_completion, width):
if is_current_completion:
token = self.token.Completion.Current
else:
token = self.token.Completion
text, tw = _trim_text(completion.display, width)
padding = ' ' * (width - tw - 1)
return [(token, ' %s%s' % (text, padding))]
def mouse_handler(self, cli, mouse_event):
"""
Handle scoll and click events.
"""
b = cli.current_buffer
def scroll_left():
b.complete_previous(count=self._rendered_rows, disable_wrap_around=True)
self.scroll = max(0, self.scroll - 1)
def scroll_right():
b.complete_next(count=self._rendered_rows, disable_wrap_around=True)
self.scroll = min(self._total_columns - self._rendered_columns, self.scroll + 1)
if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
scroll_right()
elif mouse_event.event_type == MouseEventType.SCROLL_UP:
scroll_left()
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
x = mouse_event.position.x
y = mouse_event.position.y
# Mouse click on left arrow.
if x == 0:
if self._render_left_arrow:
scroll_left()
# Mouse click on right arrow.
elif x == self._render_width - 1:
if self._render_right_arrow:
scroll_right()
# Mouse click on completion.
else:
completion = self._render_pos_to_completion.get((x, y))
if completion:
b.apply_completion(completion)
class MultiColumnCompletionsMenu(HSplit):
"""
Container that displays the completions in several columns.
When `show_meta` (a :class:`~libs.prompt_toolkit.filters.CLIFilter`) evaluates
to True, it shows the meta information at the bottom.
"""
def __init__(self, min_rows=3, suggested_max_column_width=30, show_meta=True, extra_filter=True):
show_meta = to_cli_filter(show_meta)
extra_filter = to_cli_filter(extra_filter)
# Display filter: show when there are completions but not at the point
# we are returning the input.
full_filter = HasCompletions() & ~IsDone() & extra_filter
any_completion_has_meta = Condition(lambda cli:
any(c.display_meta for c in cli.current_buffer.complete_state.current_completions))
# Create child windows.
completions_window = ConditionalContainer(
content=Window(
content=MultiColumnCompletionMenuControl(
min_rows=min_rows, suggested_max_column_width=suggested_max_column_width),
width=LayoutDimension(min=8),
height=LayoutDimension(min=1)),
filter=full_filter)
meta_window = ConditionalContainer(
content=Window(content=_SelectedCompletionMetaControl()),
filter=show_meta & full_filter & any_completion_has_meta)
# Initialise split.
super(MultiColumnCompletionsMenu, self).__init__([
completions_window,
meta_window
])
class _SelectedCompletionMetaControl(UIControl):
"""
Control that shows the meta information of the selected token.
"""
def preferred_width(self, cli, max_available_width):
"""
Report the width of the longest meta text as the preferred width of this control.
It could be that we use less width, but this way, we're sure that the
layout doesn't change when we select another completion (E.g. that
completions are suddenly shown in more or fewer columns.)
"""
if cli.current_buffer.complete_state:
state = cli.current_buffer.complete_state
return 2 + max(get_cwidth(c.display_meta) for c in state.current_completions)
else:
return 0
def preferred_height(self, cli, width, max_available_height, wrap_lines):
return 1
def create_content(self, cli, width, height):
tokens = self._get_tokens(cli)
def get_line(i):
return tokens
return UIContent(get_line=get_line, line_count=1 if tokens else 0)
def _get_tokens(self, cli):
token = Token.Menu.Completions.MultiColumnMeta
state = cli.current_buffer.complete_state
if state and state.current_completion and state.current_completion.display_meta:
return [(token, ' %s ' % state.current_completion.display_meta)]
return []

View File

@@ -0,0 +1,29 @@
from __future__ import unicode_literals
from itertools import product
from collections import defaultdict
__all__ = (
'MouseHandlers',
)
class MouseHandlers(object):
"""
Two dimentional raster of callbacks for mouse events.
"""
def __init__(self):
def dummy_callback(cli, mouse_event):
"""
:param mouse_event: `MouseEvent` instance.
"""
# Map (x,y) tuples to handlers.
self.mouse_handlers = defaultdict(lambda: dummy_callback)
def set_mouse_handler_for_range(self, x_min, x_max, y_min, y_max, handler=None):
"""
Set mouse handler for a region.
"""
for x, y in product(range(x_min, x_max), range(y_min, y_max)):
self.mouse_handlers[x,y] = handler

View File

@@ -0,0 +1,605 @@
"""
Processors are little transformation blocks that transform the token list from
a buffer before the BufferControl will render it to the screen.
They can insert tokens before or after, or highlight fragments by replacing the
token types.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
from six.moves import range
from libs.prompt_toolkit.cache import SimpleCache
from libs.prompt_toolkit.document import Document
from libs.prompt_toolkit.enums import SEARCH_BUFFER
from libs.prompt_toolkit.filters import to_cli_filter, ViInsertMultipleMode
from libs.prompt_toolkit.layout.utils import token_list_to_text
from libs.prompt_toolkit.reactive import Integer
from libs.prompt_toolkit.token import Token
from .utils import token_list_len, explode_tokens
import re
__all__ = (
'Processor',
'Transformation',
'HighlightSearchProcessor',
'HighlightSelectionProcessor',
'PasswordProcessor',
'HighlightMatchingBracketProcessor',
'DisplayMultipleCursors',
'BeforeInput',
'AfterInput',
'AppendAutoSuggestion',
'ConditionalProcessor',
'ShowLeadingWhiteSpaceProcessor',
'ShowTrailingWhiteSpaceProcessor',
'TabsProcessor',
)
class Processor(with_metaclass(ABCMeta, object)):
"""
Manipulate the tokens for a given line in a
:class:`~libs.prompt_toolkit.layout.controls.BufferControl`.
"""
@abstractmethod
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
"""
Apply transformation. Returns a :class:`.Transformation` instance.
:param cli: :class:`.CommandLineInterface` instance.
:param lineno: The number of the line to which we apply the processor.
:param source_to_display: A function that returns the position in the
`tokens` for any position in the source string. (This takes
previous processors into account.)
:param tokens: List of tokens that we can transform. (Received from the
previous processor.)
"""
return Transformation(tokens)
def has_focus(self, cli):
"""
Processors can override the focus.
(Used for the reverse-i-search prefix in DefaultPrompt.)
"""
return False
class Transformation(object):
"""
Transformation result, as returned by :meth:`.Processor.apply_transformation`.
Important: Always make sure that the length of `document.text` is equal to
the length of all the text in `tokens`!
:param tokens: The transformed tokens. To be displayed, or to pass to the
next processor.
:param source_to_display: Cursor position transformation from original string to
transformed string.
:param display_to_source: Cursor position transformed from source string to
original string.
"""
def __init__(self, tokens, source_to_display=None, display_to_source=None):
self.tokens = tokens
self.source_to_display = source_to_display or (lambda i: i)
self.display_to_source = display_to_source or (lambda i: i)
class HighlightSearchProcessor(Processor):
"""
Processor that highlights search matches in the document.
Note that this doesn't support multiline search matches yet.
:param preview_search: A Filter; when active it indicates that we take
the search text in real time while the user is typing, instead of the
last active search state.
"""
def __init__(self, preview_search=False, search_buffer_name=SEARCH_BUFFER,
get_search_state=None):
self.preview_search = to_cli_filter(preview_search)
self.search_buffer_name = search_buffer_name
self.get_search_state = get_search_state or (lambda cli: cli.search_state)
def _get_search_text(self, cli):
"""
The text we are searching for.
"""
# When the search buffer has focus, take that text.
if self.preview_search(cli) and cli.buffers[self.search_buffer_name].text:
return cli.buffers[self.search_buffer_name].text
# Otherwise, take the text of the last active search.
else:
return self.get_search_state(cli).text
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
search_text = self._get_search_text(cli)
searchmatch_current_token = (':', ) + Token.SearchMatch.Current
searchmatch_token = (':', ) + Token.SearchMatch
if search_text and not cli.is_returning:
# For each search match, replace the Token.
line_text = token_list_to_text(tokens)
tokens = explode_tokens(tokens)
flags = re.IGNORECASE if cli.is_ignoring_case else 0
# Get cursor column.
if document.cursor_position_row == lineno:
cursor_column = source_to_display(document.cursor_position_col)
else:
cursor_column = None
for match in re.finditer(re.escape(search_text), line_text, flags=flags):
if cursor_column is not None:
on_cursor = match.start() <= cursor_column < match.end()
else:
on_cursor = False
for i in range(match.start(), match.end()):
old_token, text = tokens[i]
if on_cursor:
tokens[i] = (old_token + searchmatch_current_token, tokens[i][1])
else:
tokens[i] = (old_token + searchmatch_token, tokens[i][1])
return Transformation(tokens)
class HighlightSelectionProcessor(Processor):
"""
Processor that highlights the selection in the document.
"""
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
selected_token = (':', ) + Token.SelectedText
# In case of selection, highlight all matches.
selection_at_line = document.selection_range_at_line(lineno)
if selection_at_line:
from_, to = selection_at_line
from_ = source_to_display(from_)
to = source_to_display(to)
tokens = explode_tokens(tokens)
if from_ == 0 and to == 0 and len(tokens) == 0:
# When this is an empty line, insert a space in order to
# visualiase the selection.
return Transformation([(Token.SelectedText, ' ')])
else:
for i in range(from_, to + 1):
if i < len(tokens):
old_token, old_text = tokens[i]
tokens[i] = (old_token + selected_token, old_text)
return Transformation(tokens)
class PasswordProcessor(Processor):
"""
Processor that turns masks the input. (For passwords.)
:param char: (string) Character to be used. "*" by default.
"""
def __init__(self, char='*'):
self.char = char
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
tokens = [(token, self.char * len(text)) for token, text in tokens]
return Transformation(tokens)
class HighlightMatchingBracketProcessor(Processor):
"""
When the cursor is on or right after a bracket, it highlights the matching
bracket.
:param max_cursor_distance: Only highlight matching brackets when the
cursor is within this distance. (From inside a `Processor`, we can't
know which lines will be visible on the screen. But we also don't want
to scan the whole document for matching brackets on each key press, so
we limit to this value.)
"""
_closing_braces = '])}>'
def __init__(self, chars='[](){}<>', max_cursor_distance=1000):
self.chars = chars
self.max_cursor_distance = max_cursor_distance
self._positions_cache = SimpleCache(maxsize=8)
def _get_positions_to_highlight(self, document):
"""
Return a list of (row, col) tuples that need to be highlighted.
"""
# Try for the character under the cursor.
if document.current_char and document.current_char in self.chars:
pos = document.find_matching_bracket_position(
start_pos=document.cursor_position - self.max_cursor_distance,
end_pos=document.cursor_position + self.max_cursor_distance)
# Try for the character before the cursor.
elif (document.char_before_cursor and document.char_before_cursor in
self._closing_braces and document.char_before_cursor in self.chars):
document = Document(document.text, document.cursor_position - 1)
pos = document.find_matching_bracket_position(
start_pos=document.cursor_position - self.max_cursor_distance,
end_pos=document.cursor_position + self.max_cursor_distance)
else:
pos = None
# Return a list of (row, col) tuples that need to be highlighted.
if pos:
pos += document.cursor_position # pos is relative.
row, col = document.translate_index_to_position(pos)
return [(row, col), (document.cursor_position_row, document.cursor_position_col)]
else:
return []
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
# Get the highlight positions.
key = (cli.render_counter, document.text, document.cursor_position)
positions = self._positions_cache.get(
key, lambda: self._get_positions_to_highlight(document))
# Apply if positions were found at this line.
if positions:
for row, col in positions:
if row == lineno:
col = source_to_display(col)
tokens = explode_tokens(tokens)
token, text = tokens[col]
if col == document.cursor_position_col:
token += (':', ) + Token.MatchingBracket.Cursor
else:
token += (':', ) + Token.MatchingBracket.Other
tokens[col] = (token, text)
return Transformation(tokens)
class DisplayMultipleCursors(Processor):
"""
When we're in Vi block insert mode, display all the cursors.
"""
_insert_multiple = ViInsertMultipleMode()
def __init__(self, buffer_name):
self.buffer_name = buffer_name
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
buff = cli.buffers[self.buffer_name]
if self._insert_multiple(cli):
positions = buff.multiple_cursor_positions
tokens = explode_tokens(tokens)
# If any cursor appears on the current line, highlight that.
start_pos = document.translate_row_col_to_index(lineno, 0)
end_pos = start_pos + len(document.lines[lineno])
token_suffix = (':', ) + Token.MultipleCursors.Cursor
for p in positions:
if start_pos <= p < end_pos:
column = source_to_display(p - start_pos)
# Replace token.
token, text = tokens[column]
token += token_suffix
tokens[column] = (token, text)
elif p == end_pos:
tokens.append((token_suffix, ' '))
return Transformation(tokens)
else:
return Transformation(tokens)
class BeforeInput(Processor):
"""
Insert tokens before the input.
:param get_tokens: Callable that takes a
:class:`~libs.prompt_toolkit.interface.CommandLineInterface` and returns the
list of tokens to be inserted.
"""
def __init__(self, get_tokens):
assert callable(get_tokens)
self.get_tokens = get_tokens
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
if lineno == 0:
tokens_before = self.get_tokens(cli)
tokens = tokens_before + tokens
shift_position = token_list_len(tokens_before)
source_to_display = lambda i: i + shift_position
display_to_source = lambda i: i - shift_position
else:
source_to_display = None
display_to_source = None
return Transformation(tokens, source_to_display=source_to_display,
display_to_source=display_to_source)
@classmethod
def static(cls, text, token=Token):
"""
Create a :class:`.BeforeInput` instance that always inserts the same
text.
"""
def get_static_tokens(cli):
return [(token, text)]
return cls(get_static_tokens)
def __repr__(self):
return '%s(get_tokens=%r)' % (
self.__class__.__name__, self.get_tokens)
class AfterInput(Processor):
"""
Insert tokens after the input.
:param get_tokens: Callable that takes a
:class:`~libs.prompt_toolkit.interface.CommandLineInterface` and returns the
list of tokens to be appended.
"""
def __init__(self, get_tokens):
assert callable(get_tokens)
self.get_tokens = get_tokens
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
# Insert tokens after the last line.
if lineno == document.line_count - 1:
return Transformation(tokens=tokens + self.get_tokens(cli))
else:
return Transformation(tokens=tokens)
@classmethod
def static(cls, text, token=Token):
"""
Create a :class:`.AfterInput` instance that always inserts the same
text.
"""
def get_static_tokens(cli):
return [(token, text)]
return cls(get_static_tokens)
def __repr__(self):
return '%s(get_tokens=%r)' % (
self.__class__.__name__, self.get_tokens)
class AppendAutoSuggestion(Processor):
"""
Append the auto suggestion to the input.
(The user can then press the right arrow the insert the suggestion.)
:param buffer_name: The name of the buffer from where we should take the
auto suggestion. If not given, we take the current buffer.
"""
def __init__(self, buffer_name=None, token=Token.AutoSuggestion):
self.buffer_name = buffer_name
self.token = token
def _get_buffer(self, cli):
if self.buffer_name:
return cli.buffers[self.buffer_name]
else:
return cli.current_buffer
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
# Insert tokens after the last line.
if lineno == document.line_count - 1:
buffer = self._get_buffer(cli)
if buffer.suggestion and buffer.document.is_cursor_at_the_end:
suggestion = buffer.suggestion.text
else:
suggestion = ''
return Transformation(tokens=tokens + [(self.token, suggestion)])
else:
return Transformation(tokens=tokens)
class ShowLeadingWhiteSpaceProcessor(Processor):
"""
Make leading whitespace visible.
:param get_char: Callable that takes a :class:`CommandLineInterface`
instance and returns one character.
:param token: Token to be used.
"""
def __init__(self, get_char=None, token=Token.LeadingWhiteSpace):
assert get_char is None or callable(get_char)
if get_char is None:
def get_char(cli):
if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?':
return '.'
else:
return '\xb7'
self.token = token
self.get_char = get_char
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
# Walk through all te tokens.
if tokens and token_list_to_text(tokens).startswith(' '):
t = (self.token, self.get_char(cli))
tokens = explode_tokens(tokens)
for i in range(len(tokens)):
if tokens[i][1] == ' ':
tokens[i] = t
else:
break
return Transformation(tokens)
class ShowTrailingWhiteSpaceProcessor(Processor):
"""
Make trailing whitespace visible.
:param get_char: Callable that takes a :class:`CommandLineInterface`
instance and returns one character.
:param token: Token to be used.
"""
def __init__(self, get_char=None, token=Token.TrailingWhiteSpace):
assert get_char is None or callable(get_char)
if get_char is None:
def get_char(cli):
if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?':
return '.'
else:
return '\xb7'
self.token = token
self.get_char = get_char
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
if tokens and tokens[-1][1].endswith(' '):
t = (self.token, self.get_char(cli))
tokens = explode_tokens(tokens)
# Walk backwards through all te tokens and replace whitespace.
for i in range(len(tokens) - 1, -1, -1):
char = tokens[i][1]
if char == ' ':
tokens[i] = t
else:
break
return Transformation(tokens)
class TabsProcessor(Processor):
"""
Render tabs as spaces (instead of ^I) or make them visible (for instance,
by replacing them with dots.)
:param tabstop: (Integer) Horizontal space taken by a tab.
:param get_char1: Callable that takes a `CommandLineInterface` and return a
character (text of length one). This one is used for the first space
taken by the tab.
:param get_char2: Like `get_char1`, but for the rest of the space.
"""
def __init__(self, tabstop=4, get_char1=None, get_char2=None, token=Token.Tab):
assert isinstance(tabstop, Integer)
assert get_char1 is None or callable(get_char1)
assert get_char2 is None or callable(get_char2)
self.get_char1 = get_char1 or get_char2 or (lambda cli: '|')
self.get_char2 = get_char2 or get_char1 or (lambda cli: '\u2508')
self.tabstop = tabstop
self.token = token
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
tabstop = int(self.tabstop)
token = self.token
# Create separator for tabs.
separator1 = self.get_char1(cli)
separator2 = self.get_char2(cli)
# Transform tokens.
tokens = explode_tokens(tokens)
position_mappings = {}
result_tokens = []
pos = 0
for i, token_and_text in enumerate(tokens):
position_mappings[i] = pos
if token_and_text[1] == '\t':
# Calculate how many characters we have to insert.
count = tabstop - (pos % tabstop)
if count == 0:
count = tabstop
# Insert tab.
result_tokens.append((token, separator1))
result_tokens.append((token, separator2 * (count - 1)))
pos += count
else:
result_tokens.append(token_and_text)
pos += 1
position_mappings[len(tokens)] = pos
def source_to_display(from_position):
" Maps original cursor position to the new one. "
return position_mappings[from_position]
def display_to_source(display_pos):
" Maps display cursor position to the original one. "
position_mappings_reversed = dict((v, k) for k, v in position_mappings.items())
while display_pos >= 0:
try:
return position_mappings_reversed[display_pos]
except KeyError:
display_pos -= 1
return 0
return Transformation(
result_tokens,
source_to_display=source_to_display,
display_to_source=display_to_source)
class ConditionalProcessor(Processor):
"""
Processor that applies another processor, according to a certain condition.
Example::
# Create a function that returns whether or not the processor should
# currently be applied.
def highlight_enabled(cli):
return true_or_false
# Wrapt it in a `ConditionalProcessor` for usage in a `BufferControl`.
BufferControl(input_processors=[
ConditionalProcessor(HighlightSearchProcessor(),
Condition(highlight_enabled))])
:param processor: :class:`.Processor` instance.
:param filter: :class:`~libs.prompt_toolkit.filters.CLIFilter` instance.
"""
def __init__(self, processor, filter):
assert isinstance(processor, Processor)
self.processor = processor
self.filter = to_cli_filter(filter)
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
# Run processor when enabled.
if self.filter(cli):
return self.processor.apply_transformation(
cli, document, lineno, source_to_display, tokens)
else:
return Transformation(tokens)
def has_focus(self, cli):
if self.filter(cli):
return self.processor.has_focus(cli)
else:
return False
def __repr__(self):
return '%s(processor=%r, filter=%r)' % (
self.__class__.__name__, self.processor, self.filter)

View File

@@ -0,0 +1,111 @@
from __future__ import unicode_literals
from six import text_type
from libs.prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER
from libs.prompt_toolkit.token import Token
from .utils import token_list_len
from .processors import Processor, Transformation
__all__ = (
'DefaultPrompt',
)
class DefaultPrompt(Processor):
"""
Default prompt. This one shows the 'arg' and reverse search like
Bash/readline normally do.
There are two ways to instantiate a ``DefaultPrompt``. For a prompt
with a static message, do for instance::
prompt = DefaultPrompt.from_message('prompt> ')
For a dynamic prompt, generated from a token list function::
def get_tokens(cli):
return [(Token.A, 'text'), (Token.B, 'text2')]
prompt = DefaultPrompt(get_tokens)
"""
def __init__(self, get_tokens):
assert callable(get_tokens)
self.get_tokens = get_tokens
@classmethod
def from_message(cls, message='> '):
"""
Create a default prompt with a static message text.
"""
assert isinstance(message, text_type)
def get_message_tokens(cli):
return [(Token.Prompt, message)]
return cls(get_message_tokens)
def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
# Get text before cursor.
if cli.is_searching:
before = _get_isearch_tokens(cli)
elif cli.input_processor.arg is not None:
before = _get_arg_tokens(cli)
else:
before = self.get_tokens(cli)
# Insert before buffer text.
shift_position = token_list_len(before)
# Only show the prompt before the first line. For the following lines,
# only indent using spaces.
if lineno != 0:
before = [(Token.Prompt, ' ' * shift_position)]
return Transformation(
tokens=before + tokens,
source_to_display=lambda i: i + shift_position,
display_to_source=lambda i: i - shift_position)
def has_focus(self, cli):
# Obtain focus when the CLI is searching.
# Usually, when using this `DefaultPrompt`, we don't have a
# `BufferControl` instance that displays the content of the search
# buffer. Instead the search text is displayed before the current text.
# So, we can still show the cursor here, while it's actually not this
# buffer that's focussed.
return cli.is_searching
def _get_isearch_tokens(cli):
def before():
if cli.search_state.direction == IncrementalSearchDirection.BACKWARD:
text = 'reverse-i-search'
else:
text = 'i-search'
return [(Token.Prompt.Search, '(%s)`' % text)]
def text():
return [(Token.Prompt.Search.Text, cli.buffers[SEARCH_BUFFER].text)]
def after():
return [(Token.Prompt.Search, '`: ')]
return before() + text() + after()
def _get_arg_tokens(cli):
"""
Tokens for the arg-prompt.
"""
arg = cli.input_processor.arg
return [
(Token.Prompt.Arg, '(arg: '),
(Token.Prompt.Arg.Text, str(arg)),
(Token.Prompt.Arg, ') '),
]

View File

@@ -0,0 +1,151 @@
from __future__ import unicode_literals
from libs.prompt_toolkit.cache import FastDictCache
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.utils import get_cwidth
from collections import defaultdict, namedtuple
__all__ = (
'Point',
'Size',
'Screen',
'Char',
)
Point = namedtuple('Point', 'y x')
Size = namedtuple('Size', 'rows columns')
class Char(object):
"""
Represent a single character in a :class:`.Screen`.
This should be considered immutable.
"""
__slots__ = ('char', 'token', 'width')
# If we end up having one of these special control sequences in the input string,
# we should display them as follows:
# Usually this happens after a "quoted insert".
display_mappings = {
'\x00': '^@', # Control space
'\x01': '^A',
'\x02': '^B',
'\x03': '^C',
'\x04': '^D',
'\x05': '^E',
'\x06': '^F',
'\x07': '^G',
'\x08': '^H',
'\x09': '^I',
'\x0a': '^J',
'\x0b': '^K',
'\x0c': '^L',
'\x0d': '^M',
'\x0e': '^N',
'\x0f': '^O',
'\x10': '^P',
'\x11': '^Q',
'\x12': '^R',
'\x13': '^S',
'\x14': '^T',
'\x15': '^U',
'\x16': '^V',
'\x17': '^W',
'\x18': '^X',
'\x19': '^Y',
'\x1a': '^Z',
'\x1b': '^[', # Escape
'\x1c': '^\\',
'\x1d': '^]',
'\x1f': '^_',
'\x7f': '^?', # Backspace
}
def __init__(self, char=' ', token=Token):
# If this character has to be displayed otherwise, take that one.
char = self.display_mappings.get(char, char)
self.char = char
self.token = token
# Calculate width. (We always need this, so better to store it directly
# as a member for performance.)
self.width = get_cwidth(char)
def __eq__(self, other):
return self.char == other.char and self.token == other.token
def __ne__(self, other):
# Not equal: We don't do `not char.__eq__` here, because of the
# performance of calling yet another function.
return self.char != other.char or self.token != other.token
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.char, self.token)
_CHAR_CACHE = FastDictCache(Char, size=1000 * 1000)
Transparent = Token.Transparent
class Screen(object):
"""
Two dimentional buffer of :class:`.Char` instances.
"""
def __init__(self, default_char=None, initial_width=0, initial_height=0):
if default_char is None:
default_char = _CHAR_CACHE[' ', Transparent]
self.data_buffer = defaultdict(lambda: defaultdict(lambda: default_char))
#: Escape sequences to be injected.
self.zero_width_escapes = defaultdict(lambda: defaultdict(lambda: ''))
#: Position of the cursor.
self.cursor_position = Point(y=0, x=0)
#: Visibility of the cursor.
self.show_cursor = True
#: (Optional) Where to position the menu. E.g. at the start of a completion.
#: (We can't use the cursor position, because we don't want the
#: completion menu to change its position when we browse through all the
#: completions.)
self.menu_position = None
#: Currently used width/height of the screen. This will increase when
#: data is written to the screen.
self.width = initial_width or 0
self.height = initial_height or 0
def replace_all_tokens(self, token):
"""
For all the characters in the screen. Set the token to the given `token`.
"""
b = self.data_buffer
for y, row in b.items():
for x, char in row.items():
b[y][x] = _CHAR_CACHE[char.char, token]
class WritePosition(object):
def __init__(self, xpos, ypos, width, height, extended_height=None):
assert height >= 0
assert extended_height is None or extended_height >= 0
assert width >= 0
# xpos and ypos can be negative. (A float can be partially visible.)
self.xpos = xpos
self.ypos = ypos
self.width = width
self.height = height
self.extended_height = extended_height or height
def __repr__(self):
return '%s(%r, %r, %r, %r, %r)' % (
self.__class__.__name__,
self.xpos, self.ypos, self.width, self.height, self.extended_height)

View File

@@ -0,0 +1,209 @@
from __future__ import unicode_literals
from ..enums import IncrementalSearchDirection
from .processors import BeforeInput
from .lexers import SimpleLexer
from .dimension import LayoutDimension
from .controls import BufferControl, TokenListControl, UIControl, UIContent
from .containers import Window, ConditionalContainer
from .screen import Char
from .utils import token_list_len
from libs.prompt_toolkit.enums import SEARCH_BUFFER, SYSTEM_BUFFER
from libs.prompt_toolkit.filters import HasFocus, HasArg, HasCompletions, HasValidationError, HasSearch, Always, IsDone
from libs.prompt_toolkit.token import Token
__all__ = (
'TokenListToolbar',
'ArgToolbar',
'CompletionsToolbar',
'SearchToolbar',
'SystemToolbar',
'ValidationToolbar',
)
class TokenListToolbar(ConditionalContainer):
def __init__(self, get_tokens, filter=Always(), **kw):
super(TokenListToolbar, self).__init__(
content=Window(
TokenListControl(get_tokens, **kw),
height=LayoutDimension.exact(1)),
filter=filter)
class SystemToolbarControl(BufferControl):
def __init__(self):
token = Token.Toolbar.System
super(SystemToolbarControl, self).__init__(
buffer_name=SYSTEM_BUFFER,
default_char=Char(token=token),
lexer=SimpleLexer(token=token.Text),
input_processors=[BeforeInput.static('Shell command: ', token)],)
class SystemToolbar(ConditionalContainer):
def __init__(self):
super(SystemToolbar, self).__init__(
content=Window(
SystemToolbarControl(),
height=LayoutDimension.exact(1)),
filter=HasFocus(SYSTEM_BUFFER) & ~IsDone())
class ArgToolbarControl(TokenListControl):
def __init__(self):
def get_tokens(cli):
arg = cli.input_processor.arg
if arg == '-':
arg = '-1'
return [
(Token.Toolbar.Arg, 'Repeat: '),
(Token.Toolbar.Arg.Text, arg),
]
super(ArgToolbarControl, self).__init__(get_tokens)
class ArgToolbar(ConditionalContainer):
def __init__(self):
super(ArgToolbar, self).__init__(
content=Window(
ArgToolbarControl(),
height=LayoutDimension.exact(1)),
filter=HasArg())
class SearchToolbarControl(BufferControl):
"""
:param vi_mode: Display '/' and '?' instead of I-search.
"""
def __init__(self, vi_mode=False):
token = Token.Toolbar.Search
def get_before_input(cli):
if not cli.is_searching:
text = ''
elif cli.search_state.direction == IncrementalSearchDirection.BACKWARD:
text = ('?' if vi_mode else 'I-search backward: ')
else:
text = ('/' if vi_mode else 'I-search: ')
return [(token, text)]
super(SearchToolbarControl, self).__init__(
buffer_name=SEARCH_BUFFER,
input_processors=[BeforeInput(get_before_input)],
default_char=Char(token=token),
lexer=SimpleLexer(token=token.Text))
class SearchToolbar(ConditionalContainer):
def __init__(self, vi_mode=False):
super(SearchToolbar, self).__init__(
content=Window(
SearchToolbarControl(vi_mode=vi_mode),
height=LayoutDimension.exact(1)),
filter=HasSearch() & ~IsDone())
class CompletionsToolbarControl(UIControl):
token = Token.Toolbar.Completions
def create_content(self, cli, width, height):
complete_state = cli.current_buffer.complete_state
if complete_state:
completions = complete_state.current_completions
index = complete_state.complete_index # Can be None!
# Width of the completions without the left/right arrows in the margins.
content_width = width - 6
# Booleans indicating whether we stripped from the left/right
cut_left = False
cut_right = False
# Create Menu content.
tokens = []
for i, c in enumerate(completions):
# When there is no more place for the next completion
if token_list_len(tokens) + len(c.display) >= content_width:
# If the current one was not yet displayed, page to the next sequence.
if i <= (index or 0):
tokens = []
cut_left = True
# If the current one is visible, stop here.
else:
cut_right = True
break
tokens.append((self.token.Completion.Current if i == index else self.token.Completion, c.display))
tokens.append((self.token, ' '))
# Extend/strip until the content width.
tokens.append((self.token, ' ' * (content_width - token_list_len(tokens))))
tokens = tokens[:content_width]
# Return tokens
all_tokens = [
(self.token, ' '),
(self.token.Arrow, '<' if cut_left else ' '),
(self.token, ' '),
] + tokens + [
(self.token, ' '),
(self.token.Arrow, '>' if cut_right else ' '),
(self.token, ' '),
]
else:
all_tokens = []
def get_line(i):
return all_tokens
return UIContent(get_line=get_line, line_count=1)
class CompletionsToolbar(ConditionalContainer):
def __init__(self, extra_filter=Always()):
super(CompletionsToolbar, self).__init__(
content=Window(
CompletionsToolbarControl(),
height=LayoutDimension.exact(1)),
filter=HasCompletions() & ~IsDone() & extra_filter)
class ValidationToolbarControl(TokenListControl):
def __init__(self, show_position=False):
token = Token.Toolbar.Validation
def get_tokens(cli):
buffer = cli.current_buffer
if buffer.validation_error:
row, column = buffer.document.translate_index_to_position(
buffer.validation_error.cursor_position)
if show_position:
text = '%s (line=%s column=%s)' % (
buffer.validation_error.message, row + 1, column + 1)
else:
text = buffer.validation_error.message
return [(token, text)]
else:
return []
super(ValidationToolbarControl, self).__init__(get_tokens)
class ValidationToolbar(ConditionalContainer):
def __init__(self, show_position=False):
super(ValidationToolbar, self).__init__(
content=Window(
ValidationToolbarControl(show_position=show_position),
height=LayoutDimension.exact(1)),
filter=HasValidationError() & ~IsDone())

View File

@@ -0,0 +1,181 @@
from __future__ import unicode_literals
from libs.prompt_toolkit.utils import get_cwidth
from libs.prompt_toolkit.token import Token
__all__ = (
'token_list_len',
'token_list_width',
'token_list_to_text',
'explode_tokens',
'split_lines',
'find_window_for_buffer_name',
)
def token_list_len(tokenlist):
"""
Return the amount of characters in this token list.
:param tokenlist: List of (token, text) or (token, text, mouse_handler)
tuples.
"""
ZeroWidthEscape = Token.ZeroWidthEscape
return sum(len(item[1]) for item in tokenlist if item[0] != ZeroWidthEscape)
def token_list_width(tokenlist):
"""
Return the character width of this token list.
(Take double width characters into account.)
:param tokenlist: List of (token, text) or (token, text, mouse_handler)
tuples.
"""
ZeroWidthEscape = Token.ZeroWidthEscape
return sum(get_cwidth(c) for item in tokenlist for c in item[1] if item[0] != ZeroWidthEscape)
def token_list_to_text(tokenlist):
"""
Concatenate all the text parts again.
"""
ZeroWidthEscape = Token.ZeroWidthEscape
return ''.join(item[1] for item in tokenlist if item[0] != ZeroWidthEscape)
def iter_token_lines(tokenlist):
"""
Iterator that yields tokenlists for each line.
"""
line = []
for token, c in explode_tokens(tokenlist):
line.append((token, c))
if c == '\n':
yield line
line = []
yield line
def split_lines(tokenlist):
"""
Take a single list of (Token, text) tuples and yield one such list for each
line. Just like str.split, this will yield at least one item.
:param tokenlist: List of (token, text) or (token, text, mouse_handler)
tuples.
"""
line = []
for item in tokenlist:
# For (token, text) tuples.
if len(item) == 2:
token, string = item
parts = string.split('\n')
for part in parts[:-1]:
if part:
line.append((token, part))
yield line
line = []
line.append((token, parts[-1]))
# Note that parts[-1] can be empty, and that's fine. It happens
# in the case of [(Token.SetCursorPosition, '')].
# For (token, text, mouse_handler) tuples.
# I know, partly copy/paste, but understandable and more efficient
# than many tests.
else:
token, string, mouse_handler = item
parts = string.split('\n')
for part in parts[:-1]:
if part:
line.append((token, part, mouse_handler))
yield line
line = []
line.append((token, parts[-1], mouse_handler))
# Always yield the last line, even when this is an empty line. This ensures
# that when `tokenlist` ends with a newline character, an additional empty
# line is yielded. (Otherwise, there's no way to differentiate between the
# cases where `tokenlist` does and doesn't end with a newline.)
yield line
class _ExplodedList(list):
"""
Wrapper around a list, that marks it as 'exploded'.
As soon as items are added or the list is extended, the new items are
automatically exploded as well.
"""
def __init__(self, *a, **kw):
super(_ExplodedList, self).__init__(*a, **kw)
self.exploded = True
def append(self, item):
self.extend([item])
def extend(self, lst):
super(_ExplodedList, self).extend(explode_tokens(lst))
def insert(self, index, item):
raise NotImplementedError # TODO
# TODO: When creating a copy() or [:], return also an _ExplodedList.
def __setitem__(self, index, value):
"""
Ensure that when `(Token, 'long string')` is set, the string will be
exploded.
"""
if not isinstance(index, slice):
index = slice(index, index + 1)
value = explode_tokens([value])
super(_ExplodedList, self).__setitem__(index, value)
def explode_tokens(tokenlist):
"""
Turn a list of (token, text) tuples into another list where each string is
exactly one character.
It should be fine to call this function several times. Calling this on a
list that is already exploded, is a null operation.
:param tokenlist: List of (token, text) tuples.
"""
# When the tokenlist is already exploded, don't explode again.
if getattr(tokenlist, 'exploded', False):
return tokenlist
result = []
for token, string in tokenlist:
for c in string:
result.append((token, c))
return _ExplodedList(result)
def find_window_for_buffer_name(cli, buffer_name):
"""
Look for a :class:`~libs.prompt_toolkit.layout.containers.Window` in the Layout
that contains the :class:`~libs.prompt_toolkit.layout.controls.BufferControl`
for the given buffer and return it. If no such Window is found, return None.
"""
from libs.prompt_toolkit.interface import CommandLineInterface
assert isinstance(cli, CommandLineInterface)
from .containers import Window
from .controls import BufferControl
for l in cli.layout.walk(cli):
if isinstance(l, Window) and isinstance(l.content, BufferControl):
if l.content.buffer_name == buffer_name:
return l

View File

@@ -0,0 +1,48 @@
"""
Mouse events.
How it works
------------
The renderer has a 2 dimensional grid of mouse event handlers.
(`libs.prompt_toolkit.layout.MouseHandlers`.) When the layout is rendered, the
`Window` class will make sure that this grid will also be filled with
callbacks. For vt100 terminals, mouse events are received through stdin, just
like any other key press. There is a handler among the key bindings that
catches these events and forwards them to such a mouse event handler. It passes
through the `Window` class where the coordinates are translated from absolute
coordinates to coordinates relative to the user control, and there
`UIControl.mouse_handler` is called.
"""
from __future__ import unicode_literals
__all__ = (
'MouseEventType',
'MouseEvent'
)
class MouseEventType:
MOUSE_UP = 'MOUSE_UP'
MOUSE_DOWN = 'MOUSE_DOWN'
SCROLL_UP = 'SCROLL_UP'
SCROLL_DOWN = 'SCROLL_DOWN'
MouseEventTypes = MouseEventType # Deprecated: plural for backwards compatibility.
class MouseEvent(object):
"""
Mouse event, sent to `UIControl.mouse_handler`.
:param position: `Point` instance.
:param event_type: `MouseEventType`.
"""
def __init__(self, position, event_type):
self.position = position
self.event_type = event_type
def __repr__(self):
return 'MouseEvent(%r, %r)' % (self.position, self.event_type)

View File

@@ -0,0 +1,192 @@
"""
Interface for an output.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
from libs.prompt_toolkit.layout.screen import Size
__all__ = (
'Output',
)
class Output(with_metaclass(ABCMeta, object)):
"""
Base class defining the output interface for a
:class:`~libs.prompt_toolkit.renderer.Renderer`.
Actual implementations are
:class:`~libs.prompt_toolkit.terminal.vt100_output.Vt100_Output` and
:class:`~libs.prompt_toolkit.terminal.win32_output.Win32Output`.
"""
@abstractmethod
def fileno(self):
" Return the file descriptor to which we can write for the output. "
@abstractmethod
def encoding(self):
"""
Return the encoding for this output, e.g. 'utf-8'.
(This is used mainly to know which characters are supported by the
output the data, so that the UI can provide alternatives, when
required.)
"""
@abstractmethod
def write(self, data):
" Write text (Terminal escape sequences will be removed/escaped.) "
@abstractmethod
def write_raw(self, data):
" Write text. "
@abstractmethod
def set_title(self, title):
" Set terminal title. "
@abstractmethod
def clear_title(self):
" Clear title again. (or restore previous title.) "
@abstractmethod
def flush(self):
" Write to output stream and flush. "
@abstractmethod
def erase_screen(self):
"""
Erases the screen with the background colour and moves the cursor to
home.
"""
@abstractmethod
def enter_alternate_screen(self):
" Go to the alternate screen buffer. (For full screen applications). "
@abstractmethod
def quit_alternate_screen(self):
" Leave the alternate screen buffer. "
@abstractmethod
def enable_mouse_support(self):
" Enable mouse. "
@abstractmethod
def disable_mouse_support(self):
" Disable mouse. "
@abstractmethod
def erase_end_of_line(self):
"""
Erases from the current cursor position to the end of the current line.
"""
@abstractmethod
def erase_down(self):
"""
Erases the screen from the current line down to the bottom of the
screen.
"""
@abstractmethod
def reset_attributes(self):
" Reset color and styling attributes. "
@abstractmethod
def set_attributes(self, attrs):
" Set new color and styling attributes. "
@abstractmethod
def disable_autowrap(self):
" Disable auto line wrapping. "
@abstractmethod
def enable_autowrap(self):
" Enable auto line wrapping. "
@abstractmethod
def cursor_goto(self, row=0, column=0):
" Move cursor position. "
@abstractmethod
def cursor_up(self, amount):
" Move cursor `amount` place up. "
@abstractmethod
def cursor_down(self, amount):
" Move cursor `amount` place down. "
@abstractmethod
def cursor_forward(self, amount):
" Move cursor `amount` place forward. "
@abstractmethod
def cursor_backward(self, amount):
" Move cursor `amount` place backward. "
@abstractmethod
def hide_cursor(self):
" Hide cursor. "
@abstractmethod
def show_cursor(self):
" Show cursor. "
def ask_for_cpr(self):
"""
Asks for a cursor position report (CPR).
(VT100 only.)
"""
def bell(self):
" Sound bell. "
def enable_bracketed_paste(self):
" For vt100 only. "
def disable_bracketed_paste(self):
" For vt100 only. "
class DummyOutput(Output):
"""
For testing. An output class that doesn't render anything.
"""
def fileno(self):
" There is no sensible default for fileno(). "
raise NotImplementedError
def encoding(self):
return 'utf-8'
def write(self, data): pass
def write_raw(self, data): pass
def set_title(self, title): pass
def clear_title(self): pass
def flush(self): pass
def erase_screen(self): pass
def enter_alternate_screen(self): pass
def quit_alternate_screen(self): pass
def enable_mouse_support(self): pass
def disable_mouse_support(self): pass
def erase_end_of_line(self): pass
def erase_down(self): pass
def reset_attributes(self): pass
def set_attributes(self, attrs): pass
def disable_autowrap(self): pass
def enable_autowrap(self): pass
def cursor_goto(self, row=0, column=0): pass
def cursor_up(self, amount): pass
def cursor_down(self, amount): pass
def cursor_forward(self, amount): pass
def cursor_backward(self, amount): pass
def hide_cursor(self): pass
def show_cursor(self): pass
def ask_for_cpr(self): pass
def bell(self): pass
def enable_bracketed_paste(self): pass
def disable_bracketed_paste(self): pass
def get_size(self):
return Size(rows=40, columns=80)

View File

@@ -0,0 +1,56 @@
"""
Prompt_toolkit is designed a way that the amount of changing state is reduced
to a minimum. Where possible, code is written in a pure functional way. In
general, this results in code where the flow is very easy to follow: the value
of a variable can be deducted from its first assignment.
However, often, practicality and performance beat purity and some classes still
have a changing state. In order to not having to care too much about
transferring states between several components we use some reactive
programming. Actually some kind of data binding.
We introduce two types:
- Filter: for binding a boolean state. They can be chained using & and |
operators. Have a look in the ``filters`` module. Resolving the actual value
of a filter happens by calling it.
- Integer: for binding integer values. Reactive operations (like addition and
substraction) are not suppported. Resolving the actual value happens by
casting it to int, like ``int(integer)``. This way, it is possible to use
normal integers as well for static values.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from six import with_metaclass
class Integer(with_metaclass(ABCMeta, object)):
"""
Reactive integer -- anything that can be resolved to an ``int``.
"""
@abstractmethod
def __int__(self):
return 0
@classmethod
def from_callable(cls, func):
"""
Create an Integer-like object that calls the given function when it is
resolved to an int.
"""
return _IntegerFromCallable(func)
Integer.register(int)
class _IntegerFromCallable(Integer):
def __init__(self, func=0):
self.func = func
def __repr__(self):
return 'Integer.from_callable(%r)' % self.func
def __int__(self):
return int(self.func())

View File

@@ -0,0 +1,526 @@
"""
Renders the command line on the console.
(Redraws parts of the input line that were changed.)
"""
from __future__ import unicode_literals
from libs.prompt_toolkit.filters import to_cli_filter
from libs.prompt_toolkit.layout.mouse_handlers import MouseHandlers
from libs.prompt_toolkit.layout.screen import Point, Screen, WritePosition
from libs.prompt_toolkit.output import Output
from libs.prompt_toolkit.styles import Style
from libs.prompt_toolkit.token import Token
from libs.prompt_toolkit.utils import is_windows
from six.moves import range
__all__ = (
'Renderer',
'print_tokens',
)
def _output_screen_diff(output, screen, current_pos, previous_screen=None, last_token=None,
is_done=False, attrs_for_token=None, size=None, previous_width=0): # XXX: drop is_done
"""
Render the diff between this screen and the previous screen.
This takes two `Screen` instances. The one that represents the output like
it was during the last rendering and one that represents the current
output raster. Looking at these two `Screen` instances, this function will
render the difference by calling the appropriate methods of the `Output`
object that only paint the changes to the terminal.
This is some performance-critical code which is heavily optimized.
Don't change things without profiling first.
:param current_pos: Current cursor position.
:param last_token: `Token` instance that represents the output attributes of
the last drawn character. (Color/attributes.)
:param attrs_for_token: :class:`._TokenToAttrsCache` instance.
:param width: The width of the terminal.
:param prevous_width: The width of the terminal during the last rendering.
"""
width, height = size.columns, size.rows
#: Remember the last printed character.
last_token = [last_token] # nonlocal
#: Variable for capturing the output.
write = output.write
write_raw = output.write_raw
# Create locals for the most used output methods.
# (Save expensive attribute lookups.)
_output_set_attributes = output.set_attributes
_output_reset_attributes = output.reset_attributes
_output_cursor_forward = output.cursor_forward
_output_cursor_up = output.cursor_up
_output_cursor_backward = output.cursor_backward
# Hide cursor before rendering. (Avoid flickering.)
output.hide_cursor()
def reset_attributes():
" Wrapper around Output.reset_attributes. "
_output_reset_attributes()
last_token[0] = None # Forget last char after resetting attributes.
def move_cursor(new):
" Move cursor to this `new` point. Returns the given Point. "
current_x, current_y = current_pos.x, current_pos.y
if new.y > current_y:
# Use newlines instead of CURSOR_DOWN, because this meight add new lines.
# CURSOR_DOWN will never create new lines at the bottom.
# Also reset attributes, otherwise the newline could draw a
# background color.
reset_attributes()
write('\r\n' * (new.y - current_y))
current_x = 0
_output_cursor_forward(new.x)
return new
elif new.y < current_y:
_output_cursor_up(current_y - new.y)
if current_x >= width - 1:
write('\r')
_output_cursor_forward(new.x)
elif new.x < current_x or current_x >= width - 1:
_output_cursor_backward(current_x - new.x)
elif new.x > current_x:
_output_cursor_forward(new.x - current_x)
return new
def output_char(char):
"""
Write the output of this character.
"""
# If the last printed character has the same token, it also has the
# same style, so we don't output it.
the_last_token = last_token[0]
if the_last_token and the_last_token == char.token:
write(char.char)
else:
_output_set_attributes(attrs_for_token[char.token])
write(char.char)
last_token[0] = char.token
# Disable autowrap
if not previous_screen:
output.disable_autowrap()
reset_attributes()
# When the previous screen has a different size, redraw everything anyway.
# Also when we are done. (We meight take up less rows, so clearing is important.)
if is_done or not previous_screen or previous_width != width: # XXX: also consider height??
current_pos = move_cursor(Point(0, 0))
reset_attributes()
output.erase_down()
previous_screen = Screen()
# Get height of the screen.
# (height changes as we loop over data_buffer, so remember the current value.)
# (Also make sure to clip the height to the size of the output.)
current_height = min(screen.height, height)
# Loop over the rows.
row_count = min(max(screen.height, previous_screen.height), height)
c = 0 # Column counter.
for y in range(row_count):
new_row = screen.data_buffer[y]
previous_row = previous_screen.data_buffer[y]
zero_width_escapes_row = screen.zero_width_escapes[y]
new_max_line_len = min(width - 1, max(new_row.keys()) if new_row else 0)
previous_max_line_len = min(width - 1, max(previous_row.keys()) if previous_row else 0)
# Loop over the columns.
c = 0
while c < new_max_line_len + 1:
new_char = new_row[c]
old_char = previous_row[c]
char_width = (new_char.width or 1)
# When the old and new character at this position are different,
# draw the output. (Because of the performance, we don't call
# `Char.__ne__`, but inline the same expression.)
if new_char.char != old_char.char or new_char.token != old_char.token:
current_pos = move_cursor(Point(y=y, x=c))
# Send injected escape sequences to output.
if c in zero_width_escapes_row:
write_raw(zero_width_escapes_row[c])
output_char(new_char)
current_pos = current_pos._replace(x=current_pos.x + char_width)
c += char_width
# If the new line is shorter, trim it.
if previous_screen and new_max_line_len < previous_max_line_len:
current_pos = move_cursor(Point(y=y, x=new_max_line_len+1))
reset_attributes()
output.erase_end_of_line()
# Correctly reserve vertical space as required by the layout.
# When this is a new screen (drawn for the first time), or for some reason
# higher than the previous one. Move the cursor once to the bottom of the
# output. That way, we're sure that the terminal scrolls up, even when the
# lower lines of the canvas just contain whitespace.
# The most obvious reason that we actually want this behaviour is the avoid
# the artifact of the input scrolling when the completion menu is shown.
# (If the scrolling is actually wanted, the layout can still be build in a
# way to behave that way by setting a dynamic height.)
if current_height > previous_screen.height:
current_pos = move_cursor(Point(y=current_height - 1, x=0))
# Move cursor:
if is_done:
current_pos = move_cursor(Point(y=current_height, x=0))
output.erase_down()
else:
current_pos = move_cursor(screen.cursor_position)
if is_done:
output.enable_autowrap()
# Always reset the color attributes. This is important because a background
# thread could print data to stdout and we want that to be displayed in the
# default colors. (Also, if a background color has been set, many terminals
# give weird artifacs on resize events.)
reset_attributes()
if screen.show_cursor or is_done:
output.show_cursor()
return current_pos, last_token[0]
class HeightIsUnknownError(Exception):
" Information unavailable. Did not yet receive the CPR response. "
class _TokenToAttrsCache(dict):
"""
A cache structure that maps Pygments Tokens to :class:`.Attr`.
(This is an important speed up.)
"""
def __init__(self, get_style_for_token):
self.get_style_for_token = get_style_for_token
def __missing__(self, token):
try:
result = self.get_style_for_token(token)
except KeyError:
result = None
self[token] = result
return result
class Renderer(object):
"""
Typical usage:
::
output = Vt100_Output.from_pty(sys.stdout)
r = Renderer(style, output)
r.render(cli, layout=...)
"""
def __init__(self, style, output, use_alternate_screen=False, mouse_support=False):
assert isinstance(style, Style)
assert isinstance(output, Output)
self.style = style
self.output = output
self.use_alternate_screen = use_alternate_screen
self.mouse_support = to_cli_filter(mouse_support)
self._in_alternate_screen = False
self._mouse_support_enabled = False
self._bracketed_paste_enabled = False
# Waiting for CPR flag. True when we send the request, but didn't got a
# response.
self.waiting_for_cpr = False
self.reset(_scroll=True)
def reset(self, _scroll=False, leave_alternate_screen=True):
# Reset position
self._cursor_pos = Point(x=0, y=0)
# Remember the last screen instance between renderers. This way,
# we can create a `diff` between two screens and only output the
# difference. It's also to remember the last height. (To show for
# instance a toolbar at the bottom position.)
self._last_screen = None
self._last_size = None
self._last_token = None
# When the style hash changes, we have to do a full redraw as well as
# clear the `_attrs_for_token` dictionary.
self._last_style_hash = None
self._attrs_for_token = None
# Default MouseHandlers. (Just empty.)
self.mouse_handlers = MouseHandlers()
# Remember the last title. Only set the title when it changes.
self._last_title = None
#: Space from the top of the layout, until the bottom of the terminal.
#: We don't know this until a `report_absolute_cursor_row` call.
self._min_available_height = 0
# In case of Windown, also make sure to scroll to the current cursor
# position. (Only when rendering the first time.)
if is_windows() and _scroll:
self.output.scroll_buffer_to_prompt()
# Quit alternate screen.
if self._in_alternate_screen and leave_alternate_screen:
self.output.quit_alternate_screen()
self._in_alternate_screen = False
# Disable mouse support.
if self._mouse_support_enabled:
self.output.disable_mouse_support()
self._mouse_support_enabled = False
# Disable bracketed paste.
if self._bracketed_paste_enabled:
self.output.disable_bracketed_paste()
self._bracketed_paste_enabled = False
# Flush output. `disable_mouse_support` needs to write to stdout.
self.output.flush()
@property
def height_is_known(self):
"""
True when the height from the cursor until the bottom of the terminal
is known. (It's often nicer to draw bottom toolbars only if the height
is known, in order to avoid flickering when the CPR response arrives.)
"""
return self.use_alternate_screen or self._min_available_height > 0 or \
is_windows() # On Windows, we don't have to wait for a CPR.
@property
def rows_above_layout(self):
"""
Return the number of rows visible in the terminal above the layout.
"""
if self._in_alternate_screen:
return 0
elif self._min_available_height > 0:
total_rows = self.output.get_size().rows
last_screen_height = self._last_screen.height if self._last_screen else 0
return total_rows - max(self._min_available_height, last_screen_height)
else:
raise HeightIsUnknownError('Rows above layout is unknown.')
def request_absolute_cursor_position(self):
"""
Get current cursor position.
For vt100: Do CPR request. (answer will arrive later.)
For win32: Do API call. (Answer comes immediately.)
"""
# Only do this request when the cursor is at the top row. (after a
# clear or reset). We will rely on that in `report_absolute_cursor_row`.
assert self._cursor_pos.y == 0
# For Win32, we have an API call to get the number of rows below the
# cursor.
if is_windows():
self._min_available_height = self.output.get_rows_below_cursor_position()
else:
if self.use_alternate_screen:
self._min_available_height = self.output.get_size().rows
else:
# Asks for a cursor position report (CPR).
self.waiting_for_cpr = True
self.output.ask_for_cpr()
def report_absolute_cursor_row(self, row):
"""
To be called when we know the absolute cursor position.
(As an answer of a "Cursor Position Request" response.)
"""
# Calculate the amount of rows from the cursor position until the
# bottom of the terminal.
total_rows = self.output.get_size().rows
rows_below_cursor = total_rows - row + 1
# Set the
self._min_available_height = rows_below_cursor
self.waiting_for_cpr = False
def render(self, cli, layout, is_done=False):
"""
Render the current interface to the output.
:param is_done: When True, put the cursor at the end of the interface. We
won't print any changes to this part.
"""
output = self.output
# Enter alternate screen.
if self.use_alternate_screen and not self._in_alternate_screen:
self._in_alternate_screen = True
output.enter_alternate_screen()
# Enable bracketed paste.
if not self._bracketed_paste_enabled:
self.output.enable_bracketed_paste()
self._bracketed_paste_enabled = True
# Enable/disable mouse support.
needs_mouse_support = self.mouse_support(cli)
if needs_mouse_support and not self._mouse_support_enabled:
output.enable_mouse_support()
self._mouse_support_enabled = True
elif not needs_mouse_support and self._mouse_support_enabled:
output.disable_mouse_support()
self._mouse_support_enabled = False
# Create screen and write layout to it.
size = output.get_size()
screen = Screen()
screen.show_cursor = False # Hide cursor by default, unless one of the
# containers decides to display it.
mouse_handlers = MouseHandlers()
if is_done:
height = 0 # When we are done, we don't necessary want to fill up until the bottom.
else:
height = self._last_screen.height if self._last_screen else 0
height = max(self._min_available_height, height)
# When te size changes, don't consider the previous screen.
if self._last_size != size:
self._last_screen = None
# When we render using another style, do a full repaint. (Forget about
# the previous rendered screen.)
# (But note that we still use _last_screen to calculate the height.)
if self.style.invalidation_hash() != self._last_style_hash:
self._last_screen = None
self._attrs_for_token = None
if self._attrs_for_token is None:
self._attrs_for_token = _TokenToAttrsCache(self.style.get_attrs_for_token)
self._last_style_hash = self.style.invalidation_hash()
layout.write_to_screen(cli, screen, mouse_handlers, WritePosition(
xpos=0,
ypos=0,
width=size.columns,
height=(size.rows if self.use_alternate_screen else height),
extended_height=size.rows,
))
# When grayed. Replace all tokens in the new screen.
if cli.is_aborting or cli.is_exiting:
screen.replace_all_tokens(Token.Aborted)
# Process diff and write to output.
self._cursor_pos, self._last_token = _output_screen_diff(
output, screen, self._cursor_pos,
self._last_screen, self._last_token, is_done,
attrs_for_token=self._attrs_for_token,
size=size,
previous_width=(self._last_size.columns if self._last_size else 0))
self._last_screen = screen
self._last_size = size
self.mouse_handlers = mouse_handlers
# Write title if it changed.
new_title = cli.terminal_title
if new_title != self._last_title:
if new_title is None:
self.output.clear_title()
else:
self.output.set_title(new_title)
self._last_title = new_title
output.flush()
def erase(self, leave_alternate_screen=True, erase_title=True):
"""
Hide all output and put the cursor back at the first line. This is for
instance used for running a system command (while hiding the CLI) and
later resuming the same CLI.)
:param leave_alternate_screen: When True, and when inside an alternate
screen buffer, quit the alternate screen.
:param erase_title: When True, clear the title from the title bar.
"""
output = self.output
output.cursor_backward(self._cursor_pos.x)
output.cursor_up(self._cursor_pos.y)
output.erase_down()
output.reset_attributes()
output.flush()
# Erase title.
if self._last_title and erase_title:
output.clear_title()
self.reset(leave_alternate_screen=leave_alternate_screen)
def clear(self):
"""
Clear screen and go to 0,0
"""
# Erase current output first.
self.erase()
# Send "Erase Screen" command and go to (0, 0).
output = self.output
output.erase_screen()
output.cursor_goto(0, 0)
output.flush()
self.request_absolute_cursor_position()
def print_tokens(output, tokens, style):
"""
Print a list of (Token, text) tuples in the given style to the output.
"""
assert isinstance(output, Output)
assert isinstance(style, Style)
# Reset first.
output.reset_attributes()
output.enable_autowrap()
# Print all (token, text) tuples.
attrs_for_token = _TokenToAttrsCache(style.get_attrs_for_token)
for token, text in tokens:
attrs = attrs_for_token[token]
if attrs:
output.set_attributes(attrs)
else:
output.reset_attributes()
output.write(text)
# Reset again.
output.reset_attributes()
output.flush()

View File

@@ -0,0 +1,36 @@
from .enums import IncrementalSearchDirection
from .filters import to_simple_filter
__all__ = (
'SearchState',
)
class SearchState(object):
"""
A search 'query'.
"""
__slots__ = ('text', 'direction', 'ignore_case')
def __init__(self, text='', direction=IncrementalSearchDirection.FORWARD, ignore_case=False):
ignore_case = to_simple_filter(ignore_case)
self.text = text
self.direction = direction
self.ignore_case = ignore_case
def __repr__(self):
return '%s(%r, direction=%r, ignore_case=%r)' % (
self.__class__.__name__, self.text, self.direction, self.ignore_case)
def __invert__(self):
"""
Create a new SearchState where backwards becomes forwards and the other
way around.
"""
if self.direction == IncrementalSearchDirection.BACKWARD:
direction = IncrementalSearchDirection.FORWARD
else:
direction = IncrementalSearchDirection.BACKWARD
return SearchState(text=self.text, direction=direction, ignore_case=self.ignore_case)

Some files were not shown because too many files have changed in this diff Show More