commit b3fd2f2df18ce7310c33006f1d81f10bae620010
Author: Damian Johnson <atagar(a)torproject.org>
Date: Fri May 6 21:00:02 2011 -0700
Moving help popup into a new popup toolkit
Breaking the help popup out of the controller, moving it to a nice, modular
'show popup' function. This is a drop-in replacement (besides some reordered
help options) and also allowed us to move the help content into their
respective panels.
This is far cleaner and avoids accessing private panel parameters. Next the
rest of the popups will be moved to this new toolkit, which will greatly clean
up the controller.
---
README | 3 +-
src/cli/__init__.py | 2 +-
src/cli/configPanel.py | 12 +++
src/cli/connections/connPanel.py | 16 ++++
src/cli/controller.py | 155 ++++++++++++++++----------------------
src/cli/graphing/graphPanel.py | 12 +++
src/cli/logPanel.py | 11 +++
src/cli/popups.py | 110 +++++++++++++++++++++++++++
src/cli/torrcPanel.py | 12 +++
src/util/panel.py | 9 ++
10 files changed, 250 insertions(+), 92 deletions(-)
diff --git a/README b/README
index ff03b37..e7c2e2c 100644
--- a/README
+++ b/README
@@ -157,7 +157,8 @@ Layout:
__init__.py
controller.py - main display loop, handling input and layout
headerPanel.py - top of all pages, providing general information
- descriptorPopup.py - (popup) displays connection descriptor data
+ descriptorPopup.py - displays connection descriptor data
+ popups.py - toolkit providing display popups
logPanel.py - (page 1) displays tor, arm, and torctl events
configPanel.py - (page 3) editor panel for the tor configuration
diff --git a/src/cli/__init__.py b/src/cli/__init__.py
index 171af09..1564f68 100644
--- a/src/cli/__init__.py
+++ b/src/cli/__init__.py
@@ -2,5 +2,5 @@
Panels, popups, and handlers comprising the arm user interface.
"""
-__all__ = ["configPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "torrcPanel"]
+__all__ = ["configPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "popups", "torrcPanel"]
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index fd6fb54..90c6191 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -248,6 +248,18 @@ class ConfigPanel(panel.Panel):
self.redraw(True)
self.valsLock.release()
+ def getHelp(self):
+ options = []
+ options.append(("up arrow", "scroll up a line", None))
+ options.append(("down arrow", "scroll down a line", None))
+ options.append(("page up", "scroll up a page", None))
+ options.append(("page down", "scroll down a page", None))
+ options.append(("enter", "edit configuration option", None))
+ options.append(("w", "save configuration", None))
+ options.append(("a", "toggle option filtering", None))
+ options.append(("s", "sort ordering", None))
+ return options
+
def draw(self, width, height):
self.valsLock.acquire()
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 5f4f036..40c479e 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -185,6 +185,22 @@ class ConnectionPanel(panel.Panel, threading.Thread):
drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"]
lastDraw += self._config["features.connection.refreshRate"] * drawTicks
+ def getHelp(self):
+ resolverUtil = connections.getResolver("tor").overwriteResolver
+ if resolverUtil == None: resolverUtil = "auto"
+
+ options = []
+ options.append(("up arrow", "scroll up a line", None))
+ options.append(("down arrow", "scroll down a line", None))
+ options.append(("page up", "scroll up a page", None))
+ options.append(("page down", "scroll down a page", None))
+ options.append(("enter", "edit configuration option", None))
+ options.append(("d", "raw consensus descriptor", None))
+ options.append(("l", "listed identity", self._listingType.lower()))
+ options.append(("s", "sort ordering", None))
+ options.append(("u", "resolving utility", resolverUtil))
+ return options
+
def draw(self, width, height):
self.valsLock.acquire()
diff --git a/src/cli/controller.py b/src/cli/controller.py
index b8bb5cd..1501419 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -15,6 +15,7 @@ import curses.textpad
import socket
from TorCtl import TorCtl
+import popups
import headerPanel
import graphing.graphPanel
import logPanel
@@ -30,6 +31,56 @@ import graphing.bandwidthStats
import graphing.connStats
import graphing.resourceStats
+# TODO: controller should be its own object that can be refreshed - until that
+# emulating via a 'refresh' flag
+REFRESH_FLAG = False
+
+def refresh():
+ global REFRESH_FLAG
+ REFRESH_FLAG = True
+
+# new panel params and accessors (this is part of the new controller apis)
+PANELS = {}
+STDSCR = None
+
+def getScreen():
+ return STDSCR
+
+def getPage():
+ """
+ Provides the number belonging to this page. Page numbers start at one.
+ """
+
+ return PAGE + 1
+
+def getPanel(name):
+ """
+ Provides the panel with the given identifier.
+
+ Arguments:
+ name - name of the panel to be fetched
+ """
+
+ return PANELS[name]
+
+def getPanels(page = None):
+ """
+ Provides all panels or all panels from a given page.
+
+ Arguments:
+ page - page number of the panels to be fetched, all panels if undefined
+ """
+
+ panelSet = []
+ if page == None:
+ # fetches all panel names
+ panelSet = list(PAGE_S)
+ for pagePanels in PAGES:
+ panelSet += pagePanels
+ else: panelSet = PAGES[page - 1]
+
+ return [getPanel(name) for name in panelSet]
+
CONFIRM_QUIT = True
REFRESH_RATE = 5 # seconds between redrawing screen
MAX_REGEX_FILTERS = 5 # maximum number of previous regex filters that'll be remembered
@@ -422,6 +473,9 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
otherwise unrecognized events)
"""
+ global PANELS, STDSCR, REFRESH_FLAG, PAGE
+ STDSCR = stdscr
+
# loads config for various interface components
config = conf.getConfig("arm")
config.update(CONFIG)
@@ -609,6 +663,8 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
pluralLabel = "s" if len(missingEventTypes) > 1 else ""
log.log(CONFIG["log.torEventTypeUnrecognized"], "arm doesn't recognize the following event type%s: %s (log 'UNKNOWN' events to see them)" % (pluralLabel, ", ".join(missingEventTypes)))
+ PANELS = panels
+
# tells revised panels to run as daemons
panels["header"].start()
panels["log"].start()
@@ -631,6 +687,8 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
regexFilters = [] # previously used log regex filters
panels["popup"].redraw(True) # hack to make sure popup has a window instance (not entirely sure why...)
+ PAGE = page
+
# provides notice about any unused config keys
for key in config.getUnusedKeys():
log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
@@ -866,6 +924,8 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
isVisible = i == page
for entry in PAGES[i]: panels[entry].setVisible(isVisible)
+ PAGE = page
+
panels["control"].page = page + 1
# TODO: this redraw doesn't seem necessary (redraws anyway after this
@@ -914,96 +974,7 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
finally:
panel.CURSES_LOCK.release()
elif key == ord('h') or key == ord('H'):
- # displays popup for current page's controls
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # lists commands
- popup = panels["popup"]
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Page %i Commands:" % (page + 1), curses.A_STANDOUT)
-
- pageOverrideKeys = ()
-
- if page == 0:
- graphedStats = panels["graph"].currentDisplay
- if not graphedStats: graphedStats = "none"
- popup.addfstr(1, 2, "<b>up arrow</b>: scroll log up a line")
- popup.addfstr(1, 41, "<b>down arrow</b>: scroll log down a line")
- popup.addfstr(2, 2, "<b>m</b>: increase graph size")
- popup.addfstr(2, 41, "<b>n</b>: decrease graph size")
- popup.addfstr(3, 2, "<b>s</b>: graphed stats (<b>%s</b>)" % graphedStats)
- popup.addfstr(3, 41, "<b>i</b>: graph update interval (<b>%s</b>)" % graphing.graphPanel.UPDATE_INTERVALS[panels["graph"].updateInterval][0])
- popup.addfstr(4, 2, "<b>b</b>: graph bounds (<b>%s</b>)" % panels["graph"].bounds.lower())
- popup.addfstr(4, 41, "<b>a</b>: save snapshot of the log")
- popup.addfstr(5, 2, "<b>e</b>: change logged events")
-
- regexLabel = "enabled" if panels["log"].regexFilter else "disabled"
- popup.addfstr(5, 41, "<b>f</b>: log regex filter (<b>%s</b>)" % regexLabel)
-
- hiddenEntryLabel = "visible" if panels["log"].showDuplicates else "hidden"
- popup.addfstr(6, 2, "<b>u</b>: duplicate log entries (<b>%s</b>)" % hiddenEntryLabel)
- popup.addfstr(6, 41, "<b>c</b>: clear event log")
-
- pageOverrideKeys = (ord('m'), ord('n'), ord('s'), ord('i'), ord('d'), ord('e'), ord('r'), ord('f'), ord('x'))
- if page == 1:
- popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
- popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
- popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
- popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
-
- popup.addfstr(3, 2, "<b>enter</b>: edit configuration option")
- popup.addfstr(3, 41, "<b>d</b>: raw consensus descriptor")
-
- listingType = panels["conn"]._listingType.lower()
- popup.addfstr(4, 2, "<b>l</b>: listed identity (<b>%s</b>)" % listingType)
-
- popup.addfstr(4, 41, "<b>s</b>: sort ordering")
-
- resolverUtil = connections.getResolver("tor").overwriteResolver
- if resolverUtil == None: resolverUtil = "auto"
- popup.addfstr(5, 2, "<b>u</b>: resolving utility (<b>%s</b>)" % resolverUtil)
-
- pageOverrideKeys = (ord('d'), ord('l'), ord('s'), ord('u'))
- elif page == 2:
- popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
- popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
- popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
- popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
- popup.addfstr(3, 2, "<b>enter</b>: edit configuration option")
- popup.addfstr(3, 41, "<b>w</b>: save configuration")
- popup.addfstr(4, 2, "<b>a</b>: toggle option filtering")
- popup.addfstr(4, 41, "<b>s</b>: sort ordering")
- elif page == 3:
- popup.addfstr(1, 2, "<b>up arrow</b>: scroll up a line")
- popup.addfstr(1, 41, "<b>down arrow</b>: scroll down a line")
- popup.addfstr(2, 2, "<b>page up</b>: scroll up a page")
- popup.addfstr(2, 41, "<b>page down</b>: scroll down a page")
-
- strippingLabel = "on" if panels["torrc"].stripComments else "off"
- popup.addfstr(3, 2, "<b>s</b>: comment stripping (<b>%s</b>)" % strippingLabel)
-
- lineNumLabel = "on" if panels["torrc"].showLineNum else "off"
- popup.addfstr(3, 41, "<b>n</b>: line numbering (<b>%s</b>)" % lineNumLabel)
-
- popup.addfstr(4, 2, "<b>r</b>: reload torrc")
- popup.addfstr(4, 41, "<b>x</b>: reset tor (issue sighup)")
-
- popup.addstr(7, 2, "Press any key...")
- popup.refresh()
-
- # waits for user to hit a key, if it belongs to a command then executes it
- curses.cbreak()
- helpExitKey = stdscr.getch()
- if helpExitKey in pageOverrideKeys: overrideKey = helpExitKey
- curses.halfdelay(REFRESH_RATE * 10)
-
- setPauseState(panels, isPaused, page)
- selectiveRefresh(panels, page)
- finally:
- panel.CURSES_LOCK.release()
+ overrideKey = popups.showHelpPopup()
elif page == 0 and (key == ord('s') or key == ord('S')):
# provides menu to pick stats to be graphed
#options = ["None"] + [label for label in panels["graph"].stats.keys()]
@@ -1585,6 +1556,10 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
panels["config"].handleKey(key)
elif page == 3:
panels["torrc"].handleKey(key)
+
+ if REFRESH_FLAG:
+ REFRESH_FLAG = False
+ selectiveRefresh(panels, page)
def startTorMonitor(startTime, loggedEvents, isBlindMode):
try:
diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
index 238e163..d8808b3 100644
--- a/src/cli/graphing/graphPanel.py
+++ b/src/cli/graphing/graphPanel.py
@@ -253,6 +253,18 @@ class GraphPanel(panel.Panel):
self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
+ def getHelp(self):
+ if self.currentDisplay: graphedStats = self.currentDisplay
+ else: graphedStats = "none"
+
+ options = []
+ options.append(("m", "increase graph size", None))
+ options.append(("n", "decrease graph size", None))
+ options.append(("s", "graphed stats", graphedStats))
+ options.append(("b", "graph bounds", self.bounds.lower()))
+ options.append(("i", "graph update interval", UPDATE_INTERVALS[self.updateInterval][0]))
+ return options
+
def draw(self, width, height):
""" Redraws graph panel """
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
index 6feb129..b12715e 100644
--- a/src/cli/logPanel.py
+++ b/src/cli/logPanel.py
@@ -761,6 +761,17 @@ class LogPanel(panel.Panel, threading.Thread):
self.redraw(True)
self.valsLock.release()
+ def getHelp(self):
+ options = []
+ options.append(("up arrow", "scroll log up a line", None))
+ options.append(("down arrow", "scroll log down a line", None))
+ options.append(("a", "save snapshot of the log", None))
+ options.append(("e", "change logged events", None))
+ options.append(("f", "log regex filter", "enabled" if self.regexFilter else "disabled"))
+ options.append(("u", "duplicate log entries", "visible" if self.showDuplicates else "hidden"))
+ options.append(("c", "clear event log", None))
+ return options
+
def draw(self, width, height):
"""
Redraws message log. Entries stretch to use available space and may
diff --git a/src/cli/popups.py b/src/cli/popups.py
new file mode 100644
index 0000000..ce51ee8
--- /dev/null
+++ b/src/cli/popups.py
@@ -0,0 +1,110 @@
+"""
+Functions for displaying popups in the interface.
+"""
+
+import curses
+
+import controller
+
+from util import panel, uiTools
+
+def init(height = -1, width = -1):
+ """
+ Preparation for displaying a popup. This creates a popup with a valid
+ subwindow instance. If that's successful then the curses lock is acquired
+ and this returns a tuple of the...
+ (popup, draw width, draw height)
+ Otherwise this leaves curses unlocked and returns None.
+
+ Arguments:
+ height - maximum height of the popup
+ width - maximum width of the popup
+ """
+
+ topSize = controller.getPanel("header").getHeight()
+ topSize += controller.getPanel("control").getHeight()
+
+ popup = panel.Panel(controller.getScreen(), "popup", topSize, height, width)
+ popup.setVisible(True)
+
+ # Redraws the popup to prepare a subwindow instance. If none is spawned then
+ # the panel can't be drawn (for instance, due to not being visible).
+ popup.redraw(True)
+ if popup.win != None:
+ panel.CURSES_LOCK.acquire()
+ return (popup, popup.maxX - 1, popup.maxY)
+ else: return None
+
+def finalize():
+ """
+ Cleans up after displaying a popup, releasing the cureses lock and redrawing
+ the rest of the display.
+ """
+
+ controller.refresh()
+ panel.CURSES_LOCK.release()
+
+def showHelpPopup():
+ """
+ Presents a popup with instructions for the current page's hotkeys. This
+ returns the user input used to close the popup. If the popup didn't close
+ properly, this is an arrow, enter, or scroll key then this returns None.
+ """
+
+ popup, width, height = init(9, 80)
+ if not popup: return
+
+ exitKey = None
+ try:
+ pageNum = controller.getPage()
+ pagePanels = controller.getPanels(pageNum)
+
+ # the first page is the only one with multiple panels, and it looks better
+ # with the log entries first, so reversing the order
+ pagePanels.reverse()
+
+ helpOptions = []
+ for entry in pagePanels:
+ helpOptions += entry.getHelp()
+
+ # test doing afterward in case of overwriting
+ popup.win.box()
+ popup.addstr(0, 0, "Page %i Commands:" % pageNum, curses.A_STANDOUT)
+
+ for i in range(len(helpOptions)):
+ if i / 2 >= height - 2: break
+
+ # draws entries in the form '<key>: <description>[ (<selection>)]', for
+ # instance...
+ # u: duplicate log entries (hidden)
+ key, description, selection = helpOptions[i]
+ if key: description = ": " + description
+ row = (i / 2) + 1
+ col = 2 if i % 2 == 0 else 41
+
+ popup.addstr(row, col, key, curses.A_BOLD)
+ col += len(key)
+ popup.addstr(row, col, description)
+ col += len(description)
+
+ if selection:
+ popup.addstr(row, col, " (")
+ popup.addstr(row, col + 2, selection, curses.A_BOLD)
+ popup.addstr(row, col + 2 + len(selection), ")")
+
+ # tells user to press a key if the lower left is unoccupied
+ if len(helpOptions) < 13 and height == 9:
+ popup.addstr(7, 2, "Press any key...")
+
+ popup.win.refresh()
+ curses.cbreak()
+ exitKey = controller.getScreen().getch()
+ curses.halfdelay(controller.REFRESH_RATE * 10)
+ finally: finalize()
+
+ if not uiTools.isSelectionKey(exitKey) and \
+ not uiTools.isScrollKey(exitKey) and \
+ not exitKey in (curses.KEY_LEFT, curses.KEY_RIGHT):
+ return exitKey
+ else: return None
+
diff --git a/src/cli/torrcPanel.py b/src/cli/torrcPanel.py
index b7cad86..6d7156d 100644
--- a/src/cli/torrcPanel.py
+++ b/src/cli/torrcPanel.py
@@ -60,6 +60,18 @@ class TorrcPanel(panel.Panel):
self.valsLock.release()
+ def getHelp(self):
+ options = []
+ options.append(("up arrow", "scroll up a line", None))
+ options.append(("down arrow", "scroll down a line", None))
+ options.append(("page up", "scroll up a page", None))
+ options.append(("page down", "scroll down a page", None))
+ options.append(("s", "comment stripping", "on" if self.stripComments else "off"))
+ options.append(("n", "line numbering", "on" if self.showLineNum else "off"))
+ options.append(("r", "reload torrc", None))
+ options.append(("x", "reset tor (issue sighup)", None))
+ return options
+
def draw(self, width, height):
self.valsLock.acquire()
diff --git a/src/util/panel.py b/src/util/panel.py
index 9c3dd29..7387833 100644
--- a/src/util/panel.py
+++ b/src/util/panel.py
@@ -290,6 +290,15 @@ class Panel():
if setWidth != -1: newWidth = min(newWidth, setWidth)
return (newHeight, newWidth)
+ def getHelp(self):
+ """
+ Provides help information for the controls this page provides. This is a
+ list of tuples of the form...
+ (control, description, status)
+ """
+
+ return []
+
def draw(self, width, height):
"""
Draws display's content. This is meant to be overwritten by