[tor-commits] [arm/release] Making curses text input extensible

atagar at torproject.org atagar at torproject.org
Sun Sep 25 21:38:26 UTC 2011


commit dc18065b395ef409cfff58525f24bb0727ac3ed6
Author: Damian Johnson <atagar at torproject.org>
Date:   Tue Aug 23 09:34:14 2011 -0700

    Making curses text input extensible
    
    Adding utility for text input interceptors which can be chained to add
    capabilities to text input fields (like tab completion). They'll be implemented
    as needed.
---
 README                |    1 +
 src/util/__init__.py  |    2 +-
 src/util/panel.py     |   59 +++-------------------------
 src/util/textInput.py |  101 +++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 110 insertions(+), 53 deletions(-)

diff --git a/README b/README
index cbaf31a..16d0965 100644
--- a/README
+++ b/README
@@ -205,6 +205,7 @@ Layout:
       procTools.py   - queries process & system information from /proc contents
       procName.py    - renames our process to a friendlier name
       sysTools.py    - helper for system calls, providing client side caching
+      textInput.py   - expands the capabilities of text input fields
       torConfig.py   - functions for working with the torrc and config options
       torTools.py    - TorCtl wrapper, providing caching and derived information
       uiTools.py     - helper functions for presenting the user interface
diff --git a/src/util/__init__.py b/src/util/__init__.py
index cd5d721..973527e 100644
--- a/src/util/__init__.py
+++ b/src/util/__init__.py
@@ -4,5 +4,5 @@ application's status, making cross platform system calls, parsing tor data,
 and safely working with curses (hiding some of the gory details).
 """
 
-__all__ = ["conf", "connections", "enum", "gtkTools", "hostnames", "log", "panel", "procTools", "procName", "sysTools", "torConfig", "torTools", "uiTools"]
+__all__ = ["conf", "connections", "enum", "gtkTools", "hostnames", "log", "panel", "procTools", "procName", "sysTools", "textInput", "torConfig", "torTools", "uiTools"]
 
diff --git a/src/util/panel.py b/src/util/panel.py
index 0e84005..50ae2c0 100644
--- a/src/util/panel.py
+++ b/src/util/panel.py
@@ -9,7 +9,7 @@ import curses.ascii
 import curses.textpad
 from threading import RLock
 
-from util import log, uiTools
+from util import log, textInput, uiTools
 
 # global ui lock governing all panel instances (curses isn't thread save and 
 # concurrency bugs produce especially sinister glitches)
@@ -575,7 +575,7 @@ class Panel():
         baseMsg = "Unclosed formatting tag%s:" % ("s" if len(expectedCloseTags) > 1 else "")
         raise ValueError("%s: '%s'\n  \"%s\"" % (baseMsg, "', '".join(expectedCloseTags), msg))
   
-  def getstr(self, y, x, initialText = "", format = None, maxWidth = None):
+  def getstr(self, y, x, initialText = "", format = None, maxWidth = None, validator = 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
@@ -592,6 +592,7 @@ class Panel():
       initialText - starting text in this field
       format      - format used for the text
       maxWidth    - maximum width for the text field
+      validator   - custom TextInputValidator for handling keybindings
     """
     
     if not format: format = curses.A_NORMAL
@@ -619,8 +620,11 @@ class Panel():
     
     textbox = curses.textpad.Textbox(inputSubwindow)
     
+    if not validator:
+      validator = textInput.BasicValidator()
+    
     textbox.win.attron(format)
-    userInput = textbox.edit(lambda key: _textboxValidate(textbox, key)).strip()
+    userInput = textbox.edit(lambda key: validator.validate(key, textbox)).strip()
     textbox.win.attroff(format)
     if textbox.lastcmd == curses.ascii.BEL: userInput = None
     
@@ -727,52 +731,3 @@ class Panel():
       log.log(CONFIG["log.panelRecreated"], msg)
     return recreate
 
-def _textboxValidate(textbox, key):
-  """
-  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
-  """
-  
-  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.
-    
-    currentInput = textbox.gather()
-    textbox.win.addstr(y, x + 1, currentInput[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):
-    msgLen = len(textbox.gather())
-    textbox.win.move(y, x) # reverts cursor movement during gather call
-    
-    if key == curses.KEY_END and msgLen > 0 and x < msgLen - 1:
-      # if we're in the content then move to the end
-      textbox.win.move(y, msgLen - 1)
-      return None
-    elif key == curses.KEY_RIGHT and x >= msgLen - 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 key
-
diff --git a/src/util/textInput.py b/src/util/textInput.py
new file mode 100644
index 0000000..9422f81
--- /dev/null
+++ b/src/util/textInput.py
@@ -0,0 +1,101 @@
+"""
+Provides input validators that provide text input with various capabilities.
+These can be chained together with the first matching validator taking
+precidence.
+"""
+
+import curses
+
+PASS = -1
+
+class TextInputValidator:
+  """
+  Basic interface for validators. Implementations should override the handleKey
+  method.
+  """
+  
+  def __init__(self, nextValidator = None):
+    self.nextValidator = nextValidator
+  
+  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.handleKey(key, textbox)
+    
+    if result != PASS:
+      return result
+    elif self.nextValidator:
+      return self.nextValidator.validate(key, textbox)
+    else: return key
+  
+  def handleKey(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 handleKey(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.
+      
+      currentInput = textbox.gather()
+      textbox.win.addstr(y, x + 1, currentInput[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):
+      msgLen = len(textbox.gather())
+      textbox.win.move(y, x) # reverts cursor movement during gather call
+      
+      if key == curses.KEY_END and msgLen > 0 and x < msgLen - 1:
+        # if we're in the content then move to the end
+        textbox.win.move(y, msgLen - 1)
+        return None
+      elif key == curses.KEY_RIGHT and x >= msgLen - 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
+





More information about the tor-commits mailing list