[tor-commits] [nyx/master] Replace handle_key() and get_help() with key handlers

atagar at torproject.org atagar at torproject.org
Fri Apr 1 17:13:40 UTC 2016


commit b6503d30cb60fffae52f3955f1e3493d5a1cab21
Author: Damian Johnson <atagar at torproject.org>
Date:   Fri Apr 1 10:05:04 2016 -0700

    Replace handle_key() and get_help() with key handlers
    
    Having a single KeyHandler struct that provides both help information and
    triggers the actions it describes. Besides being cleaner this provides an
    assurance that the keybindings we display are in sync with the handler.
---
 nyx/controller.py       |   6 +--
 nyx/panel/__init__.py   |  53 ++++++++++++++--------
 nyx/panel/config.py     |  32 +++++---------
 nyx/panel/connection.py | 115 +++++++++++++++++++++++-------------------------
 nyx/panel/graph.py      |  30 +++++--------
 nyx/panel/header.py     |  18 ++++----
 nyx/panel/log.py        |  45 ++++++++-----------
 nyx/panel/torrc.py      |  25 ++++-------
 nyx/popups.py           |   2 +-
 9 files changed, 152 insertions(+), 174 deletions(-)

diff --git a/nyx/controller.py b/nyx/controller.py
index 3f0d32e..b70b218 100644
--- a/nyx/controller.py
+++ b/nyx/controller.py
@@ -363,7 +363,5 @@ def start_nyx():
       control.redraw(True)
     else:
       for panel_impl in display_panels:
-        is_keystroke_consumed = panel_impl.handle_key(key)
-
-        if is_keystroke_consumed:
-          break
+        for keybinding in panel_impl.key_handlers():
+          keybinding.handle(key)
diff --git a/nyx/panel/__init__.py b/nyx/panel/__init__.py
index b725e20..34a6af1 100644
--- a/nyx/panel/__init__.py
+++ b/nyx/panel/__init__.py
@@ -3,6 +3,7 @@ Panels consisting the nyx interface.
 """
 
 import collections
+import inspect
 import time
 import curses
 import curses.ascii
@@ -19,6 +20,7 @@ PASS = -1
 __all__ = [
   'config',
   'connection',
+  'graph',
   'header',
   'log',
   'torrc',
@@ -37,17 +39,43 @@ CONFIG = conf.config_dict('nyx', {
 HALT_ACTIVITY = False  # prevents curses redraws if set
 
 
-class Help(collections.namedtuple('Help', ['key', 'description', 'current'])):
+class KeyHandler(collections.namedtuple('Help', ['key', 'description', 'current'])):
   """
-  Help information about keybindings the panel handles.
+  Action that can be taken via a given keybinding.
 
   :var str key: key the user can press
   :var str description: description of what it does
   :var str current: optional current value
+
+  :param str key: key the user can press
+  :param str description: description of what it does
+  :param func action: action to be taken, this can optionally take a single
+    argument which is the keypress
+  :param str current: current value to be displayed
+  :param func key_func: custom function to determine if this key was pressed
   """
 
-  def __new__(self, key, description, current = None):
-    return super(Help, self).__new__(self, key, description, current)
+  def __new__(self, key, description = None, action = None, current = None, key_func = None):
+    instance = super(KeyHandler, self).__new__(self, key, description, current)
+    instance._action = action
+    instance._key_func = key_func
+    return instance
+
+  def handle(self, key):
+    """
+    Triggers action if our key was pressed.
+
+    :param nyx.curses.KeyInput key: keypress to be matched against
+    """
+
+    if self._action:
+      is_match = self._key_func(key) if self._key_func else key.match(self.key)
+
+      if is_match:
+        if inspect.getargspec(self._action).args == ['key']:
+          self._action(key)
+        else:
+          self._action()
 
 
 class BasicValidator(object):
@@ -272,21 +300,10 @@ class Panel(object):
 
     return (new_height, new_width)
 
-  def handle_key(self, key):
-    """
-    Handler for user input. This returns true if the key press was consumed,
-    false otherwise.
-
-    Arguments:
-      key - keycode for the key pressed
-    """
-
-    return False
-
-  def get_help(self):
+  def key_handlers(self):
     """
-    Provides help information for the controls this page provides. This is a
-    tuple of :class:`~nyx.panel.Help` instances.
+    Provides options this panel supports. This is a tuple of
+    :class:`~nyx.panel.KeyHandler` instances.
     """
 
     return ()
diff --git a/nyx/panel/config.py b/nyx/panel/config.py
index f0eac85..dbbe65b 100644
--- a/nyx/panel/config.py
+++ b/nyx/panel/config.py
@@ -233,14 +233,15 @@ class ConfigPanel(nyx.panel.Panel):
 
     self.redraw(True)
 
-  def handle_key(self, key):
-    if key.is_scroll():
+  def key_handlers(self):
+    def _scroll(key):
       page_height = self.get_preferred_size()[0] - DETAILS_HEIGHT
       is_changed = self._scroller.handle_key(key, self._get_config_options(), page_height)
 
       if is_changed:
         self.redraw(True)
-    elif key.is_selection():
+
+    def _edit_selected_value():
       selected = self._scroller.selection(self._get_config_options())
       initial_value = selected.value() if selected.is_set() else ''
       new_value = nyx.controller.input_prompt('%s Value (esc to cancel): ' % selected.name, initial_value)
@@ -261,28 +262,17 @@ class ConfigPanel(nyx.panel.Panel):
           self.redraw(True)
         except Exception as exc:
           nyx.controller.show_message('%s (press any key)' % exc, HIGHLIGHT, max_wait = 30)
-    elif key.match('a'):
+
+    def _toggle_show_all():
       self._show_all = not self._show_all
       self.redraw(True)
-    elif key.match('s'):
-      self.show_sort_dialog()
-    elif key.match('w'):
-      self.show_write_dialog()
-    else:
-      return False
-
-    return True
 
-  def get_help(self):
     return (
-      nyx.panel.Help('up arrow', 'scroll up a line'),
-      nyx.panel.Help('down arrow', 'scroll down a line'),
-      nyx.panel.Help('page up', 'scroll up a page'),
-      nyx.panel.Help('page down', 'scroll down a page'),
-      nyx.panel.Help('enter', 'edit configuration option'),
-      nyx.panel.Help('w', 'write torrc'),
-      nyx.panel.Help('a', 'toggle filtering'),
-      nyx.panel.Help('s', 'sort ordering'),
+      nyx.panel.KeyHandler('arrows', 'scroll up and down', _scroll, key_func = lambda key: key.is_scroll()),
+      nyx.panel.KeyHandler('enter', 'edit configuration option', _edit_selected_value, key_func = lambda key: key.is_selection()),
+      nyx.panel.KeyHandler('w', 'write torrc', self.show_write_dialog),
+      nyx.panel.KeyHandler('a', 'toggle filtering', _toggle_show_all),
+      nyx.panel.KeyHandler('s', 'sort ordering', self.show_sort_dialog),
     )
 
   def draw(self, width, height):
diff --git a/nyx/panel/connection.py b/nyx/panel/connection.py
index 127cd12..76d123f 100644
--- a/nyx/panel/connection.py
+++ b/nyx/panel/connection.py
@@ -314,10 +314,36 @@ class ConnectionPanel(nyx.panel.Panel, threading.Thread):
       self._sort_order = results
       self._entries = sorted(self._entries, key = lambda entry: [entry.sort_value(attr) for attr in self._sort_order])
 
-  def handle_key(self, key):
-    user_traffic_allowed = tor_controller().is_user_traffic_allowed()
+  def run(self):
+    """
+    Keeps connections listing updated, checking for new entries at a set rate.
+    """
+
+    last_ran = -1
+
+    while not self._halt:
+      if self.is_paused() or not tor_controller().is_alive() or (time.time() - last_ran) < UPDATE_RATE:
+        with self._pause_condition:
+          if not self._halt:
+            self._pause_condition.wait(0.2)
+
+        continue  # done waiting, try again
+
+      self._update()
+      self.redraw(True)
+
+      # TODO: The following is needed to show results *but* causes curses to
+      # flicker. For our plans on this see...
+      #
+      #   https://trac.torproject.org/projects/tor/ticket/18547#comment:1
+
+      # if last_ran == -1:
+      #   nyx.tracker.get_consensus_tracker().update(tor_controller().get_network_statuses([]))
+
+      last_ran = time.time()
 
-    if key.is_scroll():
+  def key_handlers(self):
+    def _scroll(key):
       page_height = self.get_preferred_size()[0] - 1
 
       if self._show_details:
@@ -328,22 +354,12 @@ class ConnectionPanel(nyx.panel.Panel, threading.Thread):
 
       if is_changed:
         self.redraw(True)
-    elif key.is_selection():
+
+    def _show_details():
       self._show_details = not self._show_details
       self.redraw(True)
-    elif key.match('s'):
-      self.show_sort_dialog()
-    elif key.match('r'):
-      connection_tracker = nyx.tracker.get_connection_tracker()
-      options = ['auto'] + list(connection.Resolver) + list(nyx.tracker.CustomResolver)
-
-      resolver = connection_tracker.get_custom_resolver()
-      selected_index = 0 if resolver is None else options.index(resolver)
-      selected = nyx.popups.show_menu('Connection Resolver:', options, selected_index)
 
-      if selected != -1:
-        connection_tracker.set_custom_resolver(None if selected == 0 else options[selected])
-    elif key.match('d'):
+    def _show_descriptor():
       entries = self._entries
 
       while True:
@@ -367,9 +383,22 @@ class ConnectionPanel(nyx.panel.Panel, threading.Thread):
           self.handle_key(nyx.curses.KeyInput(curses.KEY_DOWN))
 
       self.redraw(True)
-    elif key.match('c') and user_traffic_allowed.inbound:
+
+    def _pick_connection_resolver():
+      connection_tracker = nyx.tracker.get_connection_tracker()
+      options = ['auto'] + list(connection.Resolver) + list(nyx.tracker.CustomResolver)
+
+      resolver = connection_tracker.get_custom_resolver()
+      selected_index = 0 if resolver is None else options.index(resolver)
+      selected = nyx.popups.show_menu('Connection Resolver:', options, selected_index)
+
+      if selected != -1:
+        connection_tracker.set_custom_resolver(None if selected == 0 else options[selected])
+
+    def _show_client_locales():
       nyx.popups.show_count_dialog('Client Locales', self._client_locale_usage)
-    elif key.match('e') and user_traffic_allowed.outbound:
+
+    def _show_exiting_port_usage():
       counts = {}
       key_width = max(map(len, self._exit_port_usage.keys()))
 
@@ -382,59 +411,23 @@ class ConnectionPanel(nyx.panel.Panel, threading.Thread):
         counts[k] = v
 
       nyx.popups.show_count_dialog('Exiting Port Usage', counts)
-    else:
-      return False
-
-    return True
-
-  def run(self):
-    """
-    Keeps connections listing updated, checking for new entries at a set rate.
-    """
-
-    last_ran = -1
-
-    while not self._halt:
-      if self.is_paused() or not tor_controller().is_alive() or (time.time() - last_ran) < UPDATE_RATE:
-        with self._pause_condition:
-          if not self._halt:
-            self._pause_condition.wait(0.2)
-
-        continue  # done waiting, try again
-
-      self._update()
-      self.redraw(True)
-
-      # TODO: The following is needed to show results *but* causes curses to
-      # flicker. For our plans on this see...
-      #
-      #   https://trac.torproject.org/projects/tor/ticket/18547#comment:1
-
-      # if last_ran == -1:
-      #   nyx.tracker.get_consensus_tracker().update(tor_controller().get_network_statuses([]))
-
-      last_ran = time.time()
 
-  def get_help(self):
     resolver = nyx.tracker.get_connection_tracker().get_custom_resolver()
     user_traffic_allowed = tor_controller().is_user_traffic_allowed()
 
     options = [
-      nyx.panel.Help('up arrow', 'scroll up a line'),
-      nyx.panel.Help('down arrow', 'scroll down a line'),
-      nyx.panel.Help('page up', 'scroll up a page'),
-      nyx.panel.Help('page down', 'scroll down a page'),
-      nyx.panel.Help('enter', 'show connection details'),
-      nyx.panel.Help('d', 'raw consensus descriptor'),
-      nyx.panel.Help('s', 'sort ordering'),
-      nyx.panel.Help('r', 'connection resolver', 'auto' if resolver is None else resolver),
+      nyx.panel.KeyHandler('arrows', 'scroll up and down', _scroll, key_func = lambda key: key.is_scroll()),
+      nyx.panel.KeyHandler('enter', 'show connection details', _show_details, key_func = lambda key: key.is_selection()),
+      nyx.panel.KeyHandler('d', 'raw consensus descriptor', _show_descriptor),
+      nyx.panel.KeyHandler('s', 'sort ordering', self.show_sort_dialog),
+      nyx.panel.KeyHandler('r', 'connection resolver', _pick_connection_resolver, 'auto' if resolver is None else resolver),
     ]
 
     if user_traffic_allowed.inbound:
-      options.append(nyx.panel.Help('c', 'client locale usage summary'))
+      options.append(nyx.panel.KeyHandler('c', 'client locale usage summary', _show_client_locales))
 
     if user_traffic_allowed.outbound:
-      options.append(nyx.panel.Help('e', 'exit port usage summary'))
+      options.append(nyx.panel.KeyHandler('e', 'exit port usage summary', _show_exiting_port_usage))
 
     return tuple(options)
 
diff --git a/nyx/panel/graph.py b/nyx/panel/graph.py
index b09c8b8..b092ecc 100644
--- a/nyx/panel/graph.py
+++ b/nyx/panel/graph.py
@@ -501,14 +501,8 @@ class GraphPanel(nyx.panel.Panel):
       finally:
         nyx.controller.show_message()
 
-  def handle_key(self, key):
-    if key.match('r'):
-      self.resize_graph()
-    elif key.match('b'):
-      # uses the next boundary type
-      self.bounds_type = Bounds.next(self.bounds_type)
-      self.redraw(True)
-    elif key.match('s'):
+  def key_handlers(self):
+    def _pick_stats():
       # provides a menu to pick the graphed stats
 
       available_stats = sorted(self.stat_options())
@@ -523,26 +517,24 @@ class GraphPanel(nyx.panel.Panel):
         self.displayed_stat = None
       elif selection != -1:
         self.displayed_stat = available_stats[selection - 1]
-    elif key.match('i'):
-      # provides menu to pick graph panel update interval
 
+    def _next_bounds():
+      self.bounds_type = Bounds.next(self.bounds_type)
+      self.redraw(True)
+
+    def _pick_interval():
       selection = nyx.popups.show_menu('Update Interval:', list(Interval), list(Interval).index(self.update_interval))
 
       if selection != -1:
         self.update_interval = list(Interval)[selection]
 
       self.redraw(True)
-    else:
-      return False
-
-    return True
 
-  def get_help(self):
     return (
-      nyx.panel.Help('r', 'resize graph'),
-      nyx.panel.Help('s', 'graphed stats', self.displayed_stat if self.displayed_stat else 'none'),
-      nyx.panel.Help('b', 'graph bounds', self.bounds_type.replace('_', ' ')),
-      nyx.panel.Help('i', 'graph update interval', self.update_interval),
+      nyx.panel.KeyHandler('r', 'resize graph', self.resize_graph),
+      nyx.panel.KeyHandler('s', 'graphed stats', _pick_stats, self.displayed_stat if self.displayed_stat else 'none'),
+      nyx.panel.KeyHandler('b', 'graph bounds', _next_bounds, self.bounds_type.replace('_', ' ')),
+      nyx.panel.KeyHandler('i', 'graph update interval', _pick_interval, self.update_interval),
     )
 
   def set_paused(self, is_pause):
diff --git a/nyx/panel/header.py b/nyx/panel/header.py
index 00d3ff2..065ade1 100644
--- a/nyx/panel/header.py
+++ b/nyx/panel/header.py
@@ -130,10 +130,11 @@ class HeaderPanel(nyx.panel.Panel, threading.Thread):
     if not self.is_wide():
       self.show_message('Requesting a new identity', HIGHLIGHT, max_wait = 1)
 
-  def handle_key(self, key):
-    if key.match('n'):
-      self.send_newnym()
-    elif key.match('r') and not self._vals.is_connected:
+  def key_handlers(self):
+    def _reconnect():
+      if self._vals.is_connected:
+        return
+
       # TODO: This is borked. Not quite sure why but our attempt to call
       # PROTOCOLINFO fails with a socket error, followed by completely freezing
       # nyx. This is exposing two bugs...
@@ -141,7 +142,7 @@ class HeaderPanel(nyx.panel.Panel, threading.Thread):
       # * This should be working. That's a stem issue.
       # * Our interface shouldn't be locking up. That's an nyx issue.
 
-      return True
+      return
 
       controller = tor_controller()
 
@@ -161,10 +162,11 @@ class HeaderPanel(nyx.panel.Panel, threading.Thread):
       except Exception as exc:
         self.show_message('Unable to reconnect (%s)' % exc, HIGHLIGHT, max_wait = 3)
         controller.close()
-    else:
-      return False
 
-    return True
+    return (
+      nyx.panel.KeyHandler('n', action = self.send_newnym),
+      nyx.panel.KeyHandler('r', action = _reconnect),
+    )
 
   def draw(self, width, height):
     vals = self._vals  # local reference to avoid concurrency concerns
diff --git a/nyx/panel/log.py b/nyx/panel/log.py
index 8a0a9c7..fc11aee 100644
--- a/nyx/panel/log.py
+++ b/nyx/panel/log.py
@@ -222,23 +222,15 @@ class LogPanel(nyx.panel.Panel, threading.Thread):
       except Exception as exc:
         raise IOError("unable to write to '%s': %s" % (path, exc))
 
-  def handle_key(self, key):
-    if key.is_scroll():
+  def key_handlers(self):
+    def _scroll(key):
       page_height = self.get_preferred_size()[0] - 1
       is_changed = self._scroller.handle_key(key, self._last_content_height, page_height)
 
       if is_changed:
         self.redraw(True)
-    elif key.match('u'):
-      self.set_duplicate_visability(not self._show_duplicates)
-      self.redraw(True)
-    elif key.match('c'):
-      msg = 'This will clear the log. Are you sure (c again to confirm)?'
-      key_press = nyx.controller.show_message(msg, BOLD, max_wait = 30)
 
-      if key_press.match('c'):
-        self.clear()
-    elif key.match('f'):
+    def _pick_filter():
       with nyx.curses.CURSES_LOCK:
         initial_selection = 1 if self._filter.selection() else 0
         options = ['None'] + self._filter.latest_selections() + ['New...']
@@ -251,24 +243,25 @@ class LogPanel(nyx.panel.Panel, threading.Thread):
           self.show_filter_prompt()
         elif selection != -1:
           self._filter.select(self._filter.latest_selections()[selection - 1])
-    elif key.match('e'):
-      self.show_event_selection_prompt()
-    elif key.match('a'):
-      self.show_snapshot_prompt()
-    else:
-      return False
 
-    return True
+    def _toggle_deduplication():
+      self.set_duplicate_visability(not self._show_duplicates)
+      self.redraw(True)
+
+    def _clear_log():
+      msg = 'This will clear the log. Are you sure (c again to confirm)?'
+      key_press = nyx.controller.show_message(msg, BOLD, max_wait = 30)
+
+      if key_press.match('c'):
+        self.clear()
 
-  def get_help(self):
     return (
-      nyx.panel.Help('up arrow', 'scroll log up a line'),
-      nyx.panel.Help('down arrow', 'scroll log down a line'),
-      nyx.panel.Help('a', 'save snapshot of the log'),
-      nyx.panel.Help('e', 'change logged events'),
-      nyx.panel.Help('f', 'log regex filter', 'enabled' if self._filter.selection() else 'disabled'),
-      nyx.panel.Help('u', 'duplicate log entries', 'visible' if self._show_duplicates else 'hidden'),
-      nyx.panel.Help('c', 'clear event log'),
+      nyx.panel.KeyHandler('arrows', 'scroll up and down', _scroll, key_func = lambda key: key.is_scroll()),
+      nyx.panel.KeyHandler('a', 'save snapshot of the log', self.show_snapshot_prompt),
+      nyx.panel.KeyHandler('e', 'change logged events', self.show_event_selection_prompt),
+      nyx.panel.KeyHandler('f', 'log regex filter', _pick_filter, 'enabled' if self._filter.selection() else 'disabled'),
+      nyx.panel.KeyHandler('u', 'duplicate log entries', _toggle_deduplication, 'visible' if self._show_duplicates else 'hidden'),
+      nyx.panel.KeyHandler('c', 'clear event log', _clear_log),
     )
 
   def set_paused(self, is_pause):
diff --git a/nyx/panel/torrc.py b/nyx/panel/torrc.py
index 560afb3..1e08742 100644
--- a/nyx/panel/torrc.py
+++ b/nyx/panel/torrc.py
@@ -80,31 +80,24 @@ class TorrcPanel(panel.Panel):
     self._show_line_numbers = is_visible
     self.redraw(True)
 
-  def handle_key(self, key):
-    if key.is_scroll():
+  def key_handlers(self):
+    def _scroll(key):
       page_height = self.get_preferred_size()[0] - 1
       is_changed = self._scroller.handle_key(key, self._last_content_height, page_height)
 
       if is_changed:
         self.redraw(True)
-    elif key.match('l'):
-      self.set_line_number_visible(not self._show_line_numbers)
-    elif key.match('s'):
+
+    def _toggle_comment_stripping():
       self.set_comments_visible(not self._show_comments)
-    else:
-      return False
 
-    return True
+    def _toggle_line_numbers():
+      self.set_line_number_visible(not self._show_line_numbers)
 
-  def get_help(self):
     return (
-      nyx.panel.Help('up arrow', 'scroll up a line'),
-      nyx.panel.Help('down arrow', 'scroll down a line'),
-      nyx.panel.Help('page up', 'scroll up a page'),
-      nyx.panel.Help('page down', 'scroll down a page'),
-      nyx.panel.Help('s', 'comment stripping', 'off' if self._show_comments else 'on'),
-      nyx.panel.Help('l', 'line numbering', 'on' if self._show_line_numbers else 'off'),
-      nyx.panel.Help('x', 'reset tor (issue sighup)'),
+      nyx.panel.KeyHandler('arrows', 'scroll up and down', _scroll, key_func = lambda key: key.is_scroll()),
+      nyx.panel.KeyHandler('s', 'comment stripping', _toggle_comment_stripping, 'off' if self._show_comments else 'on'),
+      nyx.panel.KeyHandler('l', 'line numbering', _toggle_line_numbers, 'on' if self._show_line_numbers else 'off'),
     )
 
   def draw(self, width, height):
diff --git a/nyx/popups.py b/nyx/popups.py
index 0952a8d..b01815c 100644
--- a/nyx/popups.py
+++ b/nyx/popups.py
@@ -87,7 +87,7 @@ def show_help_popup():
   help_options = []
 
   for panel in reversed(control.get_display_panels()):
-    help_options += panel.get_help()
+    help_options += [handler for handler in panel.key_handlers() if handler.description]
 
   def _render(subwindow):
     subwindow.box()



More information about the tor-commits mailing list