commit bf58d639c25c5d7cf83ad840b68d716c59b81437 Author: Damian Johnson <atagar@torproject.org> Date: Sun Feb 14 12:58:30 2016 -0800 Merge text_input into panel Our text_input module was primarily to support the control interpreter. We might re-introduce some of this in the future but for now only the BasicValidator is used by our codebase. --- nyx/util/__init__.py | 2 - nyx/util/panel.py | 80 +++++++++++++++++-- nyx/util/text_input.py | 213 ------------------------------------------------- 3 files changed, 75 insertions(+), 220 deletions(-) diff --git a/nyx/util/__init__.py b/nyx/util/__init__.py index e32a816..1bc3d71 100644 --- a/nyx/util/__init__.py +++ b/nyx/util/__init__.py @@ -17,8 +17,6 @@ from nyx.util import log __all__ = [ 'log', 'panel', - 'text_input', - 'tor_config', 'tracker', 'ui_tools', ] diff --git a/nyx/util/panel.py b/nyx/util/panel.py index bf16422..611a14d 100644 --- a/nyx/util/panel.py +++ b/nyx/util/panel.py @@ -9,7 +9,7 @@ import curses.ascii import curses.textpad from threading import RLock -from nyx.util import text_input, ui_tools +from nyx.util import ui_tools from stem.util import conf, log, str_tools @@ -32,6 +32,8 @@ SPECIAL_KEYS = { 'esc': 27, } +PASS = -1 + def conf_handler(key, value): if key == 'features.torrc.maxLineWrap': @@ -46,6 +48,76 @@ CONFIG = conf.config_dict('nyx', { HALT_ACTIVITY = False +class BasicValidator(object): + """ + Interceptor for keystrokes given to a textbox, doing the following: + - quits by setting the input to curses.ascii.BEL when escape is pressed + - stops the cursor at the end of the box's content when pressing the right + arrow + - home and end keys move to the start/end of the line + """ + + def validate(self, key, textbox): + """ + Processes the given key input for the textbox. This may modify the + textbox's content, cursor position, etc depending on the functionality + of the validator. This returns the key that the textbox should interpret, + PASS if this validator doesn't want to take any action. + + Arguments: + key - key code input from the user + textbox - curses Textbox instance the input came from + """ + + result = self.handle_key(key, textbox) + return key if result == PASS else result + + def handle_key(self, key, textbox): + y, x = textbox.win.getyx() + + if curses.ascii.isprint(key) and x < textbox.maxx: + # Shifts the existing text forward so input is an insert method rather + # than replacement. The curses.textpad accepts an insert mode flag but + # this has a couple issues... + # - The flag is only available for Python 2.6+, before that the + # constructor only accepted a subwindow argument as per: + # https://trac.torproject.org/projects/tor/ticket/2354 + # - The textpad doesn't shift text that has text attributes. This is + # because keycodes read by textbox.win.inch() includes formatting, + # causing the curses.ascii.isprint() check it does to fail. + + current_input = textbox.gather() + textbox.win.addstr(y, x + 1, current_input[x:textbox.maxx - 1]) + textbox.win.move(y, x) # reverts cursor movement during gather call + elif key == 27: + # curses.ascii.BEL is a character codes that causes textpad to terminate + + return curses.ascii.BEL + elif key == curses.KEY_HOME: + textbox.win.move(y, 0) + return None + elif key in (curses.KEY_END, curses.KEY_RIGHT): + msg_length = len(textbox.gather()) + textbox.win.move(y, x) # reverts cursor movement during gather call + + if key == curses.KEY_END and msg_length > 0 and x < msg_length - 1: + # if we're in the content then move to the end + + textbox.win.move(y, msg_length - 1) + return None + elif key == curses.KEY_RIGHT and x >= msg_length - 1: + # don't move the cursor if there's no content after it + + return None + elif key == 410: + # if we're resizing the display during text entry then cancel it + # (otherwise the input field is filled with nonprintable characters) + + return curses.ascii.BEL + + return PASS + + class Panel(object): """ Wrapper for curses subwindows. This hides most of the ugliness in common @@ -555,7 +627,7 @@ class Panel(object): return x, y - def getstr(self, y, x, initial_text = '', text_format = None, max_width = None, validator = None): + def getstr(self, y, x, initial_text = '', text_format = None, max_width = None): """ Provides a text field where the user can input a string, blocking until they've done so and returning the result. If the user presses escape then @@ -572,7 +644,6 @@ class Panel(object): initial_text - starting text in this field text_format - format used for the text max_width - maximum width for the text field - validator - custom TextInputValidator for handling keybindings """ if not text_format: @@ -610,8 +681,7 @@ class Panel(object): textbox = curses.textpad.Textbox(input_subwindow) - if not validator: - validator = text_input.BasicValidator() + validator = BasicValidator() textbox.win.attron(text_format) user_input = textbox.edit(lambda key: validator.validate(key, textbox)).strip() diff --git a/nyx/util/text_input.py b/nyx/util/text_input.py deleted file mode 100644 index 3172750..0000000 --- a/nyx/util/text_input.py +++ /dev/null @@ -1,213 +0,0 @@ -""" -Provides input validators that provide text input with various capabilities. -These can be chained together with the first matching validator taking -precidence. -""" - -import os -import curses - -PASS = -1 - - -class TextInputValidator: - """ - Basic interface for validators. Implementations should override the handle_key - method. - """ - - def __init__(self, next_validator = None): - self.next_validator = next_validator - - def validate(self, key, textbox): - """ - Processes the given key input for the textbox. This may modify the - textbox's content, cursor position, etc depending on the functionality - of the validator. This returns the key that the textbox should interpret, - PASS if this validator doesn't want to take any action. - - Arguments: - key - key code input from the user - textbox - curses Textbox instance the input came from - """ - - result = self.handle_key(key, textbox) - - if result != PASS: - return result - elif self.next_validator: - return self.next_validator.validate(key, textbox) - else: - return key - - def handle_key(self, key, textbox): - """ - Process the given keycode with this validator, returning the keycode for - the textbox to process, and PASS if this doesn't want to modify it. - - Arguments: - key - key code input from the user - textbox - curses Textbox instance the input came from - """ - - return PASS - - -class BasicValidator(TextInputValidator): - """ - Interceptor for keystrokes given to a textbox, doing the following: - - quits by setting the input to curses.ascii.BEL when escape is pressed - - stops the cursor at the end of the box's content when pressing the right - arrow - - home and end keys move to the start/end of the line - """ - - def handle_key(self, key, textbox): - y, x = textbox.win.getyx() - - if curses.ascii.isprint(key) and x < textbox.maxx: - # Shifts the existing text forward so input is an insert method rather - # than replacement. The curses.textpad accepts an insert mode flag but - # this has a couple issues... - # - The flag is only available for Python 2.6+, before that the - # constructor only accepted a subwindow argument as per: - # https://trac.torproject.org/projects/tor/ticket/2354 - # - The textpad doesn't shift text that has text attributes. This is - # because keycodes read by textbox.win.inch() includes formatting, - # causing the curses.ascii.isprint() check it does to fail. - - current_input = textbox.gather() - textbox.win.addstr(y, x + 1, current_input[x:textbox.maxx - 1]) - textbox.win.move(y, x) # reverts cursor movement during gather call - elif key == 27: - # curses.ascii.BEL is a character codes that causes textpad to terminate - - return curses.ascii.BEL - elif key == curses.KEY_HOME: - textbox.win.move(y, 0) - return None - elif key in (curses.KEY_END, curses.KEY_RIGHT): - msg_length = len(textbox.gather()) - textbox.win.move(y, x) # reverts cursor movement during gather call - - if key == curses.KEY_END and msg_length > 0 and x < msg_length - 1: - # if we're in the content then move to the end - - textbox.win.move(y, msg_length - 1) - return None - elif key == curses.KEY_RIGHT and x >= msg_length - 1: - # don't move the cursor if there's no content after it - - return None - elif key == 410: - # if we're resizing the display during text entry then cancel it - # (otherwise the input field is filled with nonprintable characters) - - return curses.ascii.BEL - - return PASS - - -class HistoryValidator(TextInputValidator): - """ - This intercepts the up and down arrow keys to scroll through a backlog of - previous commands. - """ - - def __init__(self, command_backlog = [], next_validator = None): - TextInputValidator.__init__(self, next_validator) - - # contents that can be scrolled back through, newest to oldest - - self.command_backlog = command_backlog - - # selected item from the backlog, -1 if we're not on a backlog item - - self.selection_index = -1 - - # the fields input prior to selecting a backlog item - - self.custom_input = '' - - def handle_key(self, key, textbox): - if key in (curses.KEY_UP, curses.KEY_DOWN): - offset = 1 if key == curses.KEY_UP else -1 - new_selection = self.selection_index + offset - - # constrains the new selection to valid bounds - - new_selection = max(-1, new_selection) - new_selection = min(len(self.command_backlog) - 1, new_selection) - - # skips if this is a no-op - - if self.selection_index == new_selection: - return None - - # saves the previous input if we weren't on the backlog - - if self.selection_index == -1: - self.custom_input = textbox.gather().strip() - - if new_selection == -1: - new_input = self.custom_input - else: - new_input = self.command_backlog[new_selection] - - y, _ = textbox.win.getyx() - _, max_x = textbox.win.getmaxyx() - textbox.win.clear() - textbox.win.addstr(y, 0, new_input[:max_x - 1]) - textbox.win.move(y, min(len(new_input), max_x - 1)) - - self.selection_index = new_selection - return None - - return PASS - - -class TabCompleter(TextInputValidator): - """ - Provides tab completion based on the current input, finishing if there's only - a single match. This expects a functor that accepts the current input and - provides matches. - """ - - def __init__(self, completer, next_validator = None): - TextInputValidator.__init__(self, next_validator) - - # functor that accepts a string and gives a list of matches - - self.completer = completer - - def handle_key(self, key, textbox): - # Matches against the tab key. The ord('\t') is nine, though strangely none - # of the curses.KEY_*TAB constants match this... - - if key == 9: - current_contents = textbox.gather().strip() - matches = self.completer(current_contents) - new_input = None - - if len(matches) == 1: - # only a single match, fill it in - new_input = matches[0] - elif len(matches) > 1: - # looks for a common prefix we can complete - common_prefix = os.path.commonprefix(matches) # weird that this comes from path... - - if common_prefix != current_contents: - new_input = common_prefix - - # TODO: somehow display matches... this is not gonna be fun - - if new_input: - y, _ = textbox.win.getyx() - _, max_x = textbox.win.getmaxyx() - textbox.win.clear() - textbox.win.addstr(y, 0, new_input[:max_x - 1]) - textbox.win.move(y, min(len(new_input), max_x - 1)) - - return None - - return PASS
participants (1)
-
atagar@torproject.org