commit a2c89f6f8261a892cc02aee5325fc0ffff7671d9 Author: Damian Johnson atagar@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
tor-commits@lists.torproject.org