[tor-commits] [nyx/master] Curses str_input() function for getting input

atagar at torproject.org atagar at torproject.org
Mon Apr 18 20:23:16 UTC 2016


commit da970b76736b836360da85de1b609d584990d002
Author: Damian Johnson <atagar at 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





More information about the tor-commits mailing list