[tor-commits] [arm/release] Moving menus fromt he controller to panels
atagar at torproject.org
atagar at torproject.org
Sun Jul 17 06:08:18 UTC 2011
commit a2c89f6f8261a892cc02aee5325fc0ffff7671d9
Author: Damian Johnson <atagar at torproject.org>
Date: Mon May 9 09:56:27 2011 -0700
Moving menus fromt he controller to panels
Moving all of the option menus from a huge if/else block in the controller to
the panels they're being used for. This included a couple minor changes to the
panel api to better accommodate them.
---
src/cli/configPanel.py | 15 ++-
src/cli/connections/connPanel.py | 39 ++++++-
src/cli/controller.py | 242 ++------------------------------------
src/cli/graphing/graphPanel.py | 36 ++++++-
src/cli/logPanel.py | 59 +++++++++-
src/cli/popups.py | 97 +++++++++++++++-
src/cli/torrcPanel.py | 6 +-
src/util/panel.py | 27 ++++
8 files changed, 272 insertions(+), 249 deletions(-)
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index 6aaafc2..31a78ab 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -247,6 +247,7 @@ class ConfigPanel(panel.Panel):
def handleKey(self, key):
self.valsLock.acquire()
+ isKeystrokeConsumed = True
if uiTools.isScrollKey(key):
pageHeight = self.getPreferredSize()[0] - 1
detailPanelHeight = self._config["features.config.selectionDetails.height"]
@@ -270,8 +271,10 @@ class ConfigPanel(panel.Panel):
# converts labels back to enums
resultEnums = [getFieldFromLabel(label) for label in results]
self.setSortOrder(resultEnums)
+ else: isKeystrokeConsumed = False
self.valsLock.release()
+ return isKeystrokeConsumed
def getHelp(self):
options = []
@@ -288,10 +291,6 @@ class ConfigPanel(panel.Panel):
def draw(self, width, height):
self.valsLock.acquire()
- # draws the top label
- configType = "Tor" if self.configType == State.TOR else "Arm"
- hiddenMsg = "press 'a' to hide most options" if self.showAll else "press 'a' to show all options"
-
# panel with details for the current selection
detailPanelHeight = self._config["features.config.selectionDetails.height"]
isScrollbarVisible = False
@@ -311,8 +310,12 @@ class ConfigPanel(panel.Panel):
self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
- titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg)
- self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+ # draws the top label
+ if self.isTitleVisible():
+ configType = "Tor" if self.configType == State.TOR else "Arm"
+ hiddenMsg = "press 'a' to hide most options" if self.showAll else "press 'a' to show all options"
+ titleLabel = "%s Configuration (%s):" % (configType, hiddenMsg)
+ self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
# draws left-hand scroll bar if content's longer than the height
scrollOffset = 1
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 8ad41d5..4d89d5c 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -131,6 +131,8 @@ class ConnectionPanel(panel.Panel, threading.Thread):
listingType - Listing instance for the primary information to be shown
"""
+ if self._listingType == listingType: return
+
self.valsLock.acquire()
self._listingType = listingType
@@ -143,6 +145,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
def handleKey(self, key):
self.valsLock.acquire()
+ isKeystrokeConsumed = True
if uiTools.isScrollKey(key):
pageHeight = self.getPreferredSize()[0] - 1
if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
@@ -159,8 +162,39 @@ class ConnectionPanel(panel.Panel, threading.Thread):
optionColors = dict([(attr, entries.SORT_COLORS[attr]) for attr in options])
results = cli.popups.showSortDialog(titleLabel, options, oldSelection, optionColors)
if results: self.setSortOrder(results)
+ elif key == ord('u') or key == ord('U'):
+ # provides a menu to pick the connection resolver
+ title = "Resolver Util:"
+ options = ["auto"] + connections.Resolver.values()
+ connResolver = connections.getResolver("tor")
+
+ currentOverwrite = connResolver.overwriteResolver
+ if currentOverwrite == None: oldSelection = 0
+ else: oldSelection = options.index(currentOverwrite)
+
+ selection = cli.popups.showMenu(title, options, oldSelection)
+
+ # applies new setting
+ if selection != -1:
+ selectedOption = options[selection] if selection != 0 else None
+ connResolver.overwriteResolver = selectedOption
+ elif key == ord('l') or key == ord('L'):
+ # provides a menu to pick the primary information we list connections by
+ title = "List By:"
+ options = entries.ListingType.values()
+
+ # dropping the HOSTNAME listing type until we support displaying that content
+ options.remove(cli.connections.entries.ListingType.HOSTNAME)
+
+ oldSelection = options.index(self._listingType)
+ selection = cli.popups.showMenu(title, options, oldSelection)
+
+ # applies new setting
+ if selection != -1: self.setListingType(options[selection])
+ else: isKeystrokeConsumed = False
self.valsLock.release()
+ return isKeystrokeConsumed
def run(self):
"""
@@ -233,8 +267,9 @@ class ConnectionPanel(panel.Panel, threading.Thread):
drawEntries[i].render(self, 1 + i, 2)
# title label with connection counts
- title = "Connection Details:" if self._showDetails else self._title
- self.addstr(0, 0, title, curses.A_STANDOUT)
+ if self.isTitleVisible():
+ title = "Connection Details:" if self._showDetails else self._title
+ self.addstr(0, 0, title, curses.A_STANDOUT)
scrollOffset = 1
if isScrollbarVisible:
diff --git a/src/cli/controller.py b/src/cli/controller.py
index a195c15..6f4b70d 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -42,6 +42,7 @@ def refresh():
# new panel params and accessors (this is part of the new controller apis)
PANELS = {}
STDSCR = None
+IS_PAUSED = False
def getScreen():
return STDSCR
@@ -83,7 +84,6 @@ def getPanels(page = None):
CONFIRM_QUIT = True
REFRESH_RATE = 5 # seconds between redrawing screen
-MAX_REGEX_FILTERS = 5 # maximum number of previous regex filters that'll be remembered
# enums for message in control label
CTL_HELP, CTL_PAUSED = range(2)
@@ -129,6 +129,9 @@ class ControlPanel(panel.Panel):
self.msgText = msgText
self.msgAttr = msgAttr
+ def revertMsg(self):
+ self.setMsg(CTL_PAUSED if IS_PAUSED else CTL_HELP)
+
def draw(self, width, height):
msgText = self.msgText
msgAttr = self.msgAttr
@@ -275,57 +278,6 @@ def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
for key in allPanels: panels[key].setPaused(overwrite or monitorIsPaused or (key not in PAGES[currentPage] and key not in PAGE_S))
-def showMenu(stdscr, popup, title, options, initialSelection):
- """
- Provides menu with options laid out in a single column. User can cancel
- selection with the escape key, in which case this proives -1. Otherwise this
- returns the index of the selection. If initialSelection is -1 then the first
- option is used and the carrot indicating past selection is ommitted.
- """
-
- selection = initialSelection if initialSelection != -1 else 0
-
- if popup.win:
- if not panel.CURSES_LOCK.acquire(False): return -1
- try:
- # TODO: should pause interface (to avoid event accumilation)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
-
- # uses smaller dimentions more fitting for small content
- popup.height = len(options) + 2
-
- newWidth = max([len(label) for label in options]) + 9
- popup.recreate(stdscr, newWidth)
-
- key = 0
- while not uiTools.isSelectionKey(key):
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, title, curses.A_STANDOUT)
-
- for i in range(len(options)):
- label = options[i]
- format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
- tab = "> " if i == initialSelection else " "
- popup.addstr(i + 1, 2, tab)
- popup.addstr(i + 1, 4, " %s " % label, format)
-
- popup.refresh()
- key = stdscr.getch()
- if key == curses.KEY_UP: selection = max(0, selection - 1)
- elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
- elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
-
- # reverts popup dimensions and conn panel label
- popup.height = 9
- popup.recreate(stdscr, 80)
-
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
-
- return selection
-
def setEventListening(selectedEvents, isBlindMode):
# creates a local copy, note that a suspected python bug causes *very*
# puzzling results otherwise when trying to discard entries (silently
@@ -381,7 +333,7 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
otherwise unrecognized events)
"""
- global PANELS, STDSCR, REFRESH_FLAG, PAGE
+ global PANELS, STDSCR, REFRESH_FLAG, PAGE, IS_PAUSED
STDSCR = stdscr
# loads config for various interface components
@@ -592,7 +544,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
isPaused = False # if true updates are frozen
overrideKey = None # immediately runs with this input rather than waiting for the user if set
page = 0
- 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
@@ -846,6 +797,7 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
panel.CURSES_LOCK.acquire()
try:
isPaused = not isPaused
+ IS_PAUSED = isPaused
setPauseState(panels, isPaused, page)
panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
finally:
@@ -883,69 +835,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
panel.CURSES_LOCK.release()
elif key == ord('h') or key == ord('H'):
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()]
- options = ["None"]
-
- # appends stats labels with first letters of each word capitalized
- initialSelection, i = -1, 1
- if not panels["graph"].currentDisplay: initialSelection = 0
- graphLabels = panels["graph"].stats.keys()
- graphLabels.sort()
- for label in graphLabels:
- if label == panels["graph"].currentDisplay: initialSelection = i
- words = label.split()
- options.append(" ".join(word[0].upper() + word[1:] for word in words))
- i += 1
-
- # hides top label of the graph panel and pauses panels
- if panels["graph"].currentDisplay:
- panels["graph"].showLabel = False
- panels["graph"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Graphed Stats:", options, initialSelection)
-
- # reverts changes made for popup
- panels["graph"].showLabel = True
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1 and selection != initialSelection:
- if selection == 0: panels["graph"].setStats(None)
- else: panels["graph"].setStats(options[selection].lower())
-
- selectiveRefresh(panels, page)
-
- # TODO: this shouldn't be necessary with the above refresh, but doesn't seem responsive otherwise...
- panels["graph"].redraw(True)
- elif page == 0 and (key == ord('i') or key == ord('I')):
- # provides menu to pick graph panel update interval
- options = [label for (label, intervalTime) in graphing.graphPanel.UPDATE_INTERVALS]
-
- initialSelection = panels["graph"].updateInterval
-
- #initialSelection = -1
- #for i in range(len(options)):
- # if options[i] == panels["graph"].updateInterval: initialSelection = i
-
- # hides top label of the graph panel and pauses panels
- if panels["graph"].currentDisplay:
- panels["graph"].showLabel = False
- panels["graph"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Update Interval:", options, initialSelection)
-
- # reverts changes made for popup
- panels["graph"].showLabel = True
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1: panels["graph"].updateInterval = selection
-
- selectiveRefresh(panels, page)
elif page == 0 and (key == ord('b') or key == ord('B')):
# uses the next boundary type for graph
panels["graph"].bounds = graphing.graphPanel.Bounds.next(panels["graph"].bounds)
@@ -1031,64 +920,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
panel.CURSES_LOCK.release()
panels["graph"].redraw(True)
- elif page == 0 and (key == ord('f') or key == ord('F')):
- # provides menu to pick previous regular expression filters or to add a new one
- # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
- options = ["None"] + regexFilters + ["New..."]
- initialSelection = 0 if not panels["log"].regexFilter else 1
-
- # hides top label of the graph panel and pauses panels
- if panels["graph"].currentDisplay:
- panels["graph"].showLabel = False
- panels["graph"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Log Filter:", options, initialSelection)
-
- # applies new setting
- if selection == 0:
- panels["log"].setFilter(None)
- elif selection == len(options) - 1:
- # selected 'New...' option - prompt user to input regular expression
- panel.CURSES_LOCK.acquire()
- try:
- # provides prompt
- panels["control"].setMsg("Regular expression: ")
- panels["control"].redraw(True)
-
- # gets user input (this blocks monitor updates)
- regexInput = panels["control"].getstr(0, 20)
-
- if regexInput:
- try:
- panels["log"].setFilter(re.compile(regexInput))
- if regexInput in regexFilters: regexFilters.remove(regexInput)
- regexFilters = [regexInput] + regexFilters
- except re.error, exc:
- panels["control"].setMsg("Unable to compile expression: %s" % str(exc), curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- finally:
- panel.CURSES_LOCK.release()
- elif selection != -1:
- try:
- panels["log"].setFilter(re.compile(regexFilters[selection - 1]))
-
- # move selection to top
- regexFilters = [regexFilters[selection - 1]] + regexFilters
- del regexFilters[selection]
- except re.error, exc:
- # shouldn't happen since we've already checked validity
- log.log(log.WARN, "Invalid regular expression ('%s': %s) - removing from listing" % (regexFilters[selection - 1], str(exc)))
- del regexFilters[selection - 1]
-
- if len(regexFilters) > MAX_REGEX_FILTERS: del regexFilters[MAX_REGEX_FILTERS:]
-
- # reverts changes made for popup
- panels["graph"].showLabel = True
- setPauseState(panels, isPaused, page)
- panels["graph"].redraw(True)
elif page == 0 and key in (ord('n'), ord('N'), ord('m'), ord('M')):
# Unfortunately modifier keys don't work with the up/down arrows (sending
# multiple keycodes. The only exception to this is shift + left/right,
@@ -1124,30 +955,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
setPauseState(panels, isPaused, page)
finally:
panel.CURSES_LOCK.release()
- elif page == 1 and (key == ord('u') or key == ord('U')):
- # provides menu to pick identification resolving utility
- options = ["auto"] + connections.Resolver.values()
-
- currentOverwrite = connections.getResolver("tor").overwriteResolver # enums correspond to indices
- if currentOverwrite == None: initialSelection = 0
- else: initialSelection = options.index(currentOverwrite)
-
- # hides top label of conn panel and pauses panels
- panelTitle = panels["conn"]._title
- panels["conn"]._title = ""
- panels["conn"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "Resolver Util:", options, initialSelection)
- selectedOption = options[selection] if selection != 0 else None
-
- # reverts changes made for popup
- panels["conn"]._title = panelTitle
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1 and selectedOption != connections.getResolver("tor").overwriteResolver:
- connections.getResolver("tor").overwriteResolver = selectedOption
elif page == 1 and key in (ord('d'), ord('D')):
# presents popup for raw consensus data
panel.CURSES_LOCK.acquire()
@@ -1165,31 +972,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
finally:
panel.CURSES_LOCK.release()
- elif page == 1 and (key == ord('l') or key == ord('L')):
- # provides a menu to pick the primary information we list connections by
- options = cli.connections.entries.ListingType.values()
-
- # dropping the HOSTNAME listing type until we support displaying that content
- options.remove(cli.connections.entries.ListingType.HOSTNAME)
-
- initialSelection = options.index(panels["conn"]._listingType)
-
- # hides top label of connection panel and pauses the display
- panelTitle = panels["conn"]._title
- panels["conn"]._title = ""
- panels["conn"].redraw(True)
- setPauseState(panels, isPaused, page, True)
-
- selection = showMenu(stdscr, panels["popup"], "List By:", options, initialSelection)
-
- # reverts changes made for popup
- panels["conn"]._title = panelTitle
- setPauseState(panels, isPaused, page)
-
- # applies new setting
- if selection != -1 and options[selection] != panels["conn"]._listingType:
- panels["conn"].setListingType(options[selection])
- panels["conn"].redraw(True)
elif page == 2 and (key == ord('w') or key == ord('W')):
# display a popup for saving the current configuration
panel.CURSES_LOCK.acquire()
@@ -1400,14 +1182,10 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
time.sleep(1)
panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- elif page == 0:
- panels["log"].handleKey(key)
- elif page == 1:
- panels["conn"].handleKey(key)
- elif page == 2:
- panels["config"].handleKey(key)
- elif page == 3:
- panels["torrc"].handleKey(key)
+ else:
+ for pagePanel in getPanels(page + 1):
+ isKeystrokeConsumed = pagePanel.handleKey(key)
+ if isKeystrokeConsumed: break
if REFRESH_FLAG:
REFRESH_FLAG = False
diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
index d8808b3..0c52f6f 100644
--- a/src/cli/graphing/graphPanel.py
+++ b/src/cli/graphing/graphPanel.py
@@ -20,6 +20,8 @@ import copy
import curses
from TorCtl import TorCtl
+import cli.popups
+
from util import enum, panel, uiTools
# time intervals at which graphs can be updated
@@ -229,7 +231,6 @@ class GraphPanel(panel.Panel):
self.graphHeight = CONFIG["features.graph.height"]
self.currentDisplay = None # label of the stats currently being displayed
self.stats = {} # available stats (mappings of label -> instance)
- self.showLabel = True # shows top label if true, hides otherwise
self.setPauseAttr("stats")
def getHeight(self):
@@ -253,6 +254,37 @@ class GraphPanel(panel.Panel):
self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
+ def handleKey(self, key):
+ isKeystrokeConsumed = True
+ if key == ord('s') or key == ord('S'):
+ # provides a menu to pick the graphed stats
+ availableStats = self.stats.keys()
+ availableStats.sort()
+
+ # uses sorted, camel cased labels for the options
+ options = ["None"]
+ for label in availableStats:
+ words = label.split()
+ options.append(" ".join(word[0].upper() + word[1:] for word in words))
+
+ if self.currentDisplay:
+ initialSelection = availableStats.index(self.currentDisplay) + 1
+ else: initialSelection = 0
+
+ selection = cli.popups.showMenu("Graphed Stats:", options, initialSelection)
+
+ # applies new setting
+ if selection == 0: self.setStats(None)
+ elif selection != -1: self.setStats(options[selection].lower())
+ elif key == ord('i') or key == ord('I'):
+ # provides menu to pick graph panel update interval
+ options = [label for (label, _) in UPDATE_INTERVALS]
+ selection = cli.popups.showMenu("Update Interval:", options, self.updateInterval)
+ if selection != -1: self.updateInterval = selection
+ else: isKeystrokeConsumed = False
+
+ return isKeystrokeConsumed
+
def getHelp(self):
if self.currentDisplay: graphedStats = self.currentDisplay
else: graphedStats = "none"
@@ -275,7 +307,7 @@ class GraphPanel(panel.Panel):
primaryColor = uiTools.getColor(param.getColor(True))
secondaryColor = uiTools.getColor(param.getColor(False))
- if self.showLabel: self.addstr(0, 0, param.getTitle(width), curses.A_STANDOUT)
+ if self.isTitleVisible(): self.addstr(0, 0, param.getTitle(width), curses.A_STANDOUT)
# top labels
left, right = param.getHeaderLabel(width / 2, True), param.getHeaderLabel(width / 2, False)
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
index b12715e..d34b640 100644
--- a/src/cli/logPanel.py
+++ b/src/cli/logPanel.py
@@ -4,13 +4,15 @@ for. This provides prepopulation from the log file and supports filtering by
regular expressions.
"""
-import time
+import re
import os
+import time
import curses
import threading
from TorCtl import TorCtl
+import popups
from version import VERSION
from util import conf, log, panel, sysTools, torTools, uiTools
@@ -75,6 +77,9 @@ CACHED_DUPLICATES_RESULT = None
# duration we'll wait for the deduplication function before giving up (in ms)
DEDUPLICATION_TIMEOUT = 100
+# maximum number of regex filters we'll remember
+MAX_REGEX_FILTERS = 5
+
def daysSince(timestamp=None):
"""
Provides the number of days since the epoch converted to local time (rounded
@@ -551,6 +556,7 @@ class LogPanel(panel.Panel, threading.Thread):
self.msgLog = [] # log entries, sorted by the timestamp
self.loggedEvents = loggedEvents # events we're listening to
self.regexFilter = None # filter for presented log events (no filtering if None)
+ self.filterOptions = [] # filters the user has input
self.lastContentHeight = 0 # height of the rendered content when last drawn
self.logFile = None # file log messages are saved to (skipped if None)
self.scroll = 0
@@ -746,6 +752,7 @@ class LogPanel(panel.Panel, threading.Thread):
raise exc
def handleKey(self, key):
+ isKeystrokeConsumed = True
if uiTools.isScrollKey(key):
pageHeight = self.getPreferredSize()[0] - 1
newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self.lastContentHeight)
@@ -760,6 +767,53 @@ class LogPanel(panel.Panel, threading.Thread):
self.showDuplicates = not self.showDuplicates
self.redraw(True)
self.valsLock.release()
+ elif key == ord('f') or key == ord('F'):
+ # Provides menu to pick regular expression filters or adding new ones:
+ # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
+ options = ["None"] + self.filterOptions + ["New..."]
+ oldSelection = 0 if not self.regexFilter else 1
+
+ # does all activity under a curses lock to prevent redraws when adding
+ # new filters
+ panel.CURSES_LOCK.acquire()
+ try:
+ selection = popups.showMenu("Log Filter:", options, oldSelection)
+
+ # applies new setting
+ if selection == 0:
+ self.setFilter(None)
+ elif selection == len(options) - 1:
+ # selected 'New...' option - prompt user to input regular expression
+ regexInput = popups.inputPrompt("Regular expression: ")
+
+ if regexInput:
+ try:
+ self.setFilter(re.compile(regexInput))
+ if regexInput in self.filterOptions: self.filterOptions.remove(regexInput)
+ self.filterOptions.insert(0, regexInput)
+ except re.error, exc:
+ popups.showMsg("Unable to compile expression: %s" % exc, 2)
+ elif selection != -1:
+ selectedOption = self.filterOptions[selection - 1]
+
+ try:
+ self.setFilter(re.compile(selectedOption))
+
+ # move selection to top
+ self.filterOptions.remove(selectedOption)
+ self.filterOptions.insert(0, selectedOption)
+ except re.error, exc:
+ # shouldn't happen since we've already checked validity
+ msg = "Invalid regular expression ('%s': %s) - removing from listing" % (selectedOption, exc)
+ log.log(log.WARN, msg)
+ self.filterOptions.remove(selectedOption)
+ finally:
+ panel.CURSES_LOCK.release()
+
+ if len(self.filterOptions) > MAX_REGEX_FILTERS: del self.filterOptions[MAX_REGEX_FILTERS:]
+ else: isKeystrokeConsumed = False
+
+ return isKeystrokeConsumed
def getHelp(self):
options = []
@@ -784,7 +838,8 @@ class LogPanel(panel.Panel, threading.Thread):
self._lastLoggedEvents, self._lastUpdate = list(currentLog), time.time()
# draws the top label
- self.addstr(0, 0, self._getTitle(width), curses.A_STANDOUT)
+ if self.isTitleVisible():
+ self.addstr(0, 0, self._getTitle(width), curses.A_STANDOUT)
# restricts scroll location to valid bounds
self.scroll = max(0, min(self.scroll, self.lastContentHeight - height + 1))
diff --git a/src/cli/popups.py b/src/cli/popups.py
index d5cf76c..81b4589 100644
--- a/src/cli/popups.py
+++ b/src/cli/popups.py
@@ -44,6 +44,46 @@ def finalize():
controller.refresh()
panel.CURSES_LOCK.release()
+def inputPrompt(msg):
+ """
+ Prompts the user to enter a string on the control line (which usually
+ displays the page number and basic controls).
+
+ Arguments:
+ msg - message to prompt the user for input with
+ """
+
+ panel.CURSES_LOCK.acquire()
+ controlPanel = controller.getPanel("control")
+ controlPanel.setMsg(msg)
+ controlPanel.redraw(True)
+ userInput = controlPanel.getstr(0, len(msg))
+ controlPanel.revertMsg()
+ panel.CURSES_LOCK.release()
+ return userInput
+
+def showMsg(msg, maxWait, attr = curses.A_STANDOUT):
+ """
+ Displays a single line message on the control line for a set time. Pressing
+ any key will end the message.
+
+ Arguments:
+ msg - message to be displayed to the user
+ maxWait - time to show the message
+ attr - attributes with which to draw the message
+ """
+
+ panel.CURSES_LOCK.acquire()
+ controlPanel = controller.getPanel("control")
+ controlPanel.setMsg(msg, attr)
+ controlPanel.redraw(True)
+
+ curses.halfdelay(maxWait * 10)
+ controller.getScreen().getch()
+ controlPanel.revertMsg()
+ curses.halfdelay(controller.REFRESH_RATE * 10)
+ panel.CURSES_LOCK.release()
+
def showHelpPopup():
"""
Presents a popup with instructions for the current page's hotkeys. This
@@ -108,7 +148,7 @@ def showHelpPopup():
return exitKey
else: return None
-def showSortDialog(titleLabel, options, oldSelection, optionColors):
+def showSortDialog(title, options, oldSelection, optionColors):
"""
Displays a sorting dialog of the form:
@@ -122,7 +162,7 @@ def showSortDialog(titleLabel, options, oldSelection, optionColors):
then this returns None. Otherwise, the new ordering is provided.
Arguments:
- titleLabel - title displayed for the popup window
+ title - title displayed for the popup window
options - ordered listing of option labels
oldSelection - current ordering
optionColors - mappings of options to their color
@@ -142,7 +182,7 @@ def showSortDialog(titleLabel, options, oldSelection, optionColors):
while len(newSelections) < len(oldSelection):
popup.win.erase()
popup.win.box()
- popup.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+ popup.addstr(0, 0, title, curses.A_STANDOUT)
_drawSortSelection(popup, 1, 2, "Current Order: ", oldSelection, optionColors)
_drawSortSelection(popup, 2, 2, "New Order: ", newSelections, optionColors)
@@ -214,3 +254,54 @@ def _drawSortSelection(popup, y, x, prefix, options, optionColors):
popup.addstr(y, x, ", ", curses.A_BOLD)
x += 2
+def showMenu(title, options, oldSelection):
+ """
+ Provides menu with options laid out in a single column. User can cancel
+ selection with the escape key, in which case this proives -1. Otherwise this
+ returns the index of the selection.
+
+ Arguments:
+ title - title displayed for the popup window
+ options - ordered listing of options to display
+ oldSelection - index of the initially selected option (uses the first
+ selection without a carrot if -1)
+ """
+
+ maxWidth = max([len(label) for label in options]) + 9
+ popup, width, height = init(len(options) + 2, maxWidth)
+ if not popup: return
+ key, selection = 0, oldSelection if oldSelection != -1 else 0
+
+ try:
+ # hides the title of the first panel on the page
+ topPanel = controller.getPanels(controller.getPage())[0]
+ topPanel.setTitleVisible(False)
+ topPanel.redraw(True)
+
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+
+ while not uiTools.isSelectionKey(key):
+ popup.win.erase()
+ popup.win.box()
+ popup.addstr(0, 0, title, curses.A_STANDOUT)
+
+ for i in range(len(options)):
+ label = options[i]
+ format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+ tab = "> " if i == oldSelection else " "
+ popup.addstr(i + 1, 2, tab)
+ popup.addstr(i + 1, 4, " %s " % label, format)
+
+ popup.win.refresh()
+
+ key = controller.getScreen().getch()
+ if key == curses.KEY_UP: selection = max(0, selection - 1)
+ elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
+ elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
+
+ topPanel.setTitleVisible(True)
+ curses.halfdelay(controller.REFRESH_RATE * 10) # reset normal pausing behavior
+ finally: finalize()
+
+ return selection
+
diff --git a/src/cli/torrcPanel.py b/src/cli/torrcPanel.py
index 6d7156d..5ca839f 100644
--- a/src/cli/torrcPanel.py
+++ b/src/cli/torrcPanel.py
@@ -31,7 +31,6 @@ class TorrcPanel(panel.Panel):
self.valsLock = threading.RLock()
self.configType = configType
self.scroll = 0
- self.showLabel = True # shows top label (hides otherwise)
self.showLineNum = True # shows left aligned line numbers
self.stripComments = False # drops comments and extra whitespace
@@ -42,6 +41,7 @@ class TorrcPanel(panel.Panel):
def handleKey(self, key):
self.valsLock.acquire()
+ isKeystrokeConsumed = True
if uiTools.isScrollKey(key):
pageHeight = self.getPreferredSize()[0] - 1
newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
@@ -57,8 +57,10 @@ class TorrcPanel(panel.Panel):
self.stripComments = not self.stripComments
self._lastContentHeightArgs = None
self.redraw(True)
+ else: isKeystrokeConsumed = False
self.valsLock.release()
+ return isKeystrokeConsumed
def getHelp(self):
options = []
@@ -120,7 +122,7 @@ class TorrcPanel(panel.Panel):
displayLine = -self.scroll + 1 # line we're drawing on
# draws the top label
- if self.showLabel:
+ if self.isTitleVisible():
sourceLabel = "Tor" if self.configType == Config.TORRC else "Arm"
locationLabel = " (%s)" % confLocation if confLocation else ""
self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT)
diff --git a/src/util/panel.py b/src/util/panel.py
index 7387833..06c8649 100644
--- a/src/util/panel.py
+++ b/src/util/panel.py
@@ -61,6 +61,7 @@ class Panel():
self.panelName = name
self.parent = parent
self.visible = False
+ self.titleVisible = True
# Attributes for pausing. The pauseAttr contains variables our getAttr
# method is tracking, and the pause buffer has copies of the values from
@@ -93,6 +94,21 @@ class Panel():
return self.panelName
+ def isTitleVisible(self):
+ """
+ True if the title is configured to be visible, False otherwise.
+ """
+
+ return self.titleVisible
+
+ def setTitleVisible(self, isVisible):
+ """
+ Configures the panel's title to be visible or not when it's next redrawn.
+ This is not guarenteed to be respected (not all panels have a title).
+ """
+
+ self.titleVisible = isVisible
+
def getParent(self):
"""
Provides the parent used to create subwindows.
@@ -290,6 +306,17 @@ class Panel():
if setWidth != -1: newWidth = min(newWidth, setWidth)
return (newHeight, newWidth)
+ def handleKey(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 getHelp(self):
"""
Provides help information for the controls this page provides. This is a
More information about the tor-commits
mailing list