Complete rewrite and removed legacy bash version
This commit is contained in:
29
src/libs/PyInquirer/__init__.py
Normal file
29
src/libs/PyInquirer/__init__.py
Normal 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
|
||||
41
src/libs/PyInquirer/color_print.py
Normal file
41
src/libs/PyInquirer/color_print.py
Normal 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
|
||||
98
src/libs/PyInquirer/prompt.py
Normal file
98
src/libs/PyInquirer/prompt.py
Normal 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
|
||||
0
src/libs/PyInquirer/prompts/__init__.py
Normal file
0
src/libs/PyInquirer/prompts/__init__.py
Normal file
233
src/libs/PyInquirer/prompts/checkbox.py
Normal file
233
src/libs/PyInquirer/prompts/checkbox.py
Normal 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
|
||||
)
|
||||
92
src/libs/PyInquirer/prompts/common.py
Normal file
92
src/libs/PyInquirer/prompts/common.py
Normal 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',
|
||||
})
|
||||
82
src/libs/PyInquirer/prompts/confirm.py
Normal file
82
src/libs/PyInquirer/prompts/confirm.py
Normal 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,
|
||||
)
|
||||
197
src/libs/PyInquirer/prompts/editor.py
Normal file
197
src/libs/PyInquirer/prompts/editor.py
Normal 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
|
||||
)
|
||||
195
src/libs/PyInquirer/prompts/expand.py
Normal file
195
src/libs/PyInquirer/prompts/expand.py
Normal 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
|
||||
)
|
||||
51
src/libs/PyInquirer/prompts/input.py
Normal file
51
src/libs/PyInquirer/prompts/input.py
Normal 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
|
||||
)
|
||||
184
src/libs/PyInquirer/prompts/list.py
Normal file
184
src/libs/PyInquirer/prompts/list.py
Normal 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
|
||||
)
|
||||
14
src/libs/PyInquirer/prompts/password.py
Normal file
14
src/libs/PyInquirer/prompts/password.py
Normal 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)
|
||||
166
src/libs/PyInquirer/prompts/rawlist.py
Normal file
166
src/libs/PyInquirer/prompts/rawlist.py
Normal 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
|
||||
)
|
||||
15
src/libs/PyInquirer/separator.py
Normal file
15
src/libs/PyInquirer/separator.py
Normal 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
|
||||
35
src/libs/PyInquirer/utils.py
Normal file
35
src/libs/PyInquirer/utils.py
Normal 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)))
|
||||
22
src/libs/prompt_toolkit/__init__.py
Normal file
22
src/libs/prompt_toolkit/__init__.py
Normal 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'
|
||||
192
src/libs/prompt_toolkit/application.py
Normal file
192
src/libs/prompt_toolkit/application.py
Normal 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 = []
|
||||
88
src/libs/prompt_toolkit/auto_suggest.py
Normal file
88
src/libs/prompt_toolkit/auto_suggest.py
Normal 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)
|
||||
1415
src/libs/prompt_toolkit/buffer.py
Normal file
1415
src/libs/prompt_toolkit/buffer.py
Normal file
File diff suppressed because it is too large
Load Diff
92
src/libs/prompt_toolkit/buffer_mapping.py
Normal file
92
src/libs/prompt_toolkit/buffer_mapping.py
Normal 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.')
|
||||
111
src/libs/prompt_toolkit/cache.py
Normal file
111
src/libs/prompt_toolkit/cache.py
Normal 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
|
||||
8
src/libs/prompt_toolkit/clipboard/__init__.py
Normal file
8
src/libs/prompt_toolkit/clipboard/__init__.py
Normal 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
|
||||
62
src/libs/prompt_toolkit/clipboard/base.py
Normal file
62
src/libs/prompt_toolkit/clipboard/base.py
Normal 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.
|
||||
"""
|
||||
42
src/libs/prompt_toolkit/clipboard/in_memory.py
Normal file
42
src/libs/prompt_toolkit/clipboard/in_memory.py
Normal 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())
|
||||
39
src/libs/prompt_toolkit/clipboard/pyperclip.py
Normal file
39
src/libs/prompt_toolkit/clipboard/pyperclip.py
Normal 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)
|
||||
170
src/libs/prompt_toolkit/completion.py
Normal file
170
src/libs/prompt_toolkit/completion.py
Normal 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
|
||||
0
src/libs/prompt_toolkit/contrib/__init__.py
Normal file
0
src/libs/prompt_toolkit/contrib/__init__.py
Normal file
5
src/libs/prompt_toolkit/contrib/completers/__init__.py
Normal file
5
src/libs/prompt_toolkit/contrib/completers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .filesystem import PathCompleter
|
||||
from .base import WordCompleter
|
||||
from .system import SystemCompleter
|
||||
61
src/libs/prompt_toolkit/contrib/completers/base.py
Normal file
61
src/libs/prompt_toolkit/contrib/completers/base.py
Normal 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)
|
||||
105
src/libs/prompt_toolkit/contrib/completers/filesystem.py
Normal file
105
src/libs/prompt_toolkit/contrib/completers/filesystem.py
Normal 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),
|
||||
56
src/libs/prompt_toolkit/contrib/completers/system.py
Normal file
56
src/libs/prompt_toolkit/contrib/completers/system.py
Normal 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),
|
||||
})
|
||||
@@ -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
|
||||
408
src/libs/prompt_toolkit/contrib/regular_languages/compiler.py
Normal file
408
src/libs/prompt_toolkit/contrib/regular_languages/compiler.py
Normal 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)
|
||||
@@ -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
|
||||
90
src/libs/prompt_toolkit/contrib/regular_languages/lexer.py
Normal file
90
src/libs/prompt_toolkit/contrib/regular_languages/lexer.py
Normal 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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
2
src/libs/prompt_toolkit/contrib/telnet/__init__.py
Normal file
2
src/libs/prompt_toolkit/contrib/telnet/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .server import *
|
||||
from .application import *
|
||||
32
src/libs/prompt_toolkit/contrib/telnet/application.py
Normal file
32
src/libs/prompt_toolkit/contrib/telnet/application.py
Normal 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.
|
||||
"""
|
||||
11
src/libs/prompt_toolkit/contrib/telnet/log.py
Normal file
11
src/libs/prompt_toolkit/contrib/telnet/log.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Python logger for the telnet server.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__package__)
|
||||
|
||||
__all__ = (
|
||||
'logger',
|
||||
)
|
||||
181
src/libs/prompt_toolkit/contrib/telnet/protocol.py
Normal file
181
src/libs/prompt_toolkit/contrib/telnet/protocol.py
Normal 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))
|
||||
407
src/libs/prompt_toolkit/contrib/telnet/server.py
Normal file
407
src/libs/prompt_toolkit/contrib/telnet/server.py
Normal 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)
|
||||
34
src/libs/prompt_toolkit/contrib/validators/base.py
Normal file
34
src/libs/prompt_toolkit/contrib/validators/base.py
Normal 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)
|
||||
1001
src/libs/prompt_toolkit/document.py
Normal file
1001
src/libs/prompt_toolkit/document.py
Normal file
File diff suppressed because it is too large
Load Diff
29
src/libs/prompt_toolkit/enums.py
Normal file
29
src/libs/prompt_toolkit/enums.py
Normal 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'
|
||||
0
src/libs/prompt_toolkit/eventloop/__init__.py
Normal file
0
src/libs/prompt_toolkit/eventloop/__init__.py
Normal file
46
src/libs/prompt_toolkit/eventloop/asyncio_base.py
Normal file
46
src/libs/prompt_toolkit/eventloop/asyncio_base.py
Normal 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
|
||||
113
src/libs/prompt_toolkit/eventloop/asyncio_posix.py
Normal file
113
src/libs/prompt_toolkit/eventloop/asyncio_posix.py
Normal 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)
|
||||
83
src/libs/prompt_toolkit/eventloop/asyncio_win32.py
Normal file
83
src/libs/prompt_toolkit/eventloop/asyncio_win32.py
Normal 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)
|
||||
85
src/libs/prompt_toolkit/eventloop/base.py
Normal file
85
src/libs/prompt_toolkit/eventloop/base.py
Normal 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.)
|
||||
"""
|
||||
29
src/libs/prompt_toolkit/eventloop/callbacks.py
Normal file
29
src/libs/prompt_toolkit/eventloop/callbacks.py
Normal 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
|
||||
107
src/libs/prompt_toolkit/eventloop/inputhook.py
Normal file
107
src/libs/prompt_toolkit/eventloop/inputhook.py
Normal 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
|
||||
311
src/libs/prompt_toolkit/eventloop/posix.py
Normal file
311
src/libs/prompt_toolkit/eventloop/posix.py
Normal 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)
|
||||
82
src/libs/prompt_toolkit/eventloop/posix_utils.py
Normal file
82
src/libs/prompt_toolkit/eventloop/posix_utils.py
Normal 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)
|
||||
216
src/libs/prompt_toolkit/eventloop/select.py
Normal file
216
src/libs/prompt_toolkit/eventloop/select.py
Normal 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()
|
||||
23
src/libs/prompt_toolkit/eventloop/utils.py
Normal file
23
src/libs/prompt_toolkit/eventloop/utils.py
Normal 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
|
||||
187
src/libs/prompt_toolkit/eventloop/win32.py
Normal file
187
src/libs/prompt_toolkit/eventloop/win32.py
Normal 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)
|
||||
36
src/libs/prompt_toolkit/filters/__init__.py
Normal file
36
src/libs/prompt_toolkit/filters/__init__.py
Normal 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 *
|
||||
234
src/libs/prompt_toolkit/filters/base.py
Normal file
234
src/libs/prompt_toolkit/filters/base.py
Normal 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)
|
||||
395
src/libs/prompt_toolkit/filters/cli.py
Normal file
395
src/libs/prompt_toolkit/filters/cli.py
Normal 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()'
|
||||
55
src/libs/prompt_toolkit/filters/types.py
Normal file
55
src/libs/prompt_toolkit/filters/types.py
Normal 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 = []
|
||||
39
src/libs/prompt_toolkit/filters/utils.py
Normal file
39
src/libs/prompt_toolkit/filters/utils.py
Normal 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)
|
||||
120
src/libs/prompt_toolkit/history.py
Normal file
120
src/libs/prompt_toolkit/history.py
Normal 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)
|
||||
135
src/libs/prompt_toolkit/input.py
Normal file
135
src/libs/prompt_toolkit/input.py
Normal 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
|
||||
1185
src/libs/prompt_toolkit/interface.py
Normal file
1185
src/libs/prompt_toolkit/interface.py
Normal file
File diff suppressed because it is too large
Load Diff
1
src/libs/prompt_toolkit/key_binding/__init__.py
Normal file
1
src/libs/prompt_toolkit/key_binding/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from __future__ import unicode_literals
|
||||
407
src/libs/prompt_toolkit/key_binding/bindings/basic.py
Normal file
407
src/libs/prompt_toolkit/key_binding/bindings/basic.py
Normal 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
|
||||
161
src/libs/prompt_toolkit/key_binding/bindings/completion.py
Normal file
161
src/libs/prompt_toolkit/key_binding/bindings/completion.py
Normal 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)
|
||||
451
src/libs/prompt_toolkit/key_binding/bindings/emacs.py
Normal file
451
src/libs/prompt_toolkit/key_binding/bindings/emacs.py
Normal 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
|
||||
578
src/libs/prompt_toolkit/key_binding/bindings/named_commands.py
Normal file
578
src/libs/prompt_toolkit/key_binding/bindings/named_commands.py
Normal 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)
|
||||
185
src/libs/prompt_toolkit/key_binding/bindings/scroll.py
Normal file
185
src/libs/prompt_toolkit/key_binding/bindings/scroll.py
Normal 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
|
||||
25
src/libs/prompt_toolkit/key_binding/bindings/utils.py
Normal file
25
src/libs/prompt_toolkit/key_binding/bindings/utils.py
Normal 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
|
||||
1903
src/libs/prompt_toolkit/key_binding/bindings/vi.py
Normal file
1903
src/libs/prompt_toolkit/key_binding/bindings/vi.py
Normal file
File diff suppressed because it is too large
Load Diff
119
src/libs/prompt_toolkit/key_binding/defaults.py
Normal file
119
src/libs/prompt_toolkit/key_binding/defaults.py
Normal 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)
|
||||
1378
src/libs/prompt_toolkit/key_binding/digraphs.py
Normal file
1378
src/libs/prompt_toolkit/key_binding/digraphs.py
Normal file
File diff suppressed because it is too large
Load Diff
372
src/libs/prompt_toolkit/key_binding/input_processor.py
Normal file
372
src/libs/prompt_toolkit/key_binding/input_processor.py
Normal 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
|
||||
96
src/libs/prompt_toolkit/key_binding/manager.py
Normal file
96
src/libs/prompt_toolkit/key_binding/manager.py
Normal 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
|
||||
350
src/libs/prompt_toolkit/key_binding/registry.py
Normal file
350
src/libs/prompt_toolkit/key_binding/registry.py
Normal 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
|
||||
61
src/libs/prompt_toolkit/key_binding/vi_state.py
Normal file
61
src/libs/prompt_toolkit/key_binding/vi_state.py
Normal 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
|
||||
129
src/libs/prompt_toolkit/keys.py
Normal file
129
src/libs/prompt_toolkit/keys.py
Normal 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>')
|
||||
51
src/libs/prompt_toolkit/layout/__init__.py
Normal file
51
src/libs/prompt_toolkit/layout/__init__.py
Normal 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
|
||||
1665
src/libs/prompt_toolkit/layout/containers.py
Normal file
1665
src/libs/prompt_toolkit/layout/containers.py
Normal file
File diff suppressed because it is too large
Load Diff
730
src/libs/prompt_toolkit/layout/controls.py
Normal file
730
src/libs/prompt_toolkit/layout/controls.py
Normal 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()
|
||||
92
src/libs/prompt_toolkit/layout/dimension.py
Normal file
92
src/libs/prompt_toolkit/layout/dimension.py
Normal 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)
|
||||
320
src/libs/prompt_toolkit/layout/lexers.py
Normal file
320
src/libs/prompt_toolkit/layout/lexers.py
Normal 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
|
||||
253
src/libs/prompt_toolkit/layout/margins.py
Normal file
253
src/libs/prompt_toolkit/layout/margins.py
Normal 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
|
||||
496
src/libs/prompt_toolkit/layout/menus.py
Normal file
496
src/libs/prompt_toolkit/layout/menus.py
Normal 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 []
|
||||
29
src/libs/prompt_toolkit/layout/mouse_handlers.py
Normal file
29
src/libs/prompt_toolkit/layout/mouse_handlers.py
Normal 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
|
||||
605
src/libs/prompt_toolkit/layout/processors.py
Normal file
605
src/libs/prompt_toolkit/layout/processors.py
Normal 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)
|
||||
111
src/libs/prompt_toolkit/layout/prompt.py
Normal file
111
src/libs/prompt_toolkit/layout/prompt.py
Normal 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, ') '),
|
||||
]
|
||||
151
src/libs/prompt_toolkit/layout/screen.py
Normal file
151
src/libs/prompt_toolkit/layout/screen.py
Normal 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)
|
||||
209
src/libs/prompt_toolkit/layout/toolbars.py
Normal file
209
src/libs/prompt_toolkit/layout/toolbars.py
Normal 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())
|
||||
181
src/libs/prompt_toolkit/layout/utils.py
Normal file
181
src/libs/prompt_toolkit/layout/utils.py
Normal 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
|
||||
48
src/libs/prompt_toolkit/mouse_events.py
Normal file
48
src/libs/prompt_toolkit/mouse_events.py
Normal 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)
|
||||
192
src/libs/prompt_toolkit/output.py
Normal file
192
src/libs/prompt_toolkit/output.py
Normal 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)
|
||||
56
src/libs/prompt_toolkit/reactive.py
Normal file
56
src/libs/prompt_toolkit/reactive.py
Normal 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())
|
||||
526
src/libs/prompt_toolkit/renderer.py
Normal file
526
src/libs/prompt_toolkit/renderer.py
Normal 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()
|
||||
36
src/libs/prompt_toolkit/search_state.py
Normal file
36
src/libs/prompt_toolkit/search_state.py
Normal 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
Reference in New Issue
Block a user