tor-commits
Threads by month
- ----- 2025 -----
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- 1 participants
- 213395 discussions

17 Jul '11
commit dd1bde92b4d9b564cccb9bbd6fa3ba44fe3b41db
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 12 09:37:59 2011 -0700
Moving descriptor popup out of the controller
Putting descriptor popup triggering into the connection panel, and revising
descriptorPopup.py to use new styled popups.
---
src/cli/connections/connPanel.py | 4 +++
src/cli/controller.py | 17 ------------
src/cli/descriptorPopup.py | 52 ++++++++++++++++++++++----------------
src/cli/popups.py | 5 ++-
4 files changed, 37 insertions(+), 41 deletions(-)
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 8ecffc2..7fab33e 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -6,6 +6,7 @@ import time
import curses
import threading
+import cli.descriptorPopup
import cli.popups
from cli.connections import entries, connEntry, circEntry
@@ -191,6 +192,9 @@ class ConnectionPanel(panel.Panel, threading.Thread):
# applies new setting
if selection != -1: self.setListingType(options[selection])
+ elif key == ord('d') or key == ord('D'):
+ # presents popup for raw consensus data
+ cli.descriptorPopup.showDescriptorPopup(self)
else: isKeystrokeConsumed = False
self.valsLock.release()
diff --git a/src/cli/controller.py b/src/cli/controller.py
index c4b9442..20a4e7e 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -954,23 +954,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
setPauseState(panels, isPaused, page)
finally:
panel.CURSES_LOCK.release()
- elif page == 1 and key in (ord('d'), ord('D')):
- # presents popup for raw consensus data
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
- panelTitle = panels["conn"]._title
- panels["conn"]._title = ""
- panels["conn"].redraw(True)
-
- descriptorPopup.showDescriptorPopup(panels["popup"], stdscr, panels["conn"])
-
- panels["conn"]._title = panelTitle
- setPauseState(panels, isPaused, page)
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
else:
for pagePanel in getPanels(page + 1):
isKeystrokeConsumed = pagePanel.handleKey(key)
diff --git a/src/cli/descriptorPopup.py b/src/cli/descriptorPopup.py
index d57546c..920f2bc 100644
--- a/src/cli/descriptorPopup.py
+++ b/src/cli/descriptorPopup.py
@@ -7,6 +7,7 @@ import curses
import controller
import connections.connEntry
+import popups
from util import panel, torTools, uiTools
# field keywords used to identify areas for coloring
@@ -65,7 +66,7 @@ class PopupProperties:
elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - height, 0)
elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + height, len(self.text) - height))
-def showDescriptorPopup(popup, stdscr, connectionPanel):
+def showDescriptorPopup(connectionPanel):
"""
Presents consensus descriptor in popup window with the following controls:
Up, Down, Page Up, Page Down - scroll descriptor
@@ -73,10 +74,16 @@ def showDescriptorPopup(popup, stdscr, connectionPanel):
Enter, Space, d, D - close popup
"""
+ # hides the title of the first panel on the page
+ topPanel = controller.getPanels(controller.getPage())[0]
+ topPanel.setTitleVisible(False)
+ topPanel.redraw(True)
+
properties = PopupProperties()
isVisible = True
- if not panel.CURSES_LOCK.acquire(False): return
+ panel.CURSES_LOCK.acquire()
+
try:
while isVisible:
selection = connectionPanel._scroller.getCursorSelection(connectionPanel._entryLines)
@@ -96,29 +103,30 @@ def showDescriptorPopup(popup, stdscr, connectionPanel):
# tracks number of extra lines that will be taken due to text wrap
height += (lineWidth - 2) / connectionPanel.maxX
- popup.setHeight(min(len(properties.text) + height + 2, connectionPanel.maxY))
- popup.recreate(stdscr, width)
-
while isVisible:
- draw(popup, properties)
- key = stdscr.getch()
+ popupHeight = min(len(properties.text) + height + 2, connectionPanel.maxY)
+ popup, _, _ = popups.init(popupHeight, width)
+ if not popup: break
- if uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')):
- # closes popup
- isVisible = False
- elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):
- # navigation - pass on to connPanel and recreate popup
- connectionPanel.handleKey(curses.KEY_UP if key == curses.KEY_LEFT else curses.KEY_DOWN)
- break
- else: properties.handleKey(key, popup.height - 2)
-
- popup.setHeight(9)
- popup.recreate(stdscr, 80)
- finally:
- panel.CURSES_LOCK.release()
+ try:
+ draw(popup, properties)
+ key = controller.getScreen().getch()
+
+ if uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')):
+ # closes popup
+ isVisible = False
+ elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):
+ # navigation - pass on to connPanel and recreate popup
+ connectionPanel.handleKey(curses.KEY_UP if key == curses.KEY_LEFT else curses.KEY_DOWN)
+ break
+ else: properties.handleKey(key, popup.height - 2)
+ finally: popups.finalize()
+ finally: panel.CURSES_LOCK.release()
+
+ topPanel.setTitleVisible(True)
def draw(popup, properties):
- popup.clear()
+ popup.win.erase()
popup.win.box()
xOffset = 2
@@ -174,5 +182,5 @@ def draw(popup, properties):
lineNum += 1
if lineNum > pageHeight: break
- popup.refresh()
+ popup.win.refresh()
diff --git a/src/cli/popups.py b/src/cli/popups.py
index 2b02685..de4bad6 100644
--- a/src/cli/popups.py
+++ b/src/cli/popups.py
@@ -301,9 +301,10 @@ def showMenu(title, options, oldSelection):
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()
+ finally:
+ topPanel.setTitleVisible(True)
+ finalize()
return selection
1
0

[arm/release] Moving log panel clearing handler to the log panel
by atagar@torproject.org 17 Jul '11
by atagar@torproject.org 17 Jul '11
17 Jul '11
commit bafcb04a9843443290dbfe697f5467f441941806
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 12 19:00:21 2011 -0700
Moving log panel clearing handler to the log panel
---
src/cli/controller.py | 20 --------------------
src/cli/logPanel.py | 4 ++++
src/cli/popups.py | 6 ++++--
3 files changed, 8 insertions(+), 22 deletions(-)
diff --git a/src/cli/controller.py b/src/cli/controller.py
index 20a4e7e..dc82d65 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -934,26 +934,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
if currentHeight < maxHeight + 1:
panels["graph"].setGraphHeight(panels["graph"].graphHeight + 1)
- elif page == 0 and (key == ord('c') or key == ord('C')):
- # provides prompt to confirm that arm should clear the log
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("This will clear the log. Are you sure (c again to confirm)?", curses.A_BOLD)
- panels["control"].redraw(True)
-
- curses.cbreak()
- confirmationKey = stdscr.getch()
- if confirmationKey in (ord('c'), ord('C')): panels["log"].clear()
-
- # reverts display settings
- curses.halfdelay(REFRESH_RATE * 10)
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
else:
for pagePanel in getPanels(page + 1):
isKeystrokeConsumed = pagePanel.handleKey(key)
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
index d34b640..745cb57 100644
--- a/src/cli/logPanel.py
+++ b/src/cli/logPanel.py
@@ -767,6 +767,10 @@ class LogPanel(panel.Panel, threading.Thread):
self.showDuplicates = not self.showDuplicates
self.redraw(True)
self.valsLock.release()
+ elif key == ord('c') or key == ord('C'):
+ msg = "This will clear the log. Are you sure (c again to confirm)?"
+ keyPress = popups.showMsg(msg, attr = curses.A_BOLD)
+ if keyPress in (ord('c'), ord('C')): self.clear()
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
diff --git a/src/cli/popups.py b/src/cli/popups.py
index de4bad6..b957186 100644
--- a/src/cli/popups.py
+++ b/src/cli/popups.py
@@ -66,7 +66,7 @@ def inputPrompt(msg, initialValue = ""):
def showMsg(msg, maxWait = -1, attr = curses.A_STANDOUT):
"""
Displays a single line message on the control line for a set time. Pressing
- any key will end the message.
+ any key will end the message. This returns the key pressed.
Arguments:
msg - message to be displayed to the user
@@ -81,10 +81,12 @@ def showMsg(msg, maxWait = -1, attr = curses.A_STANDOUT):
if maxWait == -1: curses.cbreak()
else: curses.halfdelay(maxWait * 10)
- controller.getScreen().getch()
+ keyPress = controller.getScreen().getch()
controlPanel.revertMsg()
curses.halfdelay(controller.REFRESH_RATE * 10)
panel.CURSES_LOCK.release()
+
+ return keyPress
def showHelpPopup():
"""
1
0

17 Jul '11
commit 4986780baff0740a7c55eaf7717697f54e305a5d
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 12 20:45:55 2011 -0700
Moving event logging selection to the log panel
---
src/cli/controller.py | 77 -------------------------------------------------
src/cli/logPanel.py | 56 ++++++++++++++++++++++++++++++++++-
2 files changed, 54 insertions(+), 79 deletions(-)
diff --git a/src/cli/controller.py b/src/cli/controller.py
index 035ef07..84205cb 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -277,32 +277,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 setEventListening(selectedEvents, isBlindMode):
- # creates a local copy, note that a suspected python bug causes *very*
- # puzzling results otherwise when trying to discard entries (silently
- # returning out of this function!)
- events = set(selectedEvents)
- isLoggingUnknown = "UNKNOWN" in events
-
- # removes special types only used in arm (UNKNOWN, TORCTL, ARM_DEBUG, etc)
- toDiscard = []
- for eventType in events:
- if eventType not in logPanel.TOR_EVENT_TYPES.values(): toDiscard += [eventType]
-
- for eventType in list(toDiscard): events.discard(eventType)
-
- # adds events unrecognized by arm if we're listening to the 'UNKNOWN' type
- if isLoggingUnknown:
- events.update(set(logPanel.getMissingEventTypes()))
-
- setEvents = torTools.getConn().setControllerEvents(list(events))
-
- # temporary hack for providing user selected events minus those that failed
- # (wouldn't be a problem if I wasn't storing tor and non-tor events together...)
- returnVal = list(selectedEvents.difference(torTools.FAILED_EVENTS))
- returnVal.sort() # alphabetizes
- return returnVal
-
def connResetListener(conn, eventType):
"""
Pauses connection resolution when tor's shut down, and resumes if started
@@ -507,7 +481,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
if isSuccessful: panels["graph"].updateInterval = 4
# tells Tor to listen to the events we're interested
- loggedEvents = setEventListening(loggedEvents, isBlindMode)
#panels["log"].loggedEvents = loggedEvents # strips any that couldn't be set
panels["log"].setLoggedEvents(loggedEvents) # strips any that couldn't be set
@@ -869,56 +842,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
panel.CURSES_LOCK.release()
panels["graph"].redraw(True)
- elif page == 0 and (key == ord('e') or key == ord('E')):
- # allow user to enter new types of events to log - unchanged if left blank
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("Events to log: ")
- panels["control"].redraw(True)
-
- # lists event types
- popup = panels["popup"]
- popup.height = 11
- popup.recreate(stdscr, 80)
-
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT)
- lineNum = 1
- for line in logPanel.EVENT_LISTING.split("\n"):
- line = line[6:]
- popup.addstr(lineNum, 1, line)
- lineNum += 1
- popup.refresh()
-
- # gets user input (this blocks monitor updates)
- eventsInput = panels["control"].getstr(0, 15)
- if eventsInput: eventsInput = eventsInput.replace(' ', '') # strips spaces
-
- # it would be nice to quit on esc, but looks like this might not be possible...
- if eventsInput:
- try:
- expandedEvents = logPanel.expandEvents(eventsInput)
- loggedEvents = setEventListening(expandedEvents, isBlindMode)
- panels["log"].setLoggedEvents(loggedEvents)
- except ValueError, exc:
- panels["control"].setMsg("Invalid flags: %s" % str(exc), curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
-
- # reverts popup dimensions
- popup.height = 9
- popup.recreate(stdscr, 80)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
-
- panels["graph"].redraw(True)
else:
for pagePanel in getPanels(page + 1):
isKeystrokeConsumed = pagePanel.handleKey(key)
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
index 745cb57..75cdab5 100644
--- a/src/cli/logPanel.py
+++ b/src/cli/logPanel.py
@@ -163,6 +163,28 @@ def getMissingEventTypes():
return [event for event in torEventTypes if not event in armEventTypes]
else: return None # GETINFO call failed
+def setEventListening(events):
+ """
+ Configures the events Tor listens for, filtering non-tor events from what we
+ request from the controller. This returns a sorted list of the events we
+ successfully set.
+
+ Arguments:
+ events - event types to attempt to set
+ """
+
+ events = set(events) # drops duplicates
+ torEvents = events.intersection(set(TOR_EVENT_TYPES.values()))
+
+ # adds events unrecognized by arm if we're listening to the 'UNKNOWN' type
+ if "UNKNOWN" in events:
+ torEvents.update(set(getMissingEventTypes()))
+
+ setEvents = torTools.getConn().setControllerEvents(list(torEvents))
+
+ # provides back the input set minus events we failed to set
+ return sorted(events.difference(torTools.FAILED_EVENTS))
+
def loadLogMessages():
"""
Fetches a mapping of common log messages to their runlevels from the config.
@@ -552,6 +574,10 @@ class LogPanel(panel.Panel, threading.Thread):
# collapses duplicate log entries if false, showing only the most recent
self.showDuplicates = self._config["features.log.showDuplicateEntries"]
+ # restricts the input to the set of events we can listen to, and
+ # configures the controller to liten to them
+ loggedEvents = setEventListening(loggedEvents)
+
self.setPauseAttr("msgLog") # tracks the message log when we're paused
self.msgLog = [] # log entries, sorted by the timestamp
self.loggedEvents = loggedEvents # events we're listening to
@@ -693,9 +719,12 @@ class LogPanel(panel.Panel, threading.Thread):
"""
if eventTypes == self.loggedEvents: return
-
self.valsLock.acquire()
- self.loggedEvents = eventTypes
+
+ # configures the controller to listen for these tor events, and provides
+ # back a subset without anything we're failing to listen to
+ setTypes = setEventListening(eventTypes)
+ self.loggedEvents = setTypes
self.redraw(True)
self.valsLock.release()
@@ -815,6 +844,29 @@ class LogPanel(panel.Panel, threading.Thread):
panel.CURSES_LOCK.release()
if len(self.filterOptions) > MAX_REGEX_FILTERS: del self.filterOptions[MAX_REGEX_FILTERS:]
+ elif key == ord('e') or key == ord('E'):
+ # allow user to enter new types of events to log - unchanged if left blank
+ popup, width, height = popups.init(11, 80)
+
+ if popup:
+ try:
+ # displays the available flags
+ popup.win.box()
+ popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT)
+ eventLines = EVENT_LISTING.split("\n")
+
+ for i in range(len(eventLines)):
+ popup.addstr(i + 1, 1, eventLines[i][6:])
+
+ popup.win.refresh()
+
+ userInput = popups.inputPrompt("Events to log: ")
+ if userInput:
+ userInput = userInput.replace(' ', '') # strips spaces
+ try: self.setLoggedEvents(expandEvents(userInput))
+ except ValueError, exc:
+ popups.showMsg("Invalid flags: %s" % str(exc), 2)
+ finally: popups.finalize()
else: isKeystrokeConsumed = False
return isKeystrokeConsumed
1
0
commit 0df2a7b33a3ffc49b1fd38b53aeaa4314d025eba
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 12 21:02:14 2011 -0700
Removing unused dimension arguments
Minor refactoring change suggested by pylint.
---
src/cli/popups.py | 6 +++---
1 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/cli/popups.py b/src/cli/popups.py
index b957186..dd56bfa 100644
--- a/src/cli/popups.py
+++ b/src/cli/popups.py
@@ -95,7 +95,7 @@ def showHelpPopup():
properly, this is an arrow, enter, or scroll key then this returns None.
"""
- popup, width, height = init(9, 80)
+ popup, _, height = init(9, 80)
if not popup: return
exitKey = None
@@ -172,7 +172,7 @@ def showSortDialog(title, options, oldSelection, optionColors):
optionColors - mappings of options to their color
"""
- popup, width, height = init(9, 80)
+ popup, _, _ = init(9, 80)
if not popup: return
newSelections = [] # new ordering
@@ -272,7 +272,7 @@ def showMenu(title, options, oldSelection):
"""
maxWidth = max([len(label) for label in options]) + 9
- popup, width, height = init(len(options) + 2, maxWidth)
+ popup, _, _ = init(len(options) + 2, maxWidth)
if not popup: return
key, selection = 0, oldSelection if oldSelection != -1 else 0
1
0

17 Jul '11
commit c231d73d9f1d3e6be90aa0f97943ad862e8e1d6f
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 12 21:07:30 2011 -0700
Refactoring sighup handler to use new utils
Making the controller handler for issuing sighups use the popup utils.
---
src/cli/controller.py | 34 +++++++---------------------------
1 files changed, 7 insertions(+), 27 deletions(-)
diff --git a/src/cli/controller.py b/src/cli/controller.py
index d7b127c..1189781 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -778,33 +778,13 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
selectiveRefresh(panels, page)
elif key == ord('x') or key == ord('X'):
# provides prompt to confirm that arm should issue a sighup
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("This will reset Tor's internal state. Are you sure (x again to confirm)?", curses.A_BOLD)
- panels["control"].redraw(True)
-
- curses.cbreak()
- confirmationKey = stdscr.getch()
- if confirmationKey in (ord('x'), ord('X')):
- try:
- torTools.getConn().reload()
- except IOError, exc:
- log.log(log.ERR, "Error detected when reloading tor: %s" % sysTools.getFileErrorMsg(exc))
-
- #errorMsg = " (%s)" % str(err) if str(err) else ""
- #panels["control"].setMsg("Sighup failed%s" % errorMsg, curses.A_STANDOUT)
- #panels["control"].redraw(True)
- #time.sleep(2)
-
- # reverts display settings
- curses.halfdelay(REFRESH_RATE * 10)
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
+ msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?"
+ confirmationKey = popups.showMsg(msg, attr = curses.A_BOLD)
+
+ if confirmationKey in (ord('x'), ord('X')):
+ try: torTools.getConn().reload()
+ except IOError, exc:
+ log.log(log.ERR, "Error detected when reloading tor: %s" % sysTools.getFileErrorMsg(exc))
elif key == ord('h') or key == ord('H'):
overrideKey = popups.showHelpPopup()
else:
1
0

17 Jul '11
commit 4474efa7379d855ede60f6edf1c678654be437a0
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 12 20:53:46 2011 -0700
Moving log snapshot saving to the log panel
---
src/cli/controller.py | 30 ------------------------------
src/cli/logPanel.py | 10 ++++++++++
2 files changed, 10 insertions(+), 30 deletions(-)
diff --git a/src/cli/controller.py b/src/cli/controller.py
index 84205cb..1384c4d 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -812,36 +812,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
panels["graph"].bounds = graphing.graphPanel.Bounds.next(panels["graph"].bounds)
selectiveRefresh(panels, page)
- elif page == 0 and (key == ord('a') or key == ord('A')):
- # allow user to enter a path to take a snapshot - abandons if left blank
- panel.CURSES_LOCK.acquire()
- try:
- setPauseState(panels, isPaused, page, True)
-
- # provides prompt
- panels["control"].setMsg("Path to save log snapshot: ")
- panels["control"].redraw(True)
-
- # gets user input (this blocks monitor updates)
- pathInput = panels["control"].getstr(0, 27)
-
- if pathInput:
- try:
- panels["log"].saveSnapshot(pathInput)
- panels["control"].setMsg("Saved: %s" % pathInput, curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
- except IOError, exc:
- panels["control"].setMsg("Unable to save snapshot: %s" % sysTools.getFileErrorMsg(exc), curses.A_STANDOUT)
- panels["control"].redraw(True)
- time.sleep(2)
-
- panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
- setPauseState(panels, isPaused, page)
- finally:
- panel.CURSES_LOCK.release()
-
- panels["graph"].redraw(True)
else:
for pagePanel in getPanels(page + 1):
isKeystrokeConsumed = pagePanel.handleKey(key)
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
index 75cdab5..34bf759 100644
--- a/src/cli/logPanel.py
+++ b/src/cli/logPanel.py
@@ -867,6 +867,16 @@ class LogPanel(panel.Panel, threading.Thread):
except ValueError, exc:
popups.showMsg("Invalid flags: %s" % str(exc), 2)
finally: popups.finalize()
+ elif key == ord('a') or key == ord('A'):
+ # lets user enter a path to take a snapshot, canceling if left blank
+ pathInput = popups.inputPrompt("Path to save log snapshot: ")
+
+ if pathInput:
+ try:
+ self.saveSnapshot(pathInput)
+ popups.showMsg("Saved: %s" % pathInput, 2)
+ except IOError, exc:
+ popups.showMsg("Unable to save snapshot: %s" % sysTools.getFileErrorMsg(exc), 2)
else: isKeystrokeConsumed = False
return isKeystrokeConsumed
1
0

17 Jul '11
commit 233bd0609cf2ebdea034e4d57882d0a0961b11d2
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 12 20:57:28 2011 -0700
Moving graph bounds switching to the graph panel
---
src/cli/controller.py | 5 -----
src/cli/graphing/graphPanel.py | 4 ++++
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/cli/controller.py b/src/cli/controller.py
index 1384c4d..d7b127c 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -807,11 +807,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('b') or key == ord('B')):
- # uses the next boundary type for graph
- panels["graph"].bounds = graphing.graphPanel.Bounds.next(panels["graph"].bounds)
-
- selectiveRefresh(panels, page)
else:
for pagePanel in getPanels(page + 1):
isKeystrokeConsumed = pagePanel.handleKey(key)
diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
index ecf8c68..ef0267d 100644
--- a/src/cli/graphing/graphPanel.py
+++ b/src/cli/graphing/graphPanel.py
@@ -271,6 +271,10 @@ class GraphPanel(panel.Panel):
if currentHeight < maxHeight + 1:
self.setGraphHeight(self.graphHeight + 1)
+ elif key == ord('b') or key == ord('B'):
+ # uses the next boundary type
+ self.bounds = Bounds.next(self.bounds)
+ self.redraw(True)
elif key == ord('s') or key == ord('S'):
# provides a menu to pick the graphed stats
availableStats = self.stats.keys()
1
0

17 Jul '11
commit 77dc38f4fdd098a124b2973a86c267bdfd126f92
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat May 7 12:29:59 2011 -0700
Moving sort selection out of the controller
Sort dialogs are now provided by the popups toolkit, and triggered by the
panels that want to sort themselves.
---
src/cli/configPanel.py | 25 ++++++++
src/cli/connections/connPanel.py | 10 +++
src/cli/controller.py | 125 --------------------------------------
src/cli/popups.py | 106 ++++++++++++++++++++++++++++++++
4 files changed, 141 insertions(+), 125 deletions(-)
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index 90c6191..6aaafc2 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -6,6 +6,8 @@ and the resulting configuration files saved.
import curses
import threading
+import popups
+
from util import conf, enum, panel, torTools, torConfig, uiTools
DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
@@ -43,6 +45,16 @@ FIELD_ATTR = {Field.CATEGORY: ("Category", "red"),
Field.MAN_ENTRY: ("Man Page Entry", "blue"),
Field.IS_DEFAULT: ("Is Default", "magenta")}
+def getFieldFromLabel(fieldLabel):
+ """
+ Converts field labels back to their enumeration, raising a ValueError if it
+ doesn't exist.
+ """
+
+ for entryEnum in FIELD_ATTR:
+ if fieldLabel == FIELD_ATTR[entryEnum][0]:
+ return entryEnum
+
class ConfigEntry():
"""
Configuration option in the panel.
@@ -246,6 +258,19 @@ class ConfigPanel(panel.Panel):
elif key == ord('a') or key == ord('A'):
self.showAll = not self.showAll
self.redraw(True)
+ elif key == ord('s') or key == ord('S'):
+ # set ordering for config options
+ titleLabel = "Config Option Ordering:"
+ options = [FIELD_ATTR[field][0] for field in Field.values()]
+ oldSelection = [FIELD_ATTR[field][0] for field in self.sortOrdering]
+ optionColors = dict([FIELD_ATTR[field] for field in Field.values()])
+ results = popups.showSortDialog(titleLabel, options, oldSelection, optionColors)
+
+ if results:
+ # converts labels back to enums
+ resultEnums = [getFieldFromLabel(label) for label in results]
+ self.setSortOrder(resultEnums)
+
self.valsLock.release()
def getHelp(self):
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 40c479e..8ad41d5 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -6,6 +6,8 @@ import time
import curses
import threading
+import cli.popups
+
from cli.connections import entries, connEntry, circEntry
from util import connections, enum, panel, torTools, uiTools
@@ -149,6 +151,14 @@ class ConnectionPanel(panel.Panel, threading.Thread):
elif uiTools.isSelectionKey(key):
self._showDetails = not self._showDetails
self.redraw(True)
+ elif key == ord('s') or key == ord('S'):
+ # set ordering for connection options
+ titleLabel = "Connection Ordering:"
+ options = entries.SortAttr.values()
+ oldSelection = self._sortOrdering
+ optionColors = dict([(attr, entries.SORT_COLORS[attr]) for attr in options])
+ results = cli.popups.showSortDialog(titleLabel, options, oldSelection, optionColors)
+ if results: self.setSortOrder(results)
self.valsLock.release()
diff --git a/src/cli/controller.py b/src/cli/controller.py
index 1501419..09e3575 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -326,98 +326,6 @@ def showMenu(stdscr, popup, title, options, initialSelection):
return selection
-def showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors):
- """
- Displays a sorting dialog of the form:
-
- Current Order: <previous selection>
- New Order: <selections made>
-
- <option 1> <option 2> <option 3> Cancel
-
- Options are colored when among the "Current Order" or "New Order", but not
- when an option below them. If cancel is selected or the user presses escape
- then this returns None. Otherwise, the new ordering is provided.
-
- Arguments:
- stdscr, panels, isPaused, page - boiler plate arguments of the controller
- (should be refactored away when rewriting)
-
- titleLabel - title displayed for the popup window
- options - ordered listing of option labels
- oldSelection - current ordering
- optionColors - mappings of options to their color
-
- """
-
- panel.CURSES_LOCK.acquire()
- newSelections = [] # new ordering
-
- try:
- setPauseState(panels, isPaused, page, True)
- curses.cbreak() # wait indefinitely for key presses (no timeout)
-
- popup = panels["popup"]
- cursorLoc = 0 # index of highlighted option
-
- # label for the inital ordering
- formattedPrevListing = []
- for sortType in oldSelection:
- colorStr = optionColors.get(sortType, "white")
- formattedPrevListing.append("<%s>%s</%s>" % (colorStr, sortType, colorStr))
- prevOrderingLabel = "<b>Current Order: %s</b>" % ", ".join(formattedPrevListing)
-
- selectionOptions = list(options)
- selectionOptions.append("Cancel")
-
- while len(newSelections) < len(oldSelection):
- popup.clear()
- popup.win.box()
- popup.addstr(0, 0, titleLabel, curses.A_STANDOUT)
- popup.addfstr(1, 2, prevOrderingLabel)
-
- # provides new ordering
- formattedNewListing = []
- for sortType in newSelections:
- colorStr = optionColors.get(sortType, "white")
- formattedNewListing.append("<%s>%s</%s>" % (colorStr, sortType, colorStr))
- newOrderingLabel = "<b>New Order: %s</b>" % ", ".join(formattedNewListing)
- popup.addfstr(2, 2, newOrderingLabel)
-
- # presents remaining options, each row having up to four options with
- # spacing of nineteen cells
- row, col = 4, 0
- for i in range(len(selectionOptions)):
- popup.addstr(row, col * 19 + 2, selectionOptions[i], curses.A_STANDOUT if cursorLoc == i else curses.A_NORMAL)
- col += 1
- if col == 4: row, col = row + 1, 0
-
- popup.refresh()
-
- key = stdscr.getch()
- if key == curses.KEY_LEFT: cursorLoc = max(0, cursorLoc - 1)
- elif key == curses.KEY_RIGHT: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 1)
- elif key == curses.KEY_UP: cursorLoc = max(0, cursorLoc - 4)
- elif key == curses.KEY_DOWN: cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 4)
- elif uiTools.isSelectionKey(key):
- # selected entry (the ord of '10' seems needed to pick up enter)
- selection = selectionOptions[cursorLoc]
- if selection == "Cancel": break
- else:
- newSelections.append(selection)
- selectionOptions.remove(selection)
- cursorLoc = min(cursorLoc, len(selectionOptions) - 1)
- elif key == 27: break # esc - cancel
-
- setPauseState(panels, isPaused, page)
- curses.halfdelay(REFRESH_RATE * 10) # reset normal pausing behavior
- finally:
- panel.CURSES_LOCK.release()
-
- if len(newSelections) == len(oldSelection):
- return newSelections
- else: return None
-
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
@@ -1282,18 +1190,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
if selection != -1 and options[selection] != panels["conn"]._listingType:
panels["conn"].setListingType(options[selection])
panels["conn"].redraw(True)
- elif page == 1 and (key == ord('s') or key == ord('S')):
- # set ordering for connection options
- titleLabel = "Connection Ordering:"
- options = cli.connections.entries.SortAttr.values()
- oldSelection = panels["conn"]._sortOrdering
- optionColors = dict([(attr, cli.connections.entries.SORT_COLORS[attr]) for attr in options])
- results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
-
- if results:
- panels["conn"].setSortOrder(results)
-
- panels["conn"].redraw(True)
elif page == 2 and (key == ord('c') or key == ord('C')) and False:
# TODO: disabled for now (probably gonna be going with separate pages
# rather than popup menu)
@@ -1446,27 +1342,6 @@ def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
panel.CURSES_LOCK.release()
panels["config"].redraw(True)
- elif page == 2 and (key == ord('s') or key == ord('S')):
- # set ordering for config options
- titleLabel = "Config Option Ordering:"
- options = [configPanel.FIELD_ATTR[field][0] for field in configPanel.Field.values()]
- oldSelection = [configPanel.FIELD_ATTR[field][0] for field in panels["config"].sortOrdering]
- optionColors = dict([configPanel.FIELD_ATTR[field] for field in configPanel.Field.values()])
- results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
-
- if results:
- # converts labels back to enums
- resultEnums = []
-
- for label in results:
- for entryEnum in configPanel.FIELD_ATTR:
- if label == configPanel.FIELD_ATTR[entryEnum][0]:
- resultEnums.append(entryEnum)
- break
-
- panels["config"].setSortOrder(resultEnums)
-
- panels["config"].redraw(True)
elif page == 2 and uiTools.isSelectionKey(key):
# let the user edit the configuration value, unchanged if left blank
panel.CURSES_LOCK.acquire()
diff --git a/src/cli/popups.py b/src/cli/popups.py
index ce51ee8..d5cf76c 100644
--- a/src/cli/popups.py
+++ b/src/cli/popups.py
@@ -108,3 +108,109 @@ def showHelpPopup():
return exitKey
else: return None
+def showSortDialog(titleLabel, options, oldSelection, optionColors):
+ """
+ Displays a sorting dialog of the form:
+
+ Current Order: <previous selection>
+ New Order: <selections made>
+
+ <option 1> <option 2> <option 3> Cancel
+
+ Options are colored when among the "Current Order" or "New Order", but not
+ when an option below them. If cancel is selected or the user presses escape
+ then this returns None. Otherwise, the new ordering is provided.
+
+ Arguments:
+ titleLabel - title displayed for the popup window
+ options - ordered listing of option labels
+ oldSelection - current ordering
+ optionColors - mappings of options to their color
+ """
+
+ popup, width, height = init(9, 80)
+ if not popup: return
+ newSelections = [] # new ordering
+
+ try:
+ cursorLoc = 0 # index of highlighted option
+ curses.cbreak() # wait indefinitely for key presses (no timeout)
+
+ selectionOptions = list(options)
+ selectionOptions.append("Cancel")
+
+ while len(newSelections) < len(oldSelection):
+ popup.win.erase()
+ popup.win.box()
+ popup.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+
+ _drawSortSelection(popup, 1, 2, "Current Order: ", oldSelection, optionColors)
+ _drawSortSelection(popup, 2, 2, "New Order: ", newSelections, optionColors)
+
+ # presents remaining options, each row having up to four options with
+ # spacing of nineteen cells
+ row, col = 4, 0
+ for i in range(len(selectionOptions)):
+ optionFormat = curses.A_STANDOUT if cursorLoc == i else curses.A_NORMAL
+ popup.addstr(row, col * 19 + 2, selectionOptions[i], optionFormat)
+ col += 1
+ if col == 4: row, col = row + 1, 0
+
+ popup.win.refresh()
+
+ key = controller.getScreen().getch()
+ if key == curses.KEY_LEFT:
+ cursorLoc = max(0, cursorLoc - 1)
+ elif key == curses.KEY_RIGHT:
+ cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 1)
+ elif key == curses.KEY_UP:
+ cursorLoc = max(0, cursorLoc - 4)
+ elif key == curses.KEY_DOWN:
+ cursorLoc = min(len(selectionOptions) - 1, cursorLoc + 4)
+ elif uiTools.isSelectionKey(key):
+ selection = selectionOptions[cursorLoc]
+
+ if selection == "Cancel": break
+ else:
+ newSelections.append(selection)
+ selectionOptions.remove(selection)
+ cursorLoc = min(cursorLoc, len(selectionOptions) - 1)
+ elif key == 27: break # esc - cancel
+
+ curses.halfdelay(controller.REFRESH_RATE * 10) # reset normal pausing behavior
+ finally: finalize()
+
+ if len(newSelections) == len(oldSelection):
+ return newSelections
+ else: return None
+
+def _drawSortSelection(popup, y, x, prefix, options, optionColors):
+ """
+ Draws a series of comma separated sort selections. The whole line is bold
+ and sort options also have their specified color. Example:
+
+ Current Order: Man Page Entry, Option Name, Is Default
+
+ Arguments:
+ popup - panel in which to draw sort selection
+ y - vertical location
+ x - horizontal location
+ prefix - initial string description
+ options - sort options to be shown
+ optionColors - mappings of options to their color
+ """
+
+ popup.addstr(y, x, prefix, curses.A_BOLD)
+ x += len(prefix)
+
+ for i in range(len(options)):
+ sortType = options[i]
+ sortColor = uiTools.getColor(optionColors.get(sortType, "white"))
+ popup.addstr(y, x, sortType, sortColor | curses.A_BOLD)
+ x += len(sortType)
+
+ # comma divider between options, if this isn't the last
+ if i < len(options) - 1:
+ popup.addstr(y, x, ", ", curses.A_BOLD)
+ x += 2
+
1
0

17 Jul '11
commit a2c89f6f8261a892cc02aee5325fc0ffff7671d9
Author: Damian Johnson <atagar(a)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
1
0

17 Jul '11
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
1
0