commit da970b76736b836360da85de1b609d584990d002 Author: Damian Johnson atagar@torproject.org Date: Sun Apr 17 19:50:31 2016 -0700
Curses str_input() function for getting input
Replacing our Panel's getstr() method with simplified version in our curses module. This drops workarounds to work with python 2.5 (meh) and formatted initial text (unused). --- nyx/curses.py | 66 ++++++++++++++++++++++++ nyx/panel/__init__.py | 139 -------------------------------------------------- nyx/panel/header.py | 4 +- 3 files changed, 68 insertions(+), 141 deletions(-)
diff --git a/nyx/curses.py b/nyx/curses.py index ebe5eef..f357443 100644 --- a/nyx/curses.py +++ b/nyx/curses.py @@ -14,6 +14,7 @@ if we want Windows support in the future too. start - initializes curses with the given function raw_screen - provides direct access to the curses screen key_input - get keypress by user + str_input - text field where user can input a string curses_attr - curses encoded text attribute screen_size - provides the dimensions of our screen screenshot - dump of the present on-screen content @@ -82,6 +83,8 @@ from __future__ import absolute_import
import collections import curses +import curses.ascii +import curses.textpad import threading
import stem.util.conf @@ -239,6 +242,69 @@ def key_input(input_timeout = None): return KeyInput(CURSES_SCREEN.getch())
+def str_input(x, y, initial_text = ''): + """ + 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 + this terminates and provides back **None**. + + This blanks any content within the space that the input field is rendered + (otherwise stray characters would be interpreted as part of the initial + input). + + :param int x: horizontal location + :param int y: vertical location + :param str initial_text: initial input of the field + + :returns: **str** with the user input or **None** if the prompt is caneled + """ + + def handle_key(textbox, key): + y, x = textbox.win.getyx() + + if key == 27: + return curses.ascii.BEL # user pressed esc + elif key == curses.KEY_HOME: + textbox.win.move(y, 0) + 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: + textbox.win.move(y, msg_length - 1) # if we're in the content then move to the end + elif key == curses.KEY_RIGHT and x < msg_length - 1: + textbox.win.move(y, x + 1) # only move cursor if there's content after it + 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 + else: + return key + + with CURSES_LOCK: + try: + curses.curs_set(1) # show cursor + except curses.error: + pass + + width = screen_size().width - x + + curses_subwindow = CURSES_SCREEN.subwin(1, width, y, x) + curses_subwindow.erase() + curses_subwindow.addstr(0, 0, initial_text[:width - 1]) + + textbox = curses.textpad.Textbox(curses_subwindow, insert_mode = True) + user_input = textbox.edit(lambda key: handle_key(textbox, key)).strip() + + try: + curses.curs_set(0) # hide cursor + except curses.error: + pass + + return None if textbox.lastcmd == curses.ascii.BEL else user_input + + def curses_attr(*attributes): """ Provides encoding for the given curses text attributes. diff --git a/nyx/panel/__init__.py b/nyx/panel/__init__.py index 39b8f85..1f5143d 100644 --- a/nyx/panel/__init__.py +++ b/nyx/panel/__init__.py @@ -7,8 +7,6 @@ Panels consisting the nyx interface.
import collections import curses -import curses.ascii -import curses.textpad import inspect import threading import time @@ -19,8 +17,6 @@ import stem.util.log from nyx.curses import HIGHLIGHT from stem.util import conf, str_tools
-PASS = -1 - __all__ = [ 'config', 'connection', @@ -82,76 +78,6 @@ class KeyHandler(collections.namedtuple('Help', ['key', 'description', 'current' self._action()
-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 @@ -504,71 +430,6 @@ class Panel(object):
return x, y
- def getstr(self, y, x, initial_text = ''): - """ - 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 - this terminates and provides back None. This should only be called from - the context of a panel's draw method. - - This blanks any content within the space that the input field is rendered - (otherwise stray characters would be interpreted as part of the initial - input). - - Arguments: - y - vertical location - x - horizontal location - initial_text - starting text in this field - """ - - # makes cursor visible - - try: - previous_cursor_state = curses.curs_set(1) - except curses.error: - previous_cursor_state = 0 - - # temporary subwindow for user input - - display_width = self.get_preferred_size()[1] - - with nyx.curses.raw_screen() as stdscr: - input_subwindow = stdscr.subwin(1, display_width - x, self.top + y, self.left + x) - - # blanks the field's area, filling it with the font in case it's hilighting - - input_subwindow.clear() - input_subwindow.bkgd(' ', curses.A_NORMAL) - - # prepopulates the initial text - - if initial_text: - input_subwindow.addstr(0, 0, initial_text[:display_width - x - 1], curses.A_NORMAL) - - # Displays the text field, blocking until the user's done. This closes the - # text panel and returns user_input to the initial text if the user presses - # escape. - - textbox = curses.textpad.Textbox(input_subwindow) - - validator = BasicValidator() - - textbox.win.attron(curses.A_NORMAL) - user_input = textbox.edit(lambda key: validator.validate(key, textbox)).strip() - textbox.win.attroff(curses.A_NORMAL) - - if textbox.lastcmd == curses.ascii.BEL: - user_input = None - - # reverts visability settings - - try: - curses.curs_set(previous_cursor_state) - except curses.error: - pass - - return user_input - def add_scroll_bar(self, top, bottom, size, draw_top = 0): """ Draws a left justified scroll bar reflecting position within a vertical diff --git a/nyx/panel/header.py b/nyx/panel/header.py index 2441704..6551800 100644 --- a/nyx/panel/header.py +++ b/nyx/panel/header.py @@ -47,7 +47,7 @@ class HeaderPanel(nyx.panel.DaemonPanel): nyx.panel.DaemonPanel.__init__(self, 'header', UPDATE_RATE) self._vals = Sampling.create()
- self._last_width = nyx.curses.screen_size()[0] + self._last_width = nyx.curses.screen_size().width self._reported_inactive = False
self._message = None @@ -90,7 +90,7 @@ class HeaderPanel(nyx.panel.DaemonPanel):
self.show_message(message) self.redraw(True) - user_input = self.getstr(self.get_height() - 1, len(message), initial_value) + user_input = nyx.curses.str_input(len(message), self.get_height() - 1, initial_value) self.show_message()
return user_input