[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [arm/master] Renaming interface directory to cli



commit adf453c2402b805582d448d17f62ac173f00ec61
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date:   Tue Apr 26 19:20:55 2011 -0700

    Renaming interface directory to cli
    
    Kamran's working on an arm gui for GSoC, so renaming the interface directory to
    cli (and he'll introduce a /gui).
---
 src/cli/__init__.py                      |    6 +
 src/cli/configPanel.py                   |  364 +++++++
 src/cli/connections/__init__.py          |    6 +
 src/cli/connections/circEntry.py         |  216 ++++
 src/cli/connections/connEntry.py         |  850 ++++++++++++++++
 src/cli/connections/connPanel.py         |  398 ++++++++
 src/cli/connections/entries.py           |  164 +++
 src/cli/controller.py                    | 1584 ++++++++++++++++++++++++++++++
 src/cli/descriptorPopup.py               |  181 ++++
 src/cli/graphing/__init__.py             |    6 +
 src/cli/graphing/bandwidthStats.py       |  398 ++++++++
 src/cli/graphing/connStats.py            |   54 +
 src/cli/graphing/graphPanel.py           |  407 ++++++++
 src/cli/graphing/resourceStats.py        |   47 +
 src/cli/headerPanel.py                   |  474 +++++++++
 src/cli/logPanel.py                      | 1100 +++++++++++++++++++++
 src/cli/torrcPanel.py                    |  221 +++++
 src/interface/__init__.py                |    6 -
 src/interface/configPanel.py             |  364 -------
 src/interface/connections/__init__.py    |    6 -
 src/interface/connections/circEntry.py   |  216 ----
 src/interface/connections/connEntry.py   |  850 ----------------
 src/interface/connections/connPanel.py   |  398 --------
 src/interface/connections/entries.py     |  164 ---
 src/interface/controller.py              | 1584 ------------------------------
 src/interface/descriptorPopup.py         |  181 ----
 src/interface/graphing/__init__.py       |    6 -
 src/interface/graphing/bandwidthStats.py |  398 --------
 src/interface/graphing/connStats.py      |   54 -
 src/interface/graphing/graphPanel.py     |  407 --------
 src/interface/graphing/resourceStats.py  |   47 -
 src/interface/headerPanel.py             |  474 ---------
 src/interface/logPanel.py                | 1100 ---------------------
 src/interface/torrcPanel.py              |  221 -----
 src/starter.py                           |   10 +-
 35 files changed, 6481 insertions(+), 6481 deletions(-)

diff --git a/src/cli/__init__.py b/src/cli/__init__.py
new file mode 100644
index 0000000..0f11fc1
--- /dev/null
+++ b/src/cli/__init__.py
@@ -0,0 +1,6 @@
+"""
+Panels, popups, and handlers comprising the arm user interface.
+"""
+
+__all__ = ["configPanel", "connPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "torrcPanel"]
+
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
new file mode 100644
index 0000000..fd6fb54
--- /dev/null
+++ b/src/cli/configPanel.py
@@ -0,0 +1,364 @@
+"""
+Panel presenting the configuration state for tor or arm. Options can be edited
+and the resulting configuration files saved.
+"""
+
+import curses
+import threading
+
+from util import conf, enum, panel, torTools, torConfig, uiTools
+
+DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
+                  "features.config.state.showPrivateOptions": False,
+                  "features.config.state.showVirtualOptions": False,
+                  "features.config.state.colWidth.option": 25,
+                  "features.config.state.colWidth.value": 15}
+
+# TODO: The arm use cases are incomplete since they currently can't be
+# modified, have their descriptions fetched, or even get a complete listing
+# of what's available.
+State = enum.Enum("TOR", "ARM") # state to be presented
+
+# mappings of option categories to the color for their entries
+CATEGORY_COLOR = {torConfig.Category.GENERAL: "green",
+                  torConfig.Category.CLIENT: "blue",
+                  torConfig.Category.RELAY: "yellow",
+                  torConfig.Category.DIRECTORY: "magenta",
+                  torConfig.Category.AUTHORITY: "red",
+                  torConfig.Category.HIDDEN_SERVICE: "cyan",
+                  torConfig.Category.TESTING: "white",
+                  torConfig.Category.UNKNOWN: "white"}
+
+# attributes of a ConfigEntry
+Field = enum.Enum("CATEGORY", "OPTION", "VALUE", "TYPE", "ARG_USAGE",
+                  "SUMMARY", "DESCRIPTION", "MAN_ENTRY", "IS_DEFAULT")
+DEFAULT_SORT_ORDER = (Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT)
+FIELD_ATTR = {Field.CATEGORY: ("Category", "red"),
+              Field.OPTION: ("Option Name", "blue"),
+              Field.VALUE: ("Value", "cyan"),
+              Field.TYPE: ("Arg Type", "green"),
+              Field.ARG_USAGE: ("Arg Usage", "yellow"),
+              Field.SUMMARY: ("Summary", "green"),
+              Field.DESCRIPTION: ("Description", "white"),
+              Field.MAN_ENTRY: ("Man Page Entry", "blue"),
+              Field.IS_DEFAULT: ("Is Default", "magenta")}
+
+class ConfigEntry():
+  """
+  Configuration option in the panel.
+  """
+  
+  def __init__(self, option, type, isDefault):
+    self.fields = {}
+    self.fields[Field.OPTION] = option
+    self.fields[Field.TYPE] = type
+    self.fields[Field.IS_DEFAULT] = isDefault
+    
+    # Fetches extra infromation from external sources (the arm config and tor
+    # man page). These are None if unavailable for this config option.
+    summary = torConfig.getConfigSummary(option)
+    manEntry = torConfig.getConfigDescription(option)
+    
+    if manEntry:
+      self.fields[Field.MAN_ENTRY] = manEntry.index
+      self.fields[Field.CATEGORY] = manEntry.category
+      self.fields[Field.ARG_USAGE] = manEntry.argUsage
+      self.fields[Field.DESCRIPTION] = manEntry.description
+    else:
+      self.fields[Field.MAN_ENTRY] = 99999 # sorts non-man entries last
+      self.fields[Field.CATEGORY] = torConfig.Category.UNKNOWN
+      self.fields[Field.ARG_USAGE] = ""
+      self.fields[Field.DESCRIPTION] = ""
+    
+    # uses the full man page description if a summary is unavailable
+    self.fields[Field.SUMMARY] = summary if summary != None else self.fields[Field.DESCRIPTION]
+    
+    # cache of what's displayed for this configuration option
+    self.labelCache = None
+    self.labelCacheArgs = None
+  
+  def get(self, field):
+    """
+    Provides back the value in the given field.
+    
+    Arguments:
+      field - enum for the field to be provided back
+    """
+    
+    if field == Field.VALUE: return self._getValue()
+    else: return self.fields[field]
+  
+  def getAll(self, fields):
+    """
+    Provides back a list with the given field values.
+    
+    Arguments:
+      field - enums for the fields to be provided back
+    """
+    
+    return [self.get(field) for field in fields]
+  
+  def getLabel(self, optionWidth, valueWidth, summaryWidth):
+    """
+    Provides display string of the configuration entry with the given
+    constraints on the width of the contents.
+    
+    Arguments:
+      optionWidth  - width of the option column
+      valueWidth   - width of the value column
+      summaryWidth - width of the summary column
+    """
+    
+    # Fetching the display entries is very common so this caches the values.
+    # Doing this substantially drops cpu usage when scrolling (by around 40%).
+    
+    argSet = (optionWidth, valueWidth, summaryWidth)
+    if not self.labelCache or self.labelCacheArgs != argSet:
+      optionLabel = uiTools.cropStr(self.get(Field.OPTION), optionWidth)
+      valueLabel = uiTools.cropStr(self.get(Field.VALUE), valueWidth)
+      summaryLabel = uiTools.cropStr(self.get(Field.SUMMARY), summaryWidth, None)
+      lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth)
+      self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel)
+      self.labelCacheArgs = argSet
+    
+    return self.labelCache
+  
+  def _getValue(self):
+    """
+    Provides the current value of the configuration entry, taking advantage of
+    the torTools caching to effectively query the accurate value. This uses the
+    value's type to provide a user friendly representation if able.
+    """
+    
+    confValue = ", ".join(torTools.getConn().getOption(self.get(Field.OPTION), [], True))
+    
+    # provides nicer values for recognized types
+    if not confValue: confValue = "<none>"
+    elif self.get(Field.TYPE) == "Boolean" and confValue in ("0", "1"):
+      confValue = "False" if confValue == "0" else "True"
+    elif self.get(Field.TYPE) == "DataSize" and confValue.isdigit():
+      confValue = uiTools.getSizeLabel(int(confValue))
+    elif self.get(Field.TYPE) == "TimeInterval" and confValue.isdigit():
+      confValue = uiTools.getTimeLabel(int(confValue), isLong = True)
+    
+    return confValue
+
+class ConfigPanel(panel.Panel):
+  """
+  Renders a listing of the tor or arm configuration state, allowing options to
+  be selected and edited.
+  """
+  
+  def __init__(self, stdscr, configType, config=None):
+    panel.Panel.__init__(self, stdscr, "configState", 0)
+    
+    self.sortOrdering = DEFAULT_SORT_ORDER
+    self._config = dict(DEFAULT_CONFIG)
+    if config:
+      config.update(self._config, {
+        "features.config.selectionDetails.height": 0,
+        "features.config.state.colWidth.option": 5,
+        "features.config.state.colWidth.value": 5})
+      
+      sortFields = Field.values()
+      customOrdering = config.getIntCSV("features.config.order", None, 3, 0, len(sortFields))
+      
+      if customOrdering:
+        self.sortOrdering = [sortFields[i] for i in customOrdering]
+    
+    self.configType = configType
+    self.confContents = []
+    self.scroller = uiTools.Scroller(True)
+    self.valsLock = threading.RLock()
+    
+    # shows all configuration options if true, otherwise only the ones with
+    # the 'important' flag are shown
+    self.showAll = False
+    
+    if self.configType == State.TOR:
+      conn = torTools.getConn()
+      customOptions = torConfig.getCustomOptions()
+      configOptionLines = conn.getInfo("config/names", "").strip().split("\n")
+      
+      for line in configOptionLines:
+        # lines are of the form "<option> <type>", like:
+        # UseEntryGuards Boolean
+        confOption, confType = line.strip().split(" ", 1)
+        
+        # skips private and virtual entries if not configured to show them
+        if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"):
+          continue
+        elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual":
+          continue
+        
+        self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions))
+    elif self.configType == State.ARM:
+      # loaded via the conf utility
+      armConf = conf.getConfig("arm")
+      for key in armConf.getKeys():
+        pass # TODO: implement
+    
+    # mirror listing with only the important configuration options
+    self.confImportantContents = []
+    for entry in self.confContents:
+      if torConfig.isImportant(entry.get(Field.OPTION)):
+        self.confImportantContents.append(entry)
+    
+    # if there aren't any important options then show everything
+    if not self.confImportantContents:
+      self.confImportantContents = self.confContents
+    
+    self.setSortOrder() # initial sorting of the contents
+  
+  def getSelection(self):
+    """
+    Provides the currently selected entry.
+    """
+    
+    return self.scroller.getCursorSelection(self._getConfigOptions())
+  
+  def setSortOrder(self, ordering = None):
+    """
+    Sets the configuration attributes we're sorting by and resorts the
+    contents.
+    
+    Arguments:
+      ordering - new ordering, if undefined then this resorts with the last
+                 set ordering
+    """
+    
+    self.valsLock.acquire()
+    if ordering: self.sortOrdering = ordering
+    self.confContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
+    self.confImportantContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
+    self.valsLock.release()
+  
+  def handleKey(self, key):
+    self.valsLock.acquire()
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      detailPanelHeight = self._config["features.config.selectionDetails.height"]
+      if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight:
+        pageHeight -= (detailPanelHeight + 1)
+      
+      isChanged = self.scroller.handleKey(key, self._getConfigOptions(), pageHeight)
+      if isChanged: self.redraw(True)
+    elif key == ord('a') or key == ord('A'):
+      self.showAll = not self.showAll
+      self.redraw(True)
+    self.valsLock.release()
+  
+  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
+    if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
+      # no detail panel
+      detailPanelHeight = 0
+      scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1)
+      cursorSelection = self.getSelection()
+      isScrollbarVisible = len(self._getConfigOptions()) > height - 1
+    else:
+      # Shrink detail panel if there isn't sufficient room for the whole
+      # thing. The extra line is for the bottom border.
+      detailPanelHeight = min(height - 1, detailPanelHeight + 1)
+      scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1 - detailPanelHeight)
+      cursorSelection = self.getSelection()
+      isScrollbarVisible = len(self._getConfigOptions()) > height - detailPanelHeight - 1
+      
+      self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
+    
+    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
+    if isScrollbarVisible:
+      scrollOffset = 3
+      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self._getConfigOptions()), 1 + detailPanelHeight)
+    
+    optionWidth = self._config["features.config.state.colWidth.option"]
+    valueWidth = self._config["features.config.state.colWidth.value"]
+    descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
+    
+    for lineNum in range(scrollLoc, len(self._getConfigOptions())):
+      entry = self._getConfigOptions()[lineNum]
+      drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
+      
+      lineFormat = curses.A_NORMAL if entry.get(Field.IS_DEFAULT) else curses.A_BOLD
+      if entry.get(Field.CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(Field.CATEGORY)])
+      if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
+      
+      lineText = entry.getLabel(optionWidth, valueWidth, descriptionWidth)
+      self.addstr(drawLine, scrollOffset, lineText, lineFormat)
+      
+      if drawLine >= height: break
+    
+    self.valsLock.release()
+  
+  def _getConfigOptions(self):
+    return self.confContents if self.showAll else self.confImportantContents
+  
+  def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible):
+    """
+    Renders a panel for the selected configuration option.
+    """
+    
+    # This is a solid border unless the scrollbar is visible, in which case a
+    # 'T' pipe connects the border to the bar.
+    uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1)
+    if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE)
+    
+    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)])
+    
+    # first entry:
+    # <option> (<category> Option)
+    optionLabel =" (%s Option)" % selection.get(Field.CATEGORY)
+    self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat)
+    
+    # second entry:
+    # Value: <value> ([default|custom], <type>, usage: <argument usage>)
+    if detailPanelHeight >= 3:
+      valueAttr = []
+      valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
+      valueAttr.append(selection.get(Field.TYPE))
+      valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE)))
+      valueAttrLabel = ", ".join(valueAttr)
+      
+      valueLabelWidth = width - 12 - len(valueAttrLabel)
+      valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth)
+      
+      self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat)
+    
+    # remainder is filled with the man page description
+    descriptionHeight = max(0, detailPanelHeight - 3)
+    descriptionContent = "Description: " + selection.get(Field.DESCRIPTION)
+    
+    for i in range(descriptionHeight):
+      # checks if we're done writing the description
+      if not descriptionContent: break
+      
+      # there's a leading indent after the first line
+      if i > 0: descriptionContent = "  " + descriptionContent
+      
+      # we only want to work with content up until the next newline
+      if "\n" in descriptionContent:
+        lineContent, descriptionContent = descriptionContent.split("\n", 1)
+      else: lineContent, descriptionContent = descriptionContent, ""
+      
+      if i != descriptionHeight - 1:
+        # there's more lines to display
+        msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.Ending.HYPHEN, True)
+        descriptionContent = remainder.strip() + descriptionContent
+      else:
+        # this is the last line, end it with an ellipse
+        msg = uiTools.cropStr(lineContent, width - 2, 4, 4)
+      
+      self.addstr(3 + i, 2, msg, selectionFormat)
+
diff --git a/src/cli/connections/__init__.py b/src/cli/connections/__init__.py
new file mode 100644
index 0000000..5babdde
--- /dev/null
+++ b/src/cli/connections/__init__.py
@@ -0,0 +1,6 @@
+"""
+Connection panel related resources.
+"""
+
+__all__ = ["circEntry", "connEntry", "connPanel", "entries"]
+
diff --git a/src/cli/connections/circEntry.py b/src/cli/connections/circEntry.py
new file mode 100644
index 0000000..b15b26a
--- /dev/null
+++ b/src/cli/connections/circEntry.py
@@ -0,0 +1,216 @@
+"""
+Connection panel entries for client circuits. This includes a header entry
+followed by an entry for each hop in the circuit. For instance:
+
+89.188.20.246:42667    -->  217.172.182.26 (de)       General / Built     8.6m (CIRCUIT)
+|  85.8.28.4 (se)               98FBC3B2B93897A78CDD797EF549E6B62C9A8523    1 / Guard
+|  91.121.204.76 (fr)           546387D93F8D40CFF8842BB9D3A8EC477CEDA984    2 / Middle
++- 217.172.182.26 (de)          5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86    3 / Exit
+"""
+
+import curses
+
+from cli.connections import entries, connEntry
+from util import torTools, uiTools
+
+# cached fingerprint -> (IP Address, ORPort) results
+RELAY_INFO = {}
+
+def getRelayInfo(fingerprint):
+  """
+  Provides the (IP Address, ORPort) tuple for the given relay. If the lookup
+  fails then this returns ("192.168.0.1", "0").
+  
+  Arguments:
+    fingerprint - relay to look up
+  """
+  
+  if not fingerprint in RELAY_INFO:
+    conn = torTools.getConn()
+    failureResult = ("192.168.0.1", "0")
+    
+    nsEntry = conn.getConsensusEntry(fingerprint)
+    if not nsEntry: return failureResult
+    
+    nsLineComp = nsEntry.split("\n")[0].split(" ")
+    if len(nsLineComp) < 8: return failureResult
+    
+    RELAY_INFO[fingerprint] = (nsLineComp[6], nsLineComp[7])
+  
+  return RELAY_INFO[fingerprint]
+
+class CircEntry(connEntry.ConnectionEntry):
+  def __init__(self, circuitID, status, purpose, path):
+    connEntry.ConnectionEntry.__init__(self, "127.0.0.1", "0", "127.0.0.1", "0")
+    
+    self.circuitID = circuitID
+    self.status = status
+    
+    # drops to lowercase except the first letter
+    if len(purpose) >= 2:
+      purpose = purpose[0].upper() + purpose[1:].lower()
+    
+    self.lines = [CircHeaderLine(self.circuitID, purpose)]
+    
+    # Overwrites attributes of the initial line to make it more fitting as the
+    # header for our listing.
+    
+    self.lines[0].baseType = connEntry.Category.CIRCUIT
+    
+    self.update(status, path)
+  
+  def update(self, status, path):
+    """
+    Our status and path can change over time if the circuit is still in the
+    process of being built. Updates these attributes of our relay.
+    
+    Arguments:
+      status - new status of the circuit
+      path   - list of fingerprints for the series of relays involved in the
+               circuit
+    """
+    
+    self.status = status
+    self.lines = [self.lines[0]]
+    
+    if status == "BUILT" and not self.lines[0].isBuilt:
+      exitIp, exitORPort = getRelayInfo(path[-1])
+      self.lines[0].setExit(exitIp, exitORPort, path[-1])
+    
+    for i in range(len(path)):
+      relayFingerprint = path[i]
+      relayIp, relayOrPort = getRelayInfo(relayFingerprint)
+      
+      if i == len(path) - 1:
+        if status == "BUILT": placementType = "Exit"
+        else: placementType = "Extending"
+      elif i == 0: placementType = "Guard"
+      else: placementType = "Middle"
+      
+      placementLabel = "%i / %s" % (i + 1, placementType)
+      
+      self.lines.append(CircLine(relayIp, relayOrPort, relayFingerprint, placementLabel))
+    
+    self.lines[-1].isLast = True
+
+class CircHeaderLine(connEntry.ConnectionLine):
+  """
+  Initial line of a client entry. This has the same basic format as connection
+  lines except that its etc field has circuit attributes.
+  """
+  
+  def __init__(self, circuitID, purpose):
+    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", "0.0.0.0", "0", False, False)
+    self.circuitID = circuitID
+    self.purpose = purpose
+    self.isBuilt = False
+  
+  def setExit(self, exitIpAddr, exitPort, exitFingerprint):
+    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", exitIpAddr, exitPort, False, False)
+    self.isBuilt = True
+    self.foreign.fingerprintOverwrite = exitFingerprint
+  
+  def getType(self):
+    return connEntry.Category.CIRCUIT
+  
+  def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
+    if not self.isBuilt: return "Building..."
+    return connEntry.ConnectionLine.getDestinationLabel(self, maxLength, includeLocale, includeHostname)
+  
+  def getEtcContent(self, width, listingType):
+    """
+    Attempts to provide all circuit related stats. Anything that can't be
+    shown completely (not enough room) is dropped.
+    """
+    
+    etcAttr = ["Purpose: %s" % self.purpose, "Circuit ID: %i" % self.circuitID]
+    
+    for i in range(len(etcAttr), -1, -1):
+      etcLabel = ", ".join(etcAttr[:i])
+      if len(etcLabel) <= width:
+        return ("%%-%is" % width) % etcLabel
+    
+    return ""
+  
+  def getDetails(self, width):
+    if not self.isBuilt:
+      detailFormat = curses.A_BOLD | uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()])
+      return [uiTools.DrawEntry("Building Circuit...", detailFormat)]
+    else: return connEntry.ConnectionLine.getDetails(self, width)
+
+class CircLine(connEntry.ConnectionLine):
+  """
+  An individual hop in a circuit. This overwrites the displayed listing, but
+  otherwise makes use of the ConnectionLine attributes (for the detail display,
+  caching, etc).
+  """
+  
+  def __init__(self, fIpAddr, fPort, fFingerprint, placementLabel):
+    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", fIpAddr, fPort)
+    self.foreign.fingerprintOverwrite = fFingerprint
+    self.placementLabel = placementLabel
+    self.includePort = False
+    
+    # determines the sort of left hand bracketing we use
+    self.isLast = False
+  
+  def getType(self):
+    return connEntry.Category.CIRCUIT
+  
+  def getListingEntry(self, width, currentTime, listingType):
+    """
+    Provides the DrawEntry for this relay in the circuilt listing. Lines are
+    composed of the following components:
+      <bracket> <dst> <etc> <placement label>
+    
+    The dst and etc entries largely match their ConnectionEntry counterparts.
+    
+    Arguments:
+      width       - maximum length of the line
+      currentTime - the current unix time (ignored)
+      listingType - primary attribute we're listing connections by
+    """
+    
+    return entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType)
+  
+  def _getListingEntry(self, width, currentTime, listingType):
+    lineFormat = uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()])
+    
+    # The required widths are the sum of the following:
+    # bracketing (3 characters)
+    # placementLabel (14 characters)
+    # gap between etc and placement label (5 characters)
+    
+    if self.isLast: bracket = (curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
+    else: bracket = (curses.ACS_VLINE, ord(' '), ord(' '))
+    baselineSpace = len(bracket) + 14 + 5
+    
+    dst, etc = "", ""
+    if listingType == entries.ListingType.IP_ADDRESS:
+      # TODO: include hostname when that's available
+      # dst width is derived as:
+      # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char
+      dst = "%-53s" % self.getDestinationLabel(53, includeLocale = True)
+      etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
+    elif listingType == entries.ListingType.HOSTNAME:
+      # min space for the hostname is 40 characters
+      etc = self.getEtcContent(width - baselineSpace - 40, listingType)
+      dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
+      dst = dstLayout % self.foreign.getHostname(self.foreign.getIpAddr())
+    elif listingType == entries.ListingType.FINGERPRINT:
+      # dst width is derived as:
+      # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
+      dst = "%-55s" % self.foreign.getFingerprint()
+      etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
+    else:
+      # min space for the nickname is 56 characters
+      etc = self.getEtcContent(width - baselineSpace - 56, listingType)
+      dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
+      dst = dstLayout % self.foreign.getNickname()
+    
+    drawEntry = uiTools.DrawEntry("%-14s" % self.placementLabel, lineFormat)
+    drawEntry = uiTools.DrawEntry(" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat, drawEntry)
+    drawEntry = uiTools.DrawEntry(dst + etc, lineFormat, drawEntry)
+    drawEntry = uiTools.DrawEntry(bracket, curses.A_NORMAL, drawEntry, lockFormat = True)
+    return drawEntry
+
diff --git a/src/cli/connections/connEntry.py b/src/cli/connections/connEntry.py
new file mode 100644
index 0000000..ac45656
--- /dev/null
+++ b/src/cli/connections/connEntry.py
@@ -0,0 +1,850 @@
+"""
+Connection panel entries related to actual connections to or from the system
+(ie, results seen by netstat, lsof, etc).
+"""
+
+import time
+import curses
+
+from util import connections, enum, torTools, uiTools
+from cli.connections import entries
+
+# Connection Categories:
+#   Inbound      Relay connection, coming to us.
+#   Outbound     Relay connection, leaving us.
+#   Exit         Outbound relay connection leaving the Tor network.
+#   Hidden       Connections to a hidden service we're providing.
+#   Socks        Socks connections for applications using Tor.
+#   Circuit      Circuits our tor client has created.
+#   Directory    Fetching tor consensus information.
+#   Control      Tor controller (arm, vidalia, etc).
+
+Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL")
+CATEGORY_COLOR = {Category.INBOUND: "green",      Category.OUTBOUND: "blue",
+                  Category.EXIT: "red",           Category.HIDDEN: "magenta",
+                  Category.SOCKS: "yellow",       Category.CIRCUIT: "cyan",
+                  Category.DIRECTORY: "magenta",  Category.CONTROL: "red"}
+
+# static data for listing format
+# <src>  -->  <dst>  <etc><padding>
+LABEL_FORMAT = "%s  -->  %s  %s%s"
+LABEL_MIN_PADDING = 2 # min space between listing label and following data
+
+# sort value for scrubbed ip addresses
+SCRUBBED_IP_VAL = 255 ** 4
+
+CONFIG = {"features.connection.markInitialConnections": True,
+          "features.connection.showExitPort": True,
+          "features.connection.showColumn.fingerprint": True,
+          "features.connection.showColumn.nickname": True,
+          "features.connection.showColumn.destination": True,
+          "features.connection.showColumn.expandedIp": True}
+
+def loadConfig(config):
+  config.update(CONFIG)
+
+class Endpoint:
+  """
+  Collection of attributes associated with a connection endpoint. This is a
+  thin wrapper for torUtil functions, making use of its caching for
+  performance.
+  """
+  
+  def __init__(self, ipAddr, port):
+    self.ipAddr = ipAddr
+    self.port = port
+    
+    # if true, we treat the port as an ORPort when searching for matching
+    # fingerprints (otherwise the ORPort is assumed to be unknown)
+    self.isORPort = False
+    
+    # if set then this overwrites fingerprint lookups
+    self.fingerprintOverwrite = None
+  
+  def getIpAddr(self):
+    """
+    Provides the IP address of the endpoint.
+    """
+    
+    return self.ipAddr
+  
+  def getPort(self):
+    """
+    Provides the port of the endpoint.
+    """
+    
+    return self.port
+  
+  def getHostname(self, default = None):
+    """
+    Provides the hostname associated with the relay's address. This is a
+    non-blocking call and returns None if the address either can't be resolved
+    or hasn't been resolved yet.
+    
+    Arguments:
+      default - return value if no hostname is available
+    """
+    
+    # TODO: skipping all hostname resolution to be safe for now
+    #try:
+    #  myHostname = hostnames.resolve(self.ipAddr)
+    #except:
+    #  # either a ValueError or IOError depending on the source of the lookup failure
+    #  myHostname = None
+    #
+    #if not myHostname: return default
+    #else: return myHostname
+    
+    return default
+  
+  def getLocale(self, default=None):
+    """
+    Provides the two letter country code for the IP address' locale.
+    
+    Arguments:
+      default - return value if no locale information is available
+    """
+    
+    conn = torTools.getConn()
+    return conn.getInfo("ip-to-country/%s" % self.ipAddr, default)
+  
+  def getFingerprint(self):
+    """
+    Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be
+    determined.
+    """
+    
+    if self.fingerprintOverwrite:
+      return self.fingerprintOverwrite
+    
+    conn = torTools.getConn()
+    orPort = self.port if self.isORPort else None
+    myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort)
+    
+    if myFingerprint: return myFingerprint
+    else: return "UNKNOWN"
+  
+  def getNickname(self):
+    """
+    Provides the nickname of the relay, retuning "UNKNOWN" if it can't be
+    determined.
+    """
+    
+    myFingerprint = self.getFingerprint()
+    
+    if myFingerprint != "UNKNOWN":
+      conn = torTools.getConn()
+      myNickname = conn.getRelayNickname(myFingerprint)
+      
+      if myNickname: return myNickname
+      else: return "UNKNOWN"
+    else: return "UNKNOWN"
+
+class ConnectionEntry(entries.ConnectionPanelEntry):
+  """
+  Represents a connection being made to or from this system. These only
+  concern real connections so it includes the inbound, outbound, directory,
+  application, and controller categories.
+  """
+  
+  def __init__(self, lIpAddr, lPort, fIpAddr, fPort):
+    entries.ConnectionPanelEntry.__init__(self)
+    self.lines = [ConnectionLine(lIpAddr, lPort, fIpAddr, fPort)]
+  
+  def getSortValue(self, attr, listingType):
+    """
+    Provides the value of a single attribute used for sorting purposes.
+    """
+    
+    connLine = self.lines[0]
+    if attr == entries.SortAttr.IP_ADDRESS:
+      if connLine.isPrivate(): return SCRUBBED_IP_VAL # orders at the end
+      return connLine.sortIpAddr
+    elif attr == entries.SortAttr.PORT:
+      return connLine.sortPort
+    elif attr == entries.SortAttr.HOSTNAME:
+      if connLine.isPrivate(): return ""
+      return connLine.foreign.getHostname("")
+    elif attr == entries.SortAttr.FINGERPRINT:
+      return connLine.foreign.getFingerprint()
+    elif attr == entries.SortAttr.NICKNAME:
+      myNickname = connLine.foreign.getNickname()
+      if myNickname == "UNKNOWN": return "z" * 20 # orders at the end
+      else: return myNickname.lower()
+    elif attr == entries.SortAttr.CATEGORY:
+      return Category.indexOf(connLine.getType())
+    elif attr == entries.SortAttr.UPTIME:
+      return connLine.startTime
+    elif attr == entries.SortAttr.COUNTRY:
+      if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return ""
+      else: return connLine.foreign.getLocale("")
+    else:
+      return entries.ConnectionPanelEntry.getSortValue(self, attr, listingType)
+
+class ConnectionLine(entries.ConnectionPanelLine):
+  """
+  Display component of the ConnectionEntry.
+  """
+  
+  def __init__(self, lIpAddr, lPort, fIpAddr, fPort, includePort=True, includeExpandedIpAddr=True):
+    entries.ConnectionPanelLine.__init__(self)
+    
+    self.local = Endpoint(lIpAddr, lPort)
+    self.foreign = Endpoint(fIpAddr, fPort)
+    self.startTime = time.time()
+    self.isInitialConnection = False
+    
+    # overwrite the local fingerprint with ours
+    conn = torTools.getConn()
+    self.local.fingerprintOverwrite = conn.getInfo("fingerprint")
+    
+    # True if the connection has matched the properties of a client/directory
+    # connection every time we've checked. The criteria we check is...
+    #   client    - first hop in an established circuit
+    #   directory - matches an established single-hop circuit (probably a
+    #               directory mirror)
+    
+    self._possibleClient = True
+    self._possibleDirectory = True
+    
+    # attributes for SOCKS, HIDDEN, and CONTROL connections
+    self.appName = None
+    self.appPid = None
+    self.isAppResolving = False
+    
+    myOrPort = conn.getOption("ORPort")
+    myDirPort = conn.getOption("DirPort")
+    mySocksPort = conn.getOption("SocksPort", "9050")
+    myCtlPort = conn.getOption("ControlPort")
+    myHiddenServicePorts = conn.getHiddenServicePorts()
+    
+    # the ORListenAddress can overwrite the ORPort
+    listenAddr = conn.getOption("ORListenAddress")
+    if listenAddr and ":" in listenAddr:
+      myOrPort = listenAddr[listenAddr.find(":") + 1:]
+    
+    if lPort in (myOrPort, myDirPort):
+      self.baseType = Category.INBOUND
+      self.local.isORPort = True
+    elif lPort == mySocksPort:
+      self.baseType = Category.SOCKS
+    elif fPort in myHiddenServicePorts:
+      self.baseType = Category.HIDDEN
+    elif lPort == myCtlPort:
+      self.baseType = Category.CONTROL
+    else:
+      self.baseType = Category.OUTBOUND
+      self.foreign.isORPort = True
+    
+    self.cachedType = None
+    
+    # includes the port or expanded ip address field when displaying listing
+    # information if true
+    self.includePort = includePort
+    self.includeExpandedIpAddr = includeExpandedIpAddr
+    
+    # cached immutable values used for sorting
+    self.sortIpAddr = connections.ipToInt(self.foreign.getIpAddr())
+    self.sortPort = int(self.foreign.getPort())
+  
+  def getListingEntry(self, width, currentTime, listingType):
+    """
+    Provides the DrawEntry for this connection's listing. Lines are composed
+    of the following components:
+      <src>  -->  <dst>     <etc>     <uptime> (<type>)
+    
+    ListingType.IP_ADDRESS:
+      src - <internal addr:port> --> <external addr:port>
+      dst - <destination addr:port>
+      etc - <fingerprint> <nickname>
+    
+    ListingType.HOSTNAME:
+      src - localhost:<port>
+      dst - <destination hostname:port>
+      etc - <destination addr:port> <fingerprint> <nickname>
+    
+    ListingType.FINGERPRINT:
+      src - localhost
+      dst - <destination fingerprint>
+      etc - <nickname> <destination addr:port>
+    
+    ListingType.NICKNAME:
+      src - <source nickname>
+      dst - <destination nickname>
+      etc - <fingerprint> <destination addr:port>
+    
+    Arguments:
+      width       - maximum length of the line
+      currentTime - unix timestamp for what the results should consider to be
+                    the current time
+      listingType - primary attribute we're listing connections by
+    """
+    
+    # fetch our (most likely cached) display entry for the listing
+    myListing = entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType)
+    
+    # fill in the current uptime and return the results
+    if CONFIG["features.connection.markInitialConnections"]:
+      timePrefix = "+" if self.isInitialConnection else " "
+    else: timePrefix = ""
+    
+    timeEntry = myListing.getNext()
+    timeEntry.text = timePrefix + "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 1)
+    
+    return myListing
+  
+  def isUnresolvedApp(self):
+    """
+    True if our display uses application information that hasn't yet been resolved.
+    """
+    
+    return self.appName == None and self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
+  
+  def _getListingEntry(self, width, currentTime, listingType):
+    entryType = self.getType()
+    
+    # Lines are split into the following components in reverse:
+    # content  - "<src>  -->  <dst>     <etc>     "
+    # time     - "<uptime>"
+    # preType  - " ("
+    # category - "<type>"
+    # postType - ")   "
+    
+    lineFormat = uiTools.getColor(CATEGORY_COLOR[entryType])
+    timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5
+    
+    drawEntry = uiTools.DrawEntry(")" + " " * (9 - len(entryType)), lineFormat)
+    drawEntry = uiTools.DrawEntry(entryType.upper(), lineFormat | curses.A_BOLD, drawEntry)
+    drawEntry = uiTools.DrawEntry(" (", lineFormat, drawEntry)
+    drawEntry = uiTools.DrawEntry(" " * timeWidth, lineFormat, drawEntry)
+    drawEntry = uiTools.DrawEntry(self._getListingContent(width - (12 + timeWidth), listingType), lineFormat, drawEntry)
+    return drawEntry
+  
+  def _getDetails(self, width):
+    """
+    Provides details on the connection, correlated against available consensus
+    data.
+    
+    Arguments:
+      width - available space to display in
+    """
+    
+    detailFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[self.getType()])
+    return [uiTools.DrawEntry(line, detailFormat) for line in self._getDetailContent(width)]
+  
+  def resetDisplay(self):
+    entries.ConnectionPanelLine.resetDisplay(self)
+    self.cachedType = None
+  
+  def isPrivate(self):
+    """
+    Returns true if the endpoint is private, possibly belonging to a client
+    connection or exit traffic.
+    """
+    
+    # This is used to scrub private information from the interface. Relaying
+    # etiquette (and wiretapping laws) say these are bad things to look at so
+    # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON!
+    
+    myType = self.getType()
+    
+    if myType == Category.INBOUND:
+      # if we're a guard or bridge and the connection doesn't belong to a
+      # known relay then it might be client traffic
+      
+      conn = torTools.getConn()
+      if "Guard" in conn.getMyFlags([]) or conn.getOption("BridgeRelay") == "1":
+        allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
+        return allMatches == []
+    elif myType == Category.EXIT:
+      # DNS connections exiting us aren't private (since they're hitting our
+      # resolvers). Everything else, however, is.
+      
+      # TODO: Ideally this would also double check that it's a UDP connection
+      # (since DNS is the only UDP connections Tor will relay), however this
+      # will take a bit more work to propagate the information up from the
+      # connection resolver.
+      return self.foreign.getPort() != "53"
+    
+    # for everything else this isn't a concern
+    return False
+  
+  def getType(self):
+    """
+    Provides our best guess at the current type of the connection. This
+    depends on consensus results, our current client circuits, etc. Results
+    are cached until this entry's display is reset.
+    """
+    
+    # caches both to simplify the calls and to keep the type consistent until
+    # we want to reflect changes
+    if not self.cachedType:
+      if self.baseType == Category.OUTBOUND:
+        # Currently the only non-static categories are OUTBOUND vs...
+        # - EXIT since this depends on the current consensus
+        # - CIRCUIT if this is likely to belong to our guard usage
+        # - DIRECTORY if this is a single-hop circuit (directory mirror?)
+        # 
+        # The exitability, circuits, and fingerprints are all cached by the
+        # torTools util keeping this a quick lookup.
+        
+        conn = torTools.getConn()
+        destFingerprint = self.foreign.getFingerprint()
+        
+        if destFingerprint == "UNKNOWN":
+          # Not a known relay. This might be an exit connection.
+          
+          if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()):
+            self.cachedType = Category.EXIT
+        elif self._possibleClient or self._possibleDirectory:
+          # This belongs to a known relay. If we haven't eliminated ourselves as
+          # a possible client or directory connection then check if it still
+          # holds true.
+          
+          myCircuits = conn.getCircuits()
+          
+          if self._possibleClient:
+            # Checks that this belongs to the first hop in a circuit that's
+            # either unestablished or longer than a single hop (ie, anything but
+            # a built 1-hop connection since those are most likely a directory
+            # mirror).
+            
+            for _, status, _, path in myCircuits:
+              if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1):
+                self.cachedType = Category.CIRCUIT # matched a probable guard connection
+            
+            # if we fell through, we can eliminate ourselves as a guard in the future
+            if not self.cachedType:
+              self._possibleClient = False
+          
+          if self._possibleDirectory:
+            # Checks if we match a built, single hop circuit.
+            
+            for _, status, _, path in myCircuits:
+              if path[0] == destFingerprint and status == "BUILT" and len(path) == 1:
+                self.cachedType = Category.DIRECTORY
+            
+            # if we fell through, eliminate ourselves as a directory connection
+            if not self.cachedType:
+              self._possibleDirectory = False
+      
+      if not self.cachedType:
+        self.cachedType = self.baseType
+    
+    return self.cachedType
+  
+  def getEtcContent(self, width, listingType):
+    """
+    Provides the optional content for the connection.
+    
+    Arguments:
+      width       - maximum length of the line
+      listingType - primary attribute we're listing connections by
+    """
+    
+    # for applications show the command/pid
+    if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
+      displayLabel = ""
+      
+      if self.appName:
+        if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid)
+        else: displayLabel = self.appName
+      elif self.isAppResolving:
+        displayLabel = "resolving..."
+      else: displayLabel = "UNKNOWN"
+      
+      if len(displayLabel) < width:
+        return ("%%-%is" % width) % displayLabel
+      else: return ""
+    
+    # for everything else display connection/consensus information
+    dstAddress = self.getDestinationLabel(26, includeLocale = True)
+    etc, usedSpace = "", 0
+    if listingType == entries.ListingType.IP_ADDRESS:
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 10 and CONFIG["features.connection.showColumn.nickname"]:
+        # show nickname (column width: remainder)
+        nicknameSpace = width - usedSpace
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+        usedSpace += nicknameSpace + 2
+    elif listingType == entries.ListingType.HOSTNAME:
+      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % dstAddress
+        usedSpace += 28
+      
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]:
+        # show nickname (column width: min 17 characters, uses half of the remainder)
+        nicknameSpace = 15 + (width - (usedSpace + 17)) / 2
+        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+        usedSpace += (nicknameSpace + 2)
+    elif listingType == entries.ListingType.FINGERPRINT:
+      if width > usedSpace + 17:
+        # show nickname (column width: min 17 characters, consumes any remaining space)
+        nicknameSpace = width - usedSpace - 2
+        
+        # if there's room then also show a column with the destination
+        # ip/port/locale (column width: 28 characters)
+        isIpLocaleIncluded = width > usedSpace + 45
+        isIpLocaleIncluded &= CONFIG["features.connection.showColumn.destination"]
+        if isIpLocaleIncluded: nicknameSpace -= 28
+        
+        if CONFIG["features.connection.showColumn.nickname"]:
+          nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
+          etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
+          usedSpace += nicknameSpace + 2
+        
+        if isIpLocaleIncluded:
+          etc += "%-26s  " % dstAddress
+          usedSpace += 28
+    else:
+      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.getFingerprint()
+        usedSpace += 42
+      
+      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % dstAddress
+        usedSpace += 28
+    
+    return ("%%-%is" % width) % etc
+  
+  def _getListingContent(self, width, listingType):
+    """
+    Provides the source, destination, and extra info for our listing.
+    
+    Arguments:
+      width       - maximum length of the line
+      listingType - primary attribute we're listing connections by
+    """
+    
+    conn = torTools.getConn()
+    myType = self.getType()
+    dstAddress = self.getDestinationLabel(26, includeLocale = True)
+    
+    # The required widths are the sum of the following:
+    # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters)
+    # - base data for the listing
+    # - that extra field plus any previous
+    
+    usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING
+    localPort = ":%s" % self.local.getPort() if self.includePort else ""
+    
+    src, dst, etc = "", "", ""
+    if listingType == entries.ListingType.IP_ADDRESS:
+      myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr())
+      addrDiffer = myExternalIpAddr != self.local.getIpAddr()
+      
+      # Expanding doesn't make sense, if the connection isn't actually
+      # going through Tor's external IP address. As there isn't a known
+      # method for checking if it is, we're checking the type instead.
+      #
+      # This isn't entirely correct. It might be a better idea to check if
+      # the source and destination addresses are both private, but that might
+      # not be perfectly reliable either.
+      
+      isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
+      
+      if isExpansionType: srcAddress = myExternalIpAddr + localPort
+      else: srcAddress = self.local.getIpAddr() + localPort
+      
+      if myType in (Category.SOCKS, Category.CONTROL):
+        # Like inbound connections these need their source and destination to
+        # be swapped. However, this only applies when listing by IP or hostname
+        # (their fingerprint and nickname are both for us). Reversing the
+        # fields here to keep the same column alignments.
+        
+        src = "%-21s" % dstAddress
+        dst = "%-26s" % srcAddress
+      else:
+        src = "%-21s" % srcAddress # ip:port = max of 21 characters
+        dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters
+      
+      usedSpace += len(src) + len(dst) # base data requires 47 characters
+      
+      # Showing the fingerprint (which has the width of 42) has priority over
+      # an expanded address field. Hence check if we either have space for
+      # both or wouldn't be showing the fingerprint regardless.
+      
+      isExpandedAddrVisible = width > usedSpace + 28
+      if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]:
+        isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70
+      
+      if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]:
+        # include the internal address in the src (extra 28 characters)
+        internalAddress = self.local.getIpAddr() + localPort
+        
+        # If this is an inbound connection then reverse ordering so it's:
+        # <foreign> --> <external> --> <internal>
+        # when the src and dst are swapped later
+        
+        if myType == Category.INBOUND: src = "%-21s  -->  %s" % (src, internalAddress)
+        else: src = "%-21s  -->  %s" % (internalAddress, src)
+        
+        usedSpace += 28
+      
+      etc = self.getEtcContent(width - usedSpace, listingType)
+      usedSpace += len(etc)
+    elif listingType == entries.ListingType.HOSTNAME:
+      # 15 characters for source, and a min of 40 reserved for the destination
+      # TODO: when actually functional the src and dst need to be swapped for
+      # SOCKS and CONTROL connections
+      src = "localhost%-6s" % localPort
+      usedSpace += len(src)
+      minHostnameSpace = 40
+      
+      etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType)
+      usedSpace += len(etc)
+      
+      hostnameSpace = width - usedSpace
+      usedSpace = width # prevents padding at the end
+      if self.isPrivate():
+        dst = ("%%-%is" % hostnameSpace) % "<scrubbed>"
+      else:
+        hostname = self.foreign.getHostname(self.foreign.getIpAddr())
+        portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else ""
+        
+        # truncates long hostnames and sets dst to <hostname>:<port>
+        hostname = uiTools.cropStr(hostname, hostnameSpace, 0)
+        dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel)
+    elif listingType == entries.ListingType.FINGERPRINT:
+      src = "localhost"
+      if myType == Category.CONTROL: dst = "localhost"
+      else: dst = self.foreign.getFingerprint()
+      dst = "%-40s" % dst
+      
+      usedSpace += len(src) + len(dst) # base data requires 49 characters
+      
+      etc = self.getEtcContent(width - usedSpace, listingType)
+      usedSpace += len(etc)
+    else:
+      # base data requires 50 min characters
+      src = self.local.getNickname()
+      if myType == Category.CONTROL: dst = self.local.getNickname()
+      else: dst = self.foreign.getNickname()
+      minBaseSpace = 50
+      
+      etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType)
+      usedSpace += len(etc)
+      
+      baseSpace = width - usedSpace
+      usedSpace = width # prevents padding at the end
+      
+      if len(src) + len(dst) > baseSpace:
+        src = uiTools.cropStr(src, baseSpace / 3)
+        dst = uiTools.cropStr(dst, baseSpace - len(src))
+      
+      # pads dst entry to its max space
+      dst = ("%%-%is" % (baseSpace - len(src))) % dst
+    
+    if myType == Category.INBOUND: src, dst = dst, src
+    padding = " " * (width - usedSpace + LABEL_MIN_PADDING)
+    return LABEL_FORMAT % (src, dst, etc, padding)
+  
+  def _getDetailContent(self, width):
+    """
+    Provides a list with detailed information for this connection.
+    
+    Arguments:
+      width - max length of lines
+    """
+    
+    lines = [""] * 7
+    lines[0] = "address: %s" % self.getDestinationLabel(width - 11)
+    lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale("??"))
+    
+    # Remaining data concerns the consensus results, with three possible cases:
+    # - if there's a single match then display its details
+    # - if there's multiple potential relays then list all of the combinations
+    #   of ORPorts / Fingerprints
+    # - if no consensus data is available then say so (probably a client or
+    #   exit connection)
+    
+    fingerprint = self.foreign.getFingerprint()
+    conn = torTools.getConn()
+    
+    if fingerprint != "UNKNOWN":
+      # single match - display information available about it
+      nsEntry = conn.getConsensusEntry(fingerprint)
+      descEntry = conn.getDescriptorEntry(fingerprint)
+      
+      # append the fingerprint to the second line
+      lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint)
+      
+      if nsEntry:
+        # example consensus entry:
+        # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0
+        # s Exit Fast Guard Named Running Stable Valid
+        # w Bandwidth=2540
+        # p accept 20-23,43,53,79-81,88,110,143,194,443
+        
+        nsLines = nsEntry.split("\n")
+        
+        firstLineComp = nsLines[0].split(" ")
+        if len(firstLineComp) >= 9:
+          _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[:9]
+        else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", ""
+        
+        flags = "unknown"
+        if len(nsLines) >= 2 and nsLines[1].startswith("s "):
+          flags = nsLines[1][2:]
+        
+        # The network status exit policy doesn't exist for older tor versions.
+        # If unavailable we'll need the full exit policy which is on the
+        # descriptor (if that's available).
+        
+        exitPolicy = "unknown"
+        if len(nsLines) >= 4 and nsLines[3].startswith("p "):
+          exitPolicy = nsLines[3][2:].replace(",", ", ")
+        elif descEntry:
+          # the descriptor has an individual line for each entry in the exit policy
+          exitPolicyEntries = []
+          
+          for line in descEntry.split("\n"):
+            if line.startswith("accept") or line.startswith("reject"):
+              exitPolicyEntries.append(line.strip())
+          
+          exitPolicy = ", ".join(exitPolicyEntries)
+        
+        dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
+        lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
+        lines[3] = "published: %s %s" % (pubDate, pubTime)
+        lines[4] = "flags: %s" % flags.replace(" ", ", ")
+        lines[5] = "exit policy: %s" % exitPolicy
+      
+      if descEntry:
+        torVersion, platform, contact = "", "", ""
+        
+        for descLine in descEntry.split("\n"):
+          if descLine.startswith("platform"):
+            # has the tor version and platform, ex:
+            # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64
+            
+            torVersion = descLine[13:descLine.find(" ", 13)]
+            platform = descLine[descLine.rfind(" on ") + 4:]
+          elif descLine.startswith("contact"):
+            contact = descLine[8:]
+            
+            # clears up some highly common obscuring
+            for alias in (" at ", " AT "): contact = contact.replace(alias, "@")
+            for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".")
+            
+            break # contact lines come after the platform
+        
+        lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, torVersion)
+        
+        # contact information is an optional field
+        if contact: lines[6] = "contact: %s" % contact
+    else:
+      allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
+      
+      if allMatches:
+        # multiple matches
+        lines[2] = "Multiple matches, possible fingerprints are:"
+        
+        for i in range(len(allMatches)):
+          isLastLine = i == 3
+          
+          relayPort, relayFingerprint = allMatches[i]
+          lineText = "%i. or port: %-5s fingerprint: %s" % (i, relayPort, relayFingerprint)
+          
+          # if there's multiple lines remaining at the end then give a count
+          remainingRelays = len(allMatches) - i
+          if isLastLine and remainingRelays > 1:
+            lineText = "... %i more" % remainingRelays
+          
+          lines[3 + i] = lineText
+          
+          if isLastLine: break
+      else:
+        # no consensus entry for this ip address
+        lines[2] = "No consensus data found"
+    
+    # crops any lines that are too long
+    for i in range(len(lines)):
+      lines[i] = uiTools.cropStr(lines[i], width - 2)
+    
+    return lines
+  
+  def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
+    """
+    Provides a short description of the destination. This is made up of two
+    components, the base <ip addr>:<port> and an extra piece of information in
+    parentheses. The IP address is scrubbed from private connections.
+    
+    Extra information is...
+    - the port's purpose for exit connections
+    - the locale and/or hostname if set to do so, the address isn't private,
+      and isn't on the local network
+    - nothing otherwise
+    
+    Arguments:
+      maxLength       - maximum length of the string returned
+      includeLocale   - possibly includes the locale
+      includeHostname - possibly includes the hostname
+    """
+    
+    # the port and port derived data can be hidden by config or without includePort
+    includePort = self.includePort and (CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT)
+    
+    # destination of the connection
+    ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr()
+    portLabel = ":%s" % self.foreign.getPort() if includePort else ""
+    dstAddress = ipLabel + portLabel
+    
+    # Only append the extra info if there's at least a couple characters of
+    # space (this is what's needed for the country codes).
+    if len(dstAddress) + 5 <= maxLength:
+      spaceAvailable = maxLength - len(dstAddress) - 3
+      
+      if self.getType() == Category.EXIT and includePort:
+        purpose = connections.getPortUsage(self.foreign.getPort())
+        
+        if purpose:
+          # BitTorrent is a common protocol to truncate, so just use "Torrent"
+          # if there's not enough room.
+          if len(purpose) > spaceAvailable and purpose == "BitTorrent":
+            purpose = "Torrent"
+          
+          # crops with a hyphen if too long
+          purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN)
+          
+          dstAddress += " (%s)" % purpose
+      elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
+        extraInfo = []
+        conn = torTools.getConn()
+        
+        if includeLocale and not conn.isGeoipUnavailable():
+          foreignLocale = self.foreign.getLocale("??")
+          extraInfo.append(foreignLocale)
+          spaceAvailable -= len(foreignLocale) + 2
+        
+        if includeHostname:
+          dstHostname = self.foreign.getHostname()
+          
+          if dstHostname:
+            # determines the full space available, taking into account the ", "
+            # dividers if there's multiple pieces of extra data
+            
+            maxHostnameSpace = spaceAvailable - 2 * len(extraInfo)
+            dstHostname = uiTools.cropStr(dstHostname, maxHostnameSpace)
+            extraInfo.append(dstHostname)
+            spaceAvailable -= len(dstHostname)
+        
+        if extraInfo:
+          dstAddress += " (%s)" % ", ".join(extraInfo)
+    
+    return dstAddress[:maxLength]
+
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
new file mode 100644
index 0000000..569f57c
--- /dev/null
+++ b/src/cli/connections/connPanel.py
@@ -0,0 +1,398 @@
+"""
+Listing of the currently established connections tor has made.
+"""
+
+import time
+import curses
+import threading
+
+from cli.connections import entries, connEntry, circEntry
+from util import connections, enum, panel, torTools, uiTools
+
+DEFAULT_CONFIG = {"features.connection.resolveApps": True,
+                  "features.connection.listingType": 0,
+                  "features.connection.refreshRate": 5}
+
+# height of the detail panel content, not counting top and bottom border
+DETAILS_HEIGHT = 7
+
+# listing types
+Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME)
+
+class ConnectionPanel(panel.Panel, threading.Thread):
+  """
+  Listing of connections tor is making, with information correlated against
+  the current consensus and other data sources.
+  """
+  
+  def __init__(self, stdscr, config=None):
+    panel.Panel.__init__(self, stdscr, "conn", 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+    
+    self._sortOrdering = DEFAULT_SORT_ORDER
+    self._config = dict(DEFAULT_CONFIG)
+    
+    if config:
+      config.update(self._config, {
+        "features.connection.listingType": (0, len(Listing.values()) - 1),
+        "features.connection.refreshRate": 1})
+      
+      sortFields = entries.SortAttr.values()
+      customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields))
+      
+      if customOrdering:
+        self._sortOrdering = [sortFields[i] for i in customOrdering]
+    
+    self._listingType = Listing.values()[self._config["features.connection.listingType"]]
+    self._scroller = uiTools.Scroller(True)
+    self._title = "Connections:" # title line of the panel
+    self._entries = []          # last fetched display entries
+    self._entryLines = []       # individual lines rendered from the entries listing
+    self._showDetails = False   # presents the details panel if true
+    
+    self._lastUpdate = -1       # time the content was last revised
+    self._isTorRunning = True   # indicates if tor is currently running or not
+    self._isPaused = True       # prevents updates if true
+    self._pauseTime = None      # time when the panel was paused
+    self._halt = False          # terminates thread if true
+    self._cond = threading.Condition()  # used for pausing the thread
+    self.valsLock = threading.RLock()
+    
+    # Last sampling received from the ConnectionResolver, used to detect when
+    # it changes.
+    self._lastResourceFetch = -1
+    
+    # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections
+    self._appResolver = connections.AppResolver("arm")
+    
+    # rate limits appResolver queries to once per update
+    self.appResolveSinceUpdate = False
+    
+    self._update()            # populates initial entries
+    self._resolveApps(False)  # resolves initial applications
+    
+    # mark the initially exitsing connection uptimes as being estimates
+    for entry in self._entries:
+      if isinstance(entry, connEntry.ConnectionEntry):
+        entry.getLines()[0].isInitialConnection = True
+    
+    # listens for when tor stops so we know to stop reflecting changes
+    torTools.getConn().addStatusListener(self.torStateListener)
+  
+  def torStateListener(self, conn, eventType):
+    """
+    Freezes the connection contents when Tor stops.
+    
+    Arguments:
+      conn      - tor controller
+      eventType - type of event detected
+    """
+    
+    self._isTorRunning = eventType == torTools.State.INIT
+    
+    if self._isPaused or not self._isTorRunning:
+      if not self._pauseTime: self._pauseTime = time.time()
+    else: self._pauseTime = None
+    
+    self.redraw(True)
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents the panel from updating.
+    """
+    
+    if not self._isPaused == isPause:
+      self._isPaused = isPause
+      
+      if isPause or not self._isTorRunning:
+        if not self._pauseTime: self._pauseTime = time.time()
+      else: self._pauseTime = None
+      
+      # redraws so the display reflects any changes between the last update
+      # and being paused
+      self.redraw(True)
+  
+  def setSortOrder(self, ordering = None):
+    """
+    Sets the connection attributes we're sorting by and resorts the contents.
+    
+    Arguments:
+      ordering - new ordering, if undefined then this resorts with the last
+                 set ordering
+    """
+    
+    self.valsLock.acquire()
+    if ordering: self._sortOrdering = ordering
+    self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
+    
+    self._entryLines = []
+    for entry in self._entries:
+      self._entryLines += entry.getLines()
+    self.valsLock.release()
+  
+  def setListingType(self, listingType):
+    """
+    Sets the priority information presented by the panel.
+    
+    Arguments:
+      listingType - Listing instance for the primary information to be shown
+    """
+    
+    self.valsLock.acquire()
+    self._listingType = listingType
+    
+    # if we're sorting by the listing then we need to resort
+    if entries.SortAttr.LISTING in self._sortOrdering:
+      self.setSortOrder()
+    
+    self.valsLock.release()
+  
+  def handleKey(self, key):
+    self.valsLock.acquire()
+    
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
+      isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight)
+      if isChanged: self.redraw(True)
+    elif uiTools.isSelectionKey(key):
+      self._showDetails = not self._showDetails
+      self.redraw(True)
+    
+    self.valsLock.release()
+  
+  def run(self):
+    """
+    Keeps connections listing updated, checking for new entries at a set rate.
+    """
+    
+    lastDraw = time.time() - 1
+    while not self._halt:
+      currentTime = time.time()
+      
+      if self._isPaused or not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]:
+        self._cond.acquire()
+        if not self._halt: self._cond.wait(0.2)
+        self._cond.release()
+      else:
+        # updates content if their's new results, otherwise just redraws
+        self._update()
+        self.redraw(True)
+        
+        # we may have missed multiple updates due to being paused, showing
+        # another panel, etc so lastDraw might need to jump multiple ticks
+        drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"]
+        lastDraw += self._config["features.connection.refreshRate"] * drawTicks
+  
+  def draw(self, width, height):
+    self.valsLock.acquire()
+    
+    # extra line when showing the detail panel is for the bottom border
+    detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0
+    isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1
+    
+    scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1)
+    cursorSelection = self._scroller.getCursorSelection(self._entryLines)
+    
+    # draws the detail panel if currently displaying it
+    if self._showDetails:
+      # This is a solid border unless the scrollbar is visible, in which case a
+      # 'T' pipe connects the border to the bar.
+      uiTools.drawBox(self, 0, 0, width, DETAILS_HEIGHT + 2)
+      if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
+      
+      drawEntries = cursorSelection.getDetails(width)
+      for i in range(min(len(drawEntries), DETAILS_HEIGHT)):
+        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)
+    
+    scrollOffset = 1
+    if isScrollbarVisible:
+      scrollOffset = 3
+      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset)
+    
+    currentTime = self._pauseTime if self._pauseTime else time.time()
+    for lineNum in range(scrollLoc, len(self._entryLines)):
+      entryLine = self._entryLines[lineNum]
+      
+      # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up
+      # resolution for the applicaitions they belong to
+      if isinstance(entryLine, connEntry.ConnectionLine) and entryLine.isUnresolvedApp():
+        self._resolveApps()
+      
+      # hilighting if this is the selected line
+      extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL
+      
+      drawEntry = entryLine.getListingEntry(width - scrollOffset, currentTime, self._listingType)
+      drawLine = lineNum + detailPanelOffset + 1 - scrollLoc
+      drawEntry.render(self, drawLine, scrollOffset, extraFormat)
+      if drawLine >= height: break
+    
+    self.valsLock.release()
+  
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+    
+    self._cond.acquire()
+    self._halt = True
+    self._cond.notifyAll()
+    self._cond.release()
+  
+  def _update(self):
+    """
+    Fetches the newest resolved connections.
+    """
+    
+    connResolver = connections.getResolver("tor")
+    currentResolutionCount = connResolver.getResolutionCount()
+    self.appResolveSinceUpdate = False
+    
+    if self._lastResourceFetch != currentResolutionCount:
+      self.valsLock.acquire()
+      
+      newEntries = [] # the new results we'll display
+      
+      # Fetches new connections and client circuits...
+      # newConnections  [(local ip, local port, foreign ip, foreign port)...]
+      # newCircuits     {circuitID => (status, purpose, path)...}
+      
+      newConnections = connResolver.getConnections()
+      newCircuits = {}
+      
+      for circuitID, status, purpose, path in torTools.getConn().getCircuits():
+        # Skips established single-hop circuits (these are for directory
+        # fetches, not client circuits)
+        if not (status == "BUILT" and len(path) == 1):
+          newCircuits[circuitID] = (status, purpose, path)
+      
+      # Populates newEntries with any of our old entries that still exist.
+      # This is both for performance and to keep from resetting the uptime
+      # attributes. Note that CircEntries are a ConnectionEntry subclass so
+      # we need to check for them first.
+      
+      for oldEntry in self._entries:
+        if isinstance(oldEntry, circEntry.CircEntry):
+          newEntry = newCircuits.get(oldEntry.circuitID)
+          
+          if newEntry:
+            oldEntry.update(newEntry[0], newEntry[2])
+            newEntries.append(oldEntry)
+            del newCircuits[oldEntry.circuitID]
+        elif isinstance(oldEntry, connEntry.ConnectionEntry):
+          connLine = oldEntry.getLines()[0]
+          connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(),
+                      connLine.foreign.getIpAddr(), connLine.foreign.getPort())
+          
+          if connAttr in newConnections:
+            newEntries.append(oldEntry)
+            newConnections.remove(connAttr)
+      
+      # Reset any display attributes for the entries we're keeping
+      for entry in newEntries: entry.resetDisplay()
+      
+      # Adds any new connection and circuit entries.
+      for lIp, lPort, fIp, fPort in newConnections:
+        newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort)
+        if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT:
+          newEntries.append(newConnEntry)
+      
+      for circuitID in newCircuits:
+        status, purpose, path = newCircuits[circuitID]
+        newEntries.append(circEntry.CircEntry(circuitID, status, purpose, path))
+      
+      # Counts the relays in each of the categories. This also flushes the
+      # type cache for all of the connections (in case its changed since last
+      # fetched).
+      
+      categoryTypes = connEntry.Category.values()
+      typeCounts = dict((type, 0) for type in categoryTypes)
+      for entry in newEntries:
+        if isinstance(entry, connEntry.ConnectionEntry):
+          typeCounts[entry.getLines()[0].getType()] += 1
+        elif isinstance(entry, circEntry.CircEntry):
+          typeCounts[connEntry.Category.CIRCUIT] += 1
+      
+      # makes labels for all the categories with connections (ie,
+      # "21 outbound", "1 control", etc)
+      countLabels = []
+      
+      for category in categoryTypes:
+        if typeCounts[category] > 0:
+          countLabels.append("%i %s" % (typeCounts[category], category.lower()))
+      
+      if countLabels: self._title = "Connections (%s):" % ", ".join(countLabels)
+      else: self._title = "Connections:"
+      
+      self._entries = newEntries
+      
+      self._entryLines = []
+      for entry in self._entries:
+        self._entryLines += entry.getLines()
+      
+      self.setSortOrder()
+      self._lastResourceFetch = currentResolutionCount
+      self.valsLock.release()
+  
+  def _resolveApps(self, flagQuery = True):
+    """
+    Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and
+    CONTROL entries.
+    
+    Arguments:
+      flagQuery - sets a flag to prevent further call from being respected
+                  until the next update if true
+    """
+    
+    if self.appResolveSinceUpdate or not self._config["features.connection.resolveApps"]: return
+    unresolvedLines = [l for l in self._entryLines if isinstance(l, connEntry.ConnectionLine) and l.isUnresolvedApp()]
+    
+    # get the ports used for unresolved applications
+    appPorts = []
+    
+    for line in unresolvedLines:
+      appConn = line.local if line.getType() == connEntry.Category.HIDDEN else line.foreign
+      appPorts.append(appConn.getPort())
+    
+    # Queue up resolution for the unresolved ports (skips if it's still working
+    # on the last query).
+    if appPorts and not self._appResolver.isResolving:
+      self._appResolver.resolve(appPorts)
+    
+    # Fetches results. If the query finishes quickly then this is what we just
+    # asked for, otherwise these belong to an earlier resolution.
+    #
+    # The application resolver might have given up querying (for instance, if
+    # the lsof lookups aren't working on this platform or lacks permissions).
+    # The isAppResolving flag lets the unresolved entries indicate if there's
+    # a lookup in progress for them or not.
+    
+    appResults = self._appResolver.getResults(0.2)
+    
+    for line in unresolvedLines:
+      isLocal = line.getType() == connEntry.Category.HIDDEN
+      linePort = line.local.getPort() if isLocal else line.foreign.getPort()
+      
+      if linePort in appResults:
+        # sets application attributes if there's a result with this as the
+        # inbound port
+        for inboundPort, outboundPort, cmd, pid in appResults[linePort]:
+          appPort = outboundPort if isLocal else inboundPort
+          
+          if linePort == appPort:
+            line.appName = cmd
+            line.appPid = pid
+            line.isAppResolving = False
+      else:
+        line.isAppResolving = self._appResolver.isResolving
+    
+    if flagQuery:
+      self.appResolveSinceUpdate = True
+
diff --git a/src/cli/connections/entries.py b/src/cli/connections/entries.py
new file mode 100644
index 0000000..6b24412
--- /dev/null
+++ b/src/cli/connections/entries.py
@@ -0,0 +1,164 @@
+"""
+Interface for entries in the connection panel. These consist of two parts: the
+entry itself (ie, Tor connection, client circuit, etc) and the lines it
+consists of in the listing.
+"""
+
+from util import enum
+
+# attributes we can list entries by
+ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT",
+                     "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY")
+
+SORT_COLORS = {SortAttr.CATEGORY: "red",      SortAttr.UPTIME: "yellow",
+               SortAttr.LISTING: "green",     SortAttr.IP_ADDRESS: "blue",
+               SortAttr.PORT: "blue",         SortAttr.HOSTNAME: "magenta",
+               SortAttr.FINGERPRINT: "cyan",  SortAttr.NICKNAME: "cyan",
+               SortAttr.COUNTRY: "blue"}
+
+# maximum number of ports a system can have
+PORT_COUNT = 65536
+
+class ConnectionPanelEntry:
+  """
+  Common parent for connection panel entries. This consists of a list of lines
+  in the panel listing. This caches results until the display indicates that
+  they should be flushed.
+  """
+  
+  def __init__(self):
+    self.lines = []
+    self.flushCache = True
+  
+  def getLines(self):
+    """
+    Provides the individual lines in the connection listing.
+    """
+    
+    if self.flushCache:
+      self.lines = self._getLines(self.lines)
+      self.flushCache = False
+    
+    return self.lines
+  
+  def _getLines(self, oldResults):
+    # implementation of getLines
+    
+    for line in oldResults:
+      line.resetDisplay()
+    
+    return oldResults
+  
+  def getSortValues(self, sortAttrs, listingType):
+    """
+    Provides the value used in comparisons to sort based on the given
+    attribute.
+    
+    Arguments:
+      sortAttrs   - list of SortAttr values for the field being sorted on
+      listingType - ListingType enumeration for the attribute we're listing
+                    entries by
+    """
+    
+    return [self.getSortValue(attr, listingType) for attr in sortAttrs]
+  
+  def getSortValue(self, attr, listingType):
+    """
+    Provides the value of a single attribute used for sorting purposes.
+    
+    Arguments:
+      attr        - list of SortAttr values for the field being sorted on
+      listingType - ListingType enumeration for the attribute we're listing
+                    entries by
+    """
+    
+    if attr == SortAttr.LISTING:
+      if listingType == ListingType.IP_ADDRESS:
+        # uses the IP address as the primary value, and port as secondary
+        sortValue = self.getSortValue(SortAttr.IP_ADDRESS, listingType) * PORT_COUNT
+        sortValue += self.getSortValue(SortAttr.PORT, listingType)
+        return sortValue
+      elif listingType == ListingType.HOSTNAME:
+        return self.getSortValue(SortAttr.HOSTNAME, listingType)
+      elif listingType == ListingType.FINGERPRINT:
+        return self.getSortValue(SortAttr.FINGERPRINT, listingType)
+      elif listingType == ListingType.NICKNAME:
+        return self.getSortValue(SortAttr.NICKNAME, listingType)
+    
+    return ""
+  
+  def resetDisplay(self):
+    """
+    Flushes cached display results.
+    """
+    
+    self.flushCache = True
+
+class ConnectionPanelLine:
+  """
+  Individual line in the connection panel listing.
+  """
+  
+  def __init__(self):
+    # cache for displayed information
+    self._listingCache = None
+    self._listingCacheArgs = (None, None)
+    
+    self._detailsCache = None
+    self._detailsCacheArgs = None
+    
+    self._descriptorCache = None
+    self._descriptorCacheArgs = None
+  
+  def getListingEntry(self, width, currentTime, listingType):
+    """
+    Provides a DrawEntry instance for contents to be displayed in the
+    connection panel listing.
+    
+    Arguments:
+      width       - available space to display in
+      currentTime - unix timestamp for what the results should consider to be
+                    the current time (this may be ignored due to caching)
+      listingType - ListingType enumeration for the highest priority content
+                    to be displayed
+    """
+    
+    if self._listingCacheArgs != (width, listingType):
+      self._listingCache = self._getListingEntry(width, currentTime, listingType)
+      self._listingCacheArgs = (width, listingType)
+    
+    return self._listingCache
+  
+  def _getListingEntry(self, width, currentTime, listingType):
+    # implementation of getListingEntry
+    return None
+  
+  def getDetails(self, width):
+    """
+    Provides a list of DrawEntry instances with detailed information for this
+    connection.
+    
+    Arguments:
+      width - available space to display in
+    """
+    
+    if self._detailsCacheArgs != width:
+      self._detailsCache = self._getDetails(width)
+      self._detailsCacheArgs = width
+    
+    return self._detailsCache
+  
+  def _getDetails(self, width):
+    # implementation of getDetails
+    return []
+  
+  def resetDisplay(self):
+    """
+    Flushes cached display results.
+    """
+    
+    self._listingCacheArgs = (None, None)
+    self._detailsCacheArgs = None
+
diff --git a/src/cli/controller.py b/src/cli/controller.py
new file mode 100644
index 0000000..2afbf6a
--- /dev/null
+++ b/src/cli/controller.py
@@ -0,0 +1,1584 @@
+#!/usr/bin/env python
+# controller.py -- arm interface (curses monitor for relay status)
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+"""
+Curses (terminal) interface for the arm relay status monitor.
+"""
+
+import os
+import re
+import math
+import time
+import curses
+import curses.textpad
+import socket
+from TorCtl import TorCtl
+
+import headerPanel
+import graphing.graphPanel
+import logPanel
+import configPanel
+import torrcPanel
+import descriptorPopup
+
+import cli.connections.connPanel
+import cli.connections.connEntry
+import cli.connections.entries
+from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools
+import graphing.bandwidthStats
+import graphing.connStats
+import graphing.resourceStats
+
+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)
+
+# panel order per page
+PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
+PAGES = [
+  ["graph", "log"],
+  ["conn"],
+  ["config"],
+  ["torrc"]]
+
+PAUSEABLE = ["header", "graph", "log", "conn"]
+
+CONFIG = {"log.torrc.readFailed": log.WARN,
+          "features.graph.type": 1,
+          "features.config.prepopulateEditValues": True,
+          "queries.refreshRate.rate": 5,
+          "log.torEventTypeUnrecognized": log.NOTICE,
+          "features.graph.bw.prepopulate": True,
+          "log.startTime": log.INFO,
+          "log.refreshRate": log.DEBUG,
+          "log.highCpuUsage": log.WARN,
+          "log.configEntryUndefined": log.NOTICE,
+          "log.torrc.validation.torStateDiffers": log.WARN,
+          "log.torrc.validation.unnecessaryTorrcEntries": log.NOTICE}
+
+class ControlPanel(panel.Panel):
+  """ Draws single line label for interface controls. """
+  
+  def __init__(self, stdscr, isBlindMode):
+    panel.Panel.__init__(self, stdscr, "control", 0, 1)
+    self.msgText = CTL_HELP           # message text to be displyed
+    self.msgAttr = curses.A_NORMAL    # formatting attributes
+    self.page = 1                     # page number currently being displayed
+    self.resolvingCounter = -1        # count of resolver when starting (-1 if we aren't working on a batch)
+    self.isBlindMode = isBlindMode
+  
+  def setMsg(self, msgText, msgAttr=curses.A_NORMAL):
+    """
+    Sets the message and display attributes. If msgType matches CTL_HELP or
+    CTL_PAUSED then uses the default message for those statuses.
+    """
+    
+    self.msgText = msgText
+    self.msgAttr = msgAttr
+  
+  def draw(self, width, height):
+    msgText = self.msgText
+    msgAttr = self.msgAttr
+    barTab = 2                # space between msgText and progress bar
+    barWidthMax = 40          # max width to progress bar
+    barWidth = -1             # space between "[ ]" in progress bar (not visible if -1)
+    barProgress = 0           # cells to fill
+    
+    if msgText == CTL_HELP:
+      msgAttr = curses.A_NORMAL
+      
+      if self.resolvingCounter != -1:
+        if hostnames.isPaused() or not hostnames.isResolving():
+          # done resolving dns batch
+          self.resolvingCounter = -1
+          curses.halfdelay(REFRESH_RATE * 10) # revert to normal refresh rate
+        else:
+          batchSize = hostnames.getRequestCount() - self.resolvingCounter
+          entryCount = batchSize - hostnames.getPendingCount()
+          if batchSize > 0: progress = 100 * entryCount / batchSize
+          else: progress = 0
+          
+          additive = "or l " if self.page == 2 else ""
+          batchSizeDigits = int(math.log10(batchSize)) + 1
+          entryCountLabel = ("%%%ii" % batchSizeDigits) % entryCount
+          #msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive)
+          msgText = "Resolving hostnames (press esc %sto cancel) - %s / %i, %2i%%" % (additive, entryCountLabel, batchSize, progress)
+          
+          barWidth = min(barWidthMax, width - len(msgText) - 3 - barTab)
+          barProgress = barWidth * entryCount / batchSize
+      
+      if self.resolvingCounter == -1:
+        currentPage = self.page
+        pageCount = len(PAGES)
+        
+        if self.isBlindMode:
+          if currentPage >= 2: currentPage -= 1
+          pageCount -= 1
+        
+        msgText = "page %i / %i - q: quit, p: pause, h: page help" % (currentPage, pageCount)
+    elif msgText == CTL_PAUSED:
+      msgText = "Paused"
+      msgAttr = curses.A_STANDOUT
+    
+    self.addstr(0, 0, msgText, msgAttr)
+    if barWidth > -1:
+      xLoc = len(msgText) + barTab
+      self.addstr(0, xLoc, "[", curses.A_BOLD)
+      self.addstr(0, xLoc + 1, " " * barProgress, curses.A_STANDOUT | uiTools.getColor("red"))
+      self.addstr(0, xLoc + barWidth + 1, "]", curses.A_BOLD)
+
+class Popup(panel.Panel):
+  """
+  Temporarily providing old panel methods until permanent workaround for popup
+  can be derrived (this passive drawing method is horrible - I'll need to
+  provide a version using the more active repaint design later in the
+  revision).
+  """
+  
+  def __init__(self, stdscr, height):
+    panel.Panel.__init__(self, stdscr, "popup", 0, height)
+  
+  # The following methods are to emulate old panel functionality (this was the
+  # only implementations to use these methods and will require a complete
+  # rewrite when refactoring gets here)
+  def clear(self):
+    if self.win:
+      self.isDisplaced = self.top > self.win.getparyx()[0]
+      if not self.isDisplaced: self.win.erase()
+  
+  def refresh(self):
+    if self.win and not self.isDisplaced: self.win.refresh()
+  
+  def recreate(self, stdscr, newWidth=-1, newTop=None):
+    self.setParent(stdscr)
+    self.setWidth(newWidth)
+    if newTop != None: self.setTop(newTop)
+    
+    newHeight, newWidth = self.getPreferredSize()
+    if newHeight > 0:
+      self.win = self.parent.subwin(newHeight, newWidth, self.top, 0)
+    elif self.win == None:
+      # don't want to leave the window as none (in very edge cases could cause
+      # problems) - rather, create a displaced instance
+      self.win = self.parent.subwin(1, newWidth, 0, 0)
+    
+    self.maxY, self.maxX = self.win.getmaxyx()
+
+def addstr_wrap(panel, y, x, text, formatting, startX = 0, endX = -1, maxY = -1):
+  """
+  Writes text with word wrapping, returning the ending y/x coordinate.
+  y: starting write line
+  x: column offset from startX
+  text / formatting: content to be written
+  startX / endX: column bounds in which text may be written
+  """
+  
+  # moved out of panel (trying not to polute new code!)
+  # TODO: unpleaseantly complex usage - replace with something else when
+  # rewriting confPanel and descriptorPopup (the only places this is used)
+  if not text: return (y, x)          # nothing to write
+  if endX == -1: endX = panel.maxX     # defaults to writing to end of panel
+  if maxY == -1: maxY = panel.maxY + 1 # defaults to writing to bottom of panel
+  lineWidth = endX - startX           # room for text
+  while True:
+    if len(text) > lineWidth - x - 1:
+      chunkSize = text.rfind(" ", 0, lineWidth - x)
+      writeText = text[:chunkSize]
+      text = text[chunkSize:].strip()
+      
+      panel.addstr(y, x + startX, writeText, formatting)
+      y, x = y + 1, 0
+      if y >= maxY: return (y, x)
+    else:
+      panel.addstr(y, x + startX, text, formatting)
+      return (y, x + len(text))
+
+class sighupListener(TorCtl.PostEventListener):
+  """
+  Listens for reload signal (hup), which is produced by:
+  pkill -sighup tor
+  causing the torrc and internal state to be reset.
+  """
+  
+  def __init__(self):
+    TorCtl.PostEventListener.__init__(self)
+    self.isReset = False
+  
+  def msg_event(self, event):
+    self.isReset |= event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)")
+
+def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
+  """
+  Resets the isPaused state of panels. If overwrite is True then this pauses
+  reguardless of the monitor is paused or not.
+  """
+  
+  for key in PAUSEABLE: 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 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
+  # 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
+  again.
+  """
+  
+  if connections.isResolverAlive("tor"):
+    resolver = connections.getResolver("tor")
+    resolver.setPaused(eventType == torTools.State.CLOSED)
+
+def selectiveRefresh(panels, page):
+  """
+  This forces a redraw of content on the currently active page (should be done
+  after changing pages, popups, or anything else that overwrites panels).
+  """
+  
+  for panelKey in PAGES[page]:
+    panels[panelKey].redraw(True)
+
+def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
+  """
+  Starts arm interface reflecting information on provided control port.
+  
+  stdscr - curses window
+  conn - active Tor control port connection
+  loggedEvents - types of events to be logged (plus an optional "UNKNOWN" for
+    otherwise unrecognized events)
+  """
+  
+  # loads config for various interface components
+  config = conf.getConfig("arm")
+  config.update(CONFIG)
+  graphing.graphPanel.loadConfig(config)
+  cli.connections.connEntry.loadConfig(config)
+  
+  # adds events needed for arm functionality to the torTools REQ_EVENTS mapping
+  # (they're then included with any setControllerEvents call, and log a more
+  # helpful error if unavailable)
+  torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function"
+  
+  if not isBlindMode:
+    torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections"
+  
+  # pauses/unpauses connection resolution according to if tor's connected or not
+  torTools.getConn().addStatusListener(connResetListener)
+  
+  # TODO: incrementally drop this requirement until everything's using the singleton
+  conn = torTools.getConn().getTorCtl()
+  
+  curses.halfdelay(REFRESH_RATE * 10)   # uses getch call as timer for REFRESH_RATE seconds
+  try: curses.use_default_colors()      # allows things like semi-transparent backgrounds (call can fail with ERR)
+  except curses.error: pass
+  
+  # attempts to make the cursor invisible (not supported in all terminals)
+  try: curses.curs_set(0)
+  except curses.error: pass
+  
+  # attempts to determine tor's current pid (left as None if unresolveable, logging an error later)
+  torPid = torTools.getConn().getMyPid()
+  
+  #try:
+  #  confLocation = conn.get_info("config-file")["config-file"]
+  #  if confLocation[0] != "/":
+  #    # relative path - attempt to add process pwd
+  #    try:
+  #      results = sysTools.call("pwdx %s" % torPid)
+  #      if len(results) == 1 and len(results[0].split()) == 2: confLocation = "%s/%s" % (results[0].split()[1], confLocation)
+  #    except IOError: pass # pwdx call failed
+  #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+  #  confLocation = ""
+  
+  # loads the torrc and provides warnings in case of validation errors
+  loadedTorrc = torConfig.getTorrc()
+  loadedTorrc.getLock().acquire()
+  
+  try:
+    loadedTorrc.load()
+  except IOError, exc:
+    msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
+    log.log(CONFIG["log.torrc.readFailed"], msg)
+  
+  if loadedTorrc.isLoaded():
+    corrections = loadedTorrc.getCorrections()
+    duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], []
+    
+    for lineNum, issue, msg in corrections:
+      if issue == torConfig.ValidationError.DUPLICATE:
+        duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1))
+      elif issue == torConfig.ValidationError.IS_DEFAULT:
+        defaultOptions.append("%s (line %i)" % (msg, lineNum + 1))
+      elif issue == torConfig.ValidationError.MISMATCH: mismatchLines.append(lineNum + 1)
+      elif issue == torConfig.ValidationError.MISSING: missingOptions.append(msg)
+    
+    if duplicateOptions or defaultOptions:
+      msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page."
+      
+      if duplicateOptions:
+        if len(duplicateOptions) > 1:
+          msg += "\n- entries ignored due to having duplicates: "
+        else:
+          msg += "\n- entry ignored due to having a duplicate: "
+        
+        duplicateOptions.sort()
+        msg += ", ".join(duplicateOptions)
+      
+      if defaultOptions:
+        if len(defaultOptions) > 1:
+          msg += "\n- entries match their default values: "
+        else:
+          msg += "\n- entry matches its default value: "
+        
+        defaultOptions.sort()
+        msg += ", ".join(defaultOptions)
+      
+      log.log(CONFIG["log.torrc.validation.unnecessaryTorrcEntries"], msg)
+    
+    if mismatchLines or missingOptions:
+      msg = "The torrc differ from what tor's using. You can issue a sighup to reload the torrc values by pressing x."
+      
+      if mismatchLines:
+        if len(mismatchLines) > 1:
+          msg += "\n- torrc values differ on lines: "
+        else:
+          msg += "\n- torrc value differs on line: "
+        
+        mismatchLines.sort()
+        msg += ", ".join([str(val + 1) for val in mismatchLines])
+        
+      if missingOptions:
+        if len(missingOptions) > 1:
+          msg += "\n- configuration values are missing from the torrc: "
+        else:
+          msg += "\n- configuration value is missing from the torrc: "
+        
+        missingOptions.sort()
+        msg += ", ".join(missingOptions)
+      
+      log.log(CONFIG["log.torrc.validation.torStateDiffers"], msg)
+  
+  loadedTorrc.getLock().release()
+  
+  # minor refinements for connection resolver
+  if not isBlindMode:
+    if torPid:
+      # use the tor pid to help narrow connection results
+      torCmdName = sysTools.getProcessName(torPid, "tor")
+      resolver = connections.getResolver(torCmdName, torPid, "tor")
+    else:
+      resolver = connections.getResolver("tor")
+  
+  # hack to display a better (arm specific) notice if all resolvers fail
+  connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)"
+  
+  panels = {
+    "header": headerPanel.HeaderPanel(stdscr, startTime, config),
+    "popup": Popup(stdscr, 9),
+    "graph": graphing.graphPanel.GraphPanel(stdscr),
+    "log": logPanel.LogPanel(stdscr, loggedEvents, config)}
+  
+  # TODO: later it would be good to set the right 'top' values during initialization, 
+  # but for now this is just necessary for the log panel (and a hack in the log...)
+  
+  # TODO: bug from not setting top is that the log panel might attempt to draw
+  # before being positioned - the following is a quick hack til rewritten
+  panels["log"].setPaused(True)
+  
+  panels["conn"] = cli.connections.connPanel.ConnectionPanel(stdscr, config)
+  
+  panels["control"] = ControlPanel(stdscr, isBlindMode)
+  panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.State.TOR, config)
+  panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.Config.TORRC, config)
+  
+  # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
+  if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing")
+  
+  # statistical monitors for graph
+  panels["graph"].addStats("bandwidth", graphing.bandwidthStats.BandwidthStats(config))
+  panels["graph"].addStats("system resources", graphing.resourceStats.ResourceStats())
+  if not isBlindMode: panels["graph"].addStats("connections", graphing.connStats.ConnStats())
+  
+  # sets graph based on config parameter
+  graphType = CONFIG["features.graph.type"]
+  if graphType == 0: panels["graph"].setStats(None)
+  elif graphType == 1: panels["graph"].setStats("bandwidth")
+  elif graphType == 2 and not isBlindMode: panels["graph"].setStats("connections")
+  elif graphType == 3: panels["graph"].setStats("system resources")
+  
+  # listeners that update bandwidth and log panels with Tor status
+  sighupTracker = sighupListener()
+  #conn.add_event_listener(panels["log"])
+  conn.add_event_listener(panels["graph"].stats["bandwidth"])
+  conn.add_event_listener(panels["graph"].stats["system resources"])
+  if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"])
+  conn.add_event_listener(sighupTracker)
+  
+  # prepopulates bandwidth values from state file
+  if CONFIG["features.graph.bw.prepopulate"]:
+    isSuccessful = panels["graph"].stats["bandwidth"].prepopulateFromState()
+    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
+  
+  # directs logged TorCtl events to log panel
+  #TorUtil.loglevel = "DEBUG"
+  #TorUtil.logfile = panels["log"]
+  #torTools.getConn().addTorCtlListener(panels["log"].tor_ctl_event)
+  
+  # provides a notice about any event types tor supports but arm doesn't
+  missingEventTypes = logPanel.getMissingEventTypes()
+  if missingEventTypes:
+    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)))
+  
+  # tells revised panels to run as daemons
+  panels["header"].start()
+  panels["log"].start()
+  panels["conn"].start()
+  
+  # warns if tor isn't updating descriptors
+  #try:
+  #  if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
+  #    warning = """Descriptors won't be updated (causing some connection information to be stale) unless:
+  #a. 'FetchUselessDescriptors 1' is set in your torrc
+  #b. the directory service is provided ('DirPort' defined)
+  #c. or tor is used as a client"""
+  #    log.log(log.WARN, warning)
+  #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
+  
+  isUnresponsive = False    # true if it's been over ten seconds since the last BW event (probably due to Tor closing)
+  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...)
+  
+  # provides notice about any unused config keys
+  for key in config.getUnusedKeys():
+    log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
+  
+  lastPerformanceLog = 0 # ensures we don't do performance logging too frequently
+  redrawStartTime = time.time()
+  
+  # TODO: popups need to force the panels it covers to redraw (or better, have
+  # a global refresh function for after changing pages, popups, etc)
+  
+  initTime = time.time() - startTime
+  log.log(CONFIG["log.startTime"], "arm started (initialization took %0.3f seconds)" % initTime)
+  
+  # attributes to give a WARN level event if arm's resource usage is too high
+  isResourceWarningGiven = False
+  lastResourceCheck = startTime
+  
+  lastSize = None
+  
+  # sets initial visiblity for the pages
+  for i in range(len(PAGES)):
+    isVisible = i == page
+    for entry in PAGES[i]: panels[entry].setVisible(isVisible)
+  
+  # TODO: come up with a nice, clean method for other threads to immediately
+  # terminate the draw loop and provide a stacktrace
+  while True:
+    # tried only refreshing when the screen was resized but it caused a
+    # noticeable lag when resizing and didn't have an appreciable effect
+    # on system usage
+    
+    panel.CURSES_LOCK.acquire()
+    try:
+      redrawStartTime = time.time()
+      
+      # if sighup received then reload related information
+      if sighupTracker.isReset:
+        #panels["header"]._updateParams(True)
+        
+        # other panels that use torrc data
+        #if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
+        #panels["graph"].stats["bandwidth"].resetOptions()
+        
+        # if bandwidth graph is being shown then height might have changed
+        if panels["graph"].currentDisplay == "bandwidth":
+          panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getContentHeight())
+        
+        # TODO: should redraw the torrcPanel
+        #panels["torrc"].loadConfig()
+        
+        # reload the torrc if it's previously been loaded
+        if loadedTorrc.isLoaded():
+          try:
+            loadedTorrc.load()
+            if page == 3: panels["torrc"].redraw(True)
+          except IOError, exc:
+            msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
+            log.log(CONFIG["log.torrc.readFailed"], msg)
+        
+        sighupTracker.isReset = False
+      
+      # gives panels a chance to take advantage of the maximum bounds
+      # originally this checked in the bounds changed but 'recreate' is a no-op
+      # if panel properties are unchanged and checking every redraw is more
+      # resilient in case of funky changes (such as resizing during popups)
+      
+      # hack to make sure header picks layout before using the dimensions below
+      #panels["header"].getPreferredSize()
+      
+      startY = 0
+      for panelKey in PAGE_S[:2]:
+        #panels[panelKey].recreate(stdscr, -1, startY)
+        panels[panelKey].setParent(stdscr)
+        panels[panelKey].setWidth(-1)
+        panels[panelKey].setTop(startY)
+        startY += panels[panelKey].getHeight()
+      
+      panels["popup"].recreate(stdscr, 80, startY)
+      
+      for panelSet in PAGES:
+        tmpStartY = startY
+        
+        for panelKey in panelSet:
+          #panels[panelKey].recreate(stdscr, -1, tmpStartY)
+          panels[panelKey].setParent(stdscr)
+          panels[panelKey].setWidth(-1)
+          panels[panelKey].setTop(tmpStartY)
+          tmpStartY += panels[panelKey].getHeight()
+      
+      # provides a notice if there's been ten seconds since the last BW event
+      lastHeartbeat = torTools.getConn().getHeartbeat()
+      if torTools.getConn().isAlive() and "BW" in torTools.getConn().getControllerEvents() and lastHeartbeat != 0:
+        if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
+          isUnresponsive = True
+          log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat))
+        elif isUnresponsive and (time.time() - lastHeartbeat) < 10:
+          # really shouldn't happen (meant Tor froze for a bit)
+          isUnresponsive = False
+          log.log(log.NOTICE, "Relay resumed")
+      
+      # TODO: part two of hack to prevent premature drawing by log panel
+      if page == 0 and not isPaused: panels["log"].setPaused(False)
+      
+      # I haven't the foggiest why, but doesn't work if redrawn out of order...
+      for panelKey in (PAGE_S + PAGES[page]):
+        # redrawing popup can result in display flicker when it should be hidden
+        if panelKey != "popup":
+          newSize = stdscr.getmaxyx()
+          isResize = lastSize != newSize
+          lastSize = newSize
+          
+          if panelKey in ("header", "graph", "log", "config", "torrc", "conn2"):
+            # revised panel (manages its own content refreshing)
+            panels[panelKey].redraw(isResize)
+          else:
+            panels[panelKey].redraw(True)
+      
+      stdscr.refresh()
+      
+      currentTime = time.time()
+      if currentTime - lastPerformanceLog >= CONFIG["queries.refreshRate.rate"]:
+        cpuTotal = sum(os.times()[:3])
+        pythonCpuAvg = cpuTotal / (currentTime - startTime)
+        sysCallCpuAvg = sysTools.getSysCpuUsage()
+        totalCpuAvg = pythonCpuAvg + sysCallCpuAvg
+        
+        if sysCallCpuAvg > 0.00001:
+          log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%% (python), %0.3f%% (system calls), %0.3f%% (total)" % (currentTime - redrawStartTime, 100 * pythonCpuAvg, 100 * sysCallCpuAvg, 100 * totalCpuAvg))
+        else:
+          # with the proc enhancements the sysCallCpuAvg is usually zero
+          log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%%" % (currentTime - redrawStartTime, 100 * totalCpuAvg))
+        
+        lastPerformanceLog = currentTime
+        
+        # once per minute check if the sustained cpu usage is above 5%, if so
+        # then give a warning (and if able, some advice for lowering it)
+        # TODO: disabling this for now (scrolling causes cpu spikes for quick
+        # redraws, ie this is usually triggered by user input)
+        if False and not isResourceWarningGiven and currentTime > (lastResourceCheck + 60):
+          if totalCpuAvg >= 0.05:
+            msg = "Arm's cpu usage is high (averaging %0.3f%%)." % (100 * totalCpuAvg)
+            
+            if not isBlindMode:
+              msg += " You could lower it by dropping the connection data (running as \"arm -b\")."
+            
+            log.log(CONFIG["log.highCpuUsage"], msg)
+            isResourceWarningGiven = True
+          
+          lastResourceCheck = currentTime
+    finally:
+      panel.CURSES_LOCK.release()
+    
+    # wait for user keyboard input until timeout (unless an override was set)
+    if overrideKey:
+      key = overrideKey
+      overrideKey = None
+    else:
+      key = stdscr.getch()
+    
+    if key == ord('q') or key == ord('Q'):
+      quitConfirmed = not CONFIRM_QUIT
+      
+      # provides prompt to confirm that arm should exit
+      if CONFIRM_QUIT:
+        panel.CURSES_LOCK.acquire()
+        try:
+          setPauseState(panels, isPaused, page, True)
+          
+          # provides prompt
+          panels["control"].setMsg("Are you sure (q again to confirm)?", curses.A_BOLD)
+          panels["control"].redraw(True)
+          
+          curses.cbreak()
+          confirmationKey = stdscr.getch()
+          quitConfirmed = confirmationKey in (ord('q'), ord('Q'))
+          curses.halfdelay(REFRESH_RATE * 10)
+          
+          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+          setPauseState(panels, isPaused, page)
+        finally:
+          panel.CURSES_LOCK.release()
+      
+      if quitConfirmed:
+        # quits arm
+        # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable"
+        # this appears to be a python bug: http://bugs.python.org/issue3014
+        # (haven't seen this is quite some time... mysteriously resolved?)
+        
+        torTools.NO_SPAWN = True # prevents further worker threads from being spawned
+        
+        # stops panel daemons
+        panels["header"].stop()
+        panels["conn"].stop()
+        panels["log"].stop()
+        
+        panels["header"].join()
+        panels["conn"].join()
+        panels["log"].join()
+        
+        # joins on utility daemon threads - this might take a moment since
+        # the internal threadpools being joined might be sleeping
+        conn = torTools.getConn()
+        myPid = conn.getMyPid()
+        
+        resourceTracker = sysTools.getResourceTracker(myPid) if (myPid and sysTools.isTrackerAlive(myPid)) else None
+        resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
+        if resourceTracker: resourceTracker.stop()
+        if resolver: resolver.stop()  # sets halt flag (returning immediately)
+        hostnames.stop()              # halts and joins on hostname worker thread pool
+        if resourceTracker: resourceTracker.join()
+        if resolver: resolver.join()  # joins on halted resolver
+        
+        conn.close() # joins on TorCtl event thread
+        break
+    elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
+      # switch page
+      if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+      else: page = (page + 1) % len(PAGES)
+      
+      # skip connections listing if it's disabled
+      if page == 1 and isBlindMode:
+        if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
+        else: page = (page + 1) % len(PAGES)
+      
+      # pauses panels that aren't visible to prevent events from accumilating
+      # (otherwise they'll wait on the curses lock which might get demanding)
+      setPauseState(panels, isPaused, page)
+      
+      # prevents panels on other pages from redrawing
+      for i in range(len(PAGES)):
+        isVisible = i == page
+        for entry in PAGES[i]: panels[entry].setVisible(isVisible)
+      
+      panels["control"].page = page + 1
+      
+      # TODO: this redraw doesn't seem necessary (redraws anyway after this
+      # loop) - look into this when refactoring
+      panels["control"].redraw(True)
+      
+      selectiveRefresh(panels, page)
+    elif key == ord('p') or key == ord('P'):
+      # toggles update freezing
+      panel.CURSES_LOCK.acquire()
+      try:
+        isPaused = not isPaused
+        setPauseState(panels, isPaused, page)
+        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+      finally:
+        panel.CURSES_LOCK.release()
+      
+      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()
+    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")
+          
+          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)")
+        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")
+          popup.addfstr(3, 2, "<b>enter</b>: connection details")
+        
+        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()
+    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)
+      
+      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)
+    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)
+    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,
+      # but for now just gonna use standard characters.
+      
+      if key in (ord('n'), ord('N')):
+        panels["graph"].setGraphHeight(panels["graph"].graphHeight - 1)
+      else:
+        # don't grow the graph if it's already consuming the whole display
+        # (plus an extra line for the graph/log gap)
+        maxHeight = panels["graph"].parent.getmaxyx()[0] - panels["graph"].top
+        currentHeight = panels["graph"].getHeight()
+        
+        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()
+    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 != "auto" 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()
+      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()
+    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 == 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)
+      # provides menu to pick config being displayed
+      #options = [confPanel.CONFIG_LABELS[confType] for confType in range(4)]
+      options = []
+      initialSelection = panels["torrc"].configType
+      
+      # hides top label of the graph panel and pauses panels
+      panels["torrc"].showLabel = False
+      panels["torrc"].redraw(True)
+      setPauseState(panels, isPaused, page, True)
+      
+      selection = showMenu(stdscr, panels["popup"], "Configuration:", options, initialSelection)
+      
+      # reverts changes made for popup
+      panels["torrc"].showLabel = True
+      setPauseState(panels, isPaused, page)
+      
+      # applies new setting
+      if selection != -1: panels["torrc"].setConfigType(selection)
+      
+      selectiveRefresh(panels, page)
+    elif page == 2 and (key == ord('w') or key == ord('W')):
+      # display a popup for saving the current configuration
+      panel.CURSES_LOCK.acquire()
+      try:
+        configLines = torConfig.getCustomOptions(True)
+        
+        # lists event types
+        popup = panels["popup"]
+        popup.height = len(configLines) + 3
+        popup.recreate(stdscr)
+        displayHeight, displayWidth = panels["popup"].getPreferredSize()
+        
+        # displayed options (truncating the labels if there's limited room)
+        if displayWidth >= 30: selectionOptions = ("Save", "Save As...", "Cancel")
+        else: selectionOptions = ("Save", "Save As", "X")
+        
+        # checks if we can show options beside the last line of visible content
+        lastIndex = min(displayHeight - 3, len(configLines) - 1)
+        isOptionLineSeparate = displayWidth < (30 + len(configLines[lastIndex]))
+        
+        # if we're showing all the content and have room to display selection
+        # options besides the text then shrink the popup by a row
+        if not isOptionLineSeparate and displayHeight == len(configLines) + 3:
+          popup.height -= 1
+          popup.recreate(stdscr)
+        
+        key, selection = 0, 2
+        while not uiTools.isSelectionKey(key):
+          # if the popup has been resized then recreate it (needed for the
+          # proper border height)
+          newHeight, newWidth = panels["popup"].getPreferredSize()
+          if (displayHeight, displayWidth) != (newHeight, newWidth):
+            displayHeight, displayWidth = newHeight, newWidth
+            popup.recreate(stdscr)
+          
+          # if there isn't room to display the popup then cancel it
+          if displayHeight <= 2:
+            selection = 2
+            break
+          
+          popup.clear()
+          popup.win.box()
+          popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
+          
+          visibleConfigLines = displayHeight - 3 if isOptionLineSeparate else displayHeight - 2
+          for i in range(visibleConfigLines):
+            line = uiTools.cropStr(configLines[i], displayWidth - 2)
+            
+            if " " in line:
+              option, arg = line.split(" ", 1)
+              popup.addstr(i + 1, 1, option, curses.A_BOLD | uiTools.getColor("green"))
+              popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD | uiTools.getColor("cyan"))
+            else:
+              popup.addstr(i + 1, 1, line, curses.A_BOLD | uiTools.getColor("green"))
+          
+          # draws 'T' between the lower left and the covered panel's scroll bar
+          if displayWidth > 1: popup.win.addch(displayHeight - 1, 1, curses.ACS_TTEE)
+          
+          # draws selection options (drawn right to left)
+          drawX = displayWidth - 1
+          for i in range(len(selectionOptions) - 1, -1, -1):
+            optionLabel = selectionOptions[i]
+            drawX -= (len(optionLabel) + 2)
+            
+            # if we've run out of room then drop the option (this will only
+            # occure on tiny displays)
+            if drawX < 1: break
+            
+            selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+            popup.addstr(displayHeight - 2, drawX, "[")
+            popup.addstr(displayHeight - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD)
+            popup.addstr(displayHeight - 2, drawX + len(optionLabel) + 1, "]")
+            
+            drawX -= 1 # space gap between the options
+          
+          popup.refresh()
+          
+          key = stdscr.getch()
+          if key == curses.KEY_LEFT: selection = max(0, selection - 1)
+          elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1)
+        
+        if selection in (0, 1):
+          loadedTorrc = torConfig.getTorrc()
+          try: configLocation = loadedTorrc.getConfigLocation()
+          except IOError: configLocation = ""
+          
+          if selection == 1:
+            # prompts user for a configuration location
+            promptMsg = "Save to (esc to cancel): "
+            panels["control"].setMsg(promptMsg)
+            panels["control"].redraw(True)
+            configLocation = panels["control"].getstr(0, len(promptMsg), configLocation)
+            if configLocation: configLocation = os.path.abspath(configLocation)
+          
+          if configLocation:
+            try:
+              # make dir if the path doesn't already exist
+              baseDir = os.path.dirname(configLocation)
+              if not os.path.exists(baseDir): os.makedirs(baseDir)
+              
+              # saves the configuration to the file
+              configFile = open(configLocation, "w")
+              configFile.write("\n".join(configLines))
+              configFile.close()
+              
+              # reloads the cached torrc if overwriting it
+              if configLocation == loadedTorrc.getConfigLocation():
+                try:
+                  loadedTorrc.load()
+                  panels["torrc"]._lastContentHeightArgs = None
+                except IOError: pass
+              
+              msg = "Saved configuration to %s" % configLocation
+            except (IOError, OSError), exc:
+              msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc)
+            
+            panels["control"].setMsg(msg, curses.A_STANDOUT)
+            panels["control"].redraw(True)
+            time.sleep(2)
+          
+          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+        
+        # reverts popup dimensions
+        popup.height = 9
+        popup.recreate(stdscr, 80)
+      finally:
+        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()
+      try:
+        setPauseState(panels, isPaused, page, True)
+        
+        # provides prompt
+        selection = panels["config"].getSelection()
+        configOption = selection.get(configPanel.Field.OPTION)
+        titleMsg = "%s Value (esc to cancel): " % configOption
+        panels["control"].setMsg(titleMsg)
+        panels["control"].redraw(True)
+        
+        displayWidth = panels["control"].getPreferredSize()[1]
+        initialValue = selection.get(configPanel.Field.VALUE)
+        
+        # initial input for the text field
+        initialText = ""
+        if CONFIG["features.config.prepopulateEditValues"] and initialValue != "<none>":
+          initialText = initialValue
+        
+        newConfigValue = panels["control"].getstr(0, len(titleMsg), initialText)
+        
+        # it would be nice to quit on esc, but looks like this might not be possible...
+        if newConfigValue != None and newConfigValue != initialValue:
+          conn = torTools.getConn()
+          
+          # if the value's a boolean then allow for 'true' and 'false' inputs
+          if selection.get(configPanel.Field.TYPE) == "Boolean":
+            if newConfigValue.lower() == "true": newConfigValue = "1"
+            elif newConfigValue.lower() == "false": newConfigValue = "0"
+          
+          try:
+            if selection.get(configPanel.Field.TYPE) == "LineList":
+              newConfigValue = newConfigValue.split(",")
+            
+            conn.setOption(configOption, newConfigValue)
+            
+            # resets the isDefault flag
+            customOptions = torConfig.getCustomOptions()
+            selection.fields[configPanel.Field.IS_DEFAULT] = not configOption in customOptions
+            
+            panels["config"].redraw(True)
+          except Exception, exc:
+            errorMsg = "%s (press any key)" % exc
+            panels["control"].setMsg(uiTools.cropStr(errorMsg, displayWidth), curses.A_STANDOUT)
+            panels["control"].redraw(True)
+            
+            curses.cbreak() # wait indefinitely for key presses (no timeout)
+            stdscr.getch()
+            curses.halfdelay(REFRESH_RATE * 10)
+        
+        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
+        setPauseState(panels, isPaused, page)
+      finally:
+        panel.CURSES_LOCK.release()
+    elif page == 3 and key == ord('r') or key == ord('R'):
+      # reloads torrc, providing a notice if successful or not
+      loadedTorrc = torConfig.getTorrc()
+      loadedTorrc.getLock().acquire()
+      
+      try:
+        loadedTorrc.load()
+        isSuccessful = True
+      except IOError:
+        isSuccessful = False
+      
+      loadedTorrc.getLock().release()
+      
+      #isSuccessful = panels["torrc"].loadConfig(logErrors = False)
+      #confTypeLabel = confPanel.CONFIG_LABELS[panels["torrc"].configType]
+      resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
+      if isSuccessful:
+        panels["torrc"]._lastContentHeightArgs = None
+        panels["torrc"].redraw(True)
+      
+      panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
+      panels["control"].redraw(True)
+      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)
+
+def startTorMonitor(startTime, loggedEvents, isBlindMode):
+  try:
+    curses.wrapper(drawTorMonitor, startTime, loggedEvents, isBlindMode)
+  except KeyboardInterrupt:
+    pass # skip printing stack trace in case of keyboard interrupt
+
diff --git a/src/cli/descriptorPopup.py b/src/cli/descriptorPopup.py
new file mode 100644
index 0000000..cdc959d
--- /dev/null
+++ b/src/cli/descriptorPopup.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+# descriptorPopup.py -- popup panel used to show raw consensus data
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import math
+import socket
+import curses
+from TorCtl import TorCtl
+
+import controller
+import connections.connEntry
+from util import panel, torTools, uiTools
+
+# field keywords used to identify areas for coloring
+LINE_NUM_COLOR = "yellow"
+HEADER_COLOR = "cyan"
+HEADER_PREFIX = ["ns/id/", "desc/id/"]
+
+SIG_COLOR = "red"
+SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
+SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
+
+UNRESOLVED_MSG = "No consensus data available"
+ERROR_MSG = "Unable to retrieve data"
+
+class PopupProperties:
+  """
+  State attributes of popup window for consensus descriptions.
+  """
+  
+  def __init__(self):
+    self.fingerprint = ""
+    self.entryColor = "white"
+    self.text = []
+    self.scroll = 0
+    self.showLineNum = True
+  
+  def reset(self, fingerprint, entryColor):
+    self.fingerprint = fingerprint
+    self.entryColor = entryColor
+    self.text = []
+    self.scroll = 0
+    
+    if fingerprint == "UNKNOWN":
+      self.fingerprint = None
+      self.showLineNum = False
+      self.text.append(UNRESOLVED_MSG)
+    else:
+      conn = torTools.getConn()
+      
+      try:
+        self.showLineNum = True
+        self.text.append("ns/id/%s" % fingerprint)
+        self.text += conn.getConsensusEntry(fingerprint).split("\n")
+      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+        self.text = self.text + [ERROR_MSG, ""]
+      
+      try:
+        descCommand = "desc/id/%s" % fingerprint
+        self.text.append("desc/id/%s" % fingerprint)
+        self.text += conn.getDescriptorEntry(fingerprint).split("\n")
+      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+        self.text = self.text + [ERROR_MSG]
+  
+  def handleKey(self, key, height):
+    if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
+    elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, len(self.text) - height))
+    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):
+  """
+  Presents consensus descriptor in popup window with the following controls:
+  Up, Down, Page Up, Page Down - scroll descriptor
+  Right, Left - next / previous connection
+  Enter, Space, d, D - close popup
+  """
+  
+  properties = PopupProperties()
+  isVisible = True
+  
+  if not panel.CURSES_LOCK.acquire(False): return
+  try:
+    while isVisible:
+      selection = connectionPanel._scroller.getCursorSelection(connectionPanel._entryLines)
+      if not selection: break
+      fingerprint = selection.foreign.getFingerprint()
+      entryColor = connections.connEntry.CATEGORY_COLOR[selection.getType()]
+      properties.reset(fingerprint, entryColor)
+      
+      # constrains popup size to match text
+      width, height = 0, 0
+      for line in properties.text:
+        # width includes content, line number field, and border
+        lineWidth = len(line) + 5
+        if properties.showLineNum: lineWidth += int(math.log10(len(properties.text))) + 1
+        width = max(width, lineWidth)
+        
+        # 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()
+        
+        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()
+
+def draw(popup, properties):
+  popup.clear()
+  popup.win.box()
+  xOffset = 2
+  
+  if properties.text:
+    if properties.fingerprint: popup.addstr(0, 0, "Consensus Descriptor (%s):" % properties.fingerprint, curses.A_STANDOUT)
+    else: popup.addstr(0, 0, "Consensus Descriptor:", curses.A_STANDOUT)
+    
+    isEncryption = False          # true if line is part of an encryption block
+    
+    # checks if first line is in an encryption block
+    for i in range(0, properties.scroll):
+      lineText = properties.text[i].strip()
+      if lineText in SIG_START_KEYS: isEncryption = True
+      elif lineText in SIG_END_KEYS: isEncryption = False
+    
+    pageHeight = popup.maxY - 2
+    numFieldWidth = int(math.log10(len(properties.text))) + 1
+    lineNum = 1
+    for i in range(properties.scroll, min(len(properties.text), properties.scroll + pageHeight)):
+      lineText = properties.text[i].strip()
+      
+      numOffset = 0     # offset for line numbering
+      if properties.showLineNum:
+        popup.addstr(lineNum, xOffset, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR))
+        numOffset = numFieldWidth + 1
+      
+      if lineText:
+        keyword = lineText.split()[0]   # first word of line
+        remainder = lineText[len(keyword):]
+        keywordFormat = curses.A_BOLD | uiTools.getColor(properties.entryColor)
+        remainderFormat = uiTools.getColor(properties.entryColor)
+        
+        if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]):
+          keyword, remainder = lineText, ""
+          keywordFormat = curses.A_BOLD | uiTools.getColor(HEADER_COLOR)
+        if lineText == UNRESOLVED_MSG or lineText == ERROR_MSG:
+          keyword, remainder = lineText, ""
+        if lineText in SIG_START_KEYS:
+          keyword, remainder = lineText, ""
+          isEncryption = True
+          keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
+        elif lineText in SIG_END_KEYS:
+          keyword, remainder = lineText, ""
+          isEncryption = False
+          keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
+        elif isEncryption:
+          keyword, remainder = lineText, ""
+          keywordFormat = uiTools.getColor(SIG_COLOR)
+        
+        lineNum, xLoc = controller.addstr_wrap(popup, lineNum, 0, keyword, keywordFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
+        lineNum, xLoc = controller.addstr_wrap(popup, lineNum, xLoc, remainder, remainderFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
+      
+      lineNum += 1
+      if lineNum > pageHeight: break
+      
+  popup.refresh()
+
diff --git a/src/cli/graphing/__init__.py b/src/cli/graphing/__init__.py
new file mode 100644
index 0000000..2dddaa3
--- /dev/null
+++ b/src/cli/graphing/__init__.py
@@ -0,0 +1,6 @@
+"""
+Graphing panel resources.
+"""
+
+__all__ = ["graphPanel", "bandwidthStats", "connStats", "resourceStats"]
+
diff --git a/src/cli/graphing/bandwidthStats.py b/src/cli/graphing/bandwidthStats.py
new file mode 100644
index 0000000..2864dd8
--- /dev/null
+++ b/src/cli/graphing/bandwidthStats.py
@@ -0,0 +1,398 @@
+"""
+Tracks bandwidth usage of the tor process, expanding to include accounting
+stats if they're set.
+"""
+
+import time
+
+from cli.graphing import graphPanel
+from util import log, sysTools, torTools, uiTools
+
+DL_COLOR, UL_COLOR = "green", "cyan"
+
+# width at which panel abandons placing optional stats (avg and total) with
+# header in favor of replacing the x-axis label
+COLLAPSE_WIDTH = 135
+
+# valid keys for the accountingInfo mapping
+ACCOUNTING_ARGS = ("status", "resetTime", "read", "written", "readLimit", "writtenLimit")
+
+PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file"
+PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
+
+DEFAULT_CONFIG = {"features.graph.bw.transferInBytes": False,
+                  "features.graph.bw.accounting.show": True,
+                  "features.graph.bw.accounting.rate": 10,
+                  "features.graph.bw.accounting.isTimeLong": False,
+                  "log.graph.bw.prepopulateSuccess": log.NOTICE,
+                  "log.graph.bw.prepopulateFailure": log.NOTICE}
+
+class BandwidthStats(graphPanel.GraphStats):
+  """
+  Uses tor BW events to generate bandwidth usage graph.
+  """
+  
+  def __init__(self, config=None):
+    graphPanel.GraphStats.__init__(self)
+    
+    self._config = dict(DEFAULT_CONFIG)
+    if config:
+      config.update(self._config, {"features.graph.bw.accounting.rate": 1})
+    
+    # stats prepopulated from tor's state file
+    self.prepopulatePrimaryTotal = 0
+    self.prepopulateSecondaryTotal = 0
+    self.prepopulateTicks = 0
+    
+    # accounting data (set by _updateAccountingInfo method)
+    self.accountingLastUpdated = 0
+    self.accountingInfo = dict([(arg, "") for arg in ACCOUNTING_ARGS])
+    
+    # listens for tor reload (sighup) events which can reset the bandwidth
+    # rate/burst and if tor's using accounting
+    conn = torTools.getConn()
+    self._titleStats, self.isAccounting = [], False
+    self.resetListener(conn, torTools.State.INIT) # initializes values
+    conn.addStatusListener(self.resetListener)
+    
+    # Initialized the bandwidth totals to the values reported by Tor. This
+    # uses a controller options introduced in ticket 2345:
+    # https://trac.torproject.org/projects/tor/ticket/2345
+    # 
+    # further updates are still handled via BW events to avoid unnecessary
+    # GETINFO requests.
+    
+    self.initialPrimaryTotal = 0
+    self.initialSecondaryTotal = 0
+    
+    readTotal = conn.getInfo("traffic/read")
+    if readTotal and readTotal.isdigit():
+      self.initialPrimaryTotal = int(readTotal) / 1024 # Bytes -> KB
+    
+    writeTotal = conn.getInfo("traffic/written")
+    if writeTotal and writeTotal.isdigit():
+      self.initialSecondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
+  
+  def resetListener(self, conn, eventType):
+    # updates title parameters and accounting status if they changed
+    self._titleStats = []     # force reset of title
+    self.new_desc_event(None) # updates title params
+    
+    if eventType == torTools.State.INIT and self._config["features.graph.bw.accounting.show"]:
+      self.isAccounting = conn.getInfo('accounting/enabled') == '1'
+  
+  def prepopulateFromState(self):
+    """
+    Attempts to use tor's state file to prepopulate values for the 15 minute
+    interval via the BWHistoryReadValues/BWHistoryWriteValues values. This
+    returns True if successful and False otherwise.
+    """
+    
+    # checks that this is a relay (if ORPort is unset, then skip)
+    conn = torTools.getConn()
+    orPort = conn.getOption("ORPort")
+    if orPort == "0": return
+    
+    # gets the uptime (using the same parameters as the header panel to take
+    # advantage of caching
+    uptime = None
+    queryPid = conn.getMyPid()
+    if queryPid:
+      queryParam = ["%cpu", "rss", "%mem", "etime"]
+      queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam))
+      psCall = sysTools.call(queryCmd, 3600, True)
+      
+      if psCall and len(psCall) == 2:
+        stats = psCall[1].strip().split()
+        if len(stats) == 4: uptime = stats[3]
+    
+    # checks if tor has been running for at least a day, the reason being that
+    # the state tracks a day's worth of data and this should only prepopulate
+    # results associated with this tor instance
+    if not uptime or not "-" in uptime:
+      msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime"
+      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      return False
+    
+    # get the user's data directory (usually '~/.tor')
+    dataDir = conn.getOption("DataDirectory")
+    if not dataDir:
+      msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
+      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      return False
+    
+    # attempt to open the state file
+    try: stateFile = open("%s%s/state" % (conn.getPathPrefix(), dataDir), "r")
+    except IOError:
+      msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file"
+      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      return False
+    
+    # get the BWHistory entries (ordered oldest to newest) and number of
+    # intervals since last recorded
+    bwReadEntries, bwWriteEntries = None, None
+    missingReadEntries, missingWriteEntries = None, None
+    
+    # converts from gmt to local with respect to DST
+    tz_offset = time.altzone if time.localtime()[8] else time.timezone
+    
+    for line in stateFile:
+      line = line.strip()
+      
+      # According to the rep_hist_update_state() function the BWHistory*Ends
+      # correspond to the start of the following sampling period. Also, the
+      # most recent values of BWHistory*Values appear to be an incremental
+      # counter for the current sampling period. Hence, offsets are added to
+      # account for both.
+      
+      if line.startswith("BWHistoryReadValues"):
+        bwReadEntries = line[20:].split(",")
+        bwReadEntries = [int(entry) / 1024.0 / 900 for entry in bwReadEntries]
+        bwReadEntries.pop()
+      elif line.startswith("BWHistoryWriteValues"):
+        bwWriteEntries = line[21:].split(",")
+        bwWriteEntries = [int(entry) / 1024.0 / 900 for entry in bwWriteEntries]
+        bwWriteEntries.pop()
+      elif line.startswith("BWHistoryReadEnds"):
+        lastReadTime = time.mktime(time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
+        lastReadTime -= 900
+        missingReadEntries = int((time.time() - lastReadTime) / 900)
+      elif line.startswith("BWHistoryWriteEnds"):
+        lastWriteTime = time.mktime(time.strptime(line[19:], "%Y-%m-%d %H:%M:%S")) - tz_offset
+        lastWriteTime -= 900
+        missingWriteEntries = int((time.time() - lastWriteTime) / 900)
+    
+    if not bwReadEntries or not bwWriteEntries or not lastReadTime or not lastWriteTime:
+      msg = PREPOPULATE_FAILURE_MSG % "bandwidth stats missing from state file"
+      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      return False
+    
+    # fills missing entries with the last value
+    bwReadEntries += [bwReadEntries[-1]] * missingReadEntries
+    bwWriteEntries += [bwWriteEntries[-1]] * missingWriteEntries
+    
+    # crops starting entries so they're the same size
+    entryCount = min(len(bwReadEntries), len(bwWriteEntries), self.maxCol)
+    bwReadEntries = bwReadEntries[len(bwReadEntries) - entryCount:]
+    bwWriteEntries = bwWriteEntries[len(bwWriteEntries) - entryCount:]
+    
+    # gets index for 15-minute interval
+    intervalIndex = 0
+    for indexEntry in graphPanel.UPDATE_INTERVALS:
+      if indexEntry[1] == 900: break
+      else: intervalIndex += 1
+    
+    # fills the graphing parameters with state information
+    for i in range(entryCount):
+      readVal, writeVal = bwReadEntries[i], bwWriteEntries[i]
+      
+      self.lastPrimary, self.lastSecondary = readVal, writeVal
+      
+      self.prepopulatePrimaryTotal += readVal * 900
+      self.prepopulateSecondaryTotal += writeVal * 900
+      self.prepopulateTicks += 900
+      
+      self.primaryCounts[intervalIndex].insert(0, readVal)
+      self.secondaryCounts[intervalIndex].insert(0, writeVal)
+    
+    self.maxPrimary[intervalIndex] = max(self.primaryCounts)
+    self.maxSecondary[intervalIndex] = max(self.secondaryCounts)
+    del self.primaryCounts[intervalIndex][self.maxCol + 1:]
+    del self.secondaryCounts[intervalIndex][self.maxCol + 1:]
+    
+    msg = PREPOPULATE_SUCCESS_MSG
+    missingSec = time.time() - min(lastReadTime, lastWriteTime)
+    if missingSec: msg += " (%s is missing)" % uiTools.getTimeLabel(missingSec, 0, True)
+    log.log(self._config["log.graph.bw.prepopulateSuccess"], msg)
+    
+    return True
+  
+  def bandwidth_event(self, event):
+    if self.isAccounting and self.isNextTickRedraw():
+      if time.time() - self.accountingLastUpdated >= self._config["features.graph.bw.accounting.rate"]:
+        self._updateAccountingInfo()
+    
+    # scales units from B to KB for graphing
+    self._processEvent(event.read / 1024.0, event.written / 1024.0)
+  
+  def draw(self, panel, width, height):
+    # line of the graph's x-axis labeling
+    labelingLine = graphPanel.GraphStats.getContentHeight(self) + panel.graphHeight - 2
+    
+    # if display is narrow, overwrites x-axis labels with avg / total stats
+    if width <= COLLAPSE_WIDTH:
+      # clears line
+      panel.addstr(labelingLine, 0, " " * width)
+      graphCol = min((width - 10) / 2, self.maxCol)
+      
+      primaryFooter = "%s, %s" % (self._getAvgLabel(True), self._getTotalLabel(True))
+      secondaryFooter = "%s, %s" % (self._getAvgLabel(False), self._getTotalLabel(False))
+      
+      panel.addstr(labelingLine, 1, primaryFooter, uiTools.getColor(self.getColor(True)))
+      panel.addstr(labelingLine, graphCol + 6, secondaryFooter, uiTools.getColor(self.getColor(False)))
+    
+    # provides accounting stats if enabled
+    if self.isAccounting:
+      if torTools.getConn().isAlive():
+        status = self.accountingInfo["status"]
+        
+        hibernateColor = "green"
+        if status == "soft": hibernateColor = "yellow"
+        elif status == "hard": hibernateColor = "red"
+        elif status == "":
+          # failed to be queried
+          status, hibernateColor = "unknown", "red"
+        
+        panel.addfstr(labelingLine + 2, 0, "<b>Accounting (<%s>%s</%s>)</b>" % (hibernateColor, status, hibernateColor))
+        
+        resetTime = self.accountingInfo["resetTime"]
+        if not resetTime: resetTime = "unknown"
+        panel.addstr(labelingLine + 2, 35, "Time to reset: %s" % resetTime)
+        
+        used, total = self.accountingInfo["read"], self.accountingInfo["readLimit"]
+        if used and total:
+          panel.addstr(labelingLine + 3, 2, "%s / %s" % (used, total), uiTools.getColor(self.getColor(True)))
+        
+        used, total = self.accountingInfo["written"], self.accountingInfo["writtenLimit"]
+        if used and total:
+          panel.addstr(labelingLine + 3, 37, "%s / %s" % (used, total), uiTools.getColor(self.getColor(False)))
+      else:
+        panel.addfstr(labelingLine + 2, 0, "<b>Accounting:</b> Connection Closed...")
+  
+  def getTitle(self, width):
+    stats = list(self._titleStats)
+    
+    while True:
+      if not stats: return "Bandwidth:"
+      else:
+        label = "Bandwidth (%s):" % ", ".join(stats)
+        
+        if len(label) > width: del stats[-1]
+        else: return label
+  
+  def getHeaderLabel(self, width, isPrimary):
+    graphType = "Download" if isPrimary else "Upload"
+    stats = [""]
+    
+    # if wide then avg and total are part of the header, otherwise they're on
+    # the x-axis
+    if width * 2 > COLLAPSE_WIDTH:
+      stats = [""] * 3
+      stats[1] = "- %s" % self._getAvgLabel(isPrimary)
+      stats[2] = ", %s" % self._getTotalLabel(isPrimary)
+    
+    stats[0] = "%-14s" % ("%s/sec" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"]))
+    
+    # drops label's components if there's not enough space
+    labeling = graphType + " (" + "".join(stats).strip() + "):"
+    while len(labeling) >= width:
+      if len(stats) > 1:
+        del stats[-1]
+        labeling = graphType + " (" + "".join(stats).strip() + "):"
+      else:
+        labeling = graphType + ":"
+        break
+    
+    return labeling
+  
+  def getColor(self, isPrimary):
+    return DL_COLOR if isPrimary else UL_COLOR
+  
+  def getContentHeight(self):
+    baseHeight = graphPanel.GraphStats.getContentHeight(self)
+    return baseHeight + 3 if self.isAccounting else baseHeight
+  
+  def new_desc_event(self, event):
+    # updates self._titleStats with updated values
+    conn = torTools.getConn()
+    if not conn.isAlive(): return # keep old values
+    
+    myFingerprint = conn.getInfo("fingerprint")
+    if not self._titleStats or not myFingerprint or (event and myFingerprint in event.idlist):
+      stats = []
+      bwRate = conn.getMyBandwidthRate()
+      bwBurst = conn.getMyBandwidthBurst()
+      bwObserved = conn.getMyBandwidthObserved()
+      bwMeasured = conn.getMyBandwidthMeasured()
+      labelInBytes = self._config["features.graph.bw.transferInBytes"]
+      
+      if bwRate and bwBurst:
+        bwRateLabel = uiTools.getSizeLabel(bwRate, 1, False, labelInBytes)
+        bwBurstLabel = uiTools.getSizeLabel(bwBurst, 1, False, labelInBytes)
+        
+        # if both are using rounded values then strip off the ".0" decimal
+        if ".0" in bwRateLabel and ".0" in bwBurstLabel:
+          bwRateLabel = bwRateLabel.replace(".0", "")
+          bwBurstLabel = bwBurstLabel.replace(".0", "")
+        
+        stats.append("limit: %s/s" % bwRateLabel)
+        stats.append("burst: %s/s" % bwBurstLabel)
+      
+      # Provide the observed bandwidth either if the measured bandwidth isn't
+      # available or if the measured bandwidth is the observed (this happens
+      # if there isn't yet enough bandwidth measurements).
+      if bwObserved and (not bwMeasured or bwMeasured == bwObserved):
+        stats.append("observed: %s/s" % uiTools.getSizeLabel(bwObserved, 1, False, labelInBytes))
+      elif bwMeasured:
+        stats.append("measured: %s/s" % uiTools.getSizeLabel(bwMeasured, 1, False, labelInBytes))
+      
+      self._titleStats = stats
+  
+  def _getAvgLabel(self, isPrimary):
+    total = self.primaryTotal if isPrimary else self.secondaryTotal
+    total += self.prepopulatePrimaryTotal if isPrimary else self.prepopulateSecondaryTotal
+    return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"])
+  
+  def _getTotalLabel(self, isPrimary):
+    total = self.primaryTotal if isPrimary else self.secondaryTotal
+    total += self.initialPrimaryTotal if isPrimary else self.initialSecondaryTotal
+    return "total: %s" % uiTools.getSizeLabel(total * 1024, 1)
+  
+  def _updateAccountingInfo(self):
+    """
+    Updates mapping used for accounting info. This includes the following keys:
+    status, resetTime, read, written, readLimit, writtenLimit
+    
+    Any failed lookups result in a mapping to an empty string.
+    """
+    
+    conn = torTools.getConn()
+    queried = dict([(arg, "") for arg in ACCOUNTING_ARGS])
+    queried["status"] = conn.getInfo("accounting/hibernating")
+    
+    # provides a nicely formatted reset time
+    endInterval = conn.getInfo("accounting/interval-end")
+    if endInterval:
+      # converts from gmt to local with respect to DST
+      if time.localtime()[8]: tz_offset = time.altzone
+      else: tz_offset = time.timezone
+      
+      sec = time.mktime(time.strptime(endInterval, "%Y-%m-%d %H:%M:%S")) - time.time() - tz_offset
+      if self._config["features.graph.bw.accounting.isTimeLong"]:
+        queried["resetTime"] = ", ".join(uiTools.getTimeLabels(sec, True))
+      else:
+        days = sec / 86400
+        sec %= 86400
+        hours = sec / 3600
+        sec %= 3600
+        minutes = sec / 60
+        sec %= 60
+        queried["resetTime"] = "%i:%02i:%02i:%02i" % (days, hours, minutes, sec)
+    
+    # number of bytes used and in total for the accounting period
+    used = conn.getInfo("accounting/bytes")
+    left = conn.getInfo("accounting/bytes-left")
+    
+    if used and left:
+      usedComp, leftComp = used.split(" "), left.split(" ")
+      read, written = int(usedComp[0]), int(usedComp[1])
+      readLeft, writtenLeft = int(leftComp[0]), int(leftComp[1])
+      
+      queried["read"] = uiTools.getSizeLabel(read)
+      queried["written"] = uiTools.getSizeLabel(written)
+      queried["readLimit"] = uiTools.getSizeLabel(read + readLeft)
+      queried["writtenLimit"] = uiTools.getSizeLabel(written + writtenLeft)
+    
+    self.accountingInfo = queried
+    self.accountingLastUpdated = time.time()
+
diff --git a/src/cli/graphing/connStats.py b/src/cli/graphing/connStats.py
new file mode 100644
index 0000000..51227b7
--- /dev/null
+++ b/src/cli/graphing/connStats.py
@@ -0,0 +1,54 @@
+"""
+Tracks stats concerning tor's current connections.
+"""
+
+from cli.graphing import graphPanel
+from util import connections, torTools
+
+class ConnStats(graphPanel.GraphStats):
+  """
+  Tracks number of connections, counting client and directory connections as 
+  outbound. Control connections are excluded from counts.
+  """
+  
+  def __init__(self):
+    graphPanel.GraphStats.__init__(self)
+    
+    # listens for tor reload (sighup) events which can reset the ports tor uses
+    conn = torTools.getConn()
+    self.orPort, self.dirPort, self.controlPort = "0", "0", "0"
+    self.resetListener(conn, torTools.State.INIT) # initialize port values
+    conn.addStatusListener(self.resetListener)
+  
+  def resetListener(self, conn, eventType):
+    if eventType == torTools.State.INIT:
+      self.orPort = conn.getOption("ORPort", "0")
+      self.dirPort = conn.getOption("DirPort", "0")
+      self.controlPort = conn.getOption("ControlPort", "0")
+  
+  def eventTick(self):
+    """
+    Fetches connection stats from cached information.
+    """
+    
+    inboundCount, outboundCount = 0, 0
+    
+    for entry in connections.getResolver("tor").getConnections():
+      localPort = entry[1]
+      if localPort in (self.orPort, self.dirPort): inboundCount += 1
+      elif localPort == self.controlPort: pass # control connection
+      else: outboundCount += 1
+    
+    self._processEvent(inboundCount, outboundCount)
+  
+  def getTitle(self, width):
+    return "Connection Count:"
+  
+  def getHeaderLabel(self, width, isPrimary):
+    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+    if isPrimary: return "Inbound (%s, avg: %s):" % (self.lastPrimary, avg)
+    else: return "Outbound (%s, avg: %s):" % (self.lastSecondary, avg)
+  
+  def getRefreshRate(self):
+    return 5
+
diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
new file mode 100644
index 0000000..e4b493d
--- /dev/null
+++ b/src/cli/graphing/graphPanel.py
@@ -0,0 +1,407 @@
+"""
+Flexible panel for presenting bar graphs for a variety of stats. This panel is
+just concerned with the rendering of information, which is actually collected
+and stored by implementations of the GraphStats interface. Panels are made up
+of a title, followed by headers and graphs for two sets of stats. For
+instance...
+
+Bandwidth (cap: 5 MB, burst: 10 MB):
+Downloaded (0.0 B/sec):           Uploaded (0.0 B/sec):
+  34                                30
+                            *                                 *
+                    **  *   *                          *      **
+      *   *  *      ** **   **          ***  **       ** **   **
+     *********      ******  ******     *********      ******  ******
+   0 ************ ****************   0 ************ ****************
+         25s  50   1m   1.6  2.0           25s  50   1m   1.6  2.0
+"""
+
+import copy
+import curses
+from TorCtl import TorCtl
+
+from util import enum, panel, uiTools
+
+# time intervals at which graphs can be updated
+UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5),   ("30 seconds", 30),
+                    ("minutely", 60),   ("15 minute", 900), ("30 minute", 1800),
+                    ("hourly", 3600),   ("daily", 86400)]
+
+DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
+DEFAULT_COLOR_PRIMARY, DEFAULT_COLOR_SECONDARY = "green", "cyan"
+MIN_GRAPH_HEIGHT = 1
+
+# enums for graph bounds:
+#   Bounds.GLOBAL_MAX - global maximum (highest value ever seen)
+#   Bounds.LOCAL_MAX - local maximum (highest value currently on the graph)
+#   Bounds.TIGHT - local maximum and minimum
+Bounds = enum.Enum("GLOBAL_MAX", "LOCAL_MAX", "TIGHT")
+
+WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
+
+# used for setting defaults when initializing GraphStats and GraphPanel instances
+CONFIG = {"features.graph.height": 7,
+          "features.graph.interval": 0,
+          "features.graph.bound": 1,
+          "features.graph.maxWidth": 150,
+          "features.graph.showIntermediateBounds": True}
+
+def loadConfig(config):
+  config.update(CONFIG, {
+    "features.graph.height": MIN_GRAPH_HEIGHT,
+    "features.graph.maxWidth": 1,
+    "features.graph.interval": (0, len(UPDATE_INTERVALS) - 1),
+    "features.graph.bound": (0, 2)})
+
+class GraphStats(TorCtl.PostEventListener):
+  """
+  Module that's expected to update dynamically and provide attributes to be
+  graphed. Up to two graphs (a 'primary' and 'secondary') can be displayed at a
+  time and timescale parameters use the labels defined in UPDATE_INTERVALS.
+  """
+  
+  def __init__(self, isPauseBuffer=False):
+    """
+    Initializes parameters needed to present a graph.
+    """
+    
+    TorCtl.PostEventListener.__init__(self)
+    
+    # panel to be redrawn when updated (set when added to GraphPanel)
+    self._graphPanel = None
+    
+    # mirror instance used to track updates when paused
+    self.isPaused, self.isPauseBuffer = False, isPauseBuffer
+    if isPauseBuffer: self._pauseBuffer = None
+    else: self._pauseBuffer = GraphStats(True)
+    
+    # tracked stats
+    self.tick = 0                                 # number of processed events
+    self.lastPrimary, self.lastSecondary = 0, 0   # most recent registered stats
+    self.primaryTotal, self.secondaryTotal = 0, 0 # sum of all stats seen
+    
+    # timescale dependent stats
+    self.maxCol = CONFIG["features.graph.maxWidth"]
+    self.maxPrimary, self.maxSecondary = {}, {}
+    self.primaryCounts, self.secondaryCounts = {}, {}
+    
+    for i in range(len(UPDATE_INTERVALS)):
+      # recent rates for graph
+      self.maxPrimary[i] = 0
+      self.maxSecondary[i] = 0
+      
+      # historic stats for graph, first is accumulator
+      # iterative insert needed to avoid making shallow copies (nasty, nasty gotcha)
+      self.primaryCounts[i] = (self.maxCol + 1) * [0]
+      self.secondaryCounts[i] = (self.maxCol + 1) * [0]
+  
+  def eventTick(self):
+    """
+    Called when it's time to process another event. All graphs use tor BW
+    events to keep in sync with each other (this happens once a second).
+    """
+    
+    pass
+  
+  def isNextTickRedraw(self):
+    """
+    Provides true if the following tick (call to _processEvent) will result in
+    being redrawn.
+    """
+    
+    if self._graphPanel and not self.isPauseBuffer and not self.isPaused:
+      # use the minimum of the current refresh rate and the panel's
+      updateRate = UPDATE_INTERVALS[self._graphPanel.updateInterval][1]
+      return (self.tick + 1) % min(updateRate, self.getRefreshRate()) == 0
+    else: return False
+  
+  def getTitle(self, width):
+    """
+    Provides top label.
+    """
+    
+    return ""
+  
+  def getHeaderLabel(self, width, isPrimary):
+    """
+    Provides labeling presented at the top of the graph.
+    """
+    
+    return ""
+  
+  def getColor(self, isPrimary):
+    """
+    Provides the color to be used for the graph and stats.
+    """
+    
+    return DEFAULT_COLOR_PRIMARY if isPrimary else DEFAULT_COLOR_SECONDARY
+  
+  def getContentHeight(self):
+    """
+    Provides the height content should take up (not including the graph).
+    """
+    
+    return DEFAULT_CONTENT_HEIGHT
+  
+  def getRefreshRate(self):
+    """
+    Provides the number of ticks between when the stats have new values to be
+    redrawn.
+    """
+    
+    return 1
+  
+  def isVisible(self):
+    """
+    True if the stat has content to present, false if it should be hidden.
+    """
+    
+    return True
+  
+  def draw(self, panel, width, height):
+    """
+    Allows for any custom drawing monitor wishes to append.
+    """
+    
+    pass
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents bandwidth updates from being presented. This is a no-op
+    if a pause buffer.
+    """
+    
+    if isPause == self.isPaused or self.isPauseBuffer: return
+    self.isPaused = isPause
+    
+    if self.isPaused: active, inactive = self._pauseBuffer, self
+    else: active, inactive = self, self._pauseBuffer
+    self._parameterSwap(active, inactive)
+  
+  def bandwidth_event(self, event):
+    self.eventTick()
+  
+  def _parameterSwap(self, active, inactive):
+    """
+    Either overwrites parameters of pauseBuffer or with the current values or
+    vice versa. This is a helper method for setPaused and should be overwritten
+    to append with additional parameters that need to be preserved when paused.
+    """
+    
+    # The pause buffer is constructed as a GraphStats instance which will
+    # become problematic if this is overridden by any implementations (which
+    # currently isn't the case). If this happens then the pause buffer will
+    # need to be of the requester's type (not quite sure how to do this
+    # gracefully...).
+    
+    active.tick = inactive.tick
+    active.lastPrimary = inactive.lastPrimary
+    active.lastSecondary = inactive.lastSecondary
+    active.primaryTotal = inactive.primaryTotal
+    active.secondaryTotal = inactive.secondaryTotal
+    active.maxPrimary = dict(inactive.maxPrimary)
+    active.maxSecondary = dict(inactive.maxSecondary)
+    active.primaryCounts = copy.deepcopy(inactive.primaryCounts)
+    active.secondaryCounts = copy.deepcopy(inactive.secondaryCounts)
+  
+  def _processEvent(self, primary, secondary):
+    """
+    Includes new stats in graphs and notifies associated GraphPanel of changes.
+    """
+    
+    if self.isPaused: self._pauseBuffer._processEvent(primary, secondary)
+    else:
+      isRedraw = self.isNextTickRedraw()
+      
+      self.lastPrimary, self.lastSecondary = primary, secondary
+      self.primaryTotal += primary
+      self.secondaryTotal += secondary
+      
+      # updates for all time intervals
+      self.tick += 1
+      for i in range(len(UPDATE_INTERVALS)):
+        lable, timescale = UPDATE_INTERVALS[i]
+        
+        self.primaryCounts[i][0] += primary
+        self.secondaryCounts[i][0] += secondary
+        
+        if self.tick % timescale == 0:
+          self.maxPrimary[i] = max(self.maxPrimary[i], self.primaryCounts[i][0] / timescale)
+          self.primaryCounts[i][0] /= timescale
+          self.primaryCounts[i].insert(0, 0)
+          del self.primaryCounts[i][self.maxCol + 1:]
+          
+          self.maxSecondary[i] = max(self.maxSecondary[i], self.secondaryCounts[i][0] / timescale)
+          self.secondaryCounts[i][0] /= timescale
+          self.secondaryCounts[i].insert(0, 0)
+          del self.secondaryCounts[i][self.maxCol + 1:]
+      
+      if isRedraw: self._graphPanel.redraw(True)
+
+class GraphPanel(panel.Panel):
+  """
+  Panel displaying a graph, drawing statistics from custom GraphStats
+  implementations.
+  """
+  
+  def __init__(self, stdscr):
+    panel.Panel.__init__(self, stdscr, "graph", 0)
+    self.updateInterval = CONFIG["features.graph.interval"]
+    self.bounds = Bounds.values()[CONFIG["features.graph.bound"]]
+    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.isPaused = False
+  
+  def getHeight(self):
+    """
+    Provides the height requested by the currently displayed GraphStats (zero
+    if hidden).
+    """
+    
+    if self.currentDisplay and self.stats[self.currentDisplay].isVisible():
+      return self.stats[self.currentDisplay].getContentHeight() + self.graphHeight
+    else: return 0
+  
+  def setGraphHeight(self, newGraphHeight):
+    """
+    Sets the preferred height used for the graph (restricted to the
+    MIN_GRAPH_HEIGHT minimum).
+    
+    Arguments:
+      newGraphHeight - new height for the graph
+    """
+    
+    self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
+  
+  def draw(self, width, height):
+    """ Redraws graph panel """
+    
+    if self.currentDisplay:
+      param = self.stats[self.currentDisplay]
+      graphCol = min((width - 10) / 2, param.maxCol)
+      
+      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)
+      
+      # top labels
+      left, right = param.getHeaderLabel(width / 2, True), param.getHeaderLabel(width / 2, False)
+      if left: self.addstr(1, 0, left, curses.A_BOLD | primaryColor)
+      if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor)
+      
+      # determines max/min value on the graph
+      if self.bounds == Bounds.GLOBAL_MAX:
+        primaryMaxBound = int(param.maxPrimary[self.updateInterval])
+        secondaryMaxBound = int(param.maxSecondary[self.updateInterval])
+      else:
+        # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima
+        if graphCol < 2:
+          # nothing being displayed
+          primaryMaxBound, secondaryMaxBound = 0, 0
+        else:
+          primaryMaxBound = int(max(param.primaryCounts[self.updateInterval][1:graphCol + 1]))
+          secondaryMaxBound = int(max(param.secondaryCounts[self.updateInterval][1:graphCol + 1]))
+      
+      primaryMinBound = secondaryMinBound = 0
+      if self.bounds == Bounds.TIGHT:
+        primaryMinBound = int(min(param.primaryCounts[self.updateInterval][1:graphCol + 1]))
+        secondaryMinBound = int(min(param.secondaryCounts[self.updateInterval][1:graphCol + 1]))
+        
+        # if the max = min (ie, all values are the same) then use zero lower
+        # bound so a graph is still displayed
+        if primaryMinBound == primaryMaxBound: primaryMinBound = 0
+        if secondaryMinBound == secondaryMaxBound: secondaryMinBound = 0
+      
+      # displays upper and lower bounds
+      self.addstr(2, 0, "%4i" % primaryMaxBound, primaryColor)
+      self.addstr(self.graphHeight + 1, 0, "%4i" % primaryMinBound, primaryColor)
+      
+      self.addstr(2, graphCol + 5, "%4i" % secondaryMaxBound, secondaryColor)
+      self.addstr(self.graphHeight + 1, graphCol + 5, "%4i" % secondaryMinBound, secondaryColor)
+      
+      # displays intermediate bounds on every other row
+      if CONFIG["features.graph.showIntermediateBounds"]:
+        ticks = (self.graphHeight - 3) / 2
+        for i in range(ticks):
+          row = self.graphHeight - (2 * i) - 3
+          if self.graphHeight % 2 == 0 and i >= (ticks / 2): row -= 1
+          
+          if primaryMinBound != primaryMaxBound:
+            primaryVal = (primaryMaxBound - primaryMinBound) / (self.graphHeight - 1) * (self.graphHeight - row - 1)
+            if not primaryVal in (primaryMinBound, primaryMaxBound): self.addstr(row + 2, 0, "%4i" % primaryVal, primaryColor)
+          
+          if secondaryMinBound != secondaryMaxBound:
+            secondaryVal = (secondaryMaxBound - secondaryMinBound) / (self.graphHeight - 1) * (self.graphHeight - row - 1)
+            if not secondaryVal in (secondaryMinBound, secondaryMaxBound): self.addstr(row + 2, graphCol + 5, "%4i" % secondaryVal, secondaryColor)
+      
+      # creates bar graph (both primary and secondary)
+      for col in range(graphCol):
+        colCount = int(param.primaryCounts[self.updateInterval][col + 1]) - primaryMinBound
+        colHeight = min(self.graphHeight, self.graphHeight * colCount / (max(1, primaryMaxBound) - primaryMinBound))
+        for row in range(colHeight): self.addstr(self.graphHeight + 1 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
+        
+        colCount = int(param.secondaryCounts[self.updateInterval][col + 1]) - secondaryMinBound
+        colHeight = min(self.graphHeight, self.graphHeight * colCount / (max(1, secondaryMaxBound) - secondaryMinBound))
+        for row in range(colHeight): self.addstr(self.graphHeight + 1 - row, col + graphCol + 10, " ", curses.A_STANDOUT | secondaryColor)
+      
+      # bottom labeling of x-axis
+      intervalSec = 1 # seconds per labeling
+      for i in range(len(UPDATE_INTERVALS)):
+        if i == self.updateInterval: intervalSec = UPDATE_INTERVALS[i][1]
+      
+      intervalSpacing = 10 if graphCol >= WIDE_LABELING_GRAPH_COL else 5
+      unitsLabel, decimalPrecision = None, 0
+      for i in range((graphCol - 4) / intervalSpacing):
+        loc = (i + 1) * intervalSpacing
+        timeLabel = uiTools.getTimeLabel(loc * intervalSec, decimalPrecision)
+        
+        if not unitsLabel: unitsLabel = timeLabel[-1]
+        elif unitsLabel != timeLabel[-1]:
+          # upped scale so also up precision of future measurements
+          unitsLabel = timeLabel[-1]
+          decimalPrecision += 1
+        else:
+          # if constrained on space then strips labeling since already provided
+          timeLabel = timeLabel[:-1]
+        
+        self.addstr(self.graphHeight + 2, 4 + loc, timeLabel, primaryColor)
+        self.addstr(self.graphHeight + 2, graphCol + 10 + loc, timeLabel, secondaryColor)
+        
+      param.draw(self, width, height) # allows current stats to modify the display
+  
+  def addStats(self, label, stats):
+    """
+    Makes GraphStats instance available in the panel.
+    """
+    
+    stats._graphPanel = self
+    stats.isPaused = True
+    self.stats[label] = stats
+  
+  def setStats(self, label):
+    """
+    Sets the currently displayed stats instance, hiding panel if None.
+    """
+    
+    if label != self.currentDisplay:
+      if self.currentDisplay: self.stats[self.currentDisplay].setPaused(True)
+      
+      if not label:
+        self.currentDisplay = None
+      elif label in self.stats.keys():
+        self.currentDisplay = label
+        self.stats[label].setPaused(self.isPaused)
+      else: raise ValueError("Unrecognized stats label: %s" % label)
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents bandwidth updates from being presented.
+    """
+    
+    if isPause == self.isPaused: return
+    self.isPaused = isPause
+    if self.currentDisplay: self.stats[self.currentDisplay].setPaused(self.isPaused)
+
diff --git a/src/cli/graphing/resourceStats.py b/src/cli/graphing/resourceStats.py
new file mode 100644
index 0000000..f26d5c1
--- /dev/null
+++ b/src/cli/graphing/resourceStats.py
@@ -0,0 +1,47 @@
+"""
+Tracks the system resource usage (cpu and memory) of the tor process.
+"""
+
+from cli.graphing import graphPanel
+from util import sysTools, torTools, uiTools
+
+class ResourceStats(graphPanel.GraphStats):
+  """
+  System resource usage tracker.
+  """
+  
+  def __init__(self):
+    graphPanel.GraphStats.__init__(self)
+    self.queryPid = torTools.getConn().getMyPid()
+  
+  def getTitle(self, width):
+    return "System Resources:"
+  
+  def getHeaderLabel(self, width, isPrimary):
+    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
+    lastAmount = self.lastPrimary if isPrimary else self.lastSecondary
+    
+    if isPrimary:
+      return "CPU (%0.1f%%, avg: %0.1f%%):" % (lastAmount, avg)
+    else:
+      # memory sizes are converted from MB to B before generating labels
+      usageLabel = uiTools.getSizeLabel(lastAmount * 1048576, 1)
+      avgLabel = uiTools.getSizeLabel(avg * 1048576, 1)
+      return "Memory (%s, avg: %s):" % (usageLabel, avgLabel)
+  
+  def eventTick(self):
+    """
+    Fetch the cached measurement of resource usage from the ResourceTracker.
+    """
+    
+    primary, secondary = 0, 0
+    if self.queryPid:
+      resourceTracker = sysTools.getResourceTracker(self.queryPid)
+      
+      if not resourceTracker.lastQueryFailed():
+        primary, _, secondary, _ = resourceTracker.getResourceUsage()
+        primary *= 100        # decimal percentage to whole numbers
+        secondary /= 1048576  # translate size to MB so axis labels are short
+    
+    self._processEvent(primary, secondary)
+
diff --git a/src/cli/headerPanel.py b/src/cli/headerPanel.py
new file mode 100644
index 0000000..f653299
--- /dev/null
+++ b/src/cli/headerPanel.py
@@ -0,0 +1,474 @@
+"""
+Top panel for every page, containing basic system and tor related information.
+If there's room available then this expands to present its information in two
+columns, otherwise it's laid out as follows:
+  arm - <hostname> (<os> <sys/version>)         Tor <tor/version> (<new, old, recommended, etc>)
+  <nickname> - <address>:<orPort>, [Dir Port: <dirPort>, ]Control Port (<open, password, cookie>): <controlPort>
+  cpu: <cpu%> mem: <mem> (<mem%>) uid: <uid> uptime: <upmin>:<upsec>
+  fingerprint: <fingerprint>
+
+Example:
+  arm - odin (Linux 2.6.24-24-generic)         Tor 0.2.1.19 (recommended)
+  odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
+  cpu: 14.6%    mem: 42 MB (4.2%)    pid: 20060   uptime: 48:27
+  fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
+"""
+
+import os
+import time
+import curses
+import threading
+
+from util import log, panel, sysTools, torTools, uiTools
+
+# minimum width for which panel attempts to double up contents (two columns to
+# better use screen real estate)
+MIN_DUAL_COL_WIDTH = 141
+
+FLAG_COLORS = {"Authority": "white",  "BadExit": "red",     "BadDirectory": "red",    "Exit": "cyan",
+               "Fast": "yellow",      "Guard": "green",     "HSDir": "magenta",       "Named": "blue",
+               "Stable": "blue",      "Running": "yellow",  "Unnamed": "magenta",     "Valid": "green",
+               "V2Dir": "cyan",       "V3Dir": "white"}
+
+VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green",  
+                         "old": "red",  "unrecommended": "red",  "unknown": "cyan"}
+
+DEFAULT_CONFIG = {"features.showFdUsage": False,
+                  "log.fdUsageSixtyPercent": log.NOTICE,
+                  "log.fdUsageNinetyPercent": log.WARN}
+
+class HeaderPanel(panel.Panel, threading.Thread):
+  """
+  Top area contenting tor settings and system information. Stats are stored in
+  the vals mapping, keys including:
+    tor/  version, versionStatus, nickname, orPort, dirPort, controlPort,
+          exitPolicy, isAuthPassword (bool), isAuthCookie (bool),
+          orListenAddr, *address, *fingerprint, *flags, pid, startTime,
+          *fdUsed, fdLimit, isFdLimitEstimate
+    sys/  hostname, os, version
+    stat/ *%torCpu, *%armCpu, *rss, *%mem
+  
+  * volatile parameter that'll be reset on each update
+  """
+  
+  def __init__(self, stdscr, startTime, config = None):
+    panel.Panel.__init__(self, stdscr, "header", 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+    
+    self._config = dict(DEFAULT_CONFIG)
+    if config: config.update(self._config)
+    
+    self._isTorConnected = True
+    self._lastUpdate = -1       # time the content was last revised
+    self._isPaused = False      # prevents updates if true
+    self._halt = False          # terminates thread if true
+    self._cond = threading.Condition()  # used for pausing the thread
+    
+    # Time when the panel was paused or tor was stopped. This is used to
+    # freeze the uptime statistic (uptime increments normally when None).
+    self._haltTime = None
+    
+    # The last arm cpu usage sampling taken. This is a tuple of the form:
+    # (total arm cpu time, sampling timestamp)
+    # 
+    # The initial cpu total should be zero. However, at startup the cpu time
+    # in practice is often greater than the real time causing the initially
+    # reported cpu usage to be over 100% (which shouldn't be possible on
+    # single core systems).
+    # 
+    # Setting the initial cpu total to the value at this panel's init tends to
+    # give smoother results (staying in the same ballpark as the second
+    # sampling) so fudging the numbers this way for now.
+    
+    self._armCpuSampling = (sum(os.times()[:3]), startTime)
+    
+    # Last sampling received from the ResourceTracker, used to detect when it
+    # changes.
+    self._lastResourceFetch = -1
+    
+    # flag to indicate if we've already given file descriptor warnings
+    self._isFdSixtyPercentWarned = False
+    self._isFdNinetyPercentWarned = False
+    
+    self.vals = {}
+    self.valsLock = threading.RLock()
+    self._update(True)
+    
+    # listens for tor reload (sighup) events
+    torTools.getConn().addStatusListener(self.resetListener)
+  
+  def getHeight(self):
+    """
+    Provides the height of the content, which is dynamically determined by the
+    panel's maximum width.
+    """
+    
+    isWide = self.getParent().getmaxyx()[1] >= MIN_DUAL_COL_WIDTH
+    if self.vals["tor/orPort"]: return 4 if isWide else 6
+    else: return 3 if isWide else 4
+  
+  def draw(self, width, height):
+    self.valsLock.acquire()
+    isWide = width + 1 >= MIN_DUAL_COL_WIDTH
+    
+    # space available for content
+    if isWide:
+      leftWidth = max(width / 2, 77)
+      rightWidth = width - leftWidth
+    else: leftWidth = rightWidth = width
+    
+    # Line 1 / Line 1 Left (system and tor version information)
+    sysNameLabel = "arm - %s" % self.vals["sys/hostname"]
+    contentSpace = min(leftWidth, 40)
+    
+    if len(sysNameLabel) + 10 <= contentSpace:
+      sysTypeLabel = "%s %s" % (self.vals["sys/os"], self.vals["sys/version"])
+      sysTypeLabel = uiTools.cropStr(sysTypeLabel, contentSpace - len(sysNameLabel) - 3, 4)
+      self.addstr(0, 0, "%s (%s)" % (sysNameLabel, sysTypeLabel))
+    else:
+      self.addstr(0, 0, uiTools.cropStr(sysNameLabel, contentSpace))
+    
+    contentSpace = leftWidth - 43
+    if 7 + len(self.vals["tor/version"]) + len(self.vals["tor/versionStatus"]) <= contentSpace:
+      versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \
+          self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white"
+      versionStatusMsg = "<%s>%s</%s>" % (versionColor, self.vals["tor/versionStatus"], versionColor)
+      self.addfstr(0, 43, "Tor %s (%s)" % (self.vals["tor/version"], versionStatusMsg))
+    elif 11 <= contentSpace:
+      self.addstr(0, 43, uiTools.cropStr("Tor %s" % self.vals["tor/version"], contentSpace, 4))
+    
+    # Line 2 / Line 2 Left (tor ip/port information)
+    if self.vals["tor/orPort"]:
+      myAddress = "Unknown"
+      if self.vals["tor/orListenAddr"]: myAddress = self.vals["tor/orListenAddr"]
+      elif self.vals["tor/address"]: myAddress = self.vals["tor/address"]
+      
+      # acting as a relay (we can assume certain parameters are set
+      entry = ""
+      dirPortLabel = ", Dir Port: %s" % self.vals["tor/dirPort"] if self.vals["tor/dirPort"] != "0" else ""
+      for label in (self.vals["tor/nickname"], " - " + myAddress, ":" + self.vals["tor/orPort"], dirPortLabel):
+        if len(entry) + len(label) <= leftWidth: entry += label
+        else: break
+    else:
+      # non-relay (client only)
+      # TODO: not sure what sort of stats to provide...
+      entry = "<red><b>Relaying Disabled</b></red>"
+    
+    if self.vals["tor/isAuthPassword"]: authType = "password"
+    elif self.vals["tor/isAuthCookie"]: authType = "cookie"
+    else: authType = "open"
+    
+    if len(entry) + 19 + len(self.vals["tor/controlPort"]) + len(authType) <= leftWidth:
+      authColor = "red" if authType == "open" else "green"
+      authLabel = "<%s>%s</%s>" % (authColor, authType, authColor)
+      self.addfstr(1, 0, "%s, Control Port (%s): %s" % (entry, authLabel, self.vals["tor/controlPort"]))
+    elif len(entry) + 16 + len(self.vals["tor/controlPort"]) <= leftWidth:
+      self.addstr(1, 0, "%s, Control Port: %s" % (entry, self.vals["tor/controlPort"]))
+    else: self.addstr(1, 0, entry)
+    
+    # Line 3 / Line 1 Right (system usage info)
+    y, x = (0, leftWidth) if isWide else (2, 0)
+    if self.vals["stat/rss"] != "0": memoryLabel = uiTools.getSizeLabel(int(self.vals["stat/rss"]))
+    else: memoryLabel = "0"
+    
+    uptimeLabel = ""
+    if self.vals["tor/startTime"]:
+      if self._haltTime:
+        # freeze the uptime when paused or the tor process is stopped
+        uptimeLabel = uiTools.getShortTimeLabel(self._haltTime - self.vals["tor/startTime"])
+      else:
+        uptimeLabel = uiTools.getShortTimeLabel(time.time() - self.vals["tor/startTime"])
+    
+    sysFields = ((0, "cpu: %s%% tor, %s%% arm" % (self.vals["stat/%torCpu"], self.vals["stat/%armCpu"])),
+                 (27, "mem: %s (%s%%)" % (memoryLabel, self.vals["stat/%mem"])),
+                 (47, "pid: %s" % (self.vals["tor/pid"] if self._isTorConnected else "")),
+                 (59, "uptime: %s" % uptimeLabel))
+    
+    for (start, label) in sysFields:
+      if start + len(label) <= rightWidth: self.addstr(y, x + start, label)
+      else: break
+    
+    if self.vals["tor/orPort"]:
+      # Line 4 / Line 2 Right (fingerprint, and possibly file descriptor usage)
+      y, x = (1, leftWidth) if isWide else (3, 0)
+      
+      fingerprintLabel = uiTools.cropStr("fingerprint: %s" % self.vals["tor/fingerprint"], width)
+      self.addstr(y, x, fingerprintLabel)
+      
+      # if there's room and we're able to retrieve both the file descriptor
+      # usage and limit then it might be presented
+      if width - x - 59 >= 20 and self.vals["tor/fdUsed"] and self.vals["tor/fdLimit"]:
+        # display file descriptor usage if we're either configured to do so or
+        # running out
+        
+        fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"]
+        
+        if fdPercent >= 60 or self._config["features.showFdUsage"]:
+          fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL
+          if fdPercent >= 95:
+            fdPercentFormat = curses.A_BOLD | uiTools.getColor("red")
+          elif fdPercent >= 90:
+            fdPercentFormat = uiTools.getColor("red")
+          elif fdPercent >= 60:
+            fdPercentFormat = uiTools.getColor("yellow")
+          
+          estimateChar = "?" if self.vals["tor/isFdLimitEstimate"] else ""
+          baseLabel = "file desc: %i / %i%s (" % (self.vals["tor/fdUsed"], self.vals["tor/fdLimit"], estimateChar)
+          
+          self.addstr(y, x + 59, baseLabel)
+          self.addstr(y, x + 59 + len(baseLabel), fdPercentLabel, fdPercentFormat)
+          self.addstr(y, x + 59 + len(baseLabel) + len(fdPercentLabel), ")")
+      
+      # Line 5 / Line 3 Left (flags)
+      if self._isTorConnected:
+        flagLine = "flags: "
+        for flag in self.vals["tor/flags"]:
+          flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white"
+          flagLine += "<b><%s>%s</%s></b>, " % (flagColor, flag, flagColor)
+        
+        if len(self.vals["tor/flags"]) > 0: flagLine = flagLine[:-2]
+        else: flagLine += "<b><cyan>none</cyan></b>"
+        
+        self.addfstr(2 if isWide else 4, 0, flagLine)
+      else:
+        statusTime = torTools.getConn().getStatus()[1]
+        statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime))
+        self.addfstr(2 if isWide else 4, 0, "<b><red>Tor Disconnected</red></b> (%s)" % statusTimeLabel)
+      
+      # Undisplayed / Line 3 Right (exit policy)
+      if isWide:
+        exitPolicy = self.vals["tor/exitPolicy"]
+        
+        # adds note when default exit policy is appended
+        if exitPolicy == "": exitPolicy = "<default>"
+        elif not exitPolicy.endswith((" *:*", " *")): exitPolicy += ", <default>"
+        
+        # color codes accepts to be green, rejects to be red, and default marker to be cyan
+        isSimple = len(exitPolicy) > rightWidth - 13
+        policies = exitPolicy.split(", ")
+        for i in range(len(policies)):
+          policy = policies[i].strip()
+          displayedPolicy = policy.replace("accept", "").replace("reject", "").strip() if isSimple else policy
+          if policy.startswith("accept"): policy = "<green><b>%s</b></green>" % displayedPolicy
+          elif policy.startswith("reject"): policy = "<red><b>%s</b></red>" % displayedPolicy
+          elif policy.startswith("<default>"): policy = "<cyan><b>%s</b></cyan>" % displayedPolicy
+          policies[i] = policy
+        
+        self.addfstr(2, leftWidth, "exit policy: %s" % ", ".join(policies))
+    else:
+      # Client only
+      # TODO: not sure what information to provide here...
+      pass
+    
+    self.valsLock.release()
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents updates from being presented.
+    """
+    
+    if not self._isPaused == isPause:
+      self._isPaused = isPause
+      if self._isTorConnected:
+        if isPause: self._haltTime = time.time()
+        else: self._haltTime = None
+      
+      # Redraw now so we'll be displaying the state right when paused
+      # (otherwise the uptime might be off by a second, and change when
+      # the panel's redrawn for other reasons).
+      self.redraw(True)
+  
+  def run(self):
+    """
+    Keeps stats updated, checking for new information at a set rate.
+    """
+    
+    lastDraw = time.time() - 1
+    while not self._halt:
+      currentTime = time.time()
+      
+      if self._isPaused or currentTime - lastDraw < 1 or not self._isTorConnected:
+        self._cond.acquire()
+        if not self._halt: self._cond.wait(0.2)
+        self._cond.release()
+      else:
+        # Update the volatile attributes (cpu, memory, flags, etc) if we have
+        # a new resource usage sampling (the most dynamic stat) or its been
+        # twenty seconds since last fetched (so we still refresh occasionally
+        # when resource fetches fail).
+        # 
+        # Otherwise, just redraw the panel to change the uptime field.
+        
+        isChanged = False
+        if self.vals["tor/pid"]:
+          resourceTracker = sysTools.getResourceTracker(self.vals["tor/pid"])
+          isChanged = self._lastResourceFetch != resourceTracker.getRunCount()
+        
+        if isChanged or currentTime - self._lastUpdate >= 20:
+          self._update()
+        
+        self.redraw(True)
+        lastDraw += 1
+  
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+    
+    self._cond.acquire()
+    self._halt = True
+    self._cond.notifyAll()
+    self._cond.release()
+  
+  def resetListener(self, conn, eventType):
+    """
+    Updates static parameters on tor reload (sighup) events.
+    
+    Arguments:
+      conn      - tor controller
+      eventType - type of event detected
+    """
+    
+    if eventType == torTools.State.INIT:
+      self._isTorConnected = True
+      if self._isPaused: self._haltTime = time.time()
+      else: self._haltTime = None
+      
+      self._update(True)
+      self.redraw(True)
+    elif eventType == torTools.State.CLOSED:
+      self._isTorConnected = False
+      self._haltTime = time.time()
+      self._update()
+      self.redraw(True)
+  
+  def _update(self, setStatic=False):
+    """
+    Updates stats in the vals mapping. By default this just revises volatile
+    attributes.
+    
+    Arguments:
+      setStatic - resets all parameters, including relatively static values
+    """
+    
+    self.valsLock.acquire()
+    conn = torTools.getConn()
+    
+    if setStatic:
+      # version is truncated to first part, for instance:
+      # 0.2.2.13-alpha (git-feb8c1b5f67f2c6f) -> 0.2.2.13-alpha
+      self.vals["tor/version"] = conn.getInfo("version", "Unknown").split()[0]
+      self.vals["tor/versionStatus"] = conn.getInfo("status/version/current", "Unknown")
+      self.vals["tor/nickname"] = conn.getOption("Nickname", "")
+      self.vals["tor/orPort"] = conn.getOption("ORPort", "0")
+      self.vals["tor/dirPort"] = conn.getOption("DirPort", "0")
+      self.vals["tor/controlPort"] = conn.getOption("ControlPort", "")
+      self.vals["tor/isAuthPassword"] = conn.getOption("HashedControlPassword") != None
+      self.vals["tor/isAuthCookie"] = conn.getOption("CookieAuthentication") == "1"
+      
+      # orport is reported as zero if unset
+      if self.vals["tor/orPort"] == "0": self.vals["tor/orPort"] = ""
+      
+      # overwrite address if ORListenAddress is set (and possibly orPort too)
+      self.vals["tor/orListenAddr"] = ""
+      listenAddr = conn.getOption("ORListenAddress")
+      if listenAddr:
+        if ":" in listenAddr:
+          # both ip and port overwritten
+          self.vals["tor/orListenAddr"] = listenAddr[:listenAddr.find(":")]
+          self.vals["tor/orPort"] = listenAddr[listenAddr.find(":") + 1:]
+        else:
+          self.vals["tor/orListenAddr"] = listenAddr
+      
+      # fetch exit policy (might span over multiple lines)
+      policyEntries = []
+      for exitPolicy in conn.getOption("ExitPolicy", [], True):
+        policyEntries += [policy.strip() for policy in exitPolicy.split(",")]
+      self.vals["tor/exitPolicy"] = ", ".join(policyEntries)
+      
+      # file descriptor limit for the process, if this can't be determined
+      # then the limit is None
+      fdLimit, fdIsEstimate = conn.getMyFileDescriptorLimit()
+      self.vals["tor/fdLimit"] = fdLimit
+      self.vals["tor/isFdLimitEstimate"] = fdIsEstimate
+      
+      # system information
+      unameVals = os.uname()
+      self.vals["sys/hostname"] = unameVals[1]
+      self.vals["sys/os"] = unameVals[0]
+      self.vals["sys/version"] = unameVals[2]
+      
+      pid = conn.getMyPid()
+      self.vals["tor/pid"] = pid if pid else ""
+      
+      startTime = conn.getStartTime()
+      self.vals["tor/startTime"] = startTime if startTime else ""
+      
+      # reverts volatile parameters to defaults
+      self.vals["tor/fingerprint"] = "Unknown"
+      self.vals["tor/flags"] = []
+      self.vals["tor/fdUsed"] = 0
+      self.vals["stat/%torCpu"] = "0"
+      self.vals["stat/%armCpu"] = "0"
+      self.vals["stat/rss"] = "0"
+      self.vals["stat/%mem"] = "0"
+    
+    # sets volatile parameters
+    # TODO: This can change, being reported by STATUS_SERVER -> EXTERNAL_ADDRESS
+    # events. Introduce caching via torTools?
+    self.vals["tor/address"] = conn.getInfo("address", "")
+    
+    self.vals["tor/fingerprint"] = conn.getInfo("fingerprint", self.vals["tor/fingerprint"])
+    self.vals["tor/flags"] = conn.getMyFlags(self.vals["tor/flags"])
+    
+    # Updates file descriptor usage and logs if the usage is high. If we don't
+    # have a known limit or it's obviously faulty (being lower than our
+    # current usage) then omit file descriptor functionality.
+    if self.vals["tor/fdLimit"]:
+      fdUsed = conn.getMyFileDescriptorUsage()
+      if fdUsed and fdUsed <= self.vals["tor/fdLimit"]: self.vals["tor/fdUsed"] = fdUsed
+      else: self.vals["tor/fdUsed"] = 0
+    
+    if self.vals["tor/fdUsed"] and self.vals["tor/fdLimit"]:
+      fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"]
+      estimatedLabel = " estimated" if self.vals["tor/isFdLimitEstimate"] else ""
+      msg = "Tor's%s file descriptor usage is at %i%%." % (estimatedLabel, fdPercent)
+      
+      if fdPercent >= 90 and not self._isFdNinetyPercentWarned:
+        self._isFdSixtyPercentWarned, self._isFdNinetyPercentWarned = True, True
+        msg += " If you run out Tor will be unable to continue functioning."
+        log.log(self._config["log.fdUsageNinetyPercent"], msg)
+      elif fdPercent >= 60 and not self._isFdSixtyPercentWarned:
+        self._isFdSixtyPercentWarned = True
+        log.log(self._config["log.fdUsageSixtyPercent"], msg)
+    
+    # ps or proc derived resource usage stats
+    if self.vals["tor/pid"]:
+      resourceTracker = sysTools.getResourceTracker(self.vals["tor/pid"])
+      
+      if resourceTracker.lastQueryFailed():
+        self.vals["stat/%torCpu"] = "0"
+        self.vals["stat/rss"] = "0"
+        self.vals["stat/%mem"] = "0"
+      else:
+        cpuUsage, _, memUsage, memUsagePercent = resourceTracker.getResourceUsage()
+        self._lastResourceFetch = resourceTracker.getRunCount()
+        self.vals["stat/%torCpu"] = "%0.1f" % (100 * cpuUsage)
+        self.vals["stat/rss"] = str(memUsage)
+        self.vals["stat/%mem"] = "%0.1f" % (100 * memUsagePercent)
+    
+    # determines the cpu time for the arm process (including user and system
+    # time of both the primary and child processes)
+    
+    totalArmCpuTime, currentTime = sum(os.times()[:3]), time.time()
+    armCpuDelta = totalArmCpuTime - self._armCpuSampling[0]
+    armTimeDelta = currentTime - self._armCpuSampling[1]
+    pythonCpuTime = armCpuDelta / armTimeDelta
+    sysCallCpuTime = sysTools.getSysCpuUsage()
+    self.vals["stat/%armCpu"] = "%0.1f" % (100 * (pythonCpuTime + sysCallCpuTime))
+    self._armCpuSampling = (totalArmCpuTime, currentTime)
+    
+    self._lastUpdate = currentTime
+    self.valsLock.release()
+
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
new file mode 100644
index 0000000..86e680f
--- /dev/null
+++ b/src/cli/logPanel.py
@@ -0,0 +1,1100 @@
+"""
+Panel providing a chronological log of events its been configured to listen
+for. This provides prepopulation from the log file and supports filtering by
+regular expressions.
+"""
+
+import time
+import os
+import curses
+import threading
+
+from TorCtl import TorCtl
+
+from version import VERSION
+from util import conf, log, panel, sysTools, torTools, uiTools
+
+TOR_EVENT_TYPES = {
+  "d": "DEBUG",   "a": "ADDRMAP",          "k": "DESCCHANGED",  "s": "STREAM",
+  "i": "INFO",    "f": "AUTHDIR_NEWDESCS", "g": "GUARD",        "r": "STREAM_BW",
+  "n": "NOTICE",  "h": "BUILDTIMEOUT_SET", "l": "NEWCONSENSUS", "t": "STATUS_CLIENT",
+  "w": "WARN",    "b": "BW",               "m": "NEWDESC",      "u": "STATUS_GENERAL",
+  "e": "ERR",     "c": "CIRC",             "p": "NS",           "v": "STATUS_SERVER",
+                  "j": "CLIENTS_SEEN",     "q": "ORCONN"}
+
+EVENT_LISTING = """        d DEBUG      a ADDRMAP           k DESCCHANGED   s STREAM
+        i INFO       f AUTHDIR_NEWDESCS  g GUARD         r STREAM_BW
+        n NOTICE     h BUILDTIMEOUT_SET  l NEWCONSENSUS  t STATUS_CLIENT
+        w WARN       b BW                m NEWDESC       u STATUS_GENERAL
+        e ERR        c CIRC              p NS            v STATUS_SERVER
+                     j CLIENTS_SEEN      q ORCONN
+          DINWE tor runlevel+            A All Events
+          12345 arm runlevel+            X No Events
+          67890 torctl runlevel+         U Unknown Events"""
+
+RUNLEVEL_EVENT_COLOR = {log.DEBUG: "magenta", log.INFO: "blue", log.NOTICE: "green",
+                        log.WARN: "yellow", log.ERR: "red"}
+DAYBREAK_EVENT = "DAYBREAK" # special event for marking when the date changes
+TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
+
+ENTRY_INDENT = 2 # spaces an entry's message is indented after the first line
+DEFAULT_CONFIG = {"features.logFile": "",
+                  "features.log.showDateDividers": True,
+                  "features.log.showDuplicateEntries": False,
+                  "features.log.entryDuration": 7,
+                  "features.log.maxLinesPerEntry": 4,
+                  "features.log.prepopulate": True,
+                  "features.log.prepopulateReadLimit": 5000,
+                  "features.log.maxRefreshRate": 300,
+                  "cache.logPanel.size": 1000,
+                  "log.logPanel.prepopulateSuccess": log.INFO,
+                  "log.logPanel.prepopulateFailed": log.WARN,
+                  "log.logPanel.logFileOpened": log.NOTICE,
+                  "log.logPanel.logFileWriteFailed": log.ERR,
+                  "log.logPanel.forceDoubleRedraw": log.DEBUG}
+
+DUPLICATE_MSG = " [%i duplicate%s hidden]"
+
+# The height of the drawn content is estimated based on the last time we redrew
+# the panel. It's chiefly used for scrolling and the bar indicating its
+# position. Letting the estimate be too inaccurate results in a display bug, so
+# redraws the display if it's off by this threshold.
+CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
+
+# static starting portion of common log entries, fetched from the config when
+# needed if None
+COMMON_LOG_MESSAGES = None
+
+# cached values and the arguments that generated it for the getDaybreaks and
+# getDuplicates functions
+CACHED_DAYBREAKS_ARGUMENTS = (None, None) # events, current day
+CACHED_DAYBREAKS_RESULT = None
+CACHED_DUPLICATES_ARGUMENTS = None # events
+CACHED_DUPLICATES_RESULT = None
+
+# duration we'll wait for the deduplication function before giving up (in ms)
+DEDUPLICATION_TIMEOUT = 100
+
+def daysSince(timestamp=None):
+  """
+  Provides the number of days since the epoch converted to local time (rounded
+  down).
+  
+  Arguments:
+    timestamp - unix timestamp to convert, current time if undefined
+  """
+  
+  if timestamp == None: timestamp = time.time()
+  return int((timestamp - TIMEZONE_OFFSET) / 86400)
+
+def expandEvents(eventAbbr):
+  """
+  Expands event abbreviations to their full names. Beside mappings provided in
+  TOR_EVENT_TYPES this recognizes the following special events and aliases:
+  U - UKNOWN events
+  A - all events
+  X - no events
+  DINWE - runlevel and higher
+  12345 - arm runlevel and higher (ARM_DEBUG - ARM_ERR)
+  67890 - torctl runlevel and higher (TORCTL_DEBUG - TORCTL_ERR)
+  Raises ValueError with invalid input if any part isn't recognized.
+  
+  Examples:
+  "inUt" -> ["INFO", "NOTICE", "UNKNOWN", "STREAM_BW"]
+  "N4" -> ["NOTICE", "WARN", "ERR", "ARM_WARN", "ARM_ERR"]
+  "cfX" -> []
+  
+  Arguments:
+    eventAbbr - flags to be parsed to event types
+  """
+  
+  expandedEvents, invalidFlags = set(), ""
+  
+  for flag in eventAbbr:
+    if flag == "A":
+      armRunlevels = ["ARM_" + runlevel for runlevel in log.Runlevel.values()]
+      torctlRunlevels = ["TORCTL_" + runlevel for runlevel in log.Runlevel.values()]
+      expandedEvents = set(TOR_EVENT_TYPES.values() + armRunlevels + torctlRunlevels + ["UNKNOWN"])
+      break
+    elif flag == "X":
+      expandedEvents = set()
+      break
+    elif flag in "DINWE1234567890":
+      # all events for a runlevel and higher
+      if flag in "DINWE": typePrefix = ""
+      elif flag in "12345": typePrefix = "ARM_"
+      elif flag in "67890": typePrefix = "TORCTL_"
+      
+      if flag in "D16": runlevelIndex = 0
+      elif flag in "I27": runlevelIndex = 1
+      elif flag in "N38": runlevelIndex = 2
+      elif flag in "W49": runlevelIndex = 3
+      elif flag in "E50": runlevelIndex = 4
+      
+      runlevelSet = [typePrefix + runlevel for runlevel in log.Runlevel.values()[runlevelIndex:]]
+      expandedEvents = expandedEvents.union(set(runlevelSet))
+    elif flag == "U":
+      expandedEvents.add("UNKNOWN")
+    elif flag in TOR_EVENT_TYPES:
+      expandedEvents.add(TOR_EVENT_TYPES[flag])
+    else:
+      invalidFlags += flag
+  
+  if invalidFlags: raise ValueError(invalidFlags)
+  else: return expandedEvents
+
+def getMissingEventTypes():
+  """
+  Provides the event types the current torctl connection supports but arm
+  doesn't. This provides an empty list if no event types are missing, and None
+  if the GETINFO query fails.
+  """
+  
+  torEventTypes = torTools.getConn().getInfo("events/names")
+  
+  if torEventTypes:
+    torEventTypes = torEventTypes.split(" ")
+    armEventTypes = TOR_EVENT_TYPES.values()
+    return [event for event in torEventTypes if not event in armEventTypes]
+  else: return None # GETINFO call failed
+
+def loadLogMessages():
+  """
+  Fetches a mapping of common log messages to their runlevels from the config.
+  """
+  
+  global COMMON_LOG_MESSAGES
+  armConf = conf.getConfig("arm")
+  
+  COMMON_LOG_MESSAGES = {}
+  for confKey in armConf.getKeys():
+    if confKey.startswith("msg."):
+      eventType = confKey[4:].upper()
+      messages = armConf.get(confKey, [])
+      COMMON_LOG_MESSAGES[eventType] = messages
+
+def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = None):
+  """
+  Parses tor's log file for past events matching the given runlevels, providing
+  a list of log entries (ordered newest to oldest). Limiting the number of read
+  entries is suggested to avoid parsing everything from logs in the GB and TB
+  range.
+  
+  Arguments:
+    runlevels - event types (DEBUG - ERR) to be returned
+    readLimit - max lines of the log file that'll be read (unlimited if None)
+    addLimit  - maximum entries to provide back (unlimited if None)
+    config    - configuration parameters related to this panel, uses defaults
+                if left as None
+  """
+  
+  startTime = time.time()
+  if not runlevels: return []
+  
+  if not config: config = DEFAULT_CONFIG
+  
+  # checks tor's configuration for the log file's location (if any exists)
+  loggingTypes, loggingLocation = None, None
+  for loggingEntry in torTools.getConn().getOption("Log", [], True):
+    # looks for an entry like: notice file /var/log/tor/notices.log
+    entryComp = loggingEntry.split()
+    
+    if entryComp[1] == "file":
+      loggingTypes, loggingLocation = entryComp[0], entryComp[2]
+      break
+  
+  if not loggingLocation: return []
+  
+  # includes the prefix for tor paths
+  loggingLocation = torTools.getConn().getPathPrefix() + loggingLocation
+  
+  # if the runlevels argument is a superset of the log file then we can
+  # limit the read contents to the addLimit
+  runlevels = log.Runlevel.values()
+  loggingTypes = loggingTypes.upper()
+  if addLimit and (not readLimit or readLimit > addLimit):
+    if "-" in loggingTypes:
+      divIndex = loggingTypes.find("-")
+      sIndex = runlevels.index(loggingTypes[:divIndex])
+      eIndex = runlevels.index(loggingTypes[divIndex+1:])
+      logFileRunlevels = runlevels[sIndex:eIndex+1]
+    else:
+      sIndex = runlevels.index(loggingTypes)
+      logFileRunlevels = runlevels[sIndex:]
+    
+    # checks if runlevels we're reporting are a superset of the file's contents
+    isFileSubset = True
+    for runlevelType in logFileRunlevels:
+      if runlevelType not in runlevels:
+        isFileSubset = False
+        break
+    
+    if isFileSubset: readLimit = addLimit
+  
+  # tries opening the log file, cropping results to avoid choking on huge logs
+  lines = []
+  try:
+    if readLimit:
+      lines = sysTools.call("tail -n %i %s" % (readLimit, loggingLocation))
+      if not lines: raise IOError()
+    else:
+      logFile = open(loggingLocation, "r")
+      lines = logFile.readlines()
+      logFile.close()
+  except IOError:
+    msg = "Unable to read tor's log file: %s" % loggingLocation
+    log.log(config["log.logPanel.prepopulateFailed"], msg)
+  
+  if not lines: return []
+  
+  loggedEvents = []
+  currentUnixTime, currentLocalTime = time.time(), time.localtime()
+  for i in range(len(lines) - 1, -1, -1):
+    line = lines[i]
+    
+    # entries look like:
+    # Jul 15 18:29:48.806 [notice] Parsing GEOIP file.
+    lineComp = line.split()
+    eventType = lineComp[3][1:-1].upper()
+    
+    if eventType in runlevels:
+      # converts timestamp to unix time
+      timestamp = " ".join(lineComp[:3])
+      
+      # strips the decimal seconds
+      if "." in timestamp: timestamp = timestamp[:timestamp.find(".")]
+      
+      # overwrites missing time parameters with the local time (ignoring wday
+      # and yday since they aren't used)
+      eventTimeComp = list(time.strptime(timestamp, "%b %d %H:%M:%S"))
+      eventTimeComp[0] = currentLocalTime.tm_year
+      eventTimeComp[8] = currentLocalTime.tm_isdst
+      eventTime = time.mktime(eventTimeComp) # converts local to unix time
+      
+      # The above is gonna be wrong if the logs are for the previous year. If
+      # the event's in the future then correct for this.
+      if eventTime > currentUnixTime + 60:
+        eventTimeComp[0] -= 1
+        eventTime = time.mktime(eventTimeComp)
+      
+      eventMsg = " ".join(lineComp[4:])
+      loggedEvents.append(LogEntry(eventTime, eventType, eventMsg, RUNLEVEL_EVENT_COLOR[eventType]))
+    
+    if "opening log file" in line:
+      break # this entry marks the start of this tor instance
+  
+  if addLimit: loggedEvents = loggedEvents[:addLimit]
+  msg = "Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(loggedEvents), loggingLocation, readLimit, time.time() - startTime)
+  log.log(config["log.logPanel.prepopulateSuccess"], msg)
+  return loggedEvents
+
+def getDaybreaks(events, ignoreTimeForCache = False):
+  """
+  Provides the input events back with special 'DAYBREAK_EVENT' markers inserted
+  whenever the date changed between log entries (or since the most recent
+  event). The timestamp matches the beginning of the day for the following
+  entry.
+  
+  Arguments:
+    events             - chronologically ordered listing of events
+    ignoreTimeForCache - skips taking the day into consideration for providing
+                         cached results if true
+  """
+  
+  global CACHED_DAYBREAKS_ARGUMENTS, CACHED_DAYBREAKS_RESULT
+  if not events: return []
+  
+  newListing = []
+  currentDay = daysSince()
+  lastDay = currentDay
+  
+  if CACHED_DAYBREAKS_ARGUMENTS[0] == events and \
+    (ignoreTimeForCache or CACHED_DAYBREAKS_ARGUMENTS[1] == currentDay):
+    return list(CACHED_DAYBREAKS_RESULT)
+  
+  for entry in events:
+    eventDay = daysSince(entry.timestamp)
+    if eventDay != lastDay:
+      markerTimestamp = (eventDay * 86400) + TIMEZONE_OFFSET
+      newListing.append(LogEntry(markerTimestamp, DAYBREAK_EVENT, "", "white"))
+    
+    newListing.append(entry)
+    lastDay = eventDay
+  
+  CACHED_DAYBREAKS_ARGUMENTS = (list(events), currentDay)
+  CACHED_DAYBREAKS_RESULT = list(newListing)
+  
+  return newListing
+
+def getDuplicates(events):
+  """
+  Deduplicates a list of log entries, providing back a tuple listing with the
+  log entry and count of duplicates following it. Entries in different days are
+  not considered to be duplicates. This times out, returning None if it takes
+  longer than DEDUPLICATION_TIMEOUT.
+  
+  Arguments:
+    events - chronologically ordered listing of events
+  """
+  
+  global CACHED_DUPLICATES_ARGUMENTS, CACHED_DUPLICATES_RESULT
+  if CACHED_DUPLICATES_ARGUMENTS == events:
+    return list(CACHED_DUPLICATES_RESULT)
+  
+  # loads common log entries from the config if they haven't been
+  if COMMON_LOG_MESSAGES == None: loadLogMessages()
+  
+  startTime = time.time()
+  eventsRemaining = list(events)
+  returnEvents = []
+  
+  while eventsRemaining:
+    entry = eventsRemaining.pop(0)
+    duplicateIndices = isDuplicate(entry, eventsRemaining, True)
+    
+    # checks if the call timeout has been reached
+    if (time.time() - startTime) > DEDUPLICATION_TIMEOUT / 1000.0:
+      return None
+    
+    # drops duplicate entries
+    duplicateIndices.reverse()
+    for i in duplicateIndices: del eventsRemaining[i]
+    
+    returnEvents.append((entry, len(duplicateIndices)))
+  
+  CACHED_DUPLICATES_ARGUMENTS = list(events)
+  CACHED_DUPLICATES_RESULT = list(returnEvents)
+  
+  return returnEvents
+
+def isDuplicate(event, eventSet, getDuplicates = False):
+  """
+  True if the event is a duplicate for something in the eventSet, false
+  otherwise. If the getDuplicates flag is set this provides the indices of
+  the duplicates instead.
+  
+  Arguments:
+    event         - event to search for duplicates of
+    eventSet      - set to look for the event in
+    getDuplicates - instead of providing back a boolean this gives a list of
+                    the duplicate indices in the eventSet
+  """
+  
+  duplicateIndices = []
+  for i in range(len(eventSet)):
+    forwardEntry = eventSet[i]
+    
+    # if showing dates then do duplicate detection for each day, rather
+    # than globally
+    if forwardEntry.type == DAYBREAK_EVENT: break
+    
+    if event.type == forwardEntry.type:
+      isDuplicate = False
+      if event.msg == forwardEntry.msg: isDuplicate = True
+      elif event.type in COMMON_LOG_MESSAGES:
+        for commonMsg in COMMON_LOG_MESSAGES[event.type]:
+          # if it starts with an asterisk then check the whole message rather
+          # than just the start
+          if commonMsg[0] == "*":
+            isDuplicate = commonMsg[1:] in event.msg and commonMsg[1:] in forwardEntry.msg
+          else:
+            isDuplicate = event.msg.startswith(commonMsg) and forwardEntry.msg.startswith(commonMsg)
+          
+          if isDuplicate: break
+      
+      if isDuplicate:
+        if getDuplicates: duplicateIndices.append(i)
+        else: return True
+  
+  if getDuplicates: return duplicateIndices
+  else: return False
+
+class LogEntry():
+  """
+  Individual log file entry, having the following attributes:
+    timestamp - unix timestamp for when the event occurred
+    eventType - event type that occurred ("INFO", "BW", "ARM_WARN", etc)
+    msg       - message that was logged
+    color     - color of the log entry
+  """
+  
+  def __init__(self, timestamp, eventType, msg, color):
+    self.timestamp = timestamp
+    self.type = eventType
+    self.msg = msg
+    self.color = color
+    self._displayMessage = None
+  
+  def getDisplayMessage(self, includeDate = False):
+    """
+    Provides the entry's message for the log.
+    
+    Arguments:
+      includeDate - appends the event's date to the start of the message
+    """
+    
+    if includeDate:
+      # not the common case so skip caching
+      entryTime = time.localtime(self.timestamp)
+      timeLabel =  "%i/%i/%i %02i:%02i:%02i" % (entryTime[1], entryTime[2], entryTime[0], entryTime[3], entryTime[4], entryTime[5])
+      return "%s [%s] %s" % (timeLabel, self.type, self.msg)
+    
+    if not self._displayMessage:
+      entryTime = time.localtime(self.timestamp)
+      self._displayMessage = "%02i:%02i:%02i [%s] %s" % (entryTime[3], entryTime[4], entryTime[5], self.type, self.msg)
+    
+    return self._displayMessage
+
+class TorEventObserver(TorCtl.PostEventListener):
+  """
+  Listens for all types of events provided by TorCtl, providing an LogEntry
+  instance to the given callback function.
+  """
+  
+  def __init__(self, callback):
+    """
+    Tor event listener with the purpose of translating events to nicely
+    formatted calls of a callback function.
+    
+    Arguments:
+      callback - function accepting a LogEntry, called when an event of these
+                 types occur
+    """
+    
+    TorCtl.PostEventListener.__init__(self)
+    self.callback = callback
+  
+  def circ_status_event(self, event):
+    msg = "ID: %-3s STATUS: %-10s PATH: %s" % (event.circ_id, event.status, ", ".join(event.path))
+    if event.purpose: msg += " PURPOSE: %s" % event.purpose
+    if event.reason: msg += " REASON: %s" % event.reason
+    if event.remote_reason: msg += " REMOTE_REASON: %s" % event.remote_reason
+    self._notify(event, msg, "yellow")
+  
+  def buildtimeout_set_event(self, event):
+    self._notify(event, "SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (event.set_type, event.total_times, event.timeout_ms, event.xm, event.alpha, event.cutoff_quantile))
+  
+  def stream_status_event(self, event):
+    self._notify(event, "ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose))
+  
+  def or_conn_status_event(self, event):
+    msg = "STATUS: %-10s ENDPOINT: %-20s" % (event.status, event.endpoint)
+    if event.age: msg += " AGE: %-3s" % event.age
+    if event.read_bytes: msg += " READ: %-4i" % event.read_bytes
+    if event.wrote_bytes: msg += " WRITTEN: %-4i" % event.wrote_bytes
+    if event.reason: msg += " REASON: %-6s" % event.reason
+    if event.ncircs: msg += " NCIRCS: %i" % event.ncircs
+    self._notify(event, msg)
+  
+  def stream_bw_event(self, event):
+    self._notify(event, "ID: %s READ: %s WRITTEN: %s" % (event.strm_id, event.bytes_read, event.bytes_written))
+  
+  def bandwidth_event(self, event):
+    self._notify(event, "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
+  
+  def msg_event(self, event):
+    self._notify(event, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
+  
+  def new_desc_event(self, event):
+    idlistStr = [str(item) for item in event.idlist]
+    self._notify(event, ", ".join(idlistStr))
+  
+  def address_mapped_event(self, event):
+    self._notify(event, "%s, %s -> %s" % (event.when, event.from_addr, event.to_addr))
+  
+  def ns_event(self, event):
+    # NetworkStatus params: nickname, idhash, orhash, ip, orport (int),
+    #     dirport (int), flags, idhex, bandwidth, updated (datetime)
+    msg = ", ".join(["%s (%s)" % (ns.idhex, ns.nickname) for ns in event.nslist])
+    self._notify(event, "Listed (%i): %s" % (len(event.nslist), msg), "blue")
+  
+  def new_consensus_event(self, event):
+    msg = ", ".join(["%s (%s)" % (ns.idhex, ns.nickname) for ns in event.nslist])
+    self._notify(event, "Listed (%i): %s" % (len(event.nslist), msg), "magenta")
+  
+  def unknown_event(self, event):
+    msg = "(%s) %s" % (event.event_name, event.event_string)
+    self.callback(LogEntry(event.arrived_at, "UNKNOWN", msg, "red"))
+  
+  def _notify(self, event, msg, color="white"):
+    self.callback(LogEntry(event.arrived_at, event.event_name, msg, color))
+
+class LogPanel(panel.Panel, threading.Thread):
+  """
+  Listens for and displays tor, arm, and torctl events. This can prepopulate
+  from tor's log file if it exists.
+  """
+  
+  def __init__(self, stdscr, loggedEvents, config=None):
+    panel.Panel.__init__(self, stdscr, "log", 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+    
+    self._config = dict(DEFAULT_CONFIG)
+    
+    if config:
+      config.update(self._config, {
+        "features.log.maxLinesPerEntry": 1,
+        "features.log.prepopulateReadLimit": 0,
+        "features.log.maxRefreshRate": 10,
+        "cache.logPanel.size": 1000})
+    
+    # collapses duplicate log entries if false, showing only the most recent
+    self.showDuplicates = self._config["features.log.showDuplicateEntries"]
+    
+    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.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
+    self._isPaused = False
+    self._pauseBuffer = []              # location where messages are buffered if paused
+    
+    self._lastUpdate = -1               # time the content was last revised
+    self._halt = False                  # terminates thread if true
+    self._cond = threading.Condition()  # used for pausing/resuming the thread
+    
+    # restricts concurrent write access to attributes used to draw the display
+    # and pausing:
+    # msgLog, loggedEvents, regexFilter, scroll, _pauseBuffer
+    self.valsLock = threading.RLock()
+    
+    # cached parameters (invalidated if arguments for them change)
+    # last set of events we've drawn with
+    self._lastLoggedEvents = []
+    
+    # _getTitle (args: loggedEvents, regexFilter pattern, width)
+    self._titleCache = None
+    self._titleArgs = (None, None, None)
+    
+    # fetches past tor events from log file, if available
+    torEventBacklog = []
+    if self._config["features.log.prepopulate"]:
+      setRunlevels = list(set.intersection(set(self.loggedEvents), set(log.Runlevel.values())))
+      readLimit = self._config["features.log.prepopulateReadLimit"]
+      addLimit = self._config["cache.logPanel.size"]
+      torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config)
+    
+    # adds arm listener and fetches past events
+    log.LOG_LOCK.acquire()
+    try:
+      armRunlevels = [log.DEBUG, log.INFO, log.NOTICE, log.WARN, log.ERR]
+      log.addListeners(armRunlevels, self._registerArmEvent)
+      
+      # gets the set of arm events we're logging
+      setRunlevels = []
+      for i in range(len(armRunlevels)):
+        if "ARM_" + log.Runlevel.values()[i] in self.loggedEvents:
+          setRunlevels.append(armRunlevels[i])
+      
+      armEventBacklog = []
+      for level, msg, eventTime in log._getEntries(setRunlevels):
+        armEventEntry = LogEntry(eventTime, "ARM_" + level, msg, RUNLEVEL_EVENT_COLOR[level])
+        armEventBacklog.insert(0, armEventEntry)
+      
+      # joins armEventBacklog and torEventBacklog chronologically into msgLog
+      while armEventBacklog or torEventBacklog:
+        if not armEventBacklog:
+          self.msgLog.append(torEventBacklog.pop(0))
+        elif not torEventBacklog:
+          self.msgLog.append(armEventBacklog.pop(0))
+        elif armEventBacklog[0].timestamp < torEventBacklog[0].timestamp:
+          self.msgLog.append(torEventBacklog.pop(0))
+        else:
+          self.msgLog.append(armEventBacklog.pop(0))
+    finally:
+      log.LOG_LOCK.release()
+    
+    # crops events that are either too old, or more numerous than the caching size
+    self._trimEvents(self.msgLog)
+    
+    # leaving lastContentHeight as being too low causes initialization problems
+    self.lastContentHeight = len(self.msgLog)
+    
+    # adds listeners for tor and torctl events
+    conn = torTools.getConn()
+    conn.addEventListener(TorEventObserver(self.registerEvent))
+    conn.addTorCtlListener(self._registerTorCtlEvent)
+    
+    # opens log file if we'll be saving entries
+    if self._config["features.logFile"]:
+      logPath = self._config["features.logFile"]
+      
+      try:
+        # make dir if the path doesn't already exist
+        baseDir = os.path.dirname(logPath)
+        if not os.path.exists(baseDir): os.makedirs(baseDir)
+        
+        self.logFile = open(logPath, "a")
+        log.log(self._config["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath))
+      except (IOError, OSError), exc:
+        log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
+        self.logFile = None
+  
+  def registerEvent(self, event):
+    """
+    Notes event and redraws log. If paused it's held in a temporary buffer.
+    
+    Arguments:
+      event - LogEntry for the event that occurred
+    """
+    
+    if not event.type in self.loggedEvents: return
+    
+    # strips control characters to avoid screwing up the terminal
+    event.msg = uiTools.getPrintable(event.msg)
+    
+    # note event in the log file if we're saving them
+    if self.logFile:
+      try:
+        self.logFile.write(event.getDisplayMessage(True) + "\n")
+        self.logFile.flush()
+      except IOError, exc:
+        log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
+        self.logFile = None
+    
+    if self._isPaused:
+      self.valsLock.acquire()
+      self._pauseBuffer.insert(0, event)
+      self._trimEvents(self._pauseBuffer)
+      self.valsLock.release()
+    else:
+      self.valsLock.acquire()
+      self.msgLog.insert(0, event)
+      self._trimEvents(self.msgLog)
+      
+      # notifies the display that it has new content
+      if not self.regexFilter or self.regexFilter.search(event.getDisplayMessage()):
+        self._cond.acquire()
+        self._cond.notifyAll()
+        self._cond.release()
+      
+      self.valsLock.release()
+  
+  def _registerArmEvent(self, level, msg, eventTime):
+    eventColor = RUNLEVEL_EVENT_COLOR[level]
+    self.registerEvent(LogEntry(eventTime, "ARM_%s" % level, msg, eventColor))
+  
+  def _registerTorCtlEvent(self, level, msg):
+    eventColor = RUNLEVEL_EVENT_COLOR[level]
+    self.registerEvent(LogEntry(time.time(), "TORCTL_%s" % level, msg, eventColor))
+  
+  def setLoggedEvents(self, eventTypes):
+    """
+    Sets the event types recognized by the panel.
+    
+    Arguments:
+      eventTypes - event types to be logged
+    """
+    
+    if eventTypes == self.loggedEvents: return
+    
+    self.valsLock.acquire()
+    self.loggedEvents = eventTypes
+    self.redraw(True)
+    self.valsLock.release()
+  
+  def setFilter(self, logFilter):
+    """
+    Filters log entries according to the given regular expression.
+    
+    Arguments:
+      logFilter - regular expression used to determine which messages are
+                  shown, None if no filter should be applied
+    """
+    
+    if logFilter == self.regexFilter: return
+    
+    self.valsLock.acquire()
+    self.regexFilter = logFilter
+    self.redraw(True)
+    self.valsLock.release()
+  
+  def clear(self):
+    """
+    Clears the contents of the event log.
+    """
+    
+    self.valsLock.acquire()
+    self.msgLog = []
+    self.redraw(True)
+    self.valsLock.release()
+  
+  def saveSnapshot(self, path):
+    """
+    Saves the log events currently being displayed to the given path. This
+    takes filers into account. This overwrites the file if it already exists,
+    and raises an IOError if there's a problem.
+    
+    Arguments:
+      path - path where to save the log snapshot
+    """
+    
+    # make dir if the path doesn't already exist
+    baseDir = os.path.dirname(path)
+    if not os.path.exists(baseDir): os.makedirs(baseDir)
+    
+    snapshotFile = open(path, "w")
+    self.valsLock.acquire()
+    try:
+      for entry in self.msgLog:
+        isVisible = not self.regexFilter or self.regexFilter.search(entry.getDisplayMessage())
+        if isVisible: snapshotFile.write(entry.getDisplayMessage(True) + "\n")
+      
+      self.valsLock.release()
+    except Exception, exc:
+      self.valsLock.release()
+      raise exc
+  
+  def handleKey(self, key):
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self.lastContentHeight)
+      
+      if self.scroll != newScroll:
+        self.valsLock.acquire()
+        self.scroll = newScroll
+        self.redraw(True)
+        self.valsLock.release()
+    elif key in (ord('u'), ord('U')):
+      self.valsLock.acquire()
+      self.showDuplicates = not self.showDuplicates
+      self.redraw(True)
+      self.valsLock.release()
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents message log from being updated with new events.
+    """
+    
+    if isPause == self._isPaused: return
+    
+    self._isPaused = isPause
+    if self._isPaused: self._pauseBuffer = []
+    else:
+      self.valsLock.acquire()
+      self.msgLog = (self._pauseBuffer + self.msgLog)[:self._config["cache.logPanel.size"]]
+      self.redraw(True)
+      self.valsLock.release()
+  
+  def draw(self, width, height):
+    """
+    Redraws message log. Entries stretch to use available space and may
+    contain up to two lines. Starts with newest entries.
+    """
+    
+    self.valsLock.acquire()
+    self._lastLoggedEvents, self._lastUpdate = list(self.msgLog), time.time()
+    
+    # draws the top label
+    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))
+    
+    # draws left-hand scroll bar if content's longer than the height
+    msgIndent, dividerIndent = 1, 0 # offsets for scroll bar
+    isScrollBarVisible = self.lastContentHeight > height - 1
+    if isScrollBarVisible:
+      msgIndent, dividerIndent = 3, 2
+      self.addScrollBar(self.scroll, self.scroll + height - 1, self.lastContentHeight, 1)
+    
+    # draws log entries
+    lineCount = 1 - self.scroll
+    seenFirstDateDivider = False
+    dividerAttr, duplicateAttr = curses.A_BOLD | uiTools.getColor("yellow"), curses.A_BOLD | uiTools.getColor("green")
+    
+    isDatesShown = self.regexFilter == None and self._config["features.log.showDateDividers"]
+    eventLog = getDaybreaks(self.msgLog, self._isPaused) if isDatesShown else list(self.msgLog)
+    if not self.showDuplicates:
+      deduplicatedLog = getDuplicates(eventLog)
+      
+      if deduplicatedLog == None:
+        msg = "Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive."
+        log.log(log.WARN, msg)
+        self.showDuplicates = True
+        deduplicatedLog = [(entry, 0) for entry in eventLog]
+    else: deduplicatedLog = [(entry, 0) for entry in eventLog]
+    
+    # determines if we have the minimum width to show date dividers
+    showDaybreaks = width - dividerIndent >= 3
+    
+    while deduplicatedLog:
+      entry, duplicateCount = deduplicatedLog.pop(0)
+      
+      if self.regexFilter and not self.regexFilter.search(entry.getDisplayMessage()):
+        continue  # filter doesn't match log message - skip
+      
+      # checks if we should be showing a divider with the date
+      if entry.type == DAYBREAK_EVENT:
+        # bottom of the divider
+        if seenFirstDateDivider:
+          if lineCount >= 1 and lineCount < height and showDaybreaks:
+            self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER,  dividerAttr)
+            self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr)
+            self.addch(lineCount, width, curses.ACS_LRCORNER, dividerAttr)
+          
+          lineCount += 1
+        
+        # top of the divider
+        if lineCount >= 1 and lineCount < height and showDaybreaks:
+          timeLabel = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp))
+          self.addch(lineCount, dividerIndent, curses.ACS_ULCORNER, dividerAttr)
+          self.addch(lineCount, dividerIndent + 1, curses.ACS_HLINE, dividerAttr)
+          self.addstr(lineCount, dividerIndent + 2, timeLabel, curses.A_BOLD | dividerAttr)
+          
+          lineLength = width - dividerIndent - len(timeLabel) - 2
+          self.hline(lineCount, dividerIndent + len(timeLabel) + 2, lineLength, dividerAttr)
+          self.addch(lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER, dividerAttr)
+        
+        seenFirstDateDivider = True
+        lineCount += 1
+      else:
+        # entry contents to be displayed, tuples of the form:
+        # (msg, formatting, includeLinebreak)
+        displayQueue = []
+        
+        msgComp = entry.getDisplayMessage().split("\n")
+        for i in range(len(msgComp)):
+          font = curses.A_BOLD if "ERR" in entry.type else curses.A_NORMAL # emphasizes ERR messages
+          displayQueue.append((msgComp[i].strip(), font | uiTools.getColor(entry.color), i != len(msgComp) - 1))
+        
+        if duplicateCount:
+          pluralLabel = "s" if duplicateCount > 1 else ""
+          duplicateMsg = DUPLICATE_MSG % (duplicateCount, pluralLabel)
+          displayQueue.append((duplicateMsg, duplicateAttr, False))
+        
+        cursorLoc, lineOffset = msgIndent, 0
+        maxEntriesPerLine = self._config["features.log.maxLinesPerEntry"]
+        while displayQueue:
+          msg, format, includeBreak = displayQueue.pop(0)
+          drawLine = lineCount + lineOffset
+          if lineOffset == maxEntriesPerLine: break
+          
+          maxMsgSize = width - cursorLoc
+          if len(msg) > maxMsgSize:
+            # message is too long - break it up
+            if lineOffset == maxEntriesPerLine - 1:
+              msg = uiTools.cropStr(msg, maxMsgSize)
+            else:
+              msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True)
+              displayQueue.insert(0, (remainder.strip(), format, includeBreak))
+            
+            includeBreak = True
+          
+          if drawLine < height and drawLine >= 1:
+            if seenFirstDateDivider and width - dividerIndent >= 3 and showDaybreaks:
+              self.addch(drawLine, dividerIndent, curses.ACS_VLINE, dividerAttr)
+              self.addch(drawLine, width, curses.ACS_VLINE, dividerAttr)
+            
+            self.addstr(drawLine, cursorLoc, msg, format)
+          
+          cursorLoc += len(msg)
+          
+          if includeBreak or not displayQueue:
+            lineOffset += 1
+            cursorLoc = msgIndent + ENTRY_INDENT
+        
+        lineCount += lineOffset
+      
+      # if this is the last line and there's room, then draw the bottom of the divider
+      if not deduplicatedLog and seenFirstDateDivider:
+        if lineCount < height and showDaybreaks:
+          self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr)
+          self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr)
+          self.addch(lineCount, width, curses.ACS_LRCORNER, dividerAttr)
+        
+        lineCount += 1
+    
+    # redraw the display if...
+    # - lastContentHeight was off by too much
+    # - we're off the bottom of the page
+    newContentHeight = lineCount + self.scroll - 1
+    contentHeightDelta = abs(self.lastContentHeight - newContentHeight)
+    forceRedraw, forceRedrawReason = True, ""
+    
+    if contentHeightDelta >= CONTENT_HEIGHT_REDRAW_THRESHOLD:
+      forceRedrawReason = "estimate was off by %i" % contentHeightDelta
+    elif newContentHeight > height and self.scroll + height - 1 > newContentHeight:
+      forceRedrawReason = "scrolled off the bottom of the page"
+    elif not isScrollBarVisible and newContentHeight > height - 1:
+      forceRedrawReason = "scroll bar wasn't previously visible"
+    elif isScrollBarVisible and newContentHeight <= height - 1:
+      forceRedrawReason = "scroll bar shouldn't be visible"
+    else: forceRedraw = False
+    
+    self.lastContentHeight = newContentHeight
+    if forceRedraw:
+      forceRedrawReason = "redrawing the log panel with the corrected content height (%s)" % forceRedrawReason
+      log.log(self._config["log.logPanel.forceDoubleRedraw"], forceRedrawReason)
+      self.redraw(True)
+    
+    self.valsLock.release()
+  
+  def redraw(self, forceRedraw=False, block=False):
+    # determines if the content needs to be redrawn or not
+    panel.Panel.redraw(self, forceRedraw, block)
+  
+  def run(self):
+    """
+    Redraws the display, coalescing updates if events are rapidly logged (for
+    instance running at the DEBUG runlevel) while also being immediately
+    responsive if additions are less frequent.
+    """
+    
+    lastDay = daysSince() # used to determine if the date has changed
+    while not self._halt:
+      currentDay = daysSince()
+      timeSinceReset = time.time() - self._lastUpdate
+      maxLogUpdateRate = self._config["features.log.maxRefreshRate"] / 1000.0
+      
+      sleepTime = 0
+      if (self.msgLog == self._lastLoggedEvents and lastDay == currentDay) or self._isPaused:
+        sleepTime = 5
+      elif timeSinceReset < maxLogUpdateRate:
+        sleepTime = max(0.05, maxLogUpdateRate - timeSinceReset)
+      
+      if sleepTime:
+        self._cond.acquire()
+        if not self._halt: self._cond.wait(sleepTime)
+        self._cond.release()
+      else:
+        lastDay = currentDay
+        self.redraw(True)
+  
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+    
+    self._cond.acquire()
+    self._halt = True
+    self._cond.notifyAll()
+    self._cond.release()
+  
+  def _getTitle(self, width):
+    """
+    Provides the label used for the panel, looking like:
+      Events (ARM NOTICE - ERR, BW - filter: prepopulate):
+    
+    This truncates the attributes (with an ellipse) if too long, and condenses
+    runlevel ranges if there's three or more in a row (for instance ARM_INFO,
+    ARM_NOTICE, and ARM_WARN becomes "ARM_INFO - WARN").
+    
+    Arguments:
+      width - width constraint the label needs to fix in
+    """
+    
+    # usually the attributes used to make the label are decently static, so
+    # provide cached results if they're unchanged
+    self.valsLock.acquire()
+    currentPattern = self.regexFilter.pattern if self.regexFilter else None
+    isUnchanged = self._titleArgs[0] == self.loggedEvents
+    isUnchanged &= self._titleArgs[1] == currentPattern
+    isUnchanged &= self._titleArgs[2] == width
+    if isUnchanged:
+      self.valsLock.release()
+      return self._titleCache
+    
+    eventsList = list(self.loggedEvents)
+    if not eventsList:
+      if not currentPattern:
+        panelLabel = "Events:"
+      else:
+        labelPattern = uiTools.cropStr(currentPattern, width - 18)
+        panelLabel = "Events (filter: %s):" % labelPattern
+    else:
+      # does the following with all runlevel types (tor, arm, and torctl):
+      # - pulls to the start of the list
+      # - condenses range if there's three or more in a row (ex. "ARM_INFO - WARN")
+      # - condense further if there's identical runlevel ranges for multiple
+      #   types (ex. "NOTICE - ERR, ARM_NOTICE - ERR" becomes "TOR/ARM NOTICE - ERR")
+      tmpRunlevels = [] # runlevels pulled from the list (just the runlevel part)
+      runlevelRanges = [] # tuple of type, startLevel, endLevel for ranges to be consensed
+      
+      # reverses runlevels and types so they're appended in the right order
+      reversedRunlevels = log.Runlevel.values()
+      reversedRunlevels.reverse()
+      for prefix in ("TORCTL_", "ARM_", ""):
+        # blank ending runlevel forces the break condition to be reached at the end
+        for runlevel in reversedRunlevels + [""]:
+          eventType = prefix + runlevel
+          if runlevel and eventType in eventsList:
+            # runlevel event found, move to the tmp list
+            eventsList.remove(eventType)
+            tmpRunlevels.append(runlevel)
+          elif tmpRunlevels:
+            # adds all tmp list entries to the start of eventsList
+            if len(tmpRunlevels) >= 3:
+              # save condense sequential runlevels to be added later
+              runlevelRanges.append((prefix, tmpRunlevels[-1], tmpRunlevels[0]))
+            else:
+              # adds runlevels individaully
+              for tmpRunlevel in tmpRunlevels:
+                eventsList.insert(0, prefix + tmpRunlevel)
+            
+            tmpRunlevels = []
+      
+      # adds runlevel ranges, condensing if there's identical ranges
+      for i in range(len(runlevelRanges)):
+        if runlevelRanges[i]:
+          prefix, startLevel, endLevel = runlevelRanges[i]
+          
+          # check for matching ranges
+          matches = []
+          for j in range(i + 1, len(runlevelRanges)):
+            if runlevelRanges[j] and runlevelRanges[j][1] == startLevel and runlevelRanges[j][2] == endLevel:
+              matches.append(runlevelRanges[j])
+              runlevelRanges[j] = None
+          
+          if matches:
+            # strips underscores and replaces empty entries with "TOR"
+            prefixes = [entry[0] for entry in matches] + [prefix]
+            for k in range(len(prefixes)):
+              if prefixes[k] == "": prefixes[k] = "TOR"
+              else: prefixes[k] = prefixes[k].replace("_", "")
+            
+            eventsList.insert(0, "%s %s - %s" % ("/".join(prefixes), startLevel, endLevel))
+          else:
+            eventsList.insert(0, "%s%s - %s" % (prefix, startLevel, endLevel))
+      
+      # truncates to use an ellipsis if too long, for instance:
+      attrLabel = ", ".join(eventsList)
+      if currentPattern: attrLabel += " - filter: %s" % currentPattern
+      attrLabel = uiTools.cropStr(attrLabel, width - 10, 1)
+      if attrLabel: attrLabel = " (%s)" % attrLabel
+      panelLabel = "Events%s:" % attrLabel
+    
+    # cache results and return
+    self._titleCache = panelLabel
+    self._titleArgs = (list(self.loggedEvents), currentPattern, width)
+    self.valsLock.release()
+    return panelLabel
+  
+  def _trimEvents(self, eventListing):
+    """
+    Crops events that have either:
+    - grown beyond the cache limit
+    - outlived the configured log duration
+    
+    Argument:
+      eventListing - listing of log entries
+    """
+    
+    cacheSize = self._config["cache.logPanel.size"]
+    if len(eventListing) > cacheSize: del eventListing[cacheSize:]
+    
+    logTTL = self._config["features.log.entryDuration"]
+    if logTTL > 0:
+      currentDay = daysSince()
+      
+      breakpoint = None # index at which to crop from
+      for i in range(len(eventListing) - 1, -1, -1):
+        daysSinceEvent = currentDay - daysSince(eventListing[i].timestamp)
+        if daysSinceEvent > logTTL: breakpoint = i # older than the ttl
+        else: break
+      
+      # removes entries older than the ttl
+      if breakpoint != None: del eventListing[breakpoint:]
+
diff --git a/src/cli/torrcPanel.py b/src/cli/torrcPanel.py
new file mode 100644
index 0000000..b7cad86
--- /dev/null
+++ b/src/cli/torrcPanel.py
@@ -0,0 +1,221 @@
+"""
+Panel displaying the torrc or armrc with the validation done against it.
+"""
+
+import math
+import curses
+import threading
+
+from util import conf, enum, panel, torConfig, uiTools
+
+DEFAULT_CONFIG = {"features.config.file.showScrollbars": True,
+                  "features.config.file.maxLinesPerEntry": 8}
+
+# TODO: The armrc use case is incomplete. There should be equivilant reloading
+# and validation capabilities to the torrc.
+Config = enum.Enum("TORRC", "ARMRC") # configuration file types that can be displayed
+
+class TorrcPanel(panel.Panel):
+  """
+  Renders the current torrc or armrc with syntax highlighting in a scrollable
+  area.
+  """
+  
+  def __init__(self, stdscr, configType, config=None):
+    panel.Panel.__init__(self, stdscr, "configFile", 0)
+    
+    self._config = dict(DEFAULT_CONFIG)
+    if config:
+      config.update(self._config, {"features.config.file.maxLinesPerEntry": 1})
+    
+    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
+    
+    # height of the content when last rendered (the cached value is invalid if
+    # _lastContentHeightArgs is None or differs from the current dimensions)
+    self._lastContentHeight = 1
+    self._lastContentHeightArgs = None
+  
+  def handleKey(self, key):
+    self.valsLock.acquire()
+    if uiTools.isScrollKey(key):
+      pageHeight = self.getPreferredSize()[0] - 1
+      newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
+      
+      if self.scroll != newScroll:
+        self.scroll = newScroll
+        self.redraw(True)
+    elif key == ord('n') or key == ord('N'):
+      self.showLineNum = not self.showLineNum
+      self._lastContentHeightArgs = None
+      self.redraw(True)
+    elif key == ord('s') or key == ord('S'):
+      self.stripComments = not self.stripComments
+      self._lastContentHeightArgs = None
+      self.redraw(True)
+    
+    self.valsLock.release()
+  
+  def draw(self, width, height):
+    self.valsLock.acquire()
+    
+    # If true, we assume that the cached value in self._lastContentHeight is
+    # still accurate, and stop drawing when there's nothing more to display.
+    # Otherwise the self._lastContentHeight is suspect, and we'll process all
+    # the content to check if it's right (and redraw again with the corrected
+    # height if not).
+    trustLastContentHeight = self._lastContentHeightArgs == (width, height)
+    
+    # restricts scroll location to valid bounds
+    self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1))
+    
+    renderedContents, corrections, confLocation = None, {}, None
+    if self.configType == Config.TORRC:
+      loadedTorrc = torConfig.getTorrc()
+      loadedTorrc.getLock().acquire()
+      confLocation = loadedTorrc.getConfigLocation()
+      
+      if not loadedTorrc.isLoaded():
+        renderedContents = ["### Unable to load the torrc ###"]
+      else:
+        renderedContents = loadedTorrc.getDisplayContents(self.stripComments)
+        
+        # constructs a mapping of line numbers to the issue on it
+        corrections = dict((lineNum, (issue, msg)) for lineNum, issue, msg in loadedTorrc.getCorrections())
+      
+      loadedTorrc.getLock().release()
+    else:
+      loadedArmrc = conf.getConfig("arm")
+      confLocation = loadedArmrc.path
+      renderedContents = list(loadedArmrc.rawContents)
+    
+    # offset to make room for the line numbers
+    lineNumOffset = 0
+    if self.showLineNum:
+      if len(renderedContents) == 0: lineNumOffset = 2
+      else: lineNumOffset = int(math.log10(len(renderedContents))) + 2
+    
+    # draws left-hand scroll bar if content's longer than the height
+    scrollOffset = 0
+    if self._config["features.config.file.showScrollbars"] and self._lastContentHeight > height - 1:
+      scrollOffset = 3
+      self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1)
+    
+    displayLine = -self.scroll + 1 # line we're drawing on
+    
+    # draws the top label
+    if self.showLabel:
+      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)
+    
+    isMultiline = False # true if we're in the middle of a multiline torrc entry
+    for lineNumber in range(0, len(renderedContents)):
+      lineText = renderedContents[lineNumber]
+      lineText = lineText.rstrip() # remove ending whitespace
+      
+      # blank lines are hidden when stripping comments
+      if self.stripComments and not lineText: continue
+      
+      # splits the line into its component (msg, format) tuples
+      lineComp = {"option": ["", curses.A_BOLD | uiTools.getColor("green")],
+                  "argument": ["", curses.A_BOLD | uiTools.getColor("cyan")],
+                  "correction": ["", curses.A_BOLD | uiTools.getColor("cyan")],
+                  "comment": ["", uiTools.getColor("white")]}
+      
+      # parses the comment
+      commentIndex = lineText.find("#")
+      if commentIndex != -1:
+        lineComp["comment"][0] = lineText[commentIndex:]
+        lineText = lineText[:commentIndex]
+      
+      # splits the option and argument, preserving any whitespace around them
+      strippedLine = lineText.strip()
+      optionIndex = strippedLine.find(" ")
+      if isMultiline:
+        # part of a multiline entry started on a previous line so everything
+        # is part of the argument
+        lineComp["argument"][0] = lineText
+      elif optionIndex == -1:
+        # no argument provided
+        lineComp["option"][0] = lineText
+      else:
+        optionText = strippedLine[:optionIndex]
+        optionEnd = lineText.find(optionText) + len(optionText)
+        lineComp["option"][0] = lineText[:optionEnd]
+        lineComp["argument"][0] = lineText[optionEnd:]
+      
+      # flags following lines as belonging to this multiline entry if it ends
+      # with a slash
+      if strippedLine: isMultiline = strippedLine.endswith("\\")
+      
+      # gets the correction
+      if lineNumber in corrections:
+        lineIssue, lineIssueMsg = corrections[lineNumber]
+        
+        if lineIssue in (torConfig.ValidationError.DUPLICATE, torConfig.ValidationError.IS_DEFAULT):
+          lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue")
+          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue")
+        elif lineIssue == torConfig.ValidationError.MISMATCH:
+          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red")
+          lineComp["correction"][0] = " (%s)" % lineIssueMsg
+        else:
+          # For some types of configs the correction field is simply used to
+          # provide extra data (for instance, the type for tor state fields).
+          lineComp["correction"][0] = " (%s)" % lineIssueMsg
+          lineComp["correction"][1] = curses.A_BOLD | uiTools.getColor("magenta")
+      
+      # draws the line number
+      if self.showLineNum and displayLine < height and displayLine >= 1:
+        lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1)
+        self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow"))
+      
+      # draws the rest of the components with line wrap
+      cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0
+      maxLinesPerEntry = self._config["features.config.file.maxLinesPerEntry"]
+      displayQueue = [lineComp[entry] for entry in ("option", "argument", "correction", "comment")]
+      
+      while displayQueue:
+        msg, format = displayQueue.pop(0)
+        
+        maxMsgSize, includeBreak = width - cursorLoc, False
+        if len(msg) >= maxMsgSize:
+          # message is too long - break it up
+          if lineOffset == maxLinesPerEntry - 1:
+            msg = uiTools.cropStr(msg, maxMsgSize)
+          else:
+            includeBreak = True
+            msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True)
+            displayQueue.insert(0, (remainder.strip(), format))
+        
+        drawLine = displayLine + lineOffset
+        if msg and drawLine < height and drawLine >= 1:
+          self.addstr(drawLine, cursorLoc, msg, format)
+        
+        # If we're done, and have added content to this line, then start
+        # further content on the next line.
+        cursorLoc += len(msg)
+        includeBreak |= not displayQueue and cursorLoc != lineNumOffset + scrollOffset
+        
+        if includeBreak:
+          lineOffset += 1
+          cursorLoc = lineNumOffset + scrollOffset
+      
+      displayLine += max(lineOffset, 1)
+      
+      if trustLastContentHeight and displayLine >= height: break
+    
+    if not trustLastContentHeight:
+      self._lastContentHeightArgs = (width, height)
+      newContentHeight = displayLine + self.scroll - 1
+      
+      if self._lastContentHeight != newContentHeight:
+        self._lastContentHeight = newContentHeight
+        self.redraw(True)
+    
+    self.valsLock.release()
+
diff --git a/src/interface/__init__.py b/src/interface/__init__.py
deleted file mode 100644
index 0f11fc1..0000000
--- a/src/interface/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Panels, popups, and handlers comprising the arm user interface.
-"""
-
-__all__ = ["configPanel", "connPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "torrcPanel"]
-
diff --git a/src/interface/configPanel.py b/src/interface/configPanel.py
deleted file mode 100644
index fd6fb54..0000000
--- a/src/interface/configPanel.py
+++ /dev/null
@@ -1,364 +0,0 @@
-"""
-Panel presenting the configuration state for tor or arm. Options can be edited
-and the resulting configuration files saved.
-"""
-
-import curses
-import threading
-
-from util import conf, enum, panel, torTools, torConfig, uiTools
-
-DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
-                  "features.config.state.showPrivateOptions": False,
-                  "features.config.state.showVirtualOptions": False,
-                  "features.config.state.colWidth.option": 25,
-                  "features.config.state.colWidth.value": 15}
-
-# TODO: The arm use cases are incomplete since they currently can't be
-# modified, have their descriptions fetched, or even get a complete listing
-# of what's available.
-State = enum.Enum("TOR", "ARM") # state to be presented
-
-# mappings of option categories to the color for their entries
-CATEGORY_COLOR = {torConfig.Category.GENERAL: "green",
-                  torConfig.Category.CLIENT: "blue",
-                  torConfig.Category.RELAY: "yellow",
-                  torConfig.Category.DIRECTORY: "magenta",
-                  torConfig.Category.AUTHORITY: "red",
-                  torConfig.Category.HIDDEN_SERVICE: "cyan",
-                  torConfig.Category.TESTING: "white",
-                  torConfig.Category.UNKNOWN: "white"}
-
-# attributes of a ConfigEntry
-Field = enum.Enum("CATEGORY", "OPTION", "VALUE", "TYPE", "ARG_USAGE",
-                  "SUMMARY", "DESCRIPTION", "MAN_ENTRY", "IS_DEFAULT")
-DEFAULT_SORT_ORDER = (Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT)
-FIELD_ATTR = {Field.CATEGORY: ("Category", "red"),
-              Field.OPTION: ("Option Name", "blue"),
-              Field.VALUE: ("Value", "cyan"),
-              Field.TYPE: ("Arg Type", "green"),
-              Field.ARG_USAGE: ("Arg Usage", "yellow"),
-              Field.SUMMARY: ("Summary", "green"),
-              Field.DESCRIPTION: ("Description", "white"),
-              Field.MAN_ENTRY: ("Man Page Entry", "blue"),
-              Field.IS_DEFAULT: ("Is Default", "magenta")}
-
-class ConfigEntry():
-  """
-  Configuration option in the panel.
-  """
-  
-  def __init__(self, option, type, isDefault):
-    self.fields = {}
-    self.fields[Field.OPTION] = option
-    self.fields[Field.TYPE] = type
-    self.fields[Field.IS_DEFAULT] = isDefault
-    
-    # Fetches extra infromation from external sources (the arm config and tor
-    # man page). These are None if unavailable for this config option.
-    summary = torConfig.getConfigSummary(option)
-    manEntry = torConfig.getConfigDescription(option)
-    
-    if manEntry:
-      self.fields[Field.MAN_ENTRY] = manEntry.index
-      self.fields[Field.CATEGORY] = manEntry.category
-      self.fields[Field.ARG_USAGE] = manEntry.argUsage
-      self.fields[Field.DESCRIPTION] = manEntry.description
-    else:
-      self.fields[Field.MAN_ENTRY] = 99999 # sorts non-man entries last
-      self.fields[Field.CATEGORY] = torConfig.Category.UNKNOWN
-      self.fields[Field.ARG_USAGE] = ""
-      self.fields[Field.DESCRIPTION] = ""
-    
-    # uses the full man page description if a summary is unavailable
-    self.fields[Field.SUMMARY] = summary if summary != None else self.fields[Field.DESCRIPTION]
-    
-    # cache of what's displayed for this configuration option
-    self.labelCache = None
-    self.labelCacheArgs = None
-  
-  def get(self, field):
-    """
-    Provides back the value in the given field.
-    
-    Arguments:
-      field - enum for the field to be provided back
-    """
-    
-    if field == Field.VALUE: return self._getValue()
-    else: return self.fields[field]
-  
-  def getAll(self, fields):
-    """
-    Provides back a list with the given field values.
-    
-    Arguments:
-      field - enums for the fields to be provided back
-    """
-    
-    return [self.get(field) for field in fields]
-  
-  def getLabel(self, optionWidth, valueWidth, summaryWidth):
-    """
-    Provides display string of the configuration entry with the given
-    constraints on the width of the contents.
-    
-    Arguments:
-      optionWidth  - width of the option column
-      valueWidth   - width of the value column
-      summaryWidth - width of the summary column
-    """
-    
-    # Fetching the display entries is very common so this caches the values.
-    # Doing this substantially drops cpu usage when scrolling (by around 40%).
-    
-    argSet = (optionWidth, valueWidth, summaryWidth)
-    if not self.labelCache or self.labelCacheArgs != argSet:
-      optionLabel = uiTools.cropStr(self.get(Field.OPTION), optionWidth)
-      valueLabel = uiTools.cropStr(self.get(Field.VALUE), valueWidth)
-      summaryLabel = uiTools.cropStr(self.get(Field.SUMMARY), summaryWidth, None)
-      lineTextLayout = "%%-%is %%-%is %%-%is" % (optionWidth, valueWidth, summaryWidth)
-      self.labelCache = lineTextLayout % (optionLabel, valueLabel, summaryLabel)
-      self.labelCacheArgs = argSet
-    
-    return self.labelCache
-  
-  def _getValue(self):
-    """
-    Provides the current value of the configuration entry, taking advantage of
-    the torTools caching to effectively query the accurate value. This uses the
-    value's type to provide a user friendly representation if able.
-    """
-    
-    confValue = ", ".join(torTools.getConn().getOption(self.get(Field.OPTION), [], True))
-    
-    # provides nicer values for recognized types
-    if not confValue: confValue = "<none>"
-    elif self.get(Field.TYPE) == "Boolean" and confValue in ("0", "1"):
-      confValue = "False" if confValue == "0" else "True"
-    elif self.get(Field.TYPE) == "DataSize" and confValue.isdigit():
-      confValue = uiTools.getSizeLabel(int(confValue))
-    elif self.get(Field.TYPE) == "TimeInterval" and confValue.isdigit():
-      confValue = uiTools.getTimeLabel(int(confValue), isLong = True)
-    
-    return confValue
-
-class ConfigPanel(panel.Panel):
-  """
-  Renders a listing of the tor or arm configuration state, allowing options to
-  be selected and edited.
-  """
-  
-  def __init__(self, stdscr, configType, config=None):
-    panel.Panel.__init__(self, stdscr, "configState", 0)
-    
-    self.sortOrdering = DEFAULT_SORT_ORDER
-    self._config = dict(DEFAULT_CONFIG)
-    if config:
-      config.update(self._config, {
-        "features.config.selectionDetails.height": 0,
-        "features.config.state.colWidth.option": 5,
-        "features.config.state.colWidth.value": 5})
-      
-      sortFields = Field.values()
-      customOrdering = config.getIntCSV("features.config.order", None, 3, 0, len(sortFields))
-      
-      if customOrdering:
-        self.sortOrdering = [sortFields[i] for i in customOrdering]
-    
-    self.configType = configType
-    self.confContents = []
-    self.scroller = uiTools.Scroller(True)
-    self.valsLock = threading.RLock()
-    
-    # shows all configuration options if true, otherwise only the ones with
-    # the 'important' flag are shown
-    self.showAll = False
-    
-    if self.configType == State.TOR:
-      conn = torTools.getConn()
-      customOptions = torConfig.getCustomOptions()
-      configOptionLines = conn.getInfo("config/names", "").strip().split("\n")
-      
-      for line in configOptionLines:
-        # lines are of the form "<option> <type>", like:
-        # UseEntryGuards Boolean
-        confOption, confType = line.strip().split(" ", 1)
-        
-        # skips private and virtual entries if not configured to show them
-        if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"):
-          continue
-        elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual":
-          continue
-        
-        self.confContents.append(ConfigEntry(confOption, confType, not confOption in customOptions))
-    elif self.configType == State.ARM:
-      # loaded via the conf utility
-      armConf = conf.getConfig("arm")
-      for key in armConf.getKeys():
-        pass # TODO: implement
-    
-    # mirror listing with only the important configuration options
-    self.confImportantContents = []
-    for entry in self.confContents:
-      if torConfig.isImportant(entry.get(Field.OPTION)):
-        self.confImportantContents.append(entry)
-    
-    # if there aren't any important options then show everything
-    if not self.confImportantContents:
-      self.confImportantContents = self.confContents
-    
-    self.setSortOrder() # initial sorting of the contents
-  
-  def getSelection(self):
-    """
-    Provides the currently selected entry.
-    """
-    
-    return self.scroller.getCursorSelection(self._getConfigOptions())
-  
-  def setSortOrder(self, ordering = None):
-    """
-    Sets the configuration attributes we're sorting by and resorts the
-    contents.
-    
-    Arguments:
-      ordering - new ordering, if undefined then this resorts with the last
-                 set ordering
-    """
-    
-    self.valsLock.acquire()
-    if ordering: self.sortOrdering = ordering
-    self.confContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
-    self.confImportantContents.sort(key=lambda i: (i.getAll(self.sortOrdering)))
-    self.valsLock.release()
-  
-  def handleKey(self, key):
-    self.valsLock.acquire()
-    if uiTools.isScrollKey(key):
-      pageHeight = self.getPreferredSize()[0] - 1
-      detailPanelHeight = self._config["features.config.selectionDetails.height"]
-      if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight:
-        pageHeight -= (detailPanelHeight + 1)
-      
-      isChanged = self.scroller.handleKey(key, self._getConfigOptions(), pageHeight)
-      if isChanged: self.redraw(True)
-    elif key == ord('a') or key == ord('A'):
-      self.showAll = not self.showAll
-      self.redraw(True)
-    self.valsLock.release()
-  
-  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
-    if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
-      # no detail panel
-      detailPanelHeight = 0
-      scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1)
-      cursorSelection = self.getSelection()
-      isScrollbarVisible = len(self._getConfigOptions()) > height - 1
-    else:
-      # Shrink detail panel if there isn't sufficient room for the whole
-      # thing. The extra line is for the bottom border.
-      detailPanelHeight = min(height - 1, detailPanelHeight + 1)
-      scrollLoc = self.scroller.getScrollLoc(self._getConfigOptions(), height - 1 - detailPanelHeight)
-      cursorSelection = self.getSelection()
-      isScrollbarVisible = len(self._getConfigOptions()) > height - detailPanelHeight - 1
-      
-      self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, isScrollbarVisible)
-    
-    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
-    if isScrollbarVisible:
-      scrollOffset = 3
-      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self._getConfigOptions()), 1 + detailPanelHeight)
-    
-    optionWidth = self._config["features.config.state.colWidth.option"]
-    valueWidth = self._config["features.config.state.colWidth.value"]
-    descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
-    
-    for lineNum in range(scrollLoc, len(self._getConfigOptions())):
-      entry = self._getConfigOptions()[lineNum]
-      drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
-      
-      lineFormat = curses.A_NORMAL if entry.get(Field.IS_DEFAULT) else curses.A_BOLD
-      if entry.get(Field.CATEGORY): lineFormat |= uiTools.getColor(CATEGORY_COLOR[entry.get(Field.CATEGORY)])
-      if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
-      
-      lineText = entry.getLabel(optionWidth, valueWidth, descriptionWidth)
-      self.addstr(drawLine, scrollOffset, lineText, lineFormat)
-      
-      if drawLine >= height: break
-    
-    self.valsLock.release()
-  
-  def _getConfigOptions(self):
-    return self.confContents if self.showAll else self.confImportantContents
-  
-  def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible):
-    """
-    Renders a panel for the selected configuration option.
-    """
-    
-    # This is a solid border unless the scrollbar is visible, in which case a
-    # 'T' pipe connects the border to the bar.
-    uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1)
-    if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE)
-    
-    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)])
-    
-    # first entry:
-    # <option> (<category> Option)
-    optionLabel =" (%s Option)" % selection.get(Field.CATEGORY)
-    self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat)
-    
-    # second entry:
-    # Value: <value> ([default|custom], <type>, usage: <argument usage>)
-    if detailPanelHeight >= 3:
-      valueAttr = []
-      valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
-      valueAttr.append(selection.get(Field.TYPE))
-      valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE)))
-      valueAttrLabel = ", ".join(valueAttr)
-      
-      valueLabelWidth = width - 12 - len(valueAttrLabel)
-      valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth)
-      
-      self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat)
-    
-    # remainder is filled with the man page description
-    descriptionHeight = max(0, detailPanelHeight - 3)
-    descriptionContent = "Description: " + selection.get(Field.DESCRIPTION)
-    
-    for i in range(descriptionHeight):
-      # checks if we're done writing the description
-      if not descriptionContent: break
-      
-      # there's a leading indent after the first line
-      if i > 0: descriptionContent = "  " + descriptionContent
-      
-      # we only want to work with content up until the next newline
-      if "\n" in descriptionContent:
-        lineContent, descriptionContent = descriptionContent.split("\n", 1)
-      else: lineContent, descriptionContent = descriptionContent, ""
-      
-      if i != descriptionHeight - 1:
-        # there's more lines to display
-        msg, remainder = uiTools.cropStr(lineContent, width - 2, 4, 4, uiTools.Ending.HYPHEN, True)
-        descriptionContent = remainder.strip() + descriptionContent
-      else:
-        # this is the last line, end it with an ellipse
-        msg = uiTools.cropStr(lineContent, width - 2, 4, 4)
-      
-      self.addstr(3 + i, 2, msg, selectionFormat)
-
diff --git a/src/interface/connections/__init__.py b/src/interface/connections/__init__.py
deleted file mode 100644
index 7a32d77..0000000
--- a/src/interface/connections/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Panels, popups, and handlers comprising the arm user interface.
-"""
-
-__all__ = ["circEntry", "connEntry", "connPanel", "entries"]
-
diff --git a/src/interface/connections/circEntry.py b/src/interface/connections/circEntry.py
deleted file mode 100644
index 80c3f81..0000000
--- a/src/interface/connections/circEntry.py
+++ /dev/null
@@ -1,216 +0,0 @@
-"""
-Connection panel entries for client circuits. This includes a header entry
-followed by an entry for each hop in the circuit. For instance:
-
-89.188.20.246:42667    -->  217.172.182.26 (de)       General / Built     8.6m (CIRCUIT)
-|  85.8.28.4 (se)               98FBC3B2B93897A78CDD797EF549E6B62C9A8523    1 / Guard
-|  91.121.204.76 (fr)           546387D93F8D40CFF8842BB9D3A8EC477CEDA984    2 / Middle
-+- 217.172.182.26 (de)          5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86    3 / Exit
-"""
-
-import curses
-
-from interface.connections import entries, connEntry
-from util import torTools, uiTools
-
-# cached fingerprint -> (IP Address, ORPort) results
-RELAY_INFO = {}
-
-def getRelayInfo(fingerprint):
-  """
-  Provides the (IP Address, ORPort) tuple for the given relay. If the lookup
-  fails then this returns ("192.168.0.1", "0").
-  
-  Arguments:
-    fingerprint - relay to look up
-  """
-  
-  if not fingerprint in RELAY_INFO:
-    conn = torTools.getConn()
-    failureResult = ("192.168.0.1", "0")
-    
-    nsEntry = conn.getConsensusEntry(fingerprint)
-    if not nsEntry: return failureResult
-    
-    nsLineComp = nsEntry.split("\n")[0].split(" ")
-    if len(nsLineComp) < 8: return failureResult
-    
-    RELAY_INFO[fingerprint] = (nsLineComp[6], nsLineComp[7])
-  
-  return RELAY_INFO[fingerprint]
-
-class CircEntry(connEntry.ConnectionEntry):
-  def __init__(self, circuitID, status, purpose, path):
-    connEntry.ConnectionEntry.__init__(self, "127.0.0.1", "0", "127.0.0.1", "0")
-    
-    self.circuitID = circuitID
-    self.status = status
-    
-    # drops to lowercase except the first letter
-    if len(purpose) >= 2:
-      purpose = purpose[0].upper() + purpose[1:].lower()
-    
-    self.lines = [CircHeaderLine(self.circuitID, purpose)]
-    
-    # Overwrites attributes of the initial line to make it more fitting as the
-    # header for our listing.
-    
-    self.lines[0].baseType = connEntry.Category.CIRCUIT
-    
-    self.update(status, path)
-  
-  def update(self, status, path):
-    """
-    Our status and path can change over time if the circuit is still in the
-    process of being built. Updates these attributes of our relay.
-    
-    Arguments:
-      status - new status of the circuit
-      path   - list of fingerprints for the series of relays involved in the
-               circuit
-    """
-    
-    self.status = status
-    self.lines = [self.lines[0]]
-    
-    if status == "BUILT" and not self.lines[0].isBuilt:
-      exitIp, exitORPort = getRelayInfo(path[-1])
-      self.lines[0].setExit(exitIp, exitORPort, path[-1])
-    
-    for i in range(len(path)):
-      relayFingerprint = path[i]
-      relayIp, relayOrPort = getRelayInfo(relayFingerprint)
-      
-      if i == len(path) - 1:
-        if status == "BUILT": placementType = "Exit"
-        else: placementType = "Extending"
-      elif i == 0: placementType = "Guard"
-      else: placementType = "Middle"
-      
-      placementLabel = "%i / %s" % (i + 1, placementType)
-      
-      self.lines.append(CircLine(relayIp, relayOrPort, relayFingerprint, placementLabel))
-    
-    self.lines[-1].isLast = True
-
-class CircHeaderLine(connEntry.ConnectionLine):
-  """
-  Initial line of a client entry. This has the same basic format as connection
-  lines except that its etc field has circuit attributes.
-  """
-  
-  def __init__(self, circuitID, purpose):
-    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", "0.0.0.0", "0", False, False)
-    self.circuitID = circuitID
-    self.purpose = purpose
-    self.isBuilt = False
-  
-  def setExit(self, exitIpAddr, exitPort, exitFingerprint):
-    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", exitIpAddr, exitPort, False, False)
-    self.isBuilt = True
-    self.foreign.fingerprintOverwrite = exitFingerprint
-  
-  def getType(self):
-    return connEntry.Category.CIRCUIT
-  
-  def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
-    if not self.isBuilt: return "Building..."
-    return connEntry.ConnectionLine.getDestinationLabel(self, maxLength, includeLocale, includeHostname)
-  
-  def getEtcContent(self, width, listingType):
-    """
-    Attempts to provide all circuit related stats. Anything that can't be
-    shown completely (not enough room) is dropped.
-    """
-    
-    etcAttr = ["Purpose: %s" % self.purpose, "Circuit ID: %i" % self.circuitID]
-    
-    for i in range(len(etcAttr), -1, -1):
-      etcLabel = ", ".join(etcAttr[:i])
-      if len(etcLabel) <= width:
-        return ("%%-%is" % width) % etcLabel
-    
-    return ""
-  
-  def getDetails(self, width):
-    if not self.isBuilt:
-      detailFormat = curses.A_BOLD | uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()])
-      return [uiTools.DrawEntry("Building Circuit...", detailFormat)]
-    else: return connEntry.ConnectionLine.getDetails(self, width)
-
-class CircLine(connEntry.ConnectionLine):
-  """
-  An individual hop in a circuit. This overwrites the displayed listing, but
-  otherwise makes use of the ConnectionLine attributes (for the detail display,
-  caching, etc).
-  """
-  
-  def __init__(self, fIpAddr, fPort, fFingerprint, placementLabel):
-    connEntry.ConnectionLine.__init__(self, "127.0.0.1", "0", fIpAddr, fPort)
-    self.foreign.fingerprintOverwrite = fFingerprint
-    self.placementLabel = placementLabel
-    self.includePort = False
-    
-    # determines the sort of left hand bracketing we use
-    self.isLast = False
-  
-  def getType(self):
-    return connEntry.Category.CIRCUIT
-  
-  def getListingEntry(self, width, currentTime, listingType):
-    """
-    Provides the DrawEntry for this relay in the circuilt listing. Lines are
-    composed of the following components:
-      <bracket> <dst> <etc> <placement label>
-    
-    The dst and etc entries largely match their ConnectionEntry counterparts.
-    
-    Arguments:
-      width       - maximum length of the line
-      currentTime - the current unix time (ignored)
-      listingType - primary attribute we're listing connections by
-    """
-    
-    return entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType)
-  
-  def _getListingEntry(self, width, currentTime, listingType):
-    lineFormat = uiTools.getColor(connEntry.CATEGORY_COLOR[self.getType()])
-    
-    # The required widths are the sum of the following:
-    # bracketing (3 characters)
-    # placementLabel (14 characters)
-    # gap between etc and placement label (5 characters)
-    
-    if self.isLast: bracket = (curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
-    else: bracket = (curses.ACS_VLINE, ord(' '), ord(' '))
-    baselineSpace = len(bracket) + 14 + 5
-    
-    dst, etc = "", ""
-    if listingType == entries.ListingType.IP_ADDRESS:
-      # TODO: include hostname when that's available
-      # dst width is derived as:
-      # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char
-      dst = "%-53s" % self.getDestinationLabel(53, includeLocale = True)
-      etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
-    elif listingType == entries.ListingType.HOSTNAME:
-      # min space for the hostname is 40 characters
-      etc = self.getEtcContent(width - baselineSpace - 40, listingType)
-      dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
-      dst = dstLayout % self.foreign.getHostname(self.foreign.getIpAddr())
-    elif listingType == entries.ListingType.FINGERPRINT:
-      # dst width is derived as:
-      # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
-      dst = "%-55s" % self.foreign.getFingerprint()
-      etc = self.getEtcContent(width - baselineSpace - len(dst), listingType)
-    else:
-      # min space for the nickname is 56 characters
-      etc = self.getEtcContent(width - baselineSpace - 56, listingType)
-      dstLayout = "%%-%is" % (width - baselineSpace - len(etc))
-      dst = dstLayout % self.foreign.getNickname()
-    
-    drawEntry = uiTools.DrawEntry("%-14s" % self.placementLabel, lineFormat)
-    drawEntry = uiTools.DrawEntry(" " * (width - baselineSpace - len(dst) - len(etc) + 5), lineFormat, drawEntry)
-    drawEntry = uiTools.DrawEntry(dst + etc, lineFormat, drawEntry)
-    drawEntry = uiTools.DrawEntry(bracket, curses.A_NORMAL, drawEntry, lockFormat = True)
-    return drawEntry
-
diff --git a/src/interface/connections/connEntry.py b/src/interface/connections/connEntry.py
deleted file mode 100644
index e6c0d92..0000000
--- a/src/interface/connections/connEntry.py
+++ /dev/null
@@ -1,850 +0,0 @@
-"""
-Connection panel entries related to actual connections to or from the system
-(ie, results seen by netstat, lsof, etc).
-"""
-
-import time
-import curses
-
-from util import connections, enum, torTools, uiTools
-from interface.connections import entries
-
-# Connection Categories:
-#   Inbound      Relay connection, coming to us.
-#   Outbound     Relay connection, leaving us.
-#   Exit         Outbound relay connection leaving the Tor network.
-#   Hidden       Connections to a hidden service we're providing.
-#   Socks        Socks connections for applications using Tor.
-#   Circuit      Circuits our tor client has created.
-#   Directory    Fetching tor consensus information.
-#   Control      Tor controller (arm, vidalia, etc).
-
-Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL")
-CATEGORY_COLOR = {Category.INBOUND: "green",      Category.OUTBOUND: "blue",
-                  Category.EXIT: "red",           Category.HIDDEN: "magenta",
-                  Category.SOCKS: "yellow",       Category.CIRCUIT: "cyan",
-                  Category.DIRECTORY: "magenta",  Category.CONTROL: "red"}
-
-# static data for listing format
-# <src>  -->  <dst>  <etc><padding>
-LABEL_FORMAT = "%s  -->  %s  %s%s"
-LABEL_MIN_PADDING = 2 # min space between listing label and following data
-
-# sort value for scrubbed ip addresses
-SCRUBBED_IP_VAL = 255 ** 4
-
-CONFIG = {"features.connection.markInitialConnections": True,
-          "features.connection.showExitPort": True,
-          "features.connection.showColumn.fingerprint": True,
-          "features.connection.showColumn.nickname": True,
-          "features.connection.showColumn.destination": True,
-          "features.connection.showColumn.expandedIp": True}
-
-def loadConfig(config):
-  config.update(CONFIG)
-
-class Endpoint:
-  """
-  Collection of attributes associated with a connection endpoint. This is a
-  thin wrapper for torUtil functions, making use of its caching for
-  performance.
-  """
-  
-  def __init__(self, ipAddr, port):
-    self.ipAddr = ipAddr
-    self.port = port
-    
-    # if true, we treat the port as an ORPort when searching for matching
-    # fingerprints (otherwise the ORPort is assumed to be unknown)
-    self.isORPort = False
-    
-    # if set then this overwrites fingerprint lookups
-    self.fingerprintOverwrite = None
-  
-  def getIpAddr(self):
-    """
-    Provides the IP address of the endpoint.
-    """
-    
-    return self.ipAddr
-  
-  def getPort(self):
-    """
-    Provides the port of the endpoint.
-    """
-    
-    return self.port
-  
-  def getHostname(self, default = None):
-    """
-    Provides the hostname associated with the relay's address. This is a
-    non-blocking call and returns None if the address either can't be resolved
-    or hasn't been resolved yet.
-    
-    Arguments:
-      default - return value if no hostname is available
-    """
-    
-    # TODO: skipping all hostname resolution to be safe for now
-    #try:
-    #  myHostname = hostnames.resolve(self.ipAddr)
-    #except:
-    #  # either a ValueError or IOError depending on the source of the lookup failure
-    #  myHostname = None
-    #
-    #if not myHostname: return default
-    #else: return myHostname
-    
-    return default
-  
-  def getLocale(self, default=None):
-    """
-    Provides the two letter country code for the IP address' locale.
-    
-    Arguments:
-      default - return value if no locale information is available
-    """
-    
-    conn = torTools.getConn()
-    return conn.getInfo("ip-to-country/%s" % self.ipAddr, default)
-  
-  def getFingerprint(self):
-    """
-    Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be
-    determined.
-    """
-    
-    if self.fingerprintOverwrite:
-      return self.fingerprintOverwrite
-    
-    conn = torTools.getConn()
-    orPort = self.port if self.isORPort else None
-    myFingerprint = conn.getRelayFingerprint(self.ipAddr, orPort)
-    
-    if myFingerprint: return myFingerprint
-    else: return "UNKNOWN"
-  
-  def getNickname(self):
-    """
-    Provides the nickname of the relay, retuning "UNKNOWN" if it can't be
-    determined.
-    """
-    
-    myFingerprint = self.getFingerprint()
-    
-    if myFingerprint != "UNKNOWN":
-      conn = torTools.getConn()
-      myNickname = conn.getRelayNickname(myFingerprint)
-      
-      if myNickname: return myNickname
-      else: return "UNKNOWN"
-    else: return "UNKNOWN"
-
-class ConnectionEntry(entries.ConnectionPanelEntry):
-  """
-  Represents a connection being made to or from this system. These only
-  concern real connections so it includes the inbound, outbound, directory,
-  application, and controller categories.
-  """
-  
-  def __init__(self, lIpAddr, lPort, fIpAddr, fPort):
-    entries.ConnectionPanelEntry.__init__(self)
-    self.lines = [ConnectionLine(lIpAddr, lPort, fIpAddr, fPort)]
-  
-  def getSortValue(self, attr, listingType):
-    """
-    Provides the value of a single attribute used for sorting purposes.
-    """
-    
-    connLine = self.lines[0]
-    if attr == entries.SortAttr.IP_ADDRESS:
-      if connLine.isPrivate(): return SCRUBBED_IP_VAL # orders at the end
-      return connLine.sortIpAddr
-    elif attr == entries.SortAttr.PORT:
-      return connLine.sortPort
-    elif attr == entries.SortAttr.HOSTNAME:
-      if connLine.isPrivate(): return ""
-      return connLine.foreign.getHostname("")
-    elif attr == entries.SortAttr.FINGERPRINT:
-      return connLine.foreign.getFingerprint()
-    elif attr == entries.SortAttr.NICKNAME:
-      myNickname = connLine.foreign.getNickname()
-      if myNickname == "UNKNOWN": return "z" * 20 # orders at the end
-      else: return myNickname.lower()
-    elif attr == entries.SortAttr.CATEGORY:
-      return Category.indexOf(connLine.getType())
-    elif attr == entries.SortAttr.UPTIME:
-      return connLine.startTime
-    elif attr == entries.SortAttr.COUNTRY:
-      if connections.isIpAddressPrivate(self.lines[0].foreign.getIpAddr()): return ""
-      else: return connLine.foreign.getLocale("")
-    else:
-      return entries.ConnectionPanelEntry.getSortValue(self, attr, listingType)
-
-class ConnectionLine(entries.ConnectionPanelLine):
-  """
-  Display component of the ConnectionEntry.
-  """
-  
-  def __init__(self, lIpAddr, lPort, fIpAddr, fPort, includePort=True, includeExpandedIpAddr=True):
-    entries.ConnectionPanelLine.__init__(self)
-    
-    self.local = Endpoint(lIpAddr, lPort)
-    self.foreign = Endpoint(fIpAddr, fPort)
-    self.startTime = time.time()
-    self.isInitialConnection = False
-    
-    # overwrite the local fingerprint with ours
-    conn = torTools.getConn()
-    self.local.fingerprintOverwrite = conn.getInfo("fingerprint")
-    
-    # True if the connection has matched the properties of a client/directory
-    # connection every time we've checked. The criteria we check is...
-    #   client    - first hop in an established circuit
-    #   directory - matches an established single-hop circuit (probably a
-    #               directory mirror)
-    
-    self._possibleClient = True
-    self._possibleDirectory = True
-    
-    # attributes for SOCKS, HIDDEN, and CONTROL connections
-    self.appName = None
-    self.appPid = None
-    self.isAppResolving = False
-    
-    myOrPort = conn.getOption("ORPort")
-    myDirPort = conn.getOption("DirPort")
-    mySocksPort = conn.getOption("SocksPort", "9050")
-    myCtlPort = conn.getOption("ControlPort")
-    myHiddenServicePorts = conn.getHiddenServicePorts()
-    
-    # the ORListenAddress can overwrite the ORPort
-    listenAddr = conn.getOption("ORListenAddress")
-    if listenAddr and ":" in listenAddr:
-      myOrPort = listenAddr[listenAddr.find(":") + 1:]
-    
-    if lPort in (myOrPort, myDirPort):
-      self.baseType = Category.INBOUND
-      self.local.isORPort = True
-    elif lPort == mySocksPort:
-      self.baseType = Category.SOCKS
-    elif fPort in myHiddenServicePorts:
-      self.baseType = Category.HIDDEN
-    elif lPort == myCtlPort:
-      self.baseType = Category.CONTROL
-    else:
-      self.baseType = Category.OUTBOUND
-      self.foreign.isORPort = True
-    
-    self.cachedType = None
-    
-    # includes the port or expanded ip address field when displaying listing
-    # information if true
-    self.includePort = includePort
-    self.includeExpandedIpAddr = includeExpandedIpAddr
-    
-    # cached immutable values used for sorting
-    self.sortIpAddr = connections.ipToInt(self.foreign.getIpAddr())
-    self.sortPort = int(self.foreign.getPort())
-  
-  def getListingEntry(self, width, currentTime, listingType):
-    """
-    Provides the DrawEntry for this connection's listing. Lines are composed
-    of the following components:
-      <src>  -->  <dst>     <etc>     <uptime> (<type>)
-    
-    ListingType.IP_ADDRESS:
-      src - <internal addr:port> --> <external addr:port>
-      dst - <destination addr:port>
-      etc - <fingerprint> <nickname>
-    
-    ListingType.HOSTNAME:
-      src - localhost:<port>
-      dst - <destination hostname:port>
-      etc - <destination addr:port> <fingerprint> <nickname>
-    
-    ListingType.FINGERPRINT:
-      src - localhost
-      dst - <destination fingerprint>
-      etc - <nickname> <destination addr:port>
-    
-    ListingType.NICKNAME:
-      src - <source nickname>
-      dst - <destination nickname>
-      etc - <fingerprint> <destination addr:port>
-    
-    Arguments:
-      width       - maximum length of the line
-      currentTime - unix timestamp for what the results should consider to be
-                    the current time
-      listingType - primary attribute we're listing connections by
-    """
-    
-    # fetch our (most likely cached) display entry for the listing
-    myListing = entries.ConnectionPanelLine.getListingEntry(self, width, currentTime, listingType)
-    
-    # fill in the current uptime and return the results
-    if CONFIG["features.connection.markInitialConnections"]:
-      timePrefix = "+" if self.isInitialConnection else " "
-    else: timePrefix = ""
-    
-    timeEntry = myListing.getNext()
-    timeEntry.text = timePrefix + "%5s" % uiTools.getTimeLabel(currentTime - self.startTime, 1)
-    
-    return myListing
-  
-  def isUnresolvedApp(self):
-    """
-    True if our display uses application information that hasn't yet been resolved.
-    """
-    
-    return self.appName == None and self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
-  
-  def _getListingEntry(self, width, currentTime, listingType):
-    entryType = self.getType()
-    
-    # Lines are split into the following components in reverse:
-    # content  - "<src>  -->  <dst>     <etc>     "
-    # time     - "<uptime>"
-    # preType  - " ("
-    # category - "<type>"
-    # postType - ")   "
-    
-    lineFormat = uiTools.getColor(CATEGORY_COLOR[entryType])
-    timeWidth = 6 if CONFIG["features.connection.markInitialConnections"] else 5
-    
-    drawEntry = uiTools.DrawEntry(")" + " " * (9 - len(entryType)), lineFormat)
-    drawEntry = uiTools.DrawEntry(entryType.upper(), lineFormat | curses.A_BOLD, drawEntry)
-    drawEntry = uiTools.DrawEntry(" (", lineFormat, drawEntry)
-    drawEntry = uiTools.DrawEntry(" " * timeWidth, lineFormat, drawEntry)
-    drawEntry = uiTools.DrawEntry(self._getListingContent(width - (12 + timeWidth), listingType), lineFormat, drawEntry)
-    return drawEntry
-  
-  def _getDetails(self, width):
-    """
-    Provides details on the connection, correlated against available consensus
-    data.
-    
-    Arguments:
-      width - available space to display in
-    """
-    
-    detailFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[self.getType()])
-    return [uiTools.DrawEntry(line, detailFormat) for line in self._getDetailContent(width)]
-  
-  def resetDisplay(self):
-    entries.ConnectionPanelLine.resetDisplay(self)
-    self.cachedType = None
-  
-  def isPrivate(self):
-    """
-    Returns true if the endpoint is private, possibly belonging to a client
-    connection or exit traffic.
-    """
-    
-    # This is used to scrub private information from the interface. Relaying
-    # etiquette (and wiretapping laws) say these are bad things to look at so
-    # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON!
-    
-    myType = self.getType()
-    
-    if myType == Category.INBOUND:
-      # if we're a guard or bridge and the connection doesn't belong to a
-      # known relay then it might be client traffic
-      
-      conn = torTools.getConn()
-      if "Guard" in conn.getMyFlags([]) or conn.getOption("BridgeRelay") == "1":
-        allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
-        return allMatches == []
-    elif myType == Category.EXIT:
-      # DNS connections exiting us aren't private (since they're hitting our
-      # resolvers). Everything else, however, is.
-      
-      # TODO: Ideally this would also double check that it's a UDP connection
-      # (since DNS is the only UDP connections Tor will relay), however this
-      # will take a bit more work to propagate the information up from the
-      # connection resolver.
-      return self.foreign.getPort() != "53"
-    
-    # for everything else this isn't a concern
-    return False
-  
-  def getType(self):
-    """
-    Provides our best guess at the current type of the connection. This
-    depends on consensus results, our current client circuits, etc. Results
-    are cached until this entry's display is reset.
-    """
-    
-    # caches both to simplify the calls and to keep the type consistent until
-    # we want to reflect changes
-    if not self.cachedType:
-      if self.baseType == Category.OUTBOUND:
-        # Currently the only non-static categories are OUTBOUND vs...
-        # - EXIT since this depends on the current consensus
-        # - CIRCUIT if this is likely to belong to our guard usage
-        # - DIRECTORY if this is a single-hop circuit (directory mirror?)
-        # 
-        # The exitability, circuits, and fingerprints are all cached by the
-        # torTools util keeping this a quick lookup.
-        
-        conn = torTools.getConn()
-        destFingerprint = self.foreign.getFingerprint()
-        
-        if destFingerprint == "UNKNOWN":
-          # Not a known relay. This might be an exit connection.
-          
-          if conn.isExitingAllowed(self.foreign.getIpAddr(), self.foreign.getPort()):
-            self.cachedType = Category.EXIT
-        elif self._possibleClient or self._possibleDirectory:
-          # This belongs to a known relay. If we haven't eliminated ourselves as
-          # a possible client or directory connection then check if it still
-          # holds true.
-          
-          myCircuits = conn.getCircuits()
-          
-          if self._possibleClient:
-            # Checks that this belongs to the first hop in a circuit that's
-            # either unestablished or longer than a single hop (ie, anything but
-            # a built 1-hop connection since those are most likely a directory
-            # mirror).
-            
-            for _, status, _, path in myCircuits:
-              if path[0] == destFingerprint and (status != "BUILT" or len(path) > 1):
-                self.cachedType = Category.CIRCUIT # matched a probable guard connection
-            
-            # if we fell through, we can eliminate ourselves as a guard in the future
-            if not self.cachedType:
-              self._possibleClient = False
-          
-          if self._possibleDirectory:
-            # Checks if we match a built, single hop circuit.
-            
-            for _, status, _, path in myCircuits:
-              if path[0] == destFingerprint and status == "BUILT" and len(path) == 1:
-                self.cachedType = Category.DIRECTORY
-            
-            # if we fell through, eliminate ourselves as a directory connection
-            if not self.cachedType:
-              self._possibleDirectory = False
-      
-      if not self.cachedType:
-        self.cachedType = self.baseType
-    
-    return self.cachedType
-  
-  def getEtcContent(self, width, listingType):
-    """
-    Provides the optional content for the connection.
-    
-    Arguments:
-      width       - maximum length of the line
-      listingType - primary attribute we're listing connections by
-    """
-    
-    # for applications show the command/pid
-    if self.getType() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
-      displayLabel = ""
-      
-      if self.appName:
-        if self.appPid: displayLabel = "%s (%s)" % (self.appName, self.appPid)
-        else: displayLabel = self.appName
-      elif self.isAppResolving:
-        displayLabel = "resolving..."
-      else: displayLabel = "UNKNOWN"
-      
-      if len(displayLabel) < width:
-        return ("%%-%is" % width) % displayLabel
-      else: return ""
-    
-    # for everything else display connection/consensus information
-    dstAddress = self.getDestinationLabel(26, includeLocale = True)
-    etc, usedSpace = "", 0
-    if listingType == entries.ListingType.IP_ADDRESS:
-      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
-      
-      if width > usedSpace + 10 and CONFIG["features.connection.showColumn.nickname"]:
-        # show nickname (column width: remainder)
-        nicknameSpace = width - usedSpace
-        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-        usedSpace += nicknameSpace + 2
-    elif listingType == entries.ListingType.HOSTNAME:
-      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
-        # show destination ip/port/locale (column width: 28 characters)
-        etc += "%-26s  " % dstAddress
-        usedSpace += 28
-      
-      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
-      
-      if width > usedSpace + 17 and CONFIG["features.connection.showColumn.nickname"]:
-        # show nickname (column width: min 17 characters, uses half of the remainder)
-        nicknameSpace = 15 + (width - (usedSpace + 17)) / 2
-        nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-        etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-        usedSpace += (nicknameSpace + 2)
-    elif listingType == entries.ListingType.FINGERPRINT:
-      if width > usedSpace + 17:
-        # show nickname (column width: min 17 characters, consumes any remaining space)
-        nicknameSpace = width - usedSpace - 2
-        
-        # if there's room then also show a column with the destination
-        # ip/port/locale (column width: 28 characters)
-        isIpLocaleIncluded = width > usedSpace + 45
-        isIpLocaleIncluded &= CONFIG["features.connection.showColumn.destination"]
-        if isIpLocaleIncluded: nicknameSpace -= 28
-        
-        if CONFIG["features.connection.showColumn.nickname"]:
-          nicknameLabel = uiTools.cropStr(self.foreign.getNickname(), nicknameSpace, 0)
-          etc += ("%%-%is  " % nicknameSpace) % nicknameLabel
-          usedSpace += nicknameSpace + 2
-        
-        if isIpLocaleIncluded:
-          etc += "%-26s  " % dstAddress
-          usedSpace += 28
-    else:
-      if width > usedSpace + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.getFingerprint()
-        usedSpace += 42
-      
-      if width > usedSpace + 28 and CONFIG["features.connection.showColumn.destination"]:
-        # show destination ip/port/locale (column width: 28 characters)
-        etc += "%-26s  " % dstAddress
-        usedSpace += 28
-    
-    return ("%%-%is" % width) % etc
-  
-  def _getListingContent(self, width, listingType):
-    """
-    Provides the source, destination, and extra info for our listing.
-    
-    Arguments:
-      width       - maximum length of the line
-      listingType - primary attribute we're listing connections by
-    """
-    
-    conn = torTools.getConn()
-    myType = self.getType()
-    dstAddress = self.getDestinationLabel(26, includeLocale = True)
-    
-    # The required widths are the sum of the following:
-    # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters)
-    # - base data for the listing
-    # - that extra field plus any previous
-    
-    usedSpace = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING
-    localPort = ":%s" % self.local.getPort() if self.includePort else ""
-    
-    src, dst, etc = "", "", ""
-    if listingType == entries.ListingType.IP_ADDRESS:
-      myExternalIpAddr = conn.getInfo("address", self.local.getIpAddr())
-      addrDiffer = myExternalIpAddr != self.local.getIpAddr()
-      
-      # Expanding doesn't make sense, if the connection isn't actually
-      # going through Tor's external IP address. As there isn't a known
-      # method for checking if it is, we're checking the type instead.
-      #
-      # This isn't entirely correct. It might be a better idea to check if
-      # the source and destination addresses are both private, but that might
-      # not be perfectly reliable either.
-      
-      isExpansionType = not myType in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
-      
-      if isExpansionType: srcAddress = myExternalIpAddr + localPort
-      else: srcAddress = self.local.getIpAddr() + localPort
-      
-      if myType in (Category.SOCKS, Category.CONTROL):
-        # Like inbound connections these need their source and destination to
-        # be swapped. However, this only applies when listing by IP or hostname
-        # (their fingerprint and nickname are both for us). Reversing the
-        # fields here to keep the same column alignments.
-        
-        src = "%-21s" % dstAddress
-        dst = "%-26s" % srcAddress
-      else:
-        src = "%-21s" % srcAddress # ip:port = max of 21 characters
-        dst = "%-26s" % dstAddress # ip:port (xx) = max of 26 characters
-      
-      usedSpace += len(src) + len(dst) # base data requires 47 characters
-      
-      # Showing the fingerprint (which has the width of 42) has priority over
-      # an expanded address field. Hence check if we either have space for
-      # both or wouldn't be showing the fingerprint regardless.
-      
-      isExpandedAddrVisible = width > usedSpace + 28
-      if isExpandedAddrVisible and CONFIG["features.connection.showColumn.fingerprint"]:
-        isExpandedAddrVisible = width < usedSpace + 42 or width > usedSpace + 70
-      
-      if addrDiffer and isExpansionType and isExpandedAddrVisible and self.includeExpandedIpAddr and CONFIG["features.connection.showColumn.expandedIp"]:
-        # include the internal address in the src (extra 28 characters)
-        internalAddress = self.local.getIpAddr() + localPort
-        
-        # If this is an inbound connection then reverse ordering so it's:
-        # <foreign> --> <external> --> <internal>
-        # when the src and dst are swapped later
-        
-        if myType == Category.INBOUND: src = "%-21s  -->  %s" % (src, internalAddress)
-        else: src = "%-21s  -->  %s" % (internalAddress, src)
-        
-        usedSpace += 28
-      
-      etc = self.getEtcContent(width - usedSpace, listingType)
-      usedSpace += len(etc)
-    elif listingType == entries.ListingType.HOSTNAME:
-      # 15 characters for source, and a min of 40 reserved for the destination
-      # TODO: when actually functional the src and dst need to be swapped for
-      # SOCKS and CONTROL connections
-      src = "localhost%-6s" % localPort
-      usedSpace += len(src)
-      minHostnameSpace = 40
-      
-      etc = self.getEtcContent(width - usedSpace - minHostnameSpace, listingType)
-      usedSpace += len(etc)
-      
-      hostnameSpace = width - usedSpace
-      usedSpace = width # prevents padding at the end
-      if self.isPrivate():
-        dst = ("%%-%is" % hostnameSpace) % "<scrubbed>"
-      else:
-        hostname = self.foreign.getHostname(self.foreign.getIpAddr())
-        portLabel = ":%-5s" % self.foreign.getPort() if self.includePort else ""
-        
-        # truncates long hostnames and sets dst to <hostname>:<port>
-        hostname = uiTools.cropStr(hostname, hostnameSpace, 0)
-        dst = ("%%-%is" % hostnameSpace) % (hostname + portLabel)
-    elif listingType == entries.ListingType.FINGERPRINT:
-      src = "localhost"
-      if myType == Category.CONTROL: dst = "localhost"
-      else: dst = self.foreign.getFingerprint()
-      dst = "%-40s" % dst
-      
-      usedSpace += len(src) + len(dst) # base data requires 49 characters
-      
-      etc = self.getEtcContent(width - usedSpace, listingType)
-      usedSpace += len(etc)
-    else:
-      # base data requires 50 min characters
-      src = self.local.getNickname()
-      if myType == Category.CONTROL: dst = self.local.getNickname()
-      else: dst = self.foreign.getNickname()
-      minBaseSpace = 50
-      
-      etc = self.getEtcContent(width - usedSpace - minBaseSpace, listingType)
-      usedSpace += len(etc)
-      
-      baseSpace = width - usedSpace
-      usedSpace = width # prevents padding at the end
-      
-      if len(src) + len(dst) > baseSpace:
-        src = uiTools.cropStr(src, baseSpace / 3)
-        dst = uiTools.cropStr(dst, baseSpace - len(src))
-      
-      # pads dst entry to its max space
-      dst = ("%%-%is" % (baseSpace - len(src))) % dst
-    
-    if myType == Category.INBOUND: src, dst = dst, src
-    padding = " " * (width - usedSpace + LABEL_MIN_PADDING)
-    return LABEL_FORMAT % (src, dst, etc, padding)
-  
-  def _getDetailContent(self, width):
-    """
-    Provides a list with detailed information for this connection.
-    
-    Arguments:
-      width - max length of lines
-    """
-    
-    lines = [""] * 7
-    lines[0] = "address: %s" % self.getDestinationLabel(width - 11)
-    lines[1] = "locale: %s" % ("??" if self.isPrivate() else self.foreign.getLocale("??"))
-    
-    # Remaining data concerns the consensus results, with three possible cases:
-    # - if there's a single match then display its details
-    # - if there's multiple potential relays then list all of the combinations
-    #   of ORPorts / Fingerprints
-    # - if no consensus data is available then say so (probably a client or
-    #   exit connection)
-    
-    fingerprint = self.foreign.getFingerprint()
-    conn = torTools.getConn()
-    
-    if fingerprint != "UNKNOWN":
-      # single match - display information available about it
-      nsEntry = conn.getConsensusEntry(fingerprint)
-      descEntry = conn.getDescriptorEntry(fingerprint)
-      
-      # append the fingerprint to the second line
-      lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint)
-      
-      if nsEntry:
-        # example consensus entry:
-        # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0
-        # s Exit Fast Guard Named Running Stable Valid
-        # w Bandwidth=2540
-        # p accept 20-23,43,53,79-81,88,110,143,194,443
-        
-        nsLines = nsEntry.split("\n")
-        
-        firstLineComp = nsLines[0].split(" ")
-        if len(firstLineComp) >= 9:
-          _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[:9]
-        else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", ""
-        
-        flags = "unknown"
-        if len(nsLines) >= 2 and nsLines[1].startswith("s "):
-          flags = nsLines[1][2:]
-        
-        # The network status exit policy doesn't exist for older tor versions.
-        # If unavailable we'll need the full exit policy which is on the
-        # descriptor (if that's available).
-        
-        exitPolicy = "unknown"
-        if len(nsLines) >= 4 and nsLines[3].startswith("p "):
-          exitPolicy = nsLines[3][2:].replace(",", ", ")
-        elif descEntry:
-          # the descriptor has an individual line for each entry in the exit policy
-          exitPolicyEntries = []
-          
-          for line in descEntry.split("\n"):
-            if line.startswith("accept") or line.startswith("reject"):
-              exitPolicyEntries.append(line.strip())
-          
-          exitPolicy = ", ".join(exitPolicyEntries)
-        
-        dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
-        lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
-        lines[3] = "published: %s %s" % (pubDate, pubTime)
-        lines[4] = "flags: %s" % flags.replace(" ", ", ")
-        lines[5] = "exit policy: %s" % exitPolicy
-      
-      if descEntry:
-        torVersion, platform, contact = "", "", ""
-        
-        for descLine in descEntry.split("\n"):
-          if descLine.startswith("platform"):
-            # has the tor version and platform, ex:
-            # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64
-            
-            torVersion = descLine[13:descLine.find(" ", 13)]
-            platform = descLine[descLine.rfind(" on ") + 4:]
-          elif descLine.startswith("contact"):
-            contact = descLine[8:]
-            
-            # clears up some highly common obscuring
-            for alias in (" at ", " AT "): contact = contact.replace(alias, "@")
-            for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".")
-            
-            break # contact lines come after the platform
-        
-        lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, torVersion)
-        
-        # contact information is an optional field
-        if contact: lines[6] = "contact: %s" % contact
-    else:
-      allMatches = conn.getRelayFingerprint(self.foreign.getIpAddr(), getAllMatches = True)
-      
-      if allMatches:
-        # multiple matches
-        lines[2] = "Multiple matches, possible fingerprints are:"
-        
-        for i in range(len(allMatches)):
-          isLastLine = i == 3
-          
-          relayPort, relayFingerprint = allMatches[i]
-          lineText = "%i. or port: %-5s fingerprint: %s" % (i, relayPort, relayFingerprint)
-          
-          # if there's multiple lines remaining at the end then give a count
-          remainingRelays = len(allMatches) - i
-          if isLastLine and remainingRelays > 1:
-            lineText = "... %i more" % remainingRelays
-          
-          lines[3 + i] = lineText
-          
-          if isLastLine: break
-      else:
-        # no consensus entry for this ip address
-        lines[2] = "No consensus data found"
-    
-    # crops any lines that are too long
-    for i in range(len(lines)):
-      lines[i] = uiTools.cropStr(lines[i], width - 2)
-    
-    return lines
-  
-  def getDestinationLabel(self, maxLength, includeLocale=False, includeHostname=False):
-    """
-    Provides a short description of the destination. This is made up of two
-    components, the base <ip addr>:<port> and an extra piece of information in
-    parentheses. The IP address is scrubbed from private connections.
-    
-    Extra information is...
-    - the port's purpose for exit connections
-    - the locale and/or hostname if set to do so, the address isn't private,
-      and isn't on the local network
-    - nothing otherwise
-    
-    Arguments:
-      maxLength       - maximum length of the string returned
-      includeLocale   - possibly includes the locale
-      includeHostname - possibly includes the hostname
-    """
-    
-    # the port and port derived data can be hidden by config or without includePort
-    includePort = self.includePort and (CONFIG["features.connection.showExitPort"] or self.getType() != Category.EXIT)
-    
-    # destination of the connection
-    ipLabel = "<scrubbed>" if self.isPrivate() else self.foreign.getIpAddr()
-    portLabel = ":%s" % self.foreign.getPort() if includePort else ""
-    dstAddress = ipLabel + portLabel
-    
-    # Only append the extra info if there's at least a couple characters of
-    # space (this is what's needed for the country codes).
-    if len(dstAddress) + 5 <= maxLength:
-      spaceAvailable = maxLength - len(dstAddress) - 3
-      
-      if self.getType() == Category.EXIT and includePort:
-        purpose = connections.getPortUsage(self.foreign.getPort())
-        
-        if purpose:
-          # BitTorrent is a common protocol to truncate, so just use "Torrent"
-          # if there's not enough room.
-          if len(purpose) > spaceAvailable and purpose == "BitTorrent":
-            purpose = "Torrent"
-          
-          # crops with a hyphen if too long
-          purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN)
-          
-          dstAddress += " (%s)" % purpose
-      elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
-        extraInfo = []
-        conn = torTools.getConn()
-        
-        if includeLocale and not conn.isGeoipUnavailable():
-          foreignLocale = self.foreign.getLocale("??")
-          extraInfo.append(foreignLocale)
-          spaceAvailable -= len(foreignLocale) + 2
-        
-        if includeHostname:
-          dstHostname = self.foreign.getHostname()
-          
-          if dstHostname:
-            # determines the full space available, taking into account the ", "
-            # dividers if there's multiple pieces of extra data
-            
-            maxHostnameSpace = spaceAvailable - 2 * len(extraInfo)
-            dstHostname = uiTools.cropStr(dstHostname, maxHostnameSpace)
-            extraInfo.append(dstHostname)
-            spaceAvailable -= len(dstHostname)
-        
-        if extraInfo:
-          dstAddress += " (%s)" % ", ".join(extraInfo)
-    
-    return dstAddress[:maxLength]
-
diff --git a/src/interface/connections/connPanel.py b/src/interface/connections/connPanel.py
deleted file mode 100644
index 79fe9df..0000000
--- a/src/interface/connections/connPanel.py
+++ /dev/null
@@ -1,398 +0,0 @@
-"""
-Listing of the currently established connections tor has made.
-"""
-
-import time
-import curses
-import threading
-
-from interface.connections import entries, connEntry, circEntry
-from util import connections, enum, panel, torTools, uiTools
-
-DEFAULT_CONFIG = {"features.connection.resolveApps": True,
-                  "features.connection.listingType": 0,
-                  "features.connection.refreshRate": 5}
-
-# height of the detail panel content, not counting top and bottom border
-DETAILS_HEIGHT = 7
-
-# listing types
-Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-
-DEFAULT_SORT_ORDER = (entries.SortAttr.CATEGORY, entries.SortAttr.LISTING, entries.SortAttr.UPTIME)
-
-class ConnectionPanel(panel.Panel, threading.Thread):
-  """
-  Listing of connections tor is making, with information correlated against
-  the current consensus and other data sources.
-  """
-  
-  def __init__(self, stdscr, config=None):
-    panel.Panel.__init__(self, stdscr, "conn", 0)
-    threading.Thread.__init__(self)
-    self.setDaemon(True)
-    
-    self._sortOrdering = DEFAULT_SORT_ORDER
-    self._config = dict(DEFAULT_CONFIG)
-    
-    if config:
-      config.update(self._config, {
-        "features.connection.listingType": (0, len(Listing.values()) - 1),
-        "features.connection.refreshRate": 1})
-      
-      sortFields = entries.SortAttr.values()
-      customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields))
-      
-      if customOrdering:
-        self._sortOrdering = [sortFields[i] for i in customOrdering]
-    
-    self._listingType = Listing.values()[self._config["features.connection.listingType"]]
-    self._scroller = uiTools.Scroller(True)
-    self._title = "Connections:" # title line of the panel
-    self._entries = []          # last fetched display entries
-    self._entryLines = []       # individual lines rendered from the entries listing
-    self._showDetails = False   # presents the details panel if true
-    
-    self._lastUpdate = -1       # time the content was last revised
-    self._isTorRunning = True   # indicates if tor is currently running or not
-    self._isPaused = True       # prevents updates if true
-    self._pauseTime = None      # time when the panel was paused
-    self._halt = False          # terminates thread if true
-    self._cond = threading.Condition()  # used for pausing the thread
-    self.valsLock = threading.RLock()
-    
-    # Last sampling received from the ConnectionResolver, used to detect when
-    # it changes.
-    self._lastResourceFetch = -1
-    
-    # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections
-    self._appResolver = connections.AppResolver("arm")
-    
-    # rate limits appResolver queries to once per update
-    self.appResolveSinceUpdate = False
-    
-    self._update()            # populates initial entries
-    self._resolveApps(False)  # resolves initial applications
-    
-    # mark the initially exitsing connection uptimes as being estimates
-    for entry in self._entries:
-      if isinstance(entry, connEntry.ConnectionEntry):
-        entry.getLines()[0].isInitialConnection = True
-    
-    # listens for when tor stops so we know to stop reflecting changes
-    torTools.getConn().addStatusListener(self.torStateListener)
-  
-  def torStateListener(self, conn, eventType):
-    """
-    Freezes the connection contents when Tor stops.
-    
-    Arguments:
-      conn      - tor controller
-      eventType - type of event detected
-    """
-    
-    self._isTorRunning = eventType == torTools.State.INIT
-    
-    if self._isPaused or not self._isTorRunning:
-      if not self._pauseTime: self._pauseTime = time.time()
-    else: self._pauseTime = None
-    
-    self.redraw(True)
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents the panel from updating.
-    """
-    
-    if not self._isPaused == isPause:
-      self._isPaused = isPause
-      
-      if isPause or not self._isTorRunning:
-        if not self._pauseTime: self._pauseTime = time.time()
-      else: self._pauseTime = None
-      
-      # redraws so the display reflects any changes between the last update
-      # and being paused
-      self.redraw(True)
-  
-  def setSortOrder(self, ordering = None):
-    """
-    Sets the connection attributes we're sorting by and resorts the contents.
-    
-    Arguments:
-      ordering - new ordering, if undefined then this resorts with the last
-                 set ordering
-    """
-    
-    self.valsLock.acquire()
-    if ordering: self._sortOrdering = ordering
-    self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
-    
-    self._entryLines = []
-    for entry in self._entries:
-      self._entryLines += entry.getLines()
-    self.valsLock.release()
-  
-  def setListingType(self, listingType):
-    """
-    Sets the priority information presented by the panel.
-    
-    Arguments:
-      listingType - Listing instance for the primary information to be shown
-    """
-    
-    self.valsLock.acquire()
-    self._listingType = listingType
-    
-    # if we're sorting by the listing then we need to resort
-    if entries.SortAttr.LISTING in self._sortOrdering:
-      self.setSortOrder()
-    
-    self.valsLock.release()
-  
-  def handleKey(self, key):
-    self.valsLock.acquire()
-    
-    if uiTools.isScrollKey(key):
-      pageHeight = self.getPreferredSize()[0] - 1
-      if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
-      isChanged = self._scroller.handleKey(key, self._entryLines, pageHeight)
-      if isChanged: self.redraw(True)
-    elif uiTools.isSelectionKey(key):
-      self._showDetails = not self._showDetails
-      self.redraw(True)
-    
-    self.valsLock.release()
-  
-  def run(self):
-    """
-    Keeps connections listing updated, checking for new entries at a set rate.
-    """
-    
-    lastDraw = time.time() - 1
-    while not self._halt:
-      currentTime = time.time()
-      
-      if self._isPaused or not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]:
-        self._cond.acquire()
-        if not self._halt: self._cond.wait(0.2)
-        self._cond.release()
-      else:
-        # updates content if their's new results, otherwise just redraws
-        self._update()
-        self.redraw(True)
-        
-        # we may have missed multiple updates due to being paused, showing
-        # another panel, etc so lastDraw might need to jump multiple ticks
-        drawTicks = (time.time() - lastDraw) / self._config["features.connection.refreshRate"]
-        lastDraw += self._config["features.connection.refreshRate"] * drawTicks
-  
-  def draw(self, width, height):
-    self.valsLock.acquire()
-    
-    # extra line when showing the detail panel is for the bottom border
-    detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0
-    isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1
-    
-    scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1)
-    cursorSelection = self._scroller.getCursorSelection(self._entryLines)
-    
-    # draws the detail panel if currently displaying it
-    if self._showDetails:
-      # This is a solid border unless the scrollbar is visible, in which case a
-      # 'T' pipe connects the border to the bar.
-      uiTools.drawBox(self, 0, 0, width, DETAILS_HEIGHT + 2)
-      if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
-      
-      drawEntries = cursorSelection.getDetails(width)
-      for i in range(min(len(drawEntries), DETAILS_HEIGHT)):
-        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)
-    
-    scrollOffset = 1
-    if isScrollbarVisible:
-      scrollOffset = 3
-      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._entryLines), 1 + detailPanelOffset)
-    
-    currentTime = self._pauseTime if self._pauseTime else time.time()
-    for lineNum in range(scrollLoc, len(self._entryLines)):
-      entryLine = self._entryLines[lineNum]
-      
-      # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up
-      # resolution for the applicaitions they belong to
-      if isinstance(entryLine, connEntry.ConnectionLine) and entryLine.isUnresolvedApp():
-        self._resolveApps()
-      
-      # hilighting if this is the selected line
-      extraFormat = curses.A_STANDOUT if entryLine == cursorSelection else curses.A_NORMAL
-      
-      drawEntry = entryLine.getListingEntry(width - scrollOffset, currentTime, self._listingType)
-      drawLine = lineNum + detailPanelOffset + 1 - scrollLoc
-      drawEntry.render(self, drawLine, scrollOffset, extraFormat)
-      if drawLine >= height: break
-    
-    self.valsLock.release()
-  
-  def stop(self):
-    """
-    Halts further resolutions and terminates the thread.
-    """
-    
-    self._cond.acquire()
-    self._halt = True
-    self._cond.notifyAll()
-    self._cond.release()
-  
-  def _update(self):
-    """
-    Fetches the newest resolved connections.
-    """
-    
-    connResolver = connections.getResolver("tor")
-    currentResolutionCount = connResolver.getResolutionCount()
-    self.appResolveSinceUpdate = False
-    
-    if self._lastResourceFetch != currentResolutionCount:
-      self.valsLock.acquire()
-      
-      newEntries = [] # the new results we'll display
-      
-      # Fetches new connections and client circuits...
-      # newConnections  [(local ip, local port, foreign ip, foreign port)...]
-      # newCircuits     {circuitID => (status, purpose, path)...}
-      
-      newConnections = connResolver.getConnections()
-      newCircuits = {}
-      
-      for circuitID, status, purpose, path in torTools.getConn().getCircuits():
-        # Skips established single-hop circuits (these are for directory
-        # fetches, not client circuits)
-        if not (status == "BUILT" and len(path) == 1):
-          newCircuits[circuitID] = (status, purpose, path)
-      
-      # Populates newEntries with any of our old entries that still exist.
-      # This is both for performance and to keep from resetting the uptime
-      # attributes. Note that CircEntries are a ConnectionEntry subclass so
-      # we need to check for them first.
-      
-      for oldEntry in self._entries:
-        if isinstance(oldEntry, circEntry.CircEntry):
-          newEntry = newCircuits.get(oldEntry.circuitID)
-          
-          if newEntry:
-            oldEntry.update(newEntry[0], newEntry[2])
-            newEntries.append(oldEntry)
-            del newCircuits[oldEntry.circuitID]
-        elif isinstance(oldEntry, connEntry.ConnectionEntry):
-          connLine = oldEntry.getLines()[0]
-          connAttr = (connLine.local.getIpAddr(), connLine.local.getPort(),
-                      connLine.foreign.getIpAddr(), connLine.foreign.getPort())
-          
-          if connAttr in newConnections:
-            newEntries.append(oldEntry)
-            newConnections.remove(connAttr)
-      
-      # Reset any display attributes for the entries we're keeping
-      for entry in newEntries: entry.resetDisplay()
-      
-      # Adds any new connection and circuit entries.
-      for lIp, lPort, fIp, fPort in newConnections:
-        newConnEntry = connEntry.ConnectionEntry(lIp, lPort, fIp, fPort)
-        if newConnEntry.getLines()[0].getType() != connEntry.Category.CIRCUIT:
-          newEntries.append(newConnEntry)
-      
-      for circuitID in newCircuits:
-        status, purpose, path = newCircuits[circuitID]
-        newEntries.append(circEntry.CircEntry(circuitID, status, purpose, path))
-      
-      # Counts the relays in each of the categories. This also flushes the
-      # type cache for all of the connections (in case its changed since last
-      # fetched).
-      
-      categoryTypes = connEntry.Category.values()
-      typeCounts = dict((type, 0) for type in categoryTypes)
-      for entry in newEntries:
-        if isinstance(entry, connEntry.ConnectionEntry):
-          typeCounts[entry.getLines()[0].getType()] += 1
-        elif isinstance(entry, circEntry.CircEntry):
-          typeCounts[connEntry.Category.CIRCUIT] += 1
-      
-      # makes labels for all the categories with connections (ie,
-      # "21 outbound", "1 control", etc)
-      countLabels = []
-      
-      for category in categoryTypes:
-        if typeCounts[category] > 0:
-          countLabels.append("%i %s" % (typeCounts[category], category.lower()))
-      
-      if countLabels: self._title = "Connections (%s):" % ", ".join(countLabels)
-      else: self._title = "Connections:"
-      
-      self._entries = newEntries
-      
-      self._entryLines = []
-      for entry in self._entries:
-        self._entryLines += entry.getLines()
-      
-      self.setSortOrder()
-      self._lastResourceFetch = currentResolutionCount
-      self.valsLock.release()
-  
-  def _resolveApps(self, flagQuery = True):
-    """
-    Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and
-    CONTROL entries.
-    
-    Arguments:
-      flagQuery - sets a flag to prevent further call from being respected
-                  until the next update if true
-    """
-    
-    if self.appResolveSinceUpdate or not self._config["features.connection.resolveApps"]: return
-    unresolvedLines = [l for l in self._entryLines if isinstance(l, connEntry.ConnectionLine) and l.isUnresolvedApp()]
-    
-    # get the ports used for unresolved applications
-    appPorts = []
-    
-    for line in unresolvedLines:
-      appConn = line.local if line.getType() == connEntry.Category.HIDDEN else line.foreign
-      appPorts.append(appConn.getPort())
-    
-    # Queue up resolution for the unresolved ports (skips if it's still working
-    # on the last query).
-    if appPorts and not self._appResolver.isResolving:
-      self._appResolver.resolve(appPorts)
-    
-    # Fetches results. If the query finishes quickly then this is what we just
-    # asked for, otherwise these belong to an earlier resolution.
-    #
-    # The application resolver might have given up querying (for instance, if
-    # the lsof lookups aren't working on this platform or lacks permissions).
-    # The isAppResolving flag lets the unresolved entries indicate if there's
-    # a lookup in progress for them or not.
-    
-    appResults = self._appResolver.getResults(0.2)
-    
-    for line in unresolvedLines:
-      isLocal = line.getType() == connEntry.Category.HIDDEN
-      linePort = line.local.getPort() if isLocal else line.foreign.getPort()
-      
-      if linePort in appResults:
-        # sets application attributes if there's a result with this as the
-        # inbound port
-        for inboundPort, outboundPort, cmd, pid in appResults[linePort]:
-          appPort = outboundPort if isLocal else inboundPort
-          
-          if linePort == appPort:
-            line.appName = cmd
-            line.appPid = pid
-            line.isAppResolving = False
-      else:
-        line.isAppResolving = self._appResolver.isResolving
-    
-    if flagQuery:
-      self.appResolveSinceUpdate = True
-
diff --git a/src/interface/connections/entries.py b/src/interface/connections/entries.py
deleted file mode 100644
index 6b24412..0000000
--- a/src/interface/connections/entries.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""
-Interface for entries in the connection panel. These consist of two parts: the
-entry itself (ie, Tor connection, client circuit, etc) and the lines it
-consists of in the listing.
-"""
-
-from util import enum
-
-# attributes we can list entries by
-ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-
-SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT",
-                     "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY")
-
-SORT_COLORS = {SortAttr.CATEGORY: "red",      SortAttr.UPTIME: "yellow",
-               SortAttr.LISTING: "green",     SortAttr.IP_ADDRESS: "blue",
-               SortAttr.PORT: "blue",         SortAttr.HOSTNAME: "magenta",
-               SortAttr.FINGERPRINT: "cyan",  SortAttr.NICKNAME: "cyan",
-               SortAttr.COUNTRY: "blue"}
-
-# maximum number of ports a system can have
-PORT_COUNT = 65536
-
-class ConnectionPanelEntry:
-  """
-  Common parent for connection panel entries. This consists of a list of lines
-  in the panel listing. This caches results until the display indicates that
-  they should be flushed.
-  """
-  
-  def __init__(self):
-    self.lines = []
-    self.flushCache = True
-  
-  def getLines(self):
-    """
-    Provides the individual lines in the connection listing.
-    """
-    
-    if self.flushCache:
-      self.lines = self._getLines(self.lines)
-      self.flushCache = False
-    
-    return self.lines
-  
-  def _getLines(self, oldResults):
-    # implementation of getLines
-    
-    for line in oldResults:
-      line.resetDisplay()
-    
-    return oldResults
-  
-  def getSortValues(self, sortAttrs, listingType):
-    """
-    Provides the value used in comparisons to sort based on the given
-    attribute.
-    
-    Arguments:
-      sortAttrs   - list of SortAttr values for the field being sorted on
-      listingType - ListingType enumeration for the attribute we're listing
-                    entries by
-    """
-    
-    return [self.getSortValue(attr, listingType) for attr in sortAttrs]
-  
-  def getSortValue(self, attr, listingType):
-    """
-    Provides the value of a single attribute used for sorting purposes.
-    
-    Arguments:
-      attr        - list of SortAttr values for the field being sorted on
-      listingType - ListingType enumeration for the attribute we're listing
-                    entries by
-    """
-    
-    if attr == SortAttr.LISTING:
-      if listingType == ListingType.IP_ADDRESS:
-        # uses the IP address as the primary value, and port as secondary
-        sortValue = self.getSortValue(SortAttr.IP_ADDRESS, listingType) * PORT_COUNT
-        sortValue += self.getSortValue(SortAttr.PORT, listingType)
-        return sortValue
-      elif listingType == ListingType.HOSTNAME:
-        return self.getSortValue(SortAttr.HOSTNAME, listingType)
-      elif listingType == ListingType.FINGERPRINT:
-        return self.getSortValue(SortAttr.FINGERPRINT, listingType)
-      elif listingType == ListingType.NICKNAME:
-        return self.getSortValue(SortAttr.NICKNAME, listingType)
-    
-    return ""
-  
-  def resetDisplay(self):
-    """
-    Flushes cached display results.
-    """
-    
-    self.flushCache = True
-
-class ConnectionPanelLine:
-  """
-  Individual line in the connection panel listing.
-  """
-  
-  def __init__(self):
-    # cache for displayed information
-    self._listingCache = None
-    self._listingCacheArgs = (None, None)
-    
-    self._detailsCache = None
-    self._detailsCacheArgs = None
-    
-    self._descriptorCache = None
-    self._descriptorCacheArgs = None
-  
-  def getListingEntry(self, width, currentTime, listingType):
-    """
-    Provides a DrawEntry instance for contents to be displayed in the
-    connection panel listing.
-    
-    Arguments:
-      width       - available space to display in
-      currentTime - unix timestamp for what the results should consider to be
-                    the current time (this may be ignored due to caching)
-      listingType - ListingType enumeration for the highest priority content
-                    to be displayed
-    """
-    
-    if self._listingCacheArgs != (width, listingType):
-      self._listingCache = self._getListingEntry(width, currentTime, listingType)
-      self._listingCacheArgs = (width, listingType)
-    
-    return self._listingCache
-  
-  def _getListingEntry(self, width, currentTime, listingType):
-    # implementation of getListingEntry
-    return None
-  
-  def getDetails(self, width):
-    """
-    Provides a list of DrawEntry instances with detailed information for this
-    connection.
-    
-    Arguments:
-      width - available space to display in
-    """
-    
-    if self._detailsCacheArgs != width:
-      self._detailsCache = self._getDetails(width)
-      self._detailsCacheArgs = width
-    
-    return self._detailsCache
-  
-  def _getDetails(self, width):
-    # implementation of getDetails
-    return []
-  
-  def resetDisplay(self):
-    """
-    Flushes cached display results.
-    """
-    
-    self._listingCacheArgs = (None, None)
-    self._detailsCacheArgs = None
-
diff --git a/src/interface/controller.py b/src/interface/controller.py
deleted file mode 100644
index 5060188..0000000
--- a/src/interface/controller.py
+++ /dev/null
@@ -1,1584 +0,0 @@
-#!/usr/bin/env python
-# controller.py -- arm interface (curses monitor for relay status)
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-"""
-Curses (terminal) interface for the arm relay status monitor.
-"""
-
-import os
-import re
-import math
-import time
-import curses
-import curses.textpad
-import socket
-from TorCtl import TorCtl
-
-import headerPanel
-import graphing.graphPanel
-import logPanel
-import configPanel
-import torrcPanel
-import descriptorPopup
-
-import interface.connections.connPanel
-import interface.connections.connEntry
-import interface.connections.entries
-from util import conf, log, connections, hostnames, panel, sysTools, torConfig, torTools, uiTools
-import graphing.bandwidthStats
-import graphing.connStats
-import graphing.resourceStats
-
-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)
-
-# panel order per page
-PAGE_S = ["header", "control", "popup"] # sticky (ie, always available) page
-PAGES = [
-  ["graph", "log"],
-  ["conn"],
-  ["config"],
-  ["torrc"]]
-
-PAUSEABLE = ["header", "graph", "log", "conn"]
-
-CONFIG = {"log.torrc.readFailed": log.WARN,
-          "features.graph.type": 1,
-          "features.config.prepopulateEditValues": True,
-          "queries.refreshRate.rate": 5,
-          "log.torEventTypeUnrecognized": log.NOTICE,
-          "features.graph.bw.prepopulate": True,
-          "log.startTime": log.INFO,
-          "log.refreshRate": log.DEBUG,
-          "log.highCpuUsage": log.WARN,
-          "log.configEntryUndefined": log.NOTICE,
-          "log.torrc.validation.torStateDiffers": log.WARN,
-          "log.torrc.validation.unnecessaryTorrcEntries": log.NOTICE}
-
-class ControlPanel(panel.Panel):
-  """ Draws single line label for interface controls. """
-  
-  def __init__(self, stdscr, isBlindMode):
-    panel.Panel.__init__(self, stdscr, "control", 0, 1)
-    self.msgText = CTL_HELP           # message text to be displyed
-    self.msgAttr = curses.A_NORMAL    # formatting attributes
-    self.page = 1                     # page number currently being displayed
-    self.resolvingCounter = -1        # count of resolver when starting (-1 if we aren't working on a batch)
-    self.isBlindMode = isBlindMode
-  
-  def setMsg(self, msgText, msgAttr=curses.A_NORMAL):
-    """
-    Sets the message and display attributes. If msgType matches CTL_HELP or
-    CTL_PAUSED then uses the default message for those statuses.
-    """
-    
-    self.msgText = msgText
-    self.msgAttr = msgAttr
-  
-  def draw(self, width, height):
-    msgText = self.msgText
-    msgAttr = self.msgAttr
-    barTab = 2                # space between msgText and progress bar
-    barWidthMax = 40          # max width to progress bar
-    barWidth = -1             # space between "[ ]" in progress bar (not visible if -1)
-    barProgress = 0           # cells to fill
-    
-    if msgText == CTL_HELP:
-      msgAttr = curses.A_NORMAL
-      
-      if self.resolvingCounter != -1:
-        if hostnames.isPaused() or not hostnames.isResolving():
-          # done resolving dns batch
-          self.resolvingCounter = -1
-          curses.halfdelay(REFRESH_RATE * 10) # revert to normal refresh rate
-        else:
-          batchSize = hostnames.getRequestCount() - self.resolvingCounter
-          entryCount = batchSize - hostnames.getPendingCount()
-          if batchSize > 0: progress = 100 * entryCount / batchSize
-          else: progress = 0
-          
-          additive = "or l " if self.page == 2 else ""
-          batchSizeDigits = int(math.log10(batchSize)) + 1
-          entryCountLabel = ("%%%ii" % batchSizeDigits) % entryCount
-          #msgText = "Resolving hostnames (%i / %i, %i%%) - press esc %sto cancel" % (entryCount, batchSize, progress, additive)
-          msgText = "Resolving hostnames (press esc %sto cancel) - %s / %i, %2i%%" % (additive, entryCountLabel, batchSize, progress)
-          
-          barWidth = min(barWidthMax, width - len(msgText) - 3 - barTab)
-          barProgress = barWidth * entryCount / batchSize
-      
-      if self.resolvingCounter == -1:
-        currentPage = self.page
-        pageCount = len(PAGES)
-        
-        if self.isBlindMode:
-          if currentPage >= 2: currentPage -= 1
-          pageCount -= 1
-        
-        msgText = "page %i / %i - q: quit, p: pause, h: page help" % (currentPage, pageCount)
-    elif msgText == CTL_PAUSED:
-      msgText = "Paused"
-      msgAttr = curses.A_STANDOUT
-    
-    self.addstr(0, 0, msgText, msgAttr)
-    if barWidth > -1:
-      xLoc = len(msgText) + barTab
-      self.addstr(0, xLoc, "[", curses.A_BOLD)
-      self.addstr(0, xLoc + 1, " " * barProgress, curses.A_STANDOUT | uiTools.getColor("red"))
-      self.addstr(0, xLoc + barWidth + 1, "]", curses.A_BOLD)
-
-class Popup(panel.Panel):
-  """
-  Temporarily providing old panel methods until permanent workaround for popup
-  can be derrived (this passive drawing method is horrible - I'll need to
-  provide a version using the more active repaint design later in the
-  revision).
-  """
-  
-  def __init__(self, stdscr, height):
-    panel.Panel.__init__(self, stdscr, "popup", 0, height)
-  
-  # The following methods are to emulate old panel functionality (this was the
-  # only implementations to use these methods and will require a complete
-  # rewrite when refactoring gets here)
-  def clear(self):
-    if self.win:
-      self.isDisplaced = self.top > self.win.getparyx()[0]
-      if not self.isDisplaced: self.win.erase()
-  
-  def refresh(self):
-    if self.win and not self.isDisplaced: self.win.refresh()
-  
-  def recreate(self, stdscr, newWidth=-1, newTop=None):
-    self.setParent(stdscr)
-    self.setWidth(newWidth)
-    if newTop != None: self.setTop(newTop)
-    
-    newHeight, newWidth = self.getPreferredSize()
-    if newHeight > 0:
-      self.win = self.parent.subwin(newHeight, newWidth, self.top, 0)
-    elif self.win == None:
-      # don't want to leave the window as none (in very edge cases could cause
-      # problems) - rather, create a displaced instance
-      self.win = self.parent.subwin(1, newWidth, 0, 0)
-    
-    self.maxY, self.maxX = self.win.getmaxyx()
-
-def addstr_wrap(panel, y, x, text, formatting, startX = 0, endX = -1, maxY = -1):
-  """
-  Writes text with word wrapping, returning the ending y/x coordinate.
-  y: starting write line
-  x: column offset from startX
-  text / formatting: content to be written
-  startX / endX: column bounds in which text may be written
-  """
-  
-  # moved out of panel (trying not to polute new code!)
-  # TODO: unpleaseantly complex usage - replace with something else when
-  # rewriting confPanel and descriptorPopup (the only places this is used)
-  if not text: return (y, x)          # nothing to write
-  if endX == -1: endX = panel.maxX     # defaults to writing to end of panel
-  if maxY == -1: maxY = panel.maxY + 1 # defaults to writing to bottom of panel
-  lineWidth = endX - startX           # room for text
-  while True:
-    if len(text) > lineWidth - x - 1:
-      chunkSize = text.rfind(" ", 0, lineWidth - x)
-      writeText = text[:chunkSize]
-      text = text[chunkSize:].strip()
-      
-      panel.addstr(y, x + startX, writeText, formatting)
-      y, x = y + 1, 0
-      if y >= maxY: return (y, x)
-    else:
-      panel.addstr(y, x + startX, text, formatting)
-      return (y, x + len(text))
-
-class sighupListener(TorCtl.PostEventListener):
-  """
-  Listens for reload signal (hup), which is produced by:
-  pkill -sighup tor
-  causing the torrc and internal state to be reset.
-  """
-  
-  def __init__(self):
-    TorCtl.PostEventListener.__init__(self)
-    self.isReset = False
-  
-  def msg_event(self, event):
-    self.isReset |= event.level == "NOTICE" and event.msg.startswith("Received reload signal (hup)")
-
-def setPauseState(panels, monitorIsPaused, currentPage, overwrite=False):
-  """
-  Resets the isPaused state of panels. If overwrite is True then this pauses
-  reguardless of the monitor is paused or not.
-  """
-  
-  for key in PAUSEABLE: 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 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
-  # 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
-  again.
-  """
-  
-  if connections.isResolverAlive("tor"):
-    resolver = connections.getResolver("tor")
-    resolver.setPaused(eventType == torTools.State.CLOSED)
-
-def selectiveRefresh(panels, page):
-  """
-  This forces a redraw of content on the currently active page (should be done
-  after changing pages, popups, or anything else that overwrites panels).
-  """
-  
-  for panelKey in PAGES[page]:
-    panels[panelKey].redraw(True)
-
-def drawTorMonitor(stdscr, startTime, loggedEvents, isBlindMode):
-  """
-  Starts arm interface reflecting information on provided control port.
-  
-  stdscr - curses window
-  conn - active Tor control port connection
-  loggedEvents - types of events to be logged (plus an optional "UNKNOWN" for
-    otherwise unrecognized events)
-  """
-  
-  # loads config for various interface components
-  config = conf.getConfig("arm")
-  config.update(CONFIG)
-  graphing.graphPanel.loadConfig(config)
-  interface.connections.connEntry.loadConfig(config)
-  
-  # adds events needed for arm functionality to the torTools REQ_EVENTS mapping
-  # (they're then included with any setControllerEvents call, and log a more
-  # helpful error if unavailable)
-  torTools.REQ_EVENTS["BW"] = "bandwidth graph won't function"
-  
-  if not isBlindMode:
-    torTools.REQ_EVENTS["CIRC"] = "may cause issues in identifying client connections"
-  
-  # pauses/unpauses connection resolution according to if tor's connected or not
-  torTools.getConn().addStatusListener(connResetListener)
-  
-  # TODO: incrementally drop this requirement until everything's using the singleton
-  conn = torTools.getConn().getTorCtl()
-  
-  curses.halfdelay(REFRESH_RATE * 10)   # uses getch call as timer for REFRESH_RATE seconds
-  try: curses.use_default_colors()      # allows things like semi-transparent backgrounds (call can fail with ERR)
-  except curses.error: pass
-  
-  # attempts to make the cursor invisible (not supported in all terminals)
-  try: curses.curs_set(0)
-  except curses.error: pass
-  
-  # attempts to determine tor's current pid (left as None if unresolveable, logging an error later)
-  torPid = torTools.getConn().getMyPid()
-  
-  #try:
-  #  confLocation = conn.get_info("config-file")["config-file"]
-  #  if confLocation[0] != "/":
-  #    # relative path - attempt to add process pwd
-  #    try:
-  #      results = sysTools.call("pwdx %s" % torPid)
-  #      if len(results) == 1 and len(results[0].split()) == 2: confLocation = "%s/%s" % (results[0].split()[1], confLocation)
-  #    except IOError: pass # pwdx call failed
-  #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-  #  confLocation = ""
-  
-  # loads the torrc and provides warnings in case of validation errors
-  loadedTorrc = torConfig.getTorrc()
-  loadedTorrc.getLock().acquire()
-  
-  try:
-    loadedTorrc.load()
-  except IOError, exc:
-    msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
-    log.log(CONFIG["log.torrc.readFailed"], msg)
-  
-  if loadedTorrc.isLoaded():
-    corrections = loadedTorrc.getCorrections()
-    duplicateOptions, defaultOptions, mismatchLines, missingOptions = [], [], [], []
-    
-    for lineNum, issue, msg in corrections:
-      if issue == torConfig.ValidationError.DUPLICATE:
-        duplicateOptions.append("%s (line %i)" % (msg, lineNum + 1))
-      elif issue == torConfig.ValidationError.IS_DEFAULT:
-        defaultOptions.append("%s (line %i)" % (msg, lineNum + 1))
-      elif issue == torConfig.ValidationError.MISMATCH: mismatchLines.append(lineNum + 1)
-      elif issue == torConfig.ValidationError.MISSING: missingOptions.append(msg)
-    
-    if duplicateOptions or defaultOptions:
-      msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page."
-      
-      if duplicateOptions:
-        if len(duplicateOptions) > 1:
-          msg += "\n- entries ignored due to having duplicates: "
-        else:
-          msg += "\n- entry ignored due to having a duplicate: "
-        
-        duplicateOptions.sort()
-        msg += ", ".join(duplicateOptions)
-      
-      if defaultOptions:
-        if len(defaultOptions) > 1:
-          msg += "\n- entries match their default values: "
-        else:
-          msg += "\n- entry matches its default value: "
-        
-        defaultOptions.sort()
-        msg += ", ".join(defaultOptions)
-      
-      log.log(CONFIG["log.torrc.validation.unnecessaryTorrcEntries"], msg)
-    
-    if mismatchLines or missingOptions:
-      msg = "The torrc differ from what tor's using. You can issue a sighup to reload the torrc values by pressing x."
-      
-      if mismatchLines:
-        if len(mismatchLines) > 1:
-          msg += "\n- torrc values differ on lines: "
-        else:
-          msg += "\n- torrc value differs on line: "
-        
-        mismatchLines.sort()
-        msg += ", ".join([str(val + 1) for val in mismatchLines])
-        
-      if missingOptions:
-        if len(missingOptions) > 1:
-          msg += "\n- configuration values are missing from the torrc: "
-        else:
-          msg += "\n- configuration value is missing from the torrc: "
-        
-        missingOptions.sort()
-        msg += ", ".join(missingOptions)
-      
-      log.log(CONFIG["log.torrc.validation.torStateDiffers"], msg)
-  
-  loadedTorrc.getLock().release()
-  
-  # minor refinements for connection resolver
-  if not isBlindMode:
-    if torPid:
-      # use the tor pid to help narrow connection results
-      torCmdName = sysTools.getProcessName(torPid, "tor")
-      resolver = connections.getResolver(torCmdName, torPid, "tor")
-    else:
-      resolver = connections.getResolver("tor")
-  
-  # hack to display a better (arm specific) notice if all resolvers fail
-  connections.RESOLVER_FINAL_FAILURE_MSG += " (connection related portions of the monitor won't function)"
-  
-  panels = {
-    "header": headerPanel.HeaderPanel(stdscr, startTime, config),
-    "popup": Popup(stdscr, 9),
-    "graph": graphing.graphPanel.GraphPanel(stdscr),
-    "log": logPanel.LogPanel(stdscr, loggedEvents, config)}
-  
-  # TODO: later it would be good to set the right 'top' values during initialization, 
-  # but for now this is just necessary for the log panel (and a hack in the log...)
-  
-  # TODO: bug from not setting top is that the log panel might attempt to draw
-  # before being positioned - the following is a quick hack til rewritten
-  panels["log"].setPaused(True)
-  
-  panels["conn"] = interface.connections.connPanel.ConnectionPanel(stdscr, config)
-  
-  panels["control"] = ControlPanel(stdscr, isBlindMode)
-  panels["config"] = configPanel.ConfigPanel(stdscr, configPanel.State.TOR, config)
-  panels["torrc"] = torrcPanel.TorrcPanel(stdscr, torrcPanel.Config.TORRC, config)
-  
-  # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
-  if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing")
-  
-  # statistical monitors for graph
-  panels["graph"].addStats("bandwidth", graphing.bandwidthStats.BandwidthStats(config))
-  panels["graph"].addStats("system resources", graphing.resourceStats.ResourceStats())
-  if not isBlindMode: panels["graph"].addStats("connections", graphing.connStats.ConnStats())
-  
-  # sets graph based on config parameter
-  graphType = CONFIG["features.graph.type"]
-  if graphType == 0: panels["graph"].setStats(None)
-  elif graphType == 1: panels["graph"].setStats("bandwidth")
-  elif graphType == 2 and not isBlindMode: panels["graph"].setStats("connections")
-  elif graphType == 3: panels["graph"].setStats("system resources")
-  
-  # listeners that update bandwidth and log panels with Tor status
-  sighupTracker = sighupListener()
-  #conn.add_event_listener(panels["log"])
-  conn.add_event_listener(panels["graph"].stats["bandwidth"])
-  conn.add_event_listener(panels["graph"].stats["system resources"])
-  if not isBlindMode: conn.add_event_listener(panels["graph"].stats["connections"])
-  conn.add_event_listener(sighupTracker)
-  
-  # prepopulates bandwidth values from state file
-  if CONFIG["features.graph.bw.prepopulate"]:
-    isSuccessful = panels["graph"].stats["bandwidth"].prepopulateFromState()
-    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
-  
-  # directs logged TorCtl events to log panel
-  #TorUtil.loglevel = "DEBUG"
-  #TorUtil.logfile = panels["log"]
-  #torTools.getConn().addTorCtlListener(panels["log"].tor_ctl_event)
-  
-  # provides a notice about any event types tor supports but arm doesn't
-  missingEventTypes = logPanel.getMissingEventTypes()
-  if missingEventTypes:
-    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)))
-  
-  # tells revised panels to run as daemons
-  panels["header"].start()
-  panels["log"].start()
-  panels["conn"].start()
-  
-  # warns if tor isn't updating descriptors
-  #try:
-  #  if conn.get_option("FetchUselessDescriptors")[0][1] == "0" and conn.get_option("DirPort")[0][1] == "0":
-  #    warning = """Descriptors won't be updated (causing some connection information to be stale) unless:
-  #a. 'FetchUselessDescriptors 1' is set in your torrc
-  #b. the directory service is provided ('DirPort' defined)
-  #c. or tor is used as a client"""
-  #    log.log(log.WARN, warning)
-  #except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): pass
-  
-  isUnresponsive = False    # true if it's been over ten seconds since the last BW event (probably due to Tor closing)
-  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...)
-  
-  # provides notice about any unused config keys
-  for key in config.getUnusedKeys():
-    log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
-  
-  lastPerformanceLog = 0 # ensures we don't do performance logging too frequently
-  redrawStartTime = time.time()
-  
-  # TODO: popups need to force the panels it covers to redraw (or better, have
-  # a global refresh function for after changing pages, popups, etc)
-  
-  initTime = time.time() - startTime
-  log.log(CONFIG["log.startTime"], "arm started (initialization took %0.3f seconds)" % initTime)
-  
-  # attributes to give a WARN level event if arm's resource usage is too high
-  isResourceWarningGiven = False
-  lastResourceCheck = startTime
-  
-  lastSize = None
-  
-  # sets initial visiblity for the pages
-  for i in range(len(PAGES)):
-    isVisible = i == page
-    for entry in PAGES[i]: panels[entry].setVisible(isVisible)
-  
-  # TODO: come up with a nice, clean method for other threads to immediately
-  # terminate the draw loop and provide a stacktrace
-  while True:
-    # tried only refreshing when the screen was resized but it caused a
-    # noticeable lag when resizing and didn't have an appreciable effect
-    # on system usage
-    
-    panel.CURSES_LOCK.acquire()
-    try:
-      redrawStartTime = time.time()
-      
-      # if sighup received then reload related information
-      if sighupTracker.isReset:
-        #panels["header"]._updateParams(True)
-        
-        # other panels that use torrc data
-        #if not isBlindMode: panels["graph"].stats["connections"].resetOptions(conn)
-        #panels["graph"].stats["bandwidth"].resetOptions()
-        
-        # if bandwidth graph is being shown then height might have changed
-        if panels["graph"].currentDisplay == "bandwidth":
-          panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getContentHeight())
-        
-        # TODO: should redraw the torrcPanel
-        #panels["torrc"].loadConfig()
-        
-        # reload the torrc if it's previously been loaded
-        if loadedTorrc.isLoaded():
-          try:
-            loadedTorrc.load()
-            if page == 3: panels["torrc"].redraw(True)
-          except IOError, exc:
-            msg = "Unable to load torrc (%s)" % sysTools.getFileErrorMsg(exc)
-            log.log(CONFIG["log.torrc.readFailed"], msg)
-        
-        sighupTracker.isReset = False
-      
-      # gives panels a chance to take advantage of the maximum bounds
-      # originally this checked in the bounds changed but 'recreate' is a no-op
-      # if panel properties are unchanged and checking every redraw is more
-      # resilient in case of funky changes (such as resizing during popups)
-      
-      # hack to make sure header picks layout before using the dimensions below
-      #panels["header"].getPreferredSize()
-      
-      startY = 0
-      for panelKey in PAGE_S[:2]:
-        #panels[panelKey].recreate(stdscr, -1, startY)
-        panels[panelKey].setParent(stdscr)
-        panels[panelKey].setWidth(-1)
-        panels[panelKey].setTop(startY)
-        startY += panels[panelKey].getHeight()
-      
-      panels["popup"].recreate(stdscr, 80, startY)
-      
-      for panelSet in PAGES:
-        tmpStartY = startY
-        
-        for panelKey in panelSet:
-          #panels[panelKey].recreate(stdscr, -1, tmpStartY)
-          panels[panelKey].setParent(stdscr)
-          panels[panelKey].setWidth(-1)
-          panels[panelKey].setTop(tmpStartY)
-          tmpStartY += panels[panelKey].getHeight()
-      
-      # provides a notice if there's been ten seconds since the last BW event
-      lastHeartbeat = torTools.getConn().getHeartbeat()
-      if torTools.getConn().isAlive() and "BW" in torTools.getConn().getControllerEvents() and lastHeartbeat != 0:
-        if not isUnresponsive and (time.time() - lastHeartbeat) >= 10:
-          isUnresponsive = True
-          log.log(log.NOTICE, "Relay unresponsive (last heartbeat: %s)" % time.ctime(lastHeartbeat))
-        elif isUnresponsive and (time.time() - lastHeartbeat) < 10:
-          # really shouldn't happen (meant Tor froze for a bit)
-          isUnresponsive = False
-          log.log(log.NOTICE, "Relay resumed")
-      
-      # TODO: part two of hack to prevent premature drawing by log panel
-      if page == 0 and not isPaused: panels["log"].setPaused(False)
-      
-      # I haven't the foggiest why, but doesn't work if redrawn out of order...
-      for panelKey in (PAGE_S + PAGES[page]):
-        # redrawing popup can result in display flicker when it should be hidden
-        if panelKey != "popup":
-          newSize = stdscr.getmaxyx()
-          isResize = lastSize != newSize
-          lastSize = newSize
-          
-          if panelKey in ("header", "graph", "log", "config", "torrc", "conn2"):
-            # revised panel (manages its own content refreshing)
-            panels[panelKey].redraw(isResize)
-          else:
-            panels[panelKey].redraw(True)
-      
-      stdscr.refresh()
-      
-      currentTime = time.time()
-      if currentTime - lastPerformanceLog >= CONFIG["queries.refreshRate.rate"]:
-        cpuTotal = sum(os.times()[:3])
-        pythonCpuAvg = cpuTotal / (currentTime - startTime)
-        sysCallCpuAvg = sysTools.getSysCpuUsage()
-        totalCpuAvg = pythonCpuAvg + sysCallCpuAvg
-        
-        if sysCallCpuAvg > 0.00001:
-          log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%% (python), %0.3f%% (system calls), %0.3f%% (total)" % (currentTime - redrawStartTime, 100 * pythonCpuAvg, 100 * sysCallCpuAvg, 100 * totalCpuAvg))
-        else:
-          # with the proc enhancements the sysCallCpuAvg is usually zero
-          log.log(CONFIG["log.refreshRate"], "refresh rate: %0.3f seconds, average cpu usage: %0.3f%%" % (currentTime - redrawStartTime, 100 * totalCpuAvg))
-        
-        lastPerformanceLog = currentTime
-        
-        # once per minute check if the sustained cpu usage is above 5%, if so
-        # then give a warning (and if able, some advice for lowering it)
-        # TODO: disabling this for now (scrolling causes cpu spikes for quick
-        # redraws, ie this is usually triggered by user input)
-        if False and not isResourceWarningGiven and currentTime > (lastResourceCheck + 60):
-          if totalCpuAvg >= 0.05:
-            msg = "Arm's cpu usage is high (averaging %0.3f%%)." % (100 * totalCpuAvg)
-            
-            if not isBlindMode:
-              msg += " You could lower it by dropping the connection data (running as \"arm -b\")."
-            
-            log.log(CONFIG["log.highCpuUsage"], msg)
-            isResourceWarningGiven = True
-          
-          lastResourceCheck = currentTime
-    finally:
-      panel.CURSES_LOCK.release()
-    
-    # wait for user keyboard input until timeout (unless an override was set)
-    if overrideKey:
-      key = overrideKey
-      overrideKey = None
-    else:
-      key = stdscr.getch()
-    
-    if key == ord('q') or key == ord('Q'):
-      quitConfirmed = not CONFIRM_QUIT
-      
-      # provides prompt to confirm that arm should exit
-      if CONFIRM_QUIT:
-        panel.CURSES_LOCK.acquire()
-        try:
-          setPauseState(panels, isPaused, page, True)
-          
-          # provides prompt
-          panels["control"].setMsg("Are you sure (q again to confirm)?", curses.A_BOLD)
-          panels["control"].redraw(True)
-          
-          curses.cbreak()
-          confirmationKey = stdscr.getch()
-          quitConfirmed = confirmationKey in (ord('q'), ord('Q'))
-          curses.halfdelay(REFRESH_RATE * 10)
-          
-          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-          setPauseState(panels, isPaused, page)
-        finally:
-          panel.CURSES_LOCK.release()
-      
-      if quitConfirmed:
-        # quits arm
-        # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable"
-        # this appears to be a python bug: http://bugs.python.org/issue3014
-        # (haven't seen this is quite some time... mysteriously resolved?)
-        
-        torTools.NO_SPAWN = True # prevents further worker threads from being spawned
-        
-        # stops panel daemons
-        panels["header"].stop()
-        panels["conn"].stop()
-        panels["log"].stop()
-        
-        panels["header"].join()
-        panels["conn"].join()
-        panels["log"].join()
-        
-        # joins on utility daemon threads - this might take a moment since
-        # the internal threadpools being joined might be sleeping
-        conn = torTools.getConn()
-        myPid = conn.getMyPid()
-        
-        resourceTracker = sysTools.getResourceTracker(myPid) if (myPid and sysTools.isTrackerAlive(myPid)) else None
-        resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
-        if resourceTracker: resourceTracker.stop()
-        if resolver: resolver.stop()  # sets halt flag (returning immediately)
-        hostnames.stop()              # halts and joins on hostname worker thread pool
-        if resourceTracker: resourceTracker.join()
-        if resolver: resolver.join()  # joins on halted resolver
-        
-        conn.close() # joins on TorCtl event thread
-        break
-    elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
-      # switch page
-      if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
-      else: page = (page + 1) % len(PAGES)
-      
-      # skip connections listing if it's disabled
-      if page == 1 and isBlindMode:
-        if key == curses.KEY_LEFT: page = (page - 1) % len(PAGES)
-        else: page = (page + 1) % len(PAGES)
-      
-      # pauses panels that aren't visible to prevent events from accumilating
-      # (otherwise they'll wait on the curses lock which might get demanding)
-      setPauseState(panels, isPaused, page)
-      
-      # prevents panels on other pages from redrawing
-      for i in range(len(PAGES)):
-        isVisible = i == page
-        for entry in PAGES[i]: panels[entry].setVisible(isVisible)
-      
-      panels["control"].page = page + 1
-      
-      # TODO: this redraw doesn't seem necessary (redraws anyway after this
-      # loop) - look into this when refactoring
-      panels["control"].redraw(True)
-      
-      selectiveRefresh(panels, page)
-    elif key == ord('p') or key == ord('P'):
-      # toggles update freezing
-      panel.CURSES_LOCK.acquire()
-      try:
-        isPaused = not isPaused
-        setPauseState(panels, isPaused, page)
-        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-      finally:
-        panel.CURSES_LOCK.release()
-      
-      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()
-    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")
-          
-          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)")
-        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")
-          popup.addfstr(3, 2, "<b>enter</b>: connection details")
-        
-        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()
-    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)
-      
-      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)
-    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)
-    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,
-      # but for now just gonna use standard characters.
-      
-      if key in (ord('n'), ord('N')):
-        panels["graph"].setGraphHeight(panels["graph"].graphHeight - 1)
-      else:
-        # don't grow the graph if it's already consuming the whole display
-        # (plus an extra line for the graph/log gap)
-        maxHeight = panels["graph"].parent.getmaxyx()[0] - panels["graph"].top
-        currentHeight = panels["graph"].getHeight()
-        
-        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()
-    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 != "auto" 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()
-      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()
-    elif page == 1 and (key == ord('l') or key == ord('L')):
-      # provides a menu to pick the primary information we list connections by
-      options = interface.connections.entries.ListingType.values()
-      
-      # dropping the HOSTNAME listing type until we support displaying that content
-      options.remove(interface.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 == 1 and (key == ord('s') or key == ord('S')):
-      # set ordering for connection options
-      titleLabel = "Connection Ordering:"
-      options = interface.connections.entries.SortAttr.values()
-      oldSelection = panels["conn"]._sortOrdering
-      optionColors = dict([(attr, interface.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)
-      # provides menu to pick config being displayed
-      #options = [confPanel.CONFIG_LABELS[confType] for confType in range(4)]
-      options = []
-      initialSelection = panels["torrc"].configType
-      
-      # hides top label of the graph panel and pauses panels
-      panels["torrc"].showLabel = False
-      panels["torrc"].redraw(True)
-      setPauseState(panels, isPaused, page, True)
-      
-      selection = showMenu(stdscr, panels["popup"], "Configuration:", options, initialSelection)
-      
-      # reverts changes made for popup
-      panels["torrc"].showLabel = True
-      setPauseState(panels, isPaused, page)
-      
-      # applies new setting
-      if selection != -1: panels["torrc"].setConfigType(selection)
-      
-      selectiveRefresh(panels, page)
-    elif page == 2 and (key == ord('w') or key == ord('W')):
-      # display a popup for saving the current configuration
-      panel.CURSES_LOCK.acquire()
-      try:
-        configLines = torConfig.getCustomOptions(True)
-        
-        # lists event types
-        popup = panels["popup"]
-        popup.height = len(configLines) + 3
-        popup.recreate(stdscr)
-        displayHeight, displayWidth = panels["popup"].getPreferredSize()
-        
-        # displayed options (truncating the labels if there's limited room)
-        if displayWidth >= 30: selectionOptions = ("Save", "Save As...", "Cancel")
-        else: selectionOptions = ("Save", "Save As", "X")
-        
-        # checks if we can show options beside the last line of visible content
-        lastIndex = min(displayHeight - 3, len(configLines) - 1)
-        isOptionLineSeparate = displayWidth < (30 + len(configLines[lastIndex]))
-        
-        # if we're showing all the content and have room to display selection
-        # options besides the text then shrink the popup by a row
-        if not isOptionLineSeparate and displayHeight == len(configLines) + 3:
-          popup.height -= 1
-          popup.recreate(stdscr)
-        
-        key, selection = 0, 2
-        while not uiTools.isSelectionKey(key):
-          # if the popup has been resized then recreate it (needed for the
-          # proper border height)
-          newHeight, newWidth = panels["popup"].getPreferredSize()
-          if (displayHeight, displayWidth) != (newHeight, newWidth):
-            displayHeight, displayWidth = newHeight, newWidth
-            popup.recreate(stdscr)
-          
-          # if there isn't room to display the popup then cancel it
-          if displayHeight <= 2:
-            selection = 2
-            break
-          
-          popup.clear()
-          popup.win.box()
-          popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
-          
-          visibleConfigLines = displayHeight - 3 if isOptionLineSeparate else displayHeight - 2
-          for i in range(visibleConfigLines):
-            line = uiTools.cropStr(configLines[i], displayWidth - 2)
-            
-            if " " in line:
-              option, arg = line.split(" ", 1)
-              popup.addstr(i + 1, 1, option, curses.A_BOLD | uiTools.getColor("green"))
-              popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD | uiTools.getColor("cyan"))
-            else:
-              popup.addstr(i + 1, 1, line, curses.A_BOLD | uiTools.getColor("green"))
-          
-          # draws 'T' between the lower left and the covered panel's scroll bar
-          if displayWidth > 1: popup.win.addch(displayHeight - 1, 1, curses.ACS_TTEE)
-          
-          # draws selection options (drawn right to left)
-          drawX = displayWidth - 1
-          for i in range(len(selectionOptions) - 1, -1, -1):
-            optionLabel = selectionOptions[i]
-            drawX -= (len(optionLabel) + 2)
-            
-            # if we've run out of room then drop the option (this will only
-            # occure on tiny displays)
-            if drawX < 1: break
-            
-            selectionFormat = curses.A_STANDOUT if i == selection else curses.A_NORMAL
-            popup.addstr(displayHeight - 2, drawX, "[")
-            popup.addstr(displayHeight - 2, drawX + 1, optionLabel, selectionFormat | curses.A_BOLD)
-            popup.addstr(displayHeight - 2, drawX + len(optionLabel) + 1, "]")
-            
-            drawX -= 1 # space gap between the options
-          
-          popup.refresh()
-          
-          key = stdscr.getch()
-          if key == curses.KEY_LEFT: selection = max(0, selection - 1)
-          elif key == curses.KEY_RIGHT: selection = min(len(selectionOptions) - 1, selection + 1)
-        
-        if selection in (0, 1):
-          loadedTorrc = torConfig.getTorrc()
-          try: configLocation = loadedTorrc.getConfigLocation()
-          except IOError: configLocation = ""
-          
-          if selection == 1:
-            # prompts user for a configuration location
-            promptMsg = "Save to (esc to cancel): "
-            panels["control"].setMsg(promptMsg)
-            panels["control"].redraw(True)
-            configLocation = panels["control"].getstr(0, len(promptMsg), configLocation)
-            if configLocation: configLocation = os.path.abspath(configLocation)
-          
-          if configLocation:
-            try:
-              # make dir if the path doesn't already exist
-              baseDir = os.path.dirname(configLocation)
-              if not os.path.exists(baseDir): os.makedirs(baseDir)
-              
-              # saves the configuration to the file
-              configFile = open(configLocation, "w")
-              configFile.write("\n".join(configLines))
-              configFile.close()
-              
-              # reloads the cached torrc if overwriting it
-              if configLocation == loadedTorrc.getConfigLocation():
-                try:
-                  loadedTorrc.load()
-                  panels["torrc"]._lastContentHeightArgs = None
-                except IOError: pass
-              
-              msg = "Saved configuration to %s" % configLocation
-            except (IOError, OSError), exc:
-              msg = "Unable to save configuration (%s)" % sysTools.getFileErrorMsg(exc)
-            
-            panels["control"].setMsg(msg, curses.A_STANDOUT)
-            panels["control"].redraw(True)
-            time.sleep(2)
-          
-          panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-        
-        # reverts popup dimensions
-        popup.height = 9
-        popup.recreate(stdscr, 80)
-      finally:
-        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()
-      try:
-        setPauseState(panels, isPaused, page, True)
-        
-        # provides prompt
-        selection = panels["config"].getSelection()
-        configOption = selection.get(configPanel.Field.OPTION)
-        titleMsg = "%s Value (esc to cancel): " % configOption
-        panels["control"].setMsg(titleMsg)
-        panels["control"].redraw(True)
-        
-        displayWidth = panels["control"].getPreferredSize()[1]
-        initialValue = selection.get(configPanel.Field.VALUE)
-        
-        # initial input for the text field
-        initialText = ""
-        if CONFIG["features.config.prepopulateEditValues"] and initialValue != "<none>":
-          initialText = initialValue
-        
-        newConfigValue = panels["control"].getstr(0, len(titleMsg), initialText)
-        
-        # it would be nice to quit on esc, but looks like this might not be possible...
-        if newConfigValue != None and newConfigValue != initialValue:
-          conn = torTools.getConn()
-          
-          # if the value's a boolean then allow for 'true' and 'false' inputs
-          if selection.get(configPanel.Field.TYPE) == "Boolean":
-            if newConfigValue.lower() == "true": newConfigValue = "1"
-            elif newConfigValue.lower() == "false": newConfigValue = "0"
-          
-          try:
-            if selection.get(configPanel.Field.TYPE) == "LineList":
-              newConfigValue = newConfigValue.split(",")
-            
-            conn.setOption(configOption, newConfigValue)
-            
-            # resets the isDefault flag
-            customOptions = torConfig.getCustomOptions()
-            selection.fields[configPanel.Field.IS_DEFAULT] = not configOption in customOptions
-            
-            panels["config"].redraw(True)
-          except Exception, exc:
-            errorMsg = "%s (press any key)" % exc
-            panels["control"].setMsg(uiTools.cropStr(errorMsg, displayWidth), curses.A_STANDOUT)
-            panels["control"].redraw(True)
-            
-            curses.cbreak() # wait indefinitely for key presses (no timeout)
-            stdscr.getch()
-            curses.halfdelay(REFRESH_RATE * 10)
-        
-        panels["control"].setMsg(CTL_PAUSED if isPaused else CTL_HELP)
-        setPauseState(panels, isPaused, page)
-      finally:
-        panel.CURSES_LOCK.release()
-    elif page == 3 and key == ord('r') or key == ord('R'):
-      # reloads torrc, providing a notice if successful or not
-      loadedTorrc = torConfig.getTorrc()
-      loadedTorrc.getLock().acquire()
-      
-      try:
-        loadedTorrc.load()
-        isSuccessful = True
-      except IOError:
-        isSuccessful = False
-      
-      loadedTorrc.getLock().release()
-      
-      #isSuccessful = panels["torrc"].loadConfig(logErrors = False)
-      #confTypeLabel = confPanel.CONFIG_LABELS[panels["torrc"].configType]
-      resetMsg = "torrc reloaded" if isSuccessful else "failed to reload torrc"
-      if isSuccessful:
-        panels["torrc"]._lastContentHeightArgs = None
-        panels["torrc"].redraw(True)
-      
-      panels["control"].setMsg(resetMsg, curses.A_STANDOUT)
-      panels["control"].redraw(True)
-      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)
-
-def startTorMonitor(startTime, loggedEvents, isBlindMode):
-  try:
-    curses.wrapper(drawTorMonitor, startTime, loggedEvents, isBlindMode)
-  except KeyboardInterrupt:
-    pass # skip printing stack trace in case of keyboard interrupt
-
diff --git a/src/interface/descriptorPopup.py b/src/interface/descriptorPopup.py
deleted file mode 100644
index cdc959d..0000000
--- a/src/interface/descriptorPopup.py
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/usr/bin/env python
-# descriptorPopup.py -- popup panel used to show raw consensus data
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import math
-import socket
-import curses
-from TorCtl import TorCtl
-
-import controller
-import connections.connEntry
-from util import panel, torTools, uiTools
-
-# field keywords used to identify areas for coloring
-LINE_NUM_COLOR = "yellow"
-HEADER_COLOR = "cyan"
-HEADER_PREFIX = ["ns/id/", "desc/id/"]
-
-SIG_COLOR = "red"
-SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
-SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
-
-UNRESOLVED_MSG = "No consensus data available"
-ERROR_MSG = "Unable to retrieve data"
-
-class PopupProperties:
-  """
-  State attributes of popup window for consensus descriptions.
-  """
-  
-  def __init__(self):
-    self.fingerprint = ""
-    self.entryColor = "white"
-    self.text = []
-    self.scroll = 0
-    self.showLineNum = True
-  
-  def reset(self, fingerprint, entryColor):
-    self.fingerprint = fingerprint
-    self.entryColor = entryColor
-    self.text = []
-    self.scroll = 0
-    
-    if fingerprint == "UNKNOWN":
-      self.fingerprint = None
-      self.showLineNum = False
-      self.text.append(UNRESOLVED_MSG)
-    else:
-      conn = torTools.getConn()
-      
-      try:
-        self.showLineNum = True
-        self.text.append("ns/id/%s" % fingerprint)
-        self.text += conn.getConsensusEntry(fingerprint).split("\n")
-      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-        self.text = self.text + [ERROR_MSG, ""]
-      
-      try:
-        descCommand = "desc/id/%s" % fingerprint
-        self.text.append("desc/id/%s" % fingerprint)
-        self.text += conn.getDescriptorEntry(fingerprint).split("\n")
-      except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
-        self.text = self.text + [ERROR_MSG]
-  
-  def handleKey(self, key, height):
-    if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
-    elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, len(self.text) - height))
-    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):
-  """
-  Presents consensus descriptor in popup window with the following controls:
-  Up, Down, Page Up, Page Down - scroll descriptor
-  Right, Left - next / previous connection
-  Enter, Space, d, D - close popup
-  """
-  
-  properties = PopupProperties()
-  isVisible = True
-  
-  if not panel.CURSES_LOCK.acquire(False): return
-  try:
-    while isVisible:
-      selection = connectionPanel._scroller.getCursorSelection(connectionPanel._entryLines)
-      if not selection: break
-      fingerprint = selection.foreign.getFingerprint()
-      entryColor = connections.connEntry.CATEGORY_COLOR[selection.getType()]
-      properties.reset(fingerprint, entryColor)
-      
-      # constrains popup size to match text
-      width, height = 0, 0
-      for line in properties.text:
-        # width includes content, line number field, and border
-        lineWidth = len(line) + 5
-        if properties.showLineNum: lineWidth += int(math.log10(len(properties.text))) + 1
-        width = max(width, lineWidth)
-        
-        # 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()
-        
-        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()
-
-def draw(popup, properties):
-  popup.clear()
-  popup.win.box()
-  xOffset = 2
-  
-  if properties.text:
-    if properties.fingerprint: popup.addstr(0, 0, "Consensus Descriptor (%s):" % properties.fingerprint, curses.A_STANDOUT)
-    else: popup.addstr(0, 0, "Consensus Descriptor:", curses.A_STANDOUT)
-    
-    isEncryption = False          # true if line is part of an encryption block
-    
-    # checks if first line is in an encryption block
-    for i in range(0, properties.scroll):
-      lineText = properties.text[i].strip()
-      if lineText in SIG_START_KEYS: isEncryption = True
-      elif lineText in SIG_END_KEYS: isEncryption = False
-    
-    pageHeight = popup.maxY - 2
-    numFieldWidth = int(math.log10(len(properties.text))) + 1
-    lineNum = 1
-    for i in range(properties.scroll, min(len(properties.text), properties.scroll + pageHeight)):
-      lineText = properties.text[i].strip()
-      
-      numOffset = 0     # offset for line numbering
-      if properties.showLineNum:
-        popup.addstr(lineNum, xOffset, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR))
-        numOffset = numFieldWidth + 1
-      
-      if lineText:
-        keyword = lineText.split()[0]   # first word of line
-        remainder = lineText[len(keyword):]
-        keywordFormat = curses.A_BOLD | uiTools.getColor(properties.entryColor)
-        remainderFormat = uiTools.getColor(properties.entryColor)
-        
-        if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]):
-          keyword, remainder = lineText, ""
-          keywordFormat = curses.A_BOLD | uiTools.getColor(HEADER_COLOR)
-        if lineText == UNRESOLVED_MSG or lineText == ERROR_MSG:
-          keyword, remainder = lineText, ""
-        if lineText in SIG_START_KEYS:
-          keyword, remainder = lineText, ""
-          isEncryption = True
-          keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
-        elif lineText in SIG_END_KEYS:
-          keyword, remainder = lineText, ""
-          isEncryption = False
-          keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
-        elif isEncryption:
-          keyword, remainder = lineText, ""
-          keywordFormat = uiTools.getColor(SIG_COLOR)
-        
-        lineNum, xLoc = controller.addstr_wrap(popup, lineNum, 0, keyword, keywordFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
-        lineNum, xLoc = controller.addstr_wrap(popup, lineNum, xLoc, remainder, remainderFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
-      
-      lineNum += 1
-      if lineNum > pageHeight: break
-      
-  popup.refresh()
-
diff --git a/src/interface/graphing/__init__.py b/src/interface/graphing/__init__.py
deleted file mode 100644
index 9a81dbd..0000000
--- a/src/interface/graphing/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""
-Panels, popups, and handlers comprising the arm user interface.
-"""
-
-__all__ = ["graphPanel", "bandwidthStats", "connStats", "resourceStats"]
-
diff --git a/src/interface/graphing/bandwidthStats.py b/src/interface/graphing/bandwidthStats.py
deleted file mode 100644
index f8e3020..0000000
--- a/src/interface/graphing/bandwidthStats.py
+++ /dev/null
@@ -1,398 +0,0 @@
-"""
-Tracks bandwidth usage of the tor process, expanding to include accounting
-stats if they're set.
-"""
-
-import time
-
-from interface.graphing import graphPanel
-from util import log, sysTools, torTools, uiTools
-
-DL_COLOR, UL_COLOR = "green", "cyan"
-
-# width at which panel abandons placing optional stats (avg and total) with
-# header in favor of replacing the x-axis label
-COLLAPSE_WIDTH = 135
-
-# valid keys for the accountingInfo mapping
-ACCOUNTING_ARGS = ("status", "resetTime", "read", "written", "readLimit", "writtenLimit")
-
-PREPOPULATE_SUCCESS_MSG = "Read the last day of bandwidth history from the state file"
-PREPOPULATE_FAILURE_MSG = "Unable to prepopulate bandwidth information (%s)"
-
-DEFAULT_CONFIG = {"features.graph.bw.transferInBytes": False,
-                  "features.graph.bw.accounting.show": True,
-                  "features.graph.bw.accounting.rate": 10,
-                  "features.graph.bw.accounting.isTimeLong": False,
-                  "log.graph.bw.prepopulateSuccess": log.NOTICE,
-                  "log.graph.bw.prepopulateFailure": log.NOTICE}
-
-class BandwidthStats(graphPanel.GraphStats):
-  """
-  Uses tor BW events to generate bandwidth usage graph.
-  """
-  
-  def __init__(self, config=None):
-    graphPanel.GraphStats.__init__(self)
-    
-    self._config = dict(DEFAULT_CONFIG)
-    if config:
-      config.update(self._config, {"features.graph.bw.accounting.rate": 1})
-    
-    # stats prepopulated from tor's state file
-    self.prepopulatePrimaryTotal = 0
-    self.prepopulateSecondaryTotal = 0
-    self.prepopulateTicks = 0
-    
-    # accounting data (set by _updateAccountingInfo method)
-    self.accountingLastUpdated = 0
-    self.accountingInfo = dict([(arg, "") for arg in ACCOUNTING_ARGS])
-    
-    # listens for tor reload (sighup) events which can reset the bandwidth
-    # rate/burst and if tor's using accounting
-    conn = torTools.getConn()
-    self._titleStats, self.isAccounting = [], False
-    self.resetListener(conn, torTools.State.INIT) # initializes values
-    conn.addStatusListener(self.resetListener)
-    
-    # Initialized the bandwidth totals to the values reported by Tor. This
-    # uses a controller options introduced in ticket 2345:
-    # https://trac.torproject.org/projects/tor/ticket/2345
-    # 
-    # further updates are still handled via BW events to avoid unnecessary
-    # GETINFO requests.
-    
-    self.initialPrimaryTotal = 0
-    self.initialSecondaryTotal = 0
-    
-    readTotal = conn.getInfo("traffic/read")
-    if readTotal and readTotal.isdigit():
-      self.initialPrimaryTotal = int(readTotal) / 1024 # Bytes -> KB
-    
-    writeTotal = conn.getInfo("traffic/written")
-    if writeTotal and writeTotal.isdigit():
-      self.initialSecondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
-  
-  def resetListener(self, conn, eventType):
-    # updates title parameters and accounting status if they changed
-    self._titleStats = []     # force reset of title
-    self.new_desc_event(None) # updates title params
-    
-    if eventType == torTools.State.INIT and self._config["features.graph.bw.accounting.show"]:
-      self.isAccounting = conn.getInfo('accounting/enabled') == '1'
-  
-  def prepopulateFromState(self):
-    """
-    Attempts to use tor's state file to prepopulate values for the 15 minute
-    interval via the BWHistoryReadValues/BWHistoryWriteValues values. This
-    returns True if successful and False otherwise.
-    """
-    
-    # checks that this is a relay (if ORPort is unset, then skip)
-    conn = torTools.getConn()
-    orPort = conn.getOption("ORPort")
-    if orPort == "0": return
-    
-    # gets the uptime (using the same parameters as the header panel to take
-    # advantage of caching
-    uptime = None
-    queryPid = conn.getMyPid()
-    if queryPid:
-      queryParam = ["%cpu", "rss", "%mem", "etime"]
-      queryCmd = "ps -p %s -o %s" % (queryPid, ",".join(queryParam))
-      psCall = sysTools.call(queryCmd, 3600, True)
-      
-      if psCall and len(psCall) == 2:
-        stats = psCall[1].strip().split()
-        if len(stats) == 4: uptime = stats[3]
-    
-    # checks if tor has been running for at least a day, the reason being that
-    # the state tracks a day's worth of data and this should only prepopulate
-    # results associated with this tor instance
-    if not uptime or not "-" in uptime:
-      msg = PREPOPULATE_FAILURE_MSG % "insufficient uptime"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
-      return False
-    
-    # get the user's data directory (usually '~/.tor')
-    dataDir = conn.getOption("DataDirectory")
-    if not dataDir:
-      msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
-      return False
-    
-    # attempt to open the state file
-    try: stateFile = open("%s%s/state" % (conn.getPathPrefix(), dataDir), "r")
-    except IOError:
-      msg = PREPOPULATE_FAILURE_MSG % "unable to read the state file"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
-      return False
-    
-    # get the BWHistory entries (ordered oldest to newest) and number of
-    # intervals since last recorded
-    bwReadEntries, bwWriteEntries = None, None
-    missingReadEntries, missingWriteEntries = None, None
-    
-    # converts from gmt to local with respect to DST
-    tz_offset = time.altzone if time.localtime()[8] else time.timezone
-    
-    for line in stateFile:
-      line = line.strip()
-      
-      # According to the rep_hist_update_state() function the BWHistory*Ends
-      # correspond to the start of the following sampling period. Also, the
-      # most recent values of BWHistory*Values appear to be an incremental
-      # counter for the current sampling period. Hence, offsets are added to
-      # account for both.
-      
-      if line.startswith("BWHistoryReadValues"):
-        bwReadEntries = line[20:].split(",")
-        bwReadEntries = [int(entry) / 1024.0 / 900 for entry in bwReadEntries]
-        bwReadEntries.pop()
-      elif line.startswith("BWHistoryWriteValues"):
-        bwWriteEntries = line[21:].split(",")
-        bwWriteEntries = [int(entry) / 1024.0 / 900 for entry in bwWriteEntries]
-        bwWriteEntries.pop()
-      elif line.startswith("BWHistoryReadEnds"):
-        lastReadTime = time.mktime(time.strptime(line[18:], "%Y-%m-%d %H:%M:%S")) - tz_offset
-        lastReadTime -= 900
-        missingReadEntries = int((time.time() - lastReadTime) / 900)
-      elif line.startswith("BWHistoryWriteEnds"):
-        lastWriteTime = time.mktime(time.strptime(line[19:], "%Y-%m-%d %H:%M:%S")) - tz_offset
-        lastWriteTime -= 900
-        missingWriteEntries = int((time.time() - lastWriteTime) / 900)
-    
-    if not bwReadEntries or not bwWriteEntries or not lastReadTime or not lastWriteTime:
-      msg = PREPOPULATE_FAILURE_MSG % "bandwidth stats missing from state file"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
-      return False
-    
-    # fills missing entries with the last value
-    bwReadEntries += [bwReadEntries[-1]] * missingReadEntries
-    bwWriteEntries += [bwWriteEntries[-1]] * missingWriteEntries
-    
-    # crops starting entries so they're the same size
-    entryCount = min(len(bwReadEntries), len(bwWriteEntries), self.maxCol)
-    bwReadEntries = bwReadEntries[len(bwReadEntries) - entryCount:]
-    bwWriteEntries = bwWriteEntries[len(bwWriteEntries) - entryCount:]
-    
-    # gets index for 15-minute interval
-    intervalIndex = 0
-    for indexEntry in graphPanel.UPDATE_INTERVALS:
-      if indexEntry[1] == 900: break
-      else: intervalIndex += 1
-    
-    # fills the graphing parameters with state information
-    for i in range(entryCount):
-      readVal, writeVal = bwReadEntries[i], bwWriteEntries[i]
-      
-      self.lastPrimary, self.lastSecondary = readVal, writeVal
-      
-      self.prepopulatePrimaryTotal += readVal * 900
-      self.prepopulateSecondaryTotal += writeVal * 900
-      self.prepopulateTicks += 900
-      
-      self.primaryCounts[intervalIndex].insert(0, readVal)
-      self.secondaryCounts[intervalIndex].insert(0, writeVal)
-    
-    self.maxPrimary[intervalIndex] = max(self.primaryCounts)
-    self.maxSecondary[intervalIndex] = max(self.secondaryCounts)
-    del self.primaryCounts[intervalIndex][self.maxCol + 1:]
-    del self.secondaryCounts[intervalIndex][self.maxCol + 1:]
-    
-    msg = PREPOPULATE_SUCCESS_MSG
-    missingSec = time.time() - min(lastReadTime, lastWriteTime)
-    if missingSec: msg += " (%s is missing)" % uiTools.getTimeLabel(missingSec, 0, True)
-    log.log(self._config["log.graph.bw.prepopulateSuccess"], msg)
-    
-    return True
-  
-  def bandwidth_event(self, event):
-    if self.isAccounting and self.isNextTickRedraw():
-      if time.time() - self.accountingLastUpdated >= self._config["features.graph.bw.accounting.rate"]:
-        self._updateAccountingInfo()
-    
-    # scales units from B to KB for graphing
-    self._processEvent(event.read / 1024.0, event.written / 1024.0)
-  
-  def draw(self, panel, width, height):
-    # line of the graph's x-axis labeling
-    labelingLine = graphPanel.GraphStats.getContentHeight(self) + panel.graphHeight - 2
-    
-    # if display is narrow, overwrites x-axis labels with avg / total stats
-    if width <= COLLAPSE_WIDTH:
-      # clears line
-      panel.addstr(labelingLine, 0, " " * width)
-      graphCol = min((width - 10) / 2, self.maxCol)
-      
-      primaryFooter = "%s, %s" % (self._getAvgLabel(True), self._getTotalLabel(True))
-      secondaryFooter = "%s, %s" % (self._getAvgLabel(False), self._getTotalLabel(False))
-      
-      panel.addstr(labelingLine, 1, primaryFooter, uiTools.getColor(self.getColor(True)))
-      panel.addstr(labelingLine, graphCol + 6, secondaryFooter, uiTools.getColor(self.getColor(False)))
-    
-    # provides accounting stats if enabled
-    if self.isAccounting:
-      if torTools.getConn().isAlive():
-        status = self.accountingInfo["status"]
-        
-        hibernateColor = "green"
-        if status == "soft": hibernateColor = "yellow"
-        elif status == "hard": hibernateColor = "red"
-        elif status == "":
-          # failed to be queried
-          status, hibernateColor = "unknown", "red"
-        
-        panel.addfstr(labelingLine + 2, 0, "<b>Accounting (<%s>%s</%s>)</b>" % (hibernateColor, status, hibernateColor))
-        
-        resetTime = self.accountingInfo["resetTime"]
-        if not resetTime: resetTime = "unknown"
-        panel.addstr(labelingLine + 2, 35, "Time to reset: %s" % resetTime)
-        
-        used, total = self.accountingInfo["read"], self.accountingInfo["readLimit"]
-        if used and total:
-          panel.addstr(labelingLine + 3, 2, "%s / %s" % (used, total), uiTools.getColor(self.getColor(True)))
-        
-        used, total = self.accountingInfo["written"], self.accountingInfo["writtenLimit"]
-        if used and total:
-          panel.addstr(labelingLine + 3, 37, "%s / %s" % (used, total), uiTools.getColor(self.getColor(False)))
-      else:
-        panel.addfstr(labelingLine + 2, 0, "<b>Accounting:</b> Connection Closed...")
-  
-  def getTitle(self, width):
-    stats = list(self._titleStats)
-    
-    while True:
-      if not stats: return "Bandwidth:"
-      else:
-        label = "Bandwidth (%s):" % ", ".join(stats)
-        
-        if len(label) > width: del stats[-1]
-        else: return label
-  
-  def getHeaderLabel(self, width, isPrimary):
-    graphType = "Download" if isPrimary else "Upload"
-    stats = [""]
-    
-    # if wide then avg and total are part of the header, otherwise they're on
-    # the x-axis
-    if width * 2 > COLLAPSE_WIDTH:
-      stats = [""] * 3
-      stats[1] = "- %s" % self._getAvgLabel(isPrimary)
-      stats[2] = ", %s" % self._getTotalLabel(isPrimary)
-    
-    stats[0] = "%-14s" % ("%s/sec" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"]))
-    
-    # drops label's components if there's not enough space
-    labeling = graphType + " (" + "".join(stats).strip() + "):"
-    while len(labeling) >= width:
-      if len(stats) > 1:
-        del stats[-1]
-        labeling = graphType + " (" + "".join(stats).strip() + "):"
-      else:
-        labeling = graphType + ":"
-        break
-    
-    return labeling
-  
-  def getColor(self, isPrimary):
-    return DL_COLOR if isPrimary else UL_COLOR
-  
-  def getContentHeight(self):
-    baseHeight = graphPanel.GraphStats.getContentHeight(self)
-    return baseHeight + 3 if self.isAccounting else baseHeight
-  
-  def new_desc_event(self, event):
-    # updates self._titleStats with updated values
-    conn = torTools.getConn()
-    if not conn.isAlive(): return # keep old values
-    
-    myFingerprint = conn.getInfo("fingerprint")
-    if not self._titleStats or not myFingerprint or (event and myFingerprint in event.idlist):
-      stats = []
-      bwRate = conn.getMyBandwidthRate()
-      bwBurst = conn.getMyBandwidthBurst()
-      bwObserved = conn.getMyBandwidthObserved()
-      bwMeasured = conn.getMyBandwidthMeasured()
-      labelInBytes = self._config["features.graph.bw.transferInBytes"]
-      
-      if bwRate and bwBurst:
-        bwRateLabel = uiTools.getSizeLabel(bwRate, 1, False, labelInBytes)
-        bwBurstLabel = uiTools.getSizeLabel(bwBurst, 1, False, labelInBytes)
-        
-        # if both are using rounded values then strip off the ".0" decimal
-        if ".0" in bwRateLabel and ".0" in bwBurstLabel:
-          bwRateLabel = bwRateLabel.replace(".0", "")
-          bwBurstLabel = bwBurstLabel.replace(".0", "")
-        
-        stats.append("limit: %s/s" % bwRateLabel)
-        stats.append("burst: %s/s" % bwBurstLabel)
-      
-      # Provide the observed bandwidth either if the measured bandwidth isn't
-      # available or if the measured bandwidth is the observed (this happens
-      # if there isn't yet enough bandwidth measurements).
-      if bwObserved and (not bwMeasured or bwMeasured == bwObserved):
-        stats.append("observed: %s/s" % uiTools.getSizeLabel(bwObserved, 1, False, labelInBytes))
-      elif bwMeasured:
-        stats.append("measured: %s/s" % uiTools.getSizeLabel(bwMeasured, 1, False, labelInBytes))
-      
-      self._titleStats = stats
-  
-  def _getAvgLabel(self, isPrimary):
-    total = self.primaryTotal if isPrimary else self.secondaryTotal
-    total += self.prepopulatePrimaryTotal if isPrimary else self.prepopulateSecondaryTotal
-    return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, self._config["features.graph.bw.transferInBytes"])
-  
-  def _getTotalLabel(self, isPrimary):
-    total = self.primaryTotal if isPrimary else self.secondaryTotal
-    total += self.initialPrimaryTotal if isPrimary else self.initialSecondaryTotal
-    return "total: %s" % uiTools.getSizeLabel(total * 1024, 1)
-  
-  def _updateAccountingInfo(self):
-    """
-    Updates mapping used for accounting info. This includes the following keys:
-    status, resetTime, read, written, readLimit, writtenLimit
-    
-    Any failed lookups result in a mapping to an empty string.
-    """
-    
-    conn = torTools.getConn()
-    queried = dict([(arg, "") for arg in ACCOUNTING_ARGS])
-    queried["status"] = conn.getInfo("accounting/hibernating")
-    
-    # provides a nicely formatted reset time
-    endInterval = conn.getInfo("accounting/interval-end")
-    if endInterval:
-      # converts from gmt to local with respect to DST
-      if time.localtime()[8]: tz_offset = time.altzone
-      else: tz_offset = time.timezone
-      
-      sec = time.mktime(time.strptime(endInterval, "%Y-%m-%d %H:%M:%S")) - time.time() - tz_offset
-      if self._config["features.graph.bw.accounting.isTimeLong"]:
-        queried["resetTime"] = ", ".join(uiTools.getTimeLabels(sec, True))
-      else:
-        days = sec / 86400
-        sec %= 86400
-        hours = sec / 3600
-        sec %= 3600
-        minutes = sec / 60
-        sec %= 60
-        queried["resetTime"] = "%i:%02i:%02i:%02i" % (days, hours, minutes, sec)
-    
-    # number of bytes used and in total for the accounting period
-    used = conn.getInfo("accounting/bytes")
-    left = conn.getInfo("accounting/bytes-left")
-    
-    if used and left:
-      usedComp, leftComp = used.split(" "), left.split(" ")
-      read, written = int(usedComp[0]), int(usedComp[1])
-      readLeft, writtenLeft = int(leftComp[0]), int(leftComp[1])
-      
-      queried["read"] = uiTools.getSizeLabel(read)
-      queried["written"] = uiTools.getSizeLabel(written)
-      queried["readLimit"] = uiTools.getSizeLabel(read + readLeft)
-      queried["writtenLimit"] = uiTools.getSizeLabel(written + writtenLeft)
-    
-    self.accountingInfo = queried
-    self.accountingLastUpdated = time.time()
-
diff --git a/src/interface/graphing/connStats.py b/src/interface/graphing/connStats.py
deleted file mode 100644
index 511490c..0000000
--- a/src/interface/graphing/connStats.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""
-Tracks stats concerning tor's current connections.
-"""
-
-from interface.graphing import graphPanel
-from util import connections, torTools
-
-class ConnStats(graphPanel.GraphStats):
-  """
-  Tracks number of connections, counting client and directory connections as 
-  outbound. Control connections are excluded from counts.
-  """
-  
-  def __init__(self):
-    graphPanel.GraphStats.__init__(self)
-    
-    # listens for tor reload (sighup) events which can reset the ports tor uses
-    conn = torTools.getConn()
-    self.orPort, self.dirPort, self.controlPort = "0", "0", "0"
-    self.resetListener(conn, torTools.State.INIT) # initialize port values
-    conn.addStatusListener(self.resetListener)
-  
-  def resetListener(self, conn, eventType):
-    if eventType == torTools.State.INIT:
-      self.orPort = conn.getOption("ORPort", "0")
-      self.dirPort = conn.getOption("DirPort", "0")
-      self.controlPort = conn.getOption("ControlPort", "0")
-  
-  def eventTick(self):
-    """
-    Fetches connection stats from cached information.
-    """
-    
-    inboundCount, outboundCount = 0, 0
-    
-    for entry in connections.getResolver("tor").getConnections():
-      localPort = entry[1]
-      if localPort in (self.orPort, self.dirPort): inboundCount += 1
-      elif localPort == self.controlPort: pass # control connection
-      else: outboundCount += 1
-    
-    self._processEvent(inboundCount, outboundCount)
-  
-  def getTitle(self, width):
-    return "Connection Count:"
-  
-  def getHeaderLabel(self, width, isPrimary):
-    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
-    if isPrimary: return "Inbound (%s, avg: %s):" % (self.lastPrimary, avg)
-    else: return "Outbound (%s, avg: %s):" % (self.lastSecondary, avg)
-  
-  def getRefreshRate(self):
-    return 5
-
diff --git a/src/interface/graphing/graphPanel.py b/src/interface/graphing/graphPanel.py
deleted file mode 100644
index e4b493d..0000000
--- a/src/interface/graphing/graphPanel.py
+++ /dev/null
@@ -1,407 +0,0 @@
-"""
-Flexible panel for presenting bar graphs for a variety of stats. This panel is
-just concerned with the rendering of information, which is actually collected
-and stored by implementations of the GraphStats interface. Panels are made up
-of a title, followed by headers and graphs for two sets of stats. For
-instance...
-
-Bandwidth (cap: 5 MB, burst: 10 MB):
-Downloaded (0.0 B/sec):           Uploaded (0.0 B/sec):
-  34                                30
-                            *                                 *
-                    **  *   *                          *      **
-      *   *  *      ** **   **          ***  **       ** **   **
-     *********      ******  ******     *********      ******  ******
-   0 ************ ****************   0 ************ ****************
-         25s  50   1m   1.6  2.0           25s  50   1m   1.6  2.0
-"""
-
-import copy
-import curses
-from TorCtl import TorCtl
-
-from util import enum, panel, uiTools
-
-# time intervals at which graphs can be updated
-UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5),   ("30 seconds", 30),
-                    ("minutely", 60),   ("15 minute", 900), ("30 minute", 1800),
-                    ("hourly", 3600),   ("daily", 86400)]
-
-DEFAULT_CONTENT_HEIGHT = 4 # space needed for labeling above and below the graph
-DEFAULT_COLOR_PRIMARY, DEFAULT_COLOR_SECONDARY = "green", "cyan"
-MIN_GRAPH_HEIGHT = 1
-
-# enums for graph bounds:
-#   Bounds.GLOBAL_MAX - global maximum (highest value ever seen)
-#   Bounds.LOCAL_MAX - local maximum (highest value currently on the graph)
-#   Bounds.TIGHT - local maximum and minimum
-Bounds = enum.Enum("GLOBAL_MAX", "LOCAL_MAX", "TIGHT")
-
-WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
-
-# used for setting defaults when initializing GraphStats and GraphPanel instances
-CONFIG = {"features.graph.height": 7,
-          "features.graph.interval": 0,
-          "features.graph.bound": 1,
-          "features.graph.maxWidth": 150,
-          "features.graph.showIntermediateBounds": True}
-
-def loadConfig(config):
-  config.update(CONFIG, {
-    "features.graph.height": MIN_GRAPH_HEIGHT,
-    "features.graph.maxWidth": 1,
-    "features.graph.interval": (0, len(UPDATE_INTERVALS) - 1),
-    "features.graph.bound": (0, 2)})
-
-class GraphStats(TorCtl.PostEventListener):
-  """
-  Module that's expected to update dynamically and provide attributes to be
-  graphed. Up to two graphs (a 'primary' and 'secondary') can be displayed at a
-  time and timescale parameters use the labels defined in UPDATE_INTERVALS.
-  """
-  
-  def __init__(self, isPauseBuffer=False):
-    """
-    Initializes parameters needed to present a graph.
-    """
-    
-    TorCtl.PostEventListener.__init__(self)
-    
-    # panel to be redrawn when updated (set when added to GraphPanel)
-    self._graphPanel = None
-    
-    # mirror instance used to track updates when paused
-    self.isPaused, self.isPauseBuffer = False, isPauseBuffer
-    if isPauseBuffer: self._pauseBuffer = None
-    else: self._pauseBuffer = GraphStats(True)
-    
-    # tracked stats
-    self.tick = 0                                 # number of processed events
-    self.lastPrimary, self.lastSecondary = 0, 0   # most recent registered stats
-    self.primaryTotal, self.secondaryTotal = 0, 0 # sum of all stats seen
-    
-    # timescale dependent stats
-    self.maxCol = CONFIG["features.graph.maxWidth"]
-    self.maxPrimary, self.maxSecondary = {}, {}
-    self.primaryCounts, self.secondaryCounts = {}, {}
-    
-    for i in range(len(UPDATE_INTERVALS)):
-      # recent rates for graph
-      self.maxPrimary[i] = 0
-      self.maxSecondary[i] = 0
-      
-      # historic stats for graph, first is accumulator
-      # iterative insert needed to avoid making shallow copies (nasty, nasty gotcha)
-      self.primaryCounts[i] = (self.maxCol + 1) * [0]
-      self.secondaryCounts[i] = (self.maxCol + 1) * [0]
-  
-  def eventTick(self):
-    """
-    Called when it's time to process another event. All graphs use tor BW
-    events to keep in sync with each other (this happens once a second).
-    """
-    
-    pass
-  
-  def isNextTickRedraw(self):
-    """
-    Provides true if the following tick (call to _processEvent) will result in
-    being redrawn.
-    """
-    
-    if self._graphPanel and not self.isPauseBuffer and not self.isPaused:
-      # use the minimum of the current refresh rate and the panel's
-      updateRate = UPDATE_INTERVALS[self._graphPanel.updateInterval][1]
-      return (self.tick + 1) % min(updateRate, self.getRefreshRate()) == 0
-    else: return False
-  
-  def getTitle(self, width):
-    """
-    Provides top label.
-    """
-    
-    return ""
-  
-  def getHeaderLabel(self, width, isPrimary):
-    """
-    Provides labeling presented at the top of the graph.
-    """
-    
-    return ""
-  
-  def getColor(self, isPrimary):
-    """
-    Provides the color to be used for the graph and stats.
-    """
-    
-    return DEFAULT_COLOR_PRIMARY if isPrimary else DEFAULT_COLOR_SECONDARY
-  
-  def getContentHeight(self):
-    """
-    Provides the height content should take up (not including the graph).
-    """
-    
-    return DEFAULT_CONTENT_HEIGHT
-  
-  def getRefreshRate(self):
-    """
-    Provides the number of ticks between when the stats have new values to be
-    redrawn.
-    """
-    
-    return 1
-  
-  def isVisible(self):
-    """
-    True if the stat has content to present, false if it should be hidden.
-    """
-    
-    return True
-  
-  def draw(self, panel, width, height):
-    """
-    Allows for any custom drawing monitor wishes to append.
-    """
-    
-    pass
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents bandwidth updates from being presented. This is a no-op
-    if a pause buffer.
-    """
-    
-    if isPause == self.isPaused or self.isPauseBuffer: return
-    self.isPaused = isPause
-    
-    if self.isPaused: active, inactive = self._pauseBuffer, self
-    else: active, inactive = self, self._pauseBuffer
-    self._parameterSwap(active, inactive)
-  
-  def bandwidth_event(self, event):
-    self.eventTick()
-  
-  def _parameterSwap(self, active, inactive):
-    """
-    Either overwrites parameters of pauseBuffer or with the current values or
-    vice versa. This is a helper method for setPaused and should be overwritten
-    to append with additional parameters that need to be preserved when paused.
-    """
-    
-    # The pause buffer is constructed as a GraphStats instance which will
-    # become problematic if this is overridden by any implementations (which
-    # currently isn't the case). If this happens then the pause buffer will
-    # need to be of the requester's type (not quite sure how to do this
-    # gracefully...).
-    
-    active.tick = inactive.tick
-    active.lastPrimary = inactive.lastPrimary
-    active.lastSecondary = inactive.lastSecondary
-    active.primaryTotal = inactive.primaryTotal
-    active.secondaryTotal = inactive.secondaryTotal
-    active.maxPrimary = dict(inactive.maxPrimary)
-    active.maxSecondary = dict(inactive.maxSecondary)
-    active.primaryCounts = copy.deepcopy(inactive.primaryCounts)
-    active.secondaryCounts = copy.deepcopy(inactive.secondaryCounts)
-  
-  def _processEvent(self, primary, secondary):
-    """
-    Includes new stats in graphs and notifies associated GraphPanel of changes.
-    """
-    
-    if self.isPaused: self._pauseBuffer._processEvent(primary, secondary)
-    else:
-      isRedraw = self.isNextTickRedraw()
-      
-      self.lastPrimary, self.lastSecondary = primary, secondary
-      self.primaryTotal += primary
-      self.secondaryTotal += secondary
-      
-      # updates for all time intervals
-      self.tick += 1
-      for i in range(len(UPDATE_INTERVALS)):
-        lable, timescale = UPDATE_INTERVALS[i]
-        
-        self.primaryCounts[i][0] += primary
-        self.secondaryCounts[i][0] += secondary
-        
-        if self.tick % timescale == 0:
-          self.maxPrimary[i] = max(self.maxPrimary[i], self.primaryCounts[i][0] / timescale)
-          self.primaryCounts[i][0] /= timescale
-          self.primaryCounts[i].insert(0, 0)
-          del self.primaryCounts[i][self.maxCol + 1:]
-          
-          self.maxSecondary[i] = max(self.maxSecondary[i], self.secondaryCounts[i][0] / timescale)
-          self.secondaryCounts[i][0] /= timescale
-          self.secondaryCounts[i].insert(0, 0)
-          del self.secondaryCounts[i][self.maxCol + 1:]
-      
-      if isRedraw: self._graphPanel.redraw(True)
-
-class GraphPanel(panel.Panel):
-  """
-  Panel displaying a graph, drawing statistics from custom GraphStats
-  implementations.
-  """
-  
-  def __init__(self, stdscr):
-    panel.Panel.__init__(self, stdscr, "graph", 0)
-    self.updateInterval = CONFIG["features.graph.interval"]
-    self.bounds = Bounds.values()[CONFIG["features.graph.bound"]]
-    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.isPaused = False
-  
-  def getHeight(self):
-    """
-    Provides the height requested by the currently displayed GraphStats (zero
-    if hidden).
-    """
-    
-    if self.currentDisplay and self.stats[self.currentDisplay].isVisible():
-      return self.stats[self.currentDisplay].getContentHeight() + self.graphHeight
-    else: return 0
-  
-  def setGraphHeight(self, newGraphHeight):
-    """
-    Sets the preferred height used for the graph (restricted to the
-    MIN_GRAPH_HEIGHT minimum).
-    
-    Arguments:
-      newGraphHeight - new height for the graph
-    """
-    
-    self.graphHeight = max(MIN_GRAPH_HEIGHT, newGraphHeight)
-  
-  def draw(self, width, height):
-    """ Redraws graph panel """
-    
-    if self.currentDisplay:
-      param = self.stats[self.currentDisplay]
-      graphCol = min((width - 10) / 2, param.maxCol)
-      
-      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)
-      
-      # top labels
-      left, right = param.getHeaderLabel(width / 2, True), param.getHeaderLabel(width / 2, False)
-      if left: self.addstr(1, 0, left, curses.A_BOLD | primaryColor)
-      if right: self.addstr(1, graphCol + 5, right, curses.A_BOLD | secondaryColor)
-      
-      # determines max/min value on the graph
-      if self.bounds == Bounds.GLOBAL_MAX:
-        primaryMaxBound = int(param.maxPrimary[self.updateInterval])
-        secondaryMaxBound = int(param.maxSecondary[self.updateInterval])
-      else:
-        # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima
-        if graphCol < 2:
-          # nothing being displayed
-          primaryMaxBound, secondaryMaxBound = 0, 0
-        else:
-          primaryMaxBound = int(max(param.primaryCounts[self.updateInterval][1:graphCol + 1]))
-          secondaryMaxBound = int(max(param.secondaryCounts[self.updateInterval][1:graphCol + 1]))
-      
-      primaryMinBound = secondaryMinBound = 0
-      if self.bounds == Bounds.TIGHT:
-        primaryMinBound = int(min(param.primaryCounts[self.updateInterval][1:graphCol + 1]))
-        secondaryMinBound = int(min(param.secondaryCounts[self.updateInterval][1:graphCol + 1]))
-        
-        # if the max = min (ie, all values are the same) then use zero lower
-        # bound so a graph is still displayed
-        if primaryMinBound == primaryMaxBound: primaryMinBound = 0
-        if secondaryMinBound == secondaryMaxBound: secondaryMinBound = 0
-      
-      # displays upper and lower bounds
-      self.addstr(2, 0, "%4i" % primaryMaxBound, primaryColor)
-      self.addstr(self.graphHeight + 1, 0, "%4i" % primaryMinBound, primaryColor)
-      
-      self.addstr(2, graphCol + 5, "%4i" % secondaryMaxBound, secondaryColor)
-      self.addstr(self.graphHeight + 1, graphCol + 5, "%4i" % secondaryMinBound, secondaryColor)
-      
-      # displays intermediate bounds on every other row
-      if CONFIG["features.graph.showIntermediateBounds"]:
-        ticks = (self.graphHeight - 3) / 2
-        for i in range(ticks):
-          row = self.graphHeight - (2 * i) - 3
-          if self.graphHeight % 2 == 0 and i >= (ticks / 2): row -= 1
-          
-          if primaryMinBound != primaryMaxBound:
-            primaryVal = (primaryMaxBound - primaryMinBound) / (self.graphHeight - 1) * (self.graphHeight - row - 1)
-            if not primaryVal in (primaryMinBound, primaryMaxBound): self.addstr(row + 2, 0, "%4i" % primaryVal, primaryColor)
-          
-          if secondaryMinBound != secondaryMaxBound:
-            secondaryVal = (secondaryMaxBound - secondaryMinBound) / (self.graphHeight - 1) * (self.graphHeight - row - 1)
-            if not secondaryVal in (secondaryMinBound, secondaryMaxBound): self.addstr(row + 2, graphCol + 5, "%4i" % secondaryVal, secondaryColor)
-      
-      # creates bar graph (both primary and secondary)
-      for col in range(graphCol):
-        colCount = int(param.primaryCounts[self.updateInterval][col + 1]) - primaryMinBound
-        colHeight = min(self.graphHeight, self.graphHeight * colCount / (max(1, primaryMaxBound) - primaryMinBound))
-        for row in range(colHeight): self.addstr(self.graphHeight + 1 - row, col + 5, " ", curses.A_STANDOUT | primaryColor)
-        
-        colCount = int(param.secondaryCounts[self.updateInterval][col + 1]) - secondaryMinBound
-        colHeight = min(self.graphHeight, self.graphHeight * colCount / (max(1, secondaryMaxBound) - secondaryMinBound))
-        for row in range(colHeight): self.addstr(self.graphHeight + 1 - row, col + graphCol + 10, " ", curses.A_STANDOUT | secondaryColor)
-      
-      # bottom labeling of x-axis
-      intervalSec = 1 # seconds per labeling
-      for i in range(len(UPDATE_INTERVALS)):
-        if i == self.updateInterval: intervalSec = UPDATE_INTERVALS[i][1]
-      
-      intervalSpacing = 10 if graphCol >= WIDE_LABELING_GRAPH_COL else 5
-      unitsLabel, decimalPrecision = None, 0
-      for i in range((graphCol - 4) / intervalSpacing):
-        loc = (i + 1) * intervalSpacing
-        timeLabel = uiTools.getTimeLabel(loc * intervalSec, decimalPrecision)
-        
-        if not unitsLabel: unitsLabel = timeLabel[-1]
-        elif unitsLabel != timeLabel[-1]:
-          # upped scale so also up precision of future measurements
-          unitsLabel = timeLabel[-1]
-          decimalPrecision += 1
-        else:
-          # if constrained on space then strips labeling since already provided
-          timeLabel = timeLabel[:-1]
-        
-        self.addstr(self.graphHeight + 2, 4 + loc, timeLabel, primaryColor)
-        self.addstr(self.graphHeight + 2, graphCol + 10 + loc, timeLabel, secondaryColor)
-        
-      param.draw(self, width, height) # allows current stats to modify the display
-  
-  def addStats(self, label, stats):
-    """
-    Makes GraphStats instance available in the panel.
-    """
-    
-    stats._graphPanel = self
-    stats.isPaused = True
-    self.stats[label] = stats
-  
-  def setStats(self, label):
-    """
-    Sets the currently displayed stats instance, hiding panel if None.
-    """
-    
-    if label != self.currentDisplay:
-      if self.currentDisplay: self.stats[self.currentDisplay].setPaused(True)
-      
-      if not label:
-        self.currentDisplay = None
-      elif label in self.stats.keys():
-        self.currentDisplay = label
-        self.stats[label].setPaused(self.isPaused)
-      else: raise ValueError("Unrecognized stats label: %s" % label)
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents bandwidth updates from being presented.
-    """
-    
-    if isPause == self.isPaused: return
-    self.isPaused = isPause
-    if self.currentDisplay: self.stats[self.currentDisplay].setPaused(self.isPaused)
-
diff --git a/src/interface/graphing/resourceStats.py b/src/interface/graphing/resourceStats.py
deleted file mode 100644
index 864957e..0000000
--- a/src/interface/graphing/resourceStats.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""
-Tracks the system resource usage (cpu and memory) of the tor process.
-"""
-
-from interface.graphing import graphPanel
-from util import sysTools, torTools, uiTools
-
-class ResourceStats(graphPanel.GraphStats):
-  """
-  System resource usage tracker.
-  """
-  
-  def __init__(self):
-    graphPanel.GraphStats.__init__(self)
-    self.queryPid = torTools.getConn().getMyPid()
-  
-  def getTitle(self, width):
-    return "System Resources:"
-  
-  def getHeaderLabel(self, width, isPrimary):
-    avg = (self.primaryTotal if isPrimary else self.secondaryTotal) / max(1, self.tick)
-    lastAmount = self.lastPrimary if isPrimary else self.lastSecondary
-    
-    if isPrimary:
-      return "CPU (%0.1f%%, avg: %0.1f%%):" % (lastAmount, avg)
-    else:
-      # memory sizes are converted from MB to B before generating labels
-      usageLabel = uiTools.getSizeLabel(lastAmount * 1048576, 1)
-      avgLabel = uiTools.getSizeLabel(avg * 1048576, 1)
-      return "Memory (%s, avg: %s):" % (usageLabel, avgLabel)
-  
-  def eventTick(self):
-    """
-    Fetch the cached measurement of resource usage from the ResourceTracker.
-    """
-    
-    primary, secondary = 0, 0
-    if self.queryPid:
-      resourceTracker = sysTools.getResourceTracker(self.queryPid)
-      
-      if not resourceTracker.lastQueryFailed():
-        primary, _, secondary, _ = resourceTracker.getResourceUsage()
-        primary *= 100        # decimal percentage to whole numbers
-        secondary /= 1048576  # translate size to MB so axis labels are short
-    
-    self._processEvent(primary, secondary)
-
diff --git a/src/interface/headerPanel.py b/src/interface/headerPanel.py
deleted file mode 100644
index f653299..0000000
--- a/src/interface/headerPanel.py
+++ /dev/null
@@ -1,474 +0,0 @@
-"""
-Top panel for every page, containing basic system and tor related information.
-If there's room available then this expands to present its information in two
-columns, otherwise it's laid out as follows:
-  arm - <hostname> (<os> <sys/version>)         Tor <tor/version> (<new, old, recommended, etc>)
-  <nickname> - <address>:<orPort>, [Dir Port: <dirPort>, ]Control Port (<open, password, cookie>): <controlPort>
-  cpu: <cpu%> mem: <mem> (<mem%>) uid: <uid> uptime: <upmin>:<upsec>
-  fingerprint: <fingerprint>
-
-Example:
-  arm - odin (Linux 2.6.24-24-generic)         Tor 0.2.1.19 (recommended)
-  odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
-  cpu: 14.6%    mem: 42 MB (4.2%)    pid: 20060   uptime: 48:27
-  fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
-"""
-
-import os
-import time
-import curses
-import threading
-
-from util import log, panel, sysTools, torTools, uiTools
-
-# minimum width for which panel attempts to double up contents (two columns to
-# better use screen real estate)
-MIN_DUAL_COL_WIDTH = 141
-
-FLAG_COLORS = {"Authority": "white",  "BadExit": "red",     "BadDirectory": "red",    "Exit": "cyan",
-               "Fast": "yellow",      "Guard": "green",     "HSDir": "magenta",       "Named": "blue",
-               "Stable": "blue",      "Running": "yellow",  "Unnamed": "magenta",     "Valid": "green",
-               "V2Dir": "cyan",       "V3Dir": "white"}
-
-VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green",  
-                         "old": "red",  "unrecommended": "red",  "unknown": "cyan"}
-
-DEFAULT_CONFIG = {"features.showFdUsage": False,
-                  "log.fdUsageSixtyPercent": log.NOTICE,
-                  "log.fdUsageNinetyPercent": log.WARN}
-
-class HeaderPanel(panel.Panel, threading.Thread):
-  """
-  Top area contenting tor settings and system information. Stats are stored in
-  the vals mapping, keys including:
-    tor/  version, versionStatus, nickname, orPort, dirPort, controlPort,
-          exitPolicy, isAuthPassword (bool), isAuthCookie (bool),
-          orListenAddr, *address, *fingerprint, *flags, pid, startTime,
-          *fdUsed, fdLimit, isFdLimitEstimate
-    sys/  hostname, os, version
-    stat/ *%torCpu, *%armCpu, *rss, *%mem
-  
-  * volatile parameter that'll be reset on each update
-  """
-  
-  def __init__(self, stdscr, startTime, config = None):
-    panel.Panel.__init__(self, stdscr, "header", 0)
-    threading.Thread.__init__(self)
-    self.setDaemon(True)
-    
-    self._config = dict(DEFAULT_CONFIG)
-    if config: config.update(self._config)
-    
-    self._isTorConnected = True
-    self._lastUpdate = -1       # time the content was last revised
-    self._isPaused = False      # prevents updates if true
-    self._halt = False          # terminates thread if true
-    self._cond = threading.Condition()  # used for pausing the thread
-    
-    # Time when the panel was paused or tor was stopped. This is used to
-    # freeze the uptime statistic (uptime increments normally when None).
-    self._haltTime = None
-    
-    # The last arm cpu usage sampling taken. This is a tuple of the form:
-    # (total arm cpu time, sampling timestamp)
-    # 
-    # The initial cpu total should be zero. However, at startup the cpu time
-    # in practice is often greater than the real time causing the initially
-    # reported cpu usage to be over 100% (which shouldn't be possible on
-    # single core systems).
-    # 
-    # Setting the initial cpu total to the value at this panel's init tends to
-    # give smoother results (staying in the same ballpark as the second
-    # sampling) so fudging the numbers this way for now.
-    
-    self._armCpuSampling = (sum(os.times()[:3]), startTime)
-    
-    # Last sampling received from the ResourceTracker, used to detect when it
-    # changes.
-    self._lastResourceFetch = -1
-    
-    # flag to indicate if we've already given file descriptor warnings
-    self._isFdSixtyPercentWarned = False
-    self._isFdNinetyPercentWarned = False
-    
-    self.vals = {}
-    self.valsLock = threading.RLock()
-    self._update(True)
-    
-    # listens for tor reload (sighup) events
-    torTools.getConn().addStatusListener(self.resetListener)
-  
-  def getHeight(self):
-    """
-    Provides the height of the content, which is dynamically determined by the
-    panel's maximum width.
-    """
-    
-    isWide = self.getParent().getmaxyx()[1] >= MIN_DUAL_COL_WIDTH
-    if self.vals["tor/orPort"]: return 4 if isWide else 6
-    else: return 3 if isWide else 4
-  
-  def draw(self, width, height):
-    self.valsLock.acquire()
-    isWide = width + 1 >= MIN_DUAL_COL_WIDTH
-    
-    # space available for content
-    if isWide:
-      leftWidth = max(width / 2, 77)
-      rightWidth = width - leftWidth
-    else: leftWidth = rightWidth = width
-    
-    # Line 1 / Line 1 Left (system and tor version information)
-    sysNameLabel = "arm - %s" % self.vals["sys/hostname"]
-    contentSpace = min(leftWidth, 40)
-    
-    if len(sysNameLabel) + 10 <= contentSpace:
-      sysTypeLabel = "%s %s" % (self.vals["sys/os"], self.vals["sys/version"])
-      sysTypeLabel = uiTools.cropStr(sysTypeLabel, contentSpace - len(sysNameLabel) - 3, 4)
-      self.addstr(0, 0, "%s (%s)" % (sysNameLabel, sysTypeLabel))
-    else:
-      self.addstr(0, 0, uiTools.cropStr(sysNameLabel, contentSpace))
-    
-    contentSpace = leftWidth - 43
-    if 7 + len(self.vals["tor/version"]) + len(self.vals["tor/versionStatus"]) <= contentSpace:
-      versionColor = VERSION_STATUS_COLORS[self.vals["tor/versionStatus"]] if \
-          self.vals["tor/versionStatus"] in VERSION_STATUS_COLORS else "white"
-      versionStatusMsg = "<%s>%s</%s>" % (versionColor, self.vals["tor/versionStatus"], versionColor)
-      self.addfstr(0, 43, "Tor %s (%s)" % (self.vals["tor/version"], versionStatusMsg))
-    elif 11 <= contentSpace:
-      self.addstr(0, 43, uiTools.cropStr("Tor %s" % self.vals["tor/version"], contentSpace, 4))
-    
-    # Line 2 / Line 2 Left (tor ip/port information)
-    if self.vals["tor/orPort"]:
-      myAddress = "Unknown"
-      if self.vals["tor/orListenAddr"]: myAddress = self.vals["tor/orListenAddr"]
-      elif self.vals["tor/address"]: myAddress = self.vals["tor/address"]
-      
-      # acting as a relay (we can assume certain parameters are set
-      entry = ""
-      dirPortLabel = ", Dir Port: %s" % self.vals["tor/dirPort"] if self.vals["tor/dirPort"] != "0" else ""
-      for label in (self.vals["tor/nickname"], " - " + myAddress, ":" + self.vals["tor/orPort"], dirPortLabel):
-        if len(entry) + len(label) <= leftWidth: entry += label
-        else: break
-    else:
-      # non-relay (client only)
-      # TODO: not sure what sort of stats to provide...
-      entry = "<red><b>Relaying Disabled</b></red>"
-    
-    if self.vals["tor/isAuthPassword"]: authType = "password"
-    elif self.vals["tor/isAuthCookie"]: authType = "cookie"
-    else: authType = "open"
-    
-    if len(entry) + 19 + len(self.vals["tor/controlPort"]) + len(authType) <= leftWidth:
-      authColor = "red" if authType == "open" else "green"
-      authLabel = "<%s>%s</%s>" % (authColor, authType, authColor)
-      self.addfstr(1, 0, "%s, Control Port (%s): %s" % (entry, authLabel, self.vals["tor/controlPort"]))
-    elif len(entry) + 16 + len(self.vals["tor/controlPort"]) <= leftWidth:
-      self.addstr(1, 0, "%s, Control Port: %s" % (entry, self.vals["tor/controlPort"]))
-    else: self.addstr(1, 0, entry)
-    
-    # Line 3 / Line 1 Right (system usage info)
-    y, x = (0, leftWidth) if isWide else (2, 0)
-    if self.vals["stat/rss"] != "0": memoryLabel = uiTools.getSizeLabel(int(self.vals["stat/rss"]))
-    else: memoryLabel = "0"
-    
-    uptimeLabel = ""
-    if self.vals["tor/startTime"]:
-      if self._haltTime:
-        # freeze the uptime when paused or the tor process is stopped
-        uptimeLabel = uiTools.getShortTimeLabel(self._haltTime - self.vals["tor/startTime"])
-      else:
-        uptimeLabel = uiTools.getShortTimeLabel(time.time() - self.vals["tor/startTime"])
-    
-    sysFields = ((0, "cpu: %s%% tor, %s%% arm" % (self.vals["stat/%torCpu"], self.vals["stat/%armCpu"])),
-                 (27, "mem: %s (%s%%)" % (memoryLabel, self.vals["stat/%mem"])),
-                 (47, "pid: %s" % (self.vals["tor/pid"] if self._isTorConnected else "")),
-                 (59, "uptime: %s" % uptimeLabel))
-    
-    for (start, label) in sysFields:
-      if start + len(label) <= rightWidth: self.addstr(y, x + start, label)
-      else: break
-    
-    if self.vals["tor/orPort"]:
-      # Line 4 / Line 2 Right (fingerprint, and possibly file descriptor usage)
-      y, x = (1, leftWidth) if isWide else (3, 0)
-      
-      fingerprintLabel = uiTools.cropStr("fingerprint: %s" % self.vals["tor/fingerprint"], width)
-      self.addstr(y, x, fingerprintLabel)
-      
-      # if there's room and we're able to retrieve both the file descriptor
-      # usage and limit then it might be presented
-      if width - x - 59 >= 20 and self.vals["tor/fdUsed"] and self.vals["tor/fdLimit"]:
-        # display file descriptor usage if we're either configured to do so or
-        # running out
-        
-        fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"]
-        
-        if fdPercent >= 60 or self._config["features.showFdUsage"]:
-          fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL
-          if fdPercent >= 95:
-            fdPercentFormat = curses.A_BOLD | uiTools.getColor("red")
-          elif fdPercent >= 90:
-            fdPercentFormat = uiTools.getColor("red")
-          elif fdPercent >= 60:
-            fdPercentFormat = uiTools.getColor("yellow")
-          
-          estimateChar = "?" if self.vals["tor/isFdLimitEstimate"] else ""
-          baseLabel = "file desc: %i / %i%s (" % (self.vals["tor/fdUsed"], self.vals["tor/fdLimit"], estimateChar)
-          
-          self.addstr(y, x + 59, baseLabel)
-          self.addstr(y, x + 59 + len(baseLabel), fdPercentLabel, fdPercentFormat)
-          self.addstr(y, x + 59 + len(baseLabel) + len(fdPercentLabel), ")")
-      
-      # Line 5 / Line 3 Left (flags)
-      if self._isTorConnected:
-        flagLine = "flags: "
-        for flag in self.vals["tor/flags"]:
-          flagColor = FLAG_COLORS[flag] if flag in FLAG_COLORS.keys() else "white"
-          flagLine += "<b><%s>%s</%s></b>, " % (flagColor, flag, flagColor)
-        
-        if len(self.vals["tor/flags"]) > 0: flagLine = flagLine[:-2]
-        else: flagLine += "<b><cyan>none</cyan></b>"
-        
-        self.addfstr(2 if isWide else 4, 0, flagLine)
-      else:
-        statusTime = torTools.getConn().getStatus()[1]
-        statusTimeLabel = time.strftime("%H:%M %m/%d/%Y", time.localtime(statusTime))
-        self.addfstr(2 if isWide else 4, 0, "<b><red>Tor Disconnected</red></b> (%s)" % statusTimeLabel)
-      
-      # Undisplayed / Line 3 Right (exit policy)
-      if isWide:
-        exitPolicy = self.vals["tor/exitPolicy"]
-        
-        # adds note when default exit policy is appended
-        if exitPolicy == "": exitPolicy = "<default>"
-        elif not exitPolicy.endswith((" *:*", " *")): exitPolicy += ", <default>"
-        
-        # color codes accepts to be green, rejects to be red, and default marker to be cyan
-        isSimple = len(exitPolicy) > rightWidth - 13
-        policies = exitPolicy.split(", ")
-        for i in range(len(policies)):
-          policy = policies[i].strip()
-          displayedPolicy = policy.replace("accept", "").replace("reject", "").strip() if isSimple else policy
-          if policy.startswith("accept"): policy = "<green><b>%s</b></green>" % displayedPolicy
-          elif policy.startswith("reject"): policy = "<red><b>%s</b></red>" % displayedPolicy
-          elif policy.startswith("<default>"): policy = "<cyan><b>%s</b></cyan>" % displayedPolicy
-          policies[i] = policy
-        
-        self.addfstr(2, leftWidth, "exit policy: %s" % ", ".join(policies))
-    else:
-      # Client only
-      # TODO: not sure what information to provide here...
-      pass
-    
-    self.valsLock.release()
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents updates from being presented.
-    """
-    
-    if not self._isPaused == isPause:
-      self._isPaused = isPause
-      if self._isTorConnected:
-        if isPause: self._haltTime = time.time()
-        else: self._haltTime = None
-      
-      # Redraw now so we'll be displaying the state right when paused
-      # (otherwise the uptime might be off by a second, and change when
-      # the panel's redrawn for other reasons).
-      self.redraw(True)
-  
-  def run(self):
-    """
-    Keeps stats updated, checking for new information at a set rate.
-    """
-    
-    lastDraw = time.time() - 1
-    while not self._halt:
-      currentTime = time.time()
-      
-      if self._isPaused or currentTime - lastDraw < 1 or not self._isTorConnected:
-        self._cond.acquire()
-        if not self._halt: self._cond.wait(0.2)
-        self._cond.release()
-      else:
-        # Update the volatile attributes (cpu, memory, flags, etc) if we have
-        # a new resource usage sampling (the most dynamic stat) or its been
-        # twenty seconds since last fetched (so we still refresh occasionally
-        # when resource fetches fail).
-        # 
-        # Otherwise, just redraw the panel to change the uptime field.
-        
-        isChanged = False
-        if self.vals["tor/pid"]:
-          resourceTracker = sysTools.getResourceTracker(self.vals["tor/pid"])
-          isChanged = self._lastResourceFetch != resourceTracker.getRunCount()
-        
-        if isChanged or currentTime - self._lastUpdate >= 20:
-          self._update()
-        
-        self.redraw(True)
-        lastDraw += 1
-  
-  def stop(self):
-    """
-    Halts further resolutions and terminates the thread.
-    """
-    
-    self._cond.acquire()
-    self._halt = True
-    self._cond.notifyAll()
-    self._cond.release()
-  
-  def resetListener(self, conn, eventType):
-    """
-    Updates static parameters on tor reload (sighup) events.
-    
-    Arguments:
-      conn      - tor controller
-      eventType - type of event detected
-    """
-    
-    if eventType == torTools.State.INIT:
-      self._isTorConnected = True
-      if self._isPaused: self._haltTime = time.time()
-      else: self._haltTime = None
-      
-      self._update(True)
-      self.redraw(True)
-    elif eventType == torTools.State.CLOSED:
-      self._isTorConnected = False
-      self._haltTime = time.time()
-      self._update()
-      self.redraw(True)
-  
-  def _update(self, setStatic=False):
-    """
-    Updates stats in the vals mapping. By default this just revises volatile
-    attributes.
-    
-    Arguments:
-      setStatic - resets all parameters, including relatively static values
-    """
-    
-    self.valsLock.acquire()
-    conn = torTools.getConn()
-    
-    if setStatic:
-      # version is truncated to first part, for instance:
-      # 0.2.2.13-alpha (git-feb8c1b5f67f2c6f) -> 0.2.2.13-alpha
-      self.vals["tor/version"] = conn.getInfo("version", "Unknown").split()[0]
-      self.vals["tor/versionStatus"] = conn.getInfo("status/version/current", "Unknown")
-      self.vals["tor/nickname"] = conn.getOption("Nickname", "")
-      self.vals["tor/orPort"] = conn.getOption("ORPort", "0")
-      self.vals["tor/dirPort"] = conn.getOption("DirPort", "0")
-      self.vals["tor/controlPort"] = conn.getOption("ControlPort", "")
-      self.vals["tor/isAuthPassword"] = conn.getOption("HashedControlPassword") != None
-      self.vals["tor/isAuthCookie"] = conn.getOption("CookieAuthentication") == "1"
-      
-      # orport is reported as zero if unset
-      if self.vals["tor/orPort"] == "0": self.vals["tor/orPort"] = ""
-      
-      # overwrite address if ORListenAddress is set (and possibly orPort too)
-      self.vals["tor/orListenAddr"] = ""
-      listenAddr = conn.getOption("ORListenAddress")
-      if listenAddr:
-        if ":" in listenAddr:
-          # both ip and port overwritten
-          self.vals["tor/orListenAddr"] = listenAddr[:listenAddr.find(":")]
-          self.vals["tor/orPort"] = listenAddr[listenAddr.find(":") + 1:]
-        else:
-          self.vals["tor/orListenAddr"] = listenAddr
-      
-      # fetch exit policy (might span over multiple lines)
-      policyEntries = []
-      for exitPolicy in conn.getOption("ExitPolicy", [], True):
-        policyEntries += [policy.strip() for policy in exitPolicy.split(",")]
-      self.vals["tor/exitPolicy"] = ", ".join(policyEntries)
-      
-      # file descriptor limit for the process, if this can't be determined
-      # then the limit is None
-      fdLimit, fdIsEstimate = conn.getMyFileDescriptorLimit()
-      self.vals["tor/fdLimit"] = fdLimit
-      self.vals["tor/isFdLimitEstimate"] = fdIsEstimate
-      
-      # system information
-      unameVals = os.uname()
-      self.vals["sys/hostname"] = unameVals[1]
-      self.vals["sys/os"] = unameVals[0]
-      self.vals["sys/version"] = unameVals[2]
-      
-      pid = conn.getMyPid()
-      self.vals["tor/pid"] = pid if pid else ""
-      
-      startTime = conn.getStartTime()
-      self.vals["tor/startTime"] = startTime if startTime else ""
-      
-      # reverts volatile parameters to defaults
-      self.vals["tor/fingerprint"] = "Unknown"
-      self.vals["tor/flags"] = []
-      self.vals["tor/fdUsed"] = 0
-      self.vals["stat/%torCpu"] = "0"
-      self.vals["stat/%armCpu"] = "0"
-      self.vals["stat/rss"] = "0"
-      self.vals["stat/%mem"] = "0"
-    
-    # sets volatile parameters
-    # TODO: This can change, being reported by STATUS_SERVER -> EXTERNAL_ADDRESS
-    # events. Introduce caching via torTools?
-    self.vals["tor/address"] = conn.getInfo("address", "")
-    
-    self.vals["tor/fingerprint"] = conn.getInfo("fingerprint", self.vals["tor/fingerprint"])
-    self.vals["tor/flags"] = conn.getMyFlags(self.vals["tor/flags"])
-    
-    # Updates file descriptor usage and logs if the usage is high. If we don't
-    # have a known limit or it's obviously faulty (being lower than our
-    # current usage) then omit file descriptor functionality.
-    if self.vals["tor/fdLimit"]:
-      fdUsed = conn.getMyFileDescriptorUsage()
-      if fdUsed and fdUsed <= self.vals["tor/fdLimit"]: self.vals["tor/fdUsed"] = fdUsed
-      else: self.vals["tor/fdUsed"] = 0
-    
-    if self.vals["tor/fdUsed"] and self.vals["tor/fdLimit"]:
-      fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"]
-      estimatedLabel = " estimated" if self.vals["tor/isFdLimitEstimate"] else ""
-      msg = "Tor's%s file descriptor usage is at %i%%." % (estimatedLabel, fdPercent)
-      
-      if fdPercent >= 90 and not self._isFdNinetyPercentWarned:
-        self._isFdSixtyPercentWarned, self._isFdNinetyPercentWarned = True, True
-        msg += " If you run out Tor will be unable to continue functioning."
-        log.log(self._config["log.fdUsageNinetyPercent"], msg)
-      elif fdPercent >= 60 and not self._isFdSixtyPercentWarned:
-        self._isFdSixtyPercentWarned = True
-        log.log(self._config["log.fdUsageSixtyPercent"], msg)
-    
-    # ps or proc derived resource usage stats
-    if self.vals["tor/pid"]:
-      resourceTracker = sysTools.getResourceTracker(self.vals["tor/pid"])
-      
-      if resourceTracker.lastQueryFailed():
-        self.vals["stat/%torCpu"] = "0"
-        self.vals["stat/rss"] = "0"
-        self.vals["stat/%mem"] = "0"
-      else:
-        cpuUsage, _, memUsage, memUsagePercent = resourceTracker.getResourceUsage()
-        self._lastResourceFetch = resourceTracker.getRunCount()
-        self.vals["stat/%torCpu"] = "%0.1f" % (100 * cpuUsage)
-        self.vals["stat/rss"] = str(memUsage)
-        self.vals["stat/%mem"] = "%0.1f" % (100 * memUsagePercent)
-    
-    # determines the cpu time for the arm process (including user and system
-    # time of both the primary and child processes)
-    
-    totalArmCpuTime, currentTime = sum(os.times()[:3]), time.time()
-    armCpuDelta = totalArmCpuTime - self._armCpuSampling[0]
-    armTimeDelta = currentTime - self._armCpuSampling[1]
-    pythonCpuTime = armCpuDelta / armTimeDelta
-    sysCallCpuTime = sysTools.getSysCpuUsage()
-    self.vals["stat/%armCpu"] = "%0.1f" % (100 * (pythonCpuTime + sysCallCpuTime))
-    self._armCpuSampling = (totalArmCpuTime, currentTime)
-    
-    self._lastUpdate = currentTime
-    self.valsLock.release()
-
diff --git a/src/interface/logPanel.py b/src/interface/logPanel.py
deleted file mode 100644
index 86e680f..0000000
--- a/src/interface/logPanel.py
+++ /dev/null
@@ -1,1100 +0,0 @@
-"""
-Panel providing a chronological log of events its been configured to listen
-for. This provides prepopulation from the log file and supports filtering by
-regular expressions.
-"""
-
-import time
-import os
-import curses
-import threading
-
-from TorCtl import TorCtl
-
-from version import VERSION
-from util import conf, log, panel, sysTools, torTools, uiTools
-
-TOR_EVENT_TYPES = {
-  "d": "DEBUG",   "a": "ADDRMAP",          "k": "DESCCHANGED",  "s": "STREAM",
-  "i": "INFO",    "f": "AUTHDIR_NEWDESCS", "g": "GUARD",        "r": "STREAM_BW",
-  "n": "NOTICE",  "h": "BUILDTIMEOUT_SET", "l": "NEWCONSENSUS", "t": "STATUS_CLIENT",
-  "w": "WARN",    "b": "BW",               "m": "NEWDESC",      "u": "STATUS_GENERAL",
-  "e": "ERR",     "c": "CIRC",             "p": "NS",           "v": "STATUS_SERVER",
-                  "j": "CLIENTS_SEEN",     "q": "ORCONN"}
-
-EVENT_LISTING = """        d DEBUG      a ADDRMAP           k DESCCHANGED   s STREAM
-        i INFO       f AUTHDIR_NEWDESCS  g GUARD         r STREAM_BW
-        n NOTICE     h BUILDTIMEOUT_SET  l NEWCONSENSUS  t STATUS_CLIENT
-        w WARN       b BW                m NEWDESC       u STATUS_GENERAL
-        e ERR        c CIRC              p NS            v STATUS_SERVER
-                     j CLIENTS_SEEN      q ORCONN
-          DINWE tor runlevel+            A All Events
-          12345 arm runlevel+            X No Events
-          67890 torctl runlevel+         U Unknown Events"""
-
-RUNLEVEL_EVENT_COLOR = {log.DEBUG: "magenta", log.INFO: "blue", log.NOTICE: "green",
-                        log.WARN: "yellow", log.ERR: "red"}
-DAYBREAK_EVENT = "DAYBREAK" # special event for marking when the date changes
-TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
-
-ENTRY_INDENT = 2 # spaces an entry's message is indented after the first line
-DEFAULT_CONFIG = {"features.logFile": "",
-                  "features.log.showDateDividers": True,
-                  "features.log.showDuplicateEntries": False,
-                  "features.log.entryDuration": 7,
-                  "features.log.maxLinesPerEntry": 4,
-                  "features.log.prepopulate": True,
-                  "features.log.prepopulateReadLimit": 5000,
-                  "features.log.maxRefreshRate": 300,
-                  "cache.logPanel.size": 1000,
-                  "log.logPanel.prepopulateSuccess": log.INFO,
-                  "log.logPanel.prepopulateFailed": log.WARN,
-                  "log.logPanel.logFileOpened": log.NOTICE,
-                  "log.logPanel.logFileWriteFailed": log.ERR,
-                  "log.logPanel.forceDoubleRedraw": log.DEBUG}
-
-DUPLICATE_MSG = " [%i duplicate%s hidden]"
-
-# The height of the drawn content is estimated based on the last time we redrew
-# the panel. It's chiefly used for scrolling and the bar indicating its
-# position. Letting the estimate be too inaccurate results in a display bug, so
-# redraws the display if it's off by this threshold.
-CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
-
-# static starting portion of common log entries, fetched from the config when
-# needed if None
-COMMON_LOG_MESSAGES = None
-
-# cached values and the arguments that generated it for the getDaybreaks and
-# getDuplicates functions
-CACHED_DAYBREAKS_ARGUMENTS = (None, None) # events, current day
-CACHED_DAYBREAKS_RESULT = None
-CACHED_DUPLICATES_ARGUMENTS = None # events
-CACHED_DUPLICATES_RESULT = None
-
-# duration we'll wait for the deduplication function before giving up (in ms)
-DEDUPLICATION_TIMEOUT = 100
-
-def daysSince(timestamp=None):
-  """
-  Provides the number of days since the epoch converted to local time (rounded
-  down).
-  
-  Arguments:
-    timestamp - unix timestamp to convert, current time if undefined
-  """
-  
-  if timestamp == None: timestamp = time.time()
-  return int((timestamp - TIMEZONE_OFFSET) / 86400)
-
-def expandEvents(eventAbbr):
-  """
-  Expands event abbreviations to their full names. Beside mappings provided in
-  TOR_EVENT_TYPES this recognizes the following special events and aliases:
-  U - UKNOWN events
-  A - all events
-  X - no events
-  DINWE - runlevel and higher
-  12345 - arm runlevel and higher (ARM_DEBUG - ARM_ERR)
-  67890 - torctl runlevel and higher (TORCTL_DEBUG - TORCTL_ERR)
-  Raises ValueError with invalid input if any part isn't recognized.
-  
-  Examples:
-  "inUt" -> ["INFO", "NOTICE", "UNKNOWN", "STREAM_BW"]
-  "N4" -> ["NOTICE", "WARN", "ERR", "ARM_WARN", "ARM_ERR"]
-  "cfX" -> []
-  
-  Arguments:
-    eventAbbr - flags to be parsed to event types
-  """
-  
-  expandedEvents, invalidFlags = set(), ""
-  
-  for flag in eventAbbr:
-    if flag == "A":
-      armRunlevels = ["ARM_" + runlevel for runlevel in log.Runlevel.values()]
-      torctlRunlevels = ["TORCTL_" + runlevel for runlevel in log.Runlevel.values()]
-      expandedEvents = set(TOR_EVENT_TYPES.values() + armRunlevels + torctlRunlevels + ["UNKNOWN"])
-      break
-    elif flag == "X":
-      expandedEvents = set()
-      break
-    elif flag in "DINWE1234567890":
-      # all events for a runlevel and higher
-      if flag in "DINWE": typePrefix = ""
-      elif flag in "12345": typePrefix = "ARM_"
-      elif flag in "67890": typePrefix = "TORCTL_"
-      
-      if flag in "D16": runlevelIndex = 0
-      elif flag in "I27": runlevelIndex = 1
-      elif flag in "N38": runlevelIndex = 2
-      elif flag in "W49": runlevelIndex = 3
-      elif flag in "E50": runlevelIndex = 4
-      
-      runlevelSet = [typePrefix + runlevel for runlevel in log.Runlevel.values()[runlevelIndex:]]
-      expandedEvents = expandedEvents.union(set(runlevelSet))
-    elif flag == "U":
-      expandedEvents.add("UNKNOWN")
-    elif flag in TOR_EVENT_TYPES:
-      expandedEvents.add(TOR_EVENT_TYPES[flag])
-    else:
-      invalidFlags += flag
-  
-  if invalidFlags: raise ValueError(invalidFlags)
-  else: return expandedEvents
-
-def getMissingEventTypes():
-  """
-  Provides the event types the current torctl connection supports but arm
-  doesn't. This provides an empty list if no event types are missing, and None
-  if the GETINFO query fails.
-  """
-  
-  torEventTypes = torTools.getConn().getInfo("events/names")
-  
-  if torEventTypes:
-    torEventTypes = torEventTypes.split(" ")
-    armEventTypes = TOR_EVENT_TYPES.values()
-    return [event for event in torEventTypes if not event in armEventTypes]
-  else: return None # GETINFO call failed
-
-def loadLogMessages():
-  """
-  Fetches a mapping of common log messages to their runlevels from the config.
-  """
-  
-  global COMMON_LOG_MESSAGES
-  armConf = conf.getConfig("arm")
-  
-  COMMON_LOG_MESSAGES = {}
-  for confKey in armConf.getKeys():
-    if confKey.startswith("msg."):
-      eventType = confKey[4:].upper()
-      messages = armConf.get(confKey, [])
-      COMMON_LOG_MESSAGES[eventType] = messages
-
-def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = None):
-  """
-  Parses tor's log file for past events matching the given runlevels, providing
-  a list of log entries (ordered newest to oldest). Limiting the number of read
-  entries is suggested to avoid parsing everything from logs in the GB and TB
-  range.
-  
-  Arguments:
-    runlevels - event types (DEBUG - ERR) to be returned
-    readLimit - max lines of the log file that'll be read (unlimited if None)
-    addLimit  - maximum entries to provide back (unlimited if None)
-    config    - configuration parameters related to this panel, uses defaults
-                if left as None
-  """
-  
-  startTime = time.time()
-  if not runlevels: return []
-  
-  if not config: config = DEFAULT_CONFIG
-  
-  # checks tor's configuration for the log file's location (if any exists)
-  loggingTypes, loggingLocation = None, None
-  for loggingEntry in torTools.getConn().getOption("Log", [], True):
-    # looks for an entry like: notice file /var/log/tor/notices.log
-    entryComp = loggingEntry.split()
-    
-    if entryComp[1] == "file":
-      loggingTypes, loggingLocation = entryComp[0], entryComp[2]
-      break
-  
-  if not loggingLocation: return []
-  
-  # includes the prefix for tor paths
-  loggingLocation = torTools.getConn().getPathPrefix() + loggingLocation
-  
-  # if the runlevels argument is a superset of the log file then we can
-  # limit the read contents to the addLimit
-  runlevels = log.Runlevel.values()
-  loggingTypes = loggingTypes.upper()
-  if addLimit and (not readLimit or readLimit > addLimit):
-    if "-" in loggingTypes:
-      divIndex = loggingTypes.find("-")
-      sIndex = runlevels.index(loggingTypes[:divIndex])
-      eIndex = runlevels.index(loggingTypes[divIndex+1:])
-      logFileRunlevels = runlevels[sIndex:eIndex+1]
-    else:
-      sIndex = runlevels.index(loggingTypes)
-      logFileRunlevels = runlevels[sIndex:]
-    
-    # checks if runlevels we're reporting are a superset of the file's contents
-    isFileSubset = True
-    for runlevelType in logFileRunlevels:
-      if runlevelType not in runlevels:
-        isFileSubset = False
-        break
-    
-    if isFileSubset: readLimit = addLimit
-  
-  # tries opening the log file, cropping results to avoid choking on huge logs
-  lines = []
-  try:
-    if readLimit:
-      lines = sysTools.call("tail -n %i %s" % (readLimit, loggingLocation))
-      if not lines: raise IOError()
-    else:
-      logFile = open(loggingLocation, "r")
-      lines = logFile.readlines()
-      logFile.close()
-  except IOError:
-    msg = "Unable to read tor's log file: %s" % loggingLocation
-    log.log(config["log.logPanel.prepopulateFailed"], msg)
-  
-  if not lines: return []
-  
-  loggedEvents = []
-  currentUnixTime, currentLocalTime = time.time(), time.localtime()
-  for i in range(len(lines) - 1, -1, -1):
-    line = lines[i]
-    
-    # entries look like:
-    # Jul 15 18:29:48.806 [notice] Parsing GEOIP file.
-    lineComp = line.split()
-    eventType = lineComp[3][1:-1].upper()
-    
-    if eventType in runlevels:
-      # converts timestamp to unix time
-      timestamp = " ".join(lineComp[:3])
-      
-      # strips the decimal seconds
-      if "." in timestamp: timestamp = timestamp[:timestamp.find(".")]
-      
-      # overwrites missing time parameters with the local time (ignoring wday
-      # and yday since they aren't used)
-      eventTimeComp = list(time.strptime(timestamp, "%b %d %H:%M:%S"))
-      eventTimeComp[0] = currentLocalTime.tm_year
-      eventTimeComp[8] = currentLocalTime.tm_isdst
-      eventTime = time.mktime(eventTimeComp) # converts local to unix time
-      
-      # The above is gonna be wrong if the logs are for the previous year. If
-      # the event's in the future then correct for this.
-      if eventTime > currentUnixTime + 60:
-        eventTimeComp[0] -= 1
-        eventTime = time.mktime(eventTimeComp)
-      
-      eventMsg = " ".join(lineComp[4:])
-      loggedEvents.append(LogEntry(eventTime, eventType, eventMsg, RUNLEVEL_EVENT_COLOR[eventType]))
-    
-    if "opening log file" in line:
-      break # this entry marks the start of this tor instance
-  
-  if addLimit: loggedEvents = loggedEvents[:addLimit]
-  msg = "Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(loggedEvents), loggingLocation, readLimit, time.time() - startTime)
-  log.log(config["log.logPanel.prepopulateSuccess"], msg)
-  return loggedEvents
-
-def getDaybreaks(events, ignoreTimeForCache = False):
-  """
-  Provides the input events back with special 'DAYBREAK_EVENT' markers inserted
-  whenever the date changed between log entries (or since the most recent
-  event). The timestamp matches the beginning of the day for the following
-  entry.
-  
-  Arguments:
-    events             - chronologically ordered listing of events
-    ignoreTimeForCache - skips taking the day into consideration for providing
-                         cached results if true
-  """
-  
-  global CACHED_DAYBREAKS_ARGUMENTS, CACHED_DAYBREAKS_RESULT
-  if not events: return []
-  
-  newListing = []
-  currentDay = daysSince()
-  lastDay = currentDay
-  
-  if CACHED_DAYBREAKS_ARGUMENTS[0] == events and \
-    (ignoreTimeForCache or CACHED_DAYBREAKS_ARGUMENTS[1] == currentDay):
-    return list(CACHED_DAYBREAKS_RESULT)
-  
-  for entry in events:
-    eventDay = daysSince(entry.timestamp)
-    if eventDay != lastDay:
-      markerTimestamp = (eventDay * 86400) + TIMEZONE_OFFSET
-      newListing.append(LogEntry(markerTimestamp, DAYBREAK_EVENT, "", "white"))
-    
-    newListing.append(entry)
-    lastDay = eventDay
-  
-  CACHED_DAYBREAKS_ARGUMENTS = (list(events), currentDay)
-  CACHED_DAYBREAKS_RESULT = list(newListing)
-  
-  return newListing
-
-def getDuplicates(events):
-  """
-  Deduplicates a list of log entries, providing back a tuple listing with the
-  log entry and count of duplicates following it. Entries in different days are
-  not considered to be duplicates. This times out, returning None if it takes
-  longer than DEDUPLICATION_TIMEOUT.
-  
-  Arguments:
-    events - chronologically ordered listing of events
-  """
-  
-  global CACHED_DUPLICATES_ARGUMENTS, CACHED_DUPLICATES_RESULT
-  if CACHED_DUPLICATES_ARGUMENTS == events:
-    return list(CACHED_DUPLICATES_RESULT)
-  
-  # loads common log entries from the config if they haven't been
-  if COMMON_LOG_MESSAGES == None: loadLogMessages()
-  
-  startTime = time.time()
-  eventsRemaining = list(events)
-  returnEvents = []
-  
-  while eventsRemaining:
-    entry = eventsRemaining.pop(0)
-    duplicateIndices = isDuplicate(entry, eventsRemaining, True)
-    
-    # checks if the call timeout has been reached
-    if (time.time() - startTime) > DEDUPLICATION_TIMEOUT / 1000.0:
-      return None
-    
-    # drops duplicate entries
-    duplicateIndices.reverse()
-    for i in duplicateIndices: del eventsRemaining[i]
-    
-    returnEvents.append((entry, len(duplicateIndices)))
-  
-  CACHED_DUPLICATES_ARGUMENTS = list(events)
-  CACHED_DUPLICATES_RESULT = list(returnEvents)
-  
-  return returnEvents
-
-def isDuplicate(event, eventSet, getDuplicates = False):
-  """
-  True if the event is a duplicate for something in the eventSet, false
-  otherwise. If the getDuplicates flag is set this provides the indices of
-  the duplicates instead.
-  
-  Arguments:
-    event         - event to search for duplicates of
-    eventSet      - set to look for the event in
-    getDuplicates - instead of providing back a boolean this gives a list of
-                    the duplicate indices in the eventSet
-  """
-  
-  duplicateIndices = []
-  for i in range(len(eventSet)):
-    forwardEntry = eventSet[i]
-    
-    # if showing dates then do duplicate detection for each day, rather
-    # than globally
-    if forwardEntry.type == DAYBREAK_EVENT: break
-    
-    if event.type == forwardEntry.type:
-      isDuplicate = False
-      if event.msg == forwardEntry.msg: isDuplicate = True
-      elif event.type in COMMON_LOG_MESSAGES:
-        for commonMsg in COMMON_LOG_MESSAGES[event.type]:
-          # if it starts with an asterisk then check the whole message rather
-          # than just the start
-          if commonMsg[0] == "*":
-            isDuplicate = commonMsg[1:] in event.msg and commonMsg[1:] in forwardEntry.msg
-          else:
-            isDuplicate = event.msg.startswith(commonMsg) and forwardEntry.msg.startswith(commonMsg)
-          
-          if isDuplicate: break
-      
-      if isDuplicate:
-        if getDuplicates: duplicateIndices.append(i)
-        else: return True
-  
-  if getDuplicates: return duplicateIndices
-  else: return False
-
-class LogEntry():
-  """
-  Individual log file entry, having the following attributes:
-    timestamp - unix timestamp for when the event occurred
-    eventType - event type that occurred ("INFO", "BW", "ARM_WARN", etc)
-    msg       - message that was logged
-    color     - color of the log entry
-  """
-  
-  def __init__(self, timestamp, eventType, msg, color):
-    self.timestamp = timestamp
-    self.type = eventType
-    self.msg = msg
-    self.color = color
-    self._displayMessage = None
-  
-  def getDisplayMessage(self, includeDate = False):
-    """
-    Provides the entry's message for the log.
-    
-    Arguments:
-      includeDate - appends the event's date to the start of the message
-    """
-    
-    if includeDate:
-      # not the common case so skip caching
-      entryTime = time.localtime(self.timestamp)
-      timeLabel =  "%i/%i/%i %02i:%02i:%02i" % (entryTime[1], entryTime[2], entryTime[0], entryTime[3], entryTime[4], entryTime[5])
-      return "%s [%s] %s" % (timeLabel, self.type, self.msg)
-    
-    if not self._displayMessage:
-      entryTime = time.localtime(self.timestamp)
-      self._displayMessage = "%02i:%02i:%02i [%s] %s" % (entryTime[3], entryTime[4], entryTime[5], self.type, self.msg)
-    
-    return self._displayMessage
-
-class TorEventObserver(TorCtl.PostEventListener):
-  """
-  Listens for all types of events provided by TorCtl, providing an LogEntry
-  instance to the given callback function.
-  """
-  
-  def __init__(self, callback):
-    """
-    Tor event listener with the purpose of translating events to nicely
-    formatted calls of a callback function.
-    
-    Arguments:
-      callback - function accepting a LogEntry, called when an event of these
-                 types occur
-    """
-    
-    TorCtl.PostEventListener.__init__(self)
-    self.callback = callback
-  
-  def circ_status_event(self, event):
-    msg = "ID: %-3s STATUS: %-10s PATH: %s" % (event.circ_id, event.status, ", ".join(event.path))
-    if event.purpose: msg += " PURPOSE: %s" % event.purpose
-    if event.reason: msg += " REASON: %s" % event.reason
-    if event.remote_reason: msg += " REMOTE_REASON: %s" % event.remote_reason
-    self._notify(event, msg, "yellow")
-  
-  def buildtimeout_set_event(self, event):
-    self._notify(event, "SET_TYPE: %s, TOTAL_TIMES: %s, TIMEOUT_MS: %s, XM: %s, ALPHA: %s, CUTOFF_QUANTILE: %s" % (event.set_type, event.total_times, event.timeout_ms, event.xm, event.alpha, event.cutoff_quantile))
-  
-  def stream_status_event(self, event):
-    self._notify(event, "ID: %s STATUS: %s CIRC_ID: %s TARGET: %s:%s REASON: %s REMOTE_REASON: %s SOURCE: %s SOURCE_ADDR: %s PURPOSE: %s" % (event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose))
-  
-  def or_conn_status_event(self, event):
-    msg = "STATUS: %-10s ENDPOINT: %-20s" % (event.status, event.endpoint)
-    if event.age: msg += " AGE: %-3s" % event.age
-    if event.read_bytes: msg += " READ: %-4i" % event.read_bytes
-    if event.wrote_bytes: msg += " WRITTEN: %-4i" % event.wrote_bytes
-    if event.reason: msg += " REASON: %-6s" % event.reason
-    if event.ncircs: msg += " NCIRCS: %i" % event.ncircs
-    self._notify(event, msg)
-  
-  def stream_bw_event(self, event):
-    self._notify(event, "ID: %s READ: %s WRITTEN: %s" % (event.strm_id, event.bytes_read, event.bytes_written))
-  
-  def bandwidth_event(self, event):
-    self._notify(event, "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
-  
-  def msg_event(self, event):
-    self._notify(event, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
-  
-  def new_desc_event(self, event):
-    idlistStr = [str(item) for item in event.idlist]
-    self._notify(event, ", ".join(idlistStr))
-  
-  def address_mapped_event(self, event):
-    self._notify(event, "%s, %s -> %s" % (event.when, event.from_addr, event.to_addr))
-  
-  def ns_event(self, event):
-    # NetworkStatus params: nickname, idhash, orhash, ip, orport (int),
-    #     dirport (int), flags, idhex, bandwidth, updated (datetime)
-    msg = ", ".join(["%s (%s)" % (ns.idhex, ns.nickname) for ns in event.nslist])
-    self._notify(event, "Listed (%i): %s" % (len(event.nslist), msg), "blue")
-  
-  def new_consensus_event(self, event):
-    msg = ", ".join(["%s (%s)" % (ns.idhex, ns.nickname) for ns in event.nslist])
-    self._notify(event, "Listed (%i): %s" % (len(event.nslist), msg), "magenta")
-  
-  def unknown_event(self, event):
-    msg = "(%s) %s" % (event.event_name, event.event_string)
-    self.callback(LogEntry(event.arrived_at, "UNKNOWN", msg, "red"))
-  
-  def _notify(self, event, msg, color="white"):
-    self.callback(LogEntry(event.arrived_at, event.event_name, msg, color))
-
-class LogPanel(panel.Panel, threading.Thread):
-  """
-  Listens for and displays tor, arm, and torctl events. This can prepopulate
-  from tor's log file if it exists.
-  """
-  
-  def __init__(self, stdscr, loggedEvents, config=None):
-    panel.Panel.__init__(self, stdscr, "log", 0)
-    threading.Thread.__init__(self)
-    self.setDaemon(True)
-    
-    self._config = dict(DEFAULT_CONFIG)
-    
-    if config:
-      config.update(self._config, {
-        "features.log.maxLinesPerEntry": 1,
-        "features.log.prepopulateReadLimit": 0,
-        "features.log.maxRefreshRate": 10,
-        "cache.logPanel.size": 1000})
-    
-    # collapses duplicate log entries if false, showing only the most recent
-    self.showDuplicates = self._config["features.log.showDuplicateEntries"]
-    
-    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.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
-    self._isPaused = False
-    self._pauseBuffer = []              # location where messages are buffered if paused
-    
-    self._lastUpdate = -1               # time the content was last revised
-    self._halt = False                  # terminates thread if true
-    self._cond = threading.Condition()  # used for pausing/resuming the thread
-    
-    # restricts concurrent write access to attributes used to draw the display
-    # and pausing:
-    # msgLog, loggedEvents, regexFilter, scroll, _pauseBuffer
-    self.valsLock = threading.RLock()
-    
-    # cached parameters (invalidated if arguments for them change)
-    # last set of events we've drawn with
-    self._lastLoggedEvents = []
-    
-    # _getTitle (args: loggedEvents, regexFilter pattern, width)
-    self._titleCache = None
-    self._titleArgs = (None, None, None)
-    
-    # fetches past tor events from log file, if available
-    torEventBacklog = []
-    if self._config["features.log.prepopulate"]:
-      setRunlevels = list(set.intersection(set(self.loggedEvents), set(log.Runlevel.values())))
-      readLimit = self._config["features.log.prepopulateReadLimit"]
-      addLimit = self._config["cache.logPanel.size"]
-      torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config)
-    
-    # adds arm listener and fetches past events
-    log.LOG_LOCK.acquire()
-    try:
-      armRunlevels = [log.DEBUG, log.INFO, log.NOTICE, log.WARN, log.ERR]
-      log.addListeners(armRunlevels, self._registerArmEvent)
-      
-      # gets the set of arm events we're logging
-      setRunlevels = []
-      for i in range(len(armRunlevels)):
-        if "ARM_" + log.Runlevel.values()[i] in self.loggedEvents:
-          setRunlevels.append(armRunlevels[i])
-      
-      armEventBacklog = []
-      for level, msg, eventTime in log._getEntries(setRunlevels):
-        armEventEntry = LogEntry(eventTime, "ARM_" + level, msg, RUNLEVEL_EVENT_COLOR[level])
-        armEventBacklog.insert(0, armEventEntry)
-      
-      # joins armEventBacklog and torEventBacklog chronologically into msgLog
-      while armEventBacklog or torEventBacklog:
-        if not armEventBacklog:
-          self.msgLog.append(torEventBacklog.pop(0))
-        elif not torEventBacklog:
-          self.msgLog.append(armEventBacklog.pop(0))
-        elif armEventBacklog[0].timestamp < torEventBacklog[0].timestamp:
-          self.msgLog.append(torEventBacklog.pop(0))
-        else:
-          self.msgLog.append(armEventBacklog.pop(0))
-    finally:
-      log.LOG_LOCK.release()
-    
-    # crops events that are either too old, or more numerous than the caching size
-    self._trimEvents(self.msgLog)
-    
-    # leaving lastContentHeight as being too low causes initialization problems
-    self.lastContentHeight = len(self.msgLog)
-    
-    # adds listeners for tor and torctl events
-    conn = torTools.getConn()
-    conn.addEventListener(TorEventObserver(self.registerEvent))
-    conn.addTorCtlListener(self._registerTorCtlEvent)
-    
-    # opens log file if we'll be saving entries
-    if self._config["features.logFile"]:
-      logPath = self._config["features.logFile"]
-      
-      try:
-        # make dir if the path doesn't already exist
-        baseDir = os.path.dirname(logPath)
-        if not os.path.exists(baseDir): os.makedirs(baseDir)
-        
-        self.logFile = open(logPath, "a")
-        log.log(self._config["log.logPanel.logFileOpened"], "arm %s opening log file (%s)" % (VERSION, logPath))
-      except (IOError, OSError), exc:
-        log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
-        self.logFile = None
-  
-  def registerEvent(self, event):
-    """
-    Notes event and redraws log. If paused it's held in a temporary buffer.
-    
-    Arguments:
-      event - LogEntry for the event that occurred
-    """
-    
-    if not event.type in self.loggedEvents: return
-    
-    # strips control characters to avoid screwing up the terminal
-    event.msg = uiTools.getPrintable(event.msg)
-    
-    # note event in the log file if we're saving them
-    if self.logFile:
-      try:
-        self.logFile.write(event.getDisplayMessage(True) + "\n")
-        self.logFile.flush()
-      except IOError, exc:
-        log.log(self._config["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
-        self.logFile = None
-    
-    if self._isPaused:
-      self.valsLock.acquire()
-      self._pauseBuffer.insert(0, event)
-      self._trimEvents(self._pauseBuffer)
-      self.valsLock.release()
-    else:
-      self.valsLock.acquire()
-      self.msgLog.insert(0, event)
-      self._trimEvents(self.msgLog)
-      
-      # notifies the display that it has new content
-      if not self.regexFilter or self.regexFilter.search(event.getDisplayMessage()):
-        self._cond.acquire()
-        self._cond.notifyAll()
-        self._cond.release()
-      
-      self.valsLock.release()
-  
-  def _registerArmEvent(self, level, msg, eventTime):
-    eventColor = RUNLEVEL_EVENT_COLOR[level]
-    self.registerEvent(LogEntry(eventTime, "ARM_%s" % level, msg, eventColor))
-  
-  def _registerTorCtlEvent(self, level, msg):
-    eventColor = RUNLEVEL_EVENT_COLOR[level]
-    self.registerEvent(LogEntry(time.time(), "TORCTL_%s" % level, msg, eventColor))
-  
-  def setLoggedEvents(self, eventTypes):
-    """
-    Sets the event types recognized by the panel.
-    
-    Arguments:
-      eventTypes - event types to be logged
-    """
-    
-    if eventTypes == self.loggedEvents: return
-    
-    self.valsLock.acquire()
-    self.loggedEvents = eventTypes
-    self.redraw(True)
-    self.valsLock.release()
-  
-  def setFilter(self, logFilter):
-    """
-    Filters log entries according to the given regular expression.
-    
-    Arguments:
-      logFilter - regular expression used to determine which messages are
-                  shown, None if no filter should be applied
-    """
-    
-    if logFilter == self.regexFilter: return
-    
-    self.valsLock.acquire()
-    self.regexFilter = logFilter
-    self.redraw(True)
-    self.valsLock.release()
-  
-  def clear(self):
-    """
-    Clears the contents of the event log.
-    """
-    
-    self.valsLock.acquire()
-    self.msgLog = []
-    self.redraw(True)
-    self.valsLock.release()
-  
-  def saveSnapshot(self, path):
-    """
-    Saves the log events currently being displayed to the given path. This
-    takes filers into account. This overwrites the file if it already exists,
-    and raises an IOError if there's a problem.
-    
-    Arguments:
-      path - path where to save the log snapshot
-    """
-    
-    # make dir if the path doesn't already exist
-    baseDir = os.path.dirname(path)
-    if not os.path.exists(baseDir): os.makedirs(baseDir)
-    
-    snapshotFile = open(path, "w")
-    self.valsLock.acquire()
-    try:
-      for entry in self.msgLog:
-        isVisible = not self.regexFilter or self.regexFilter.search(entry.getDisplayMessage())
-        if isVisible: snapshotFile.write(entry.getDisplayMessage(True) + "\n")
-      
-      self.valsLock.release()
-    except Exception, exc:
-      self.valsLock.release()
-      raise exc
-  
-  def handleKey(self, key):
-    if uiTools.isScrollKey(key):
-      pageHeight = self.getPreferredSize()[0] - 1
-      newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self.lastContentHeight)
-      
-      if self.scroll != newScroll:
-        self.valsLock.acquire()
-        self.scroll = newScroll
-        self.redraw(True)
-        self.valsLock.release()
-    elif key in (ord('u'), ord('U')):
-      self.valsLock.acquire()
-      self.showDuplicates = not self.showDuplicates
-      self.redraw(True)
-      self.valsLock.release()
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents message log from being updated with new events.
-    """
-    
-    if isPause == self._isPaused: return
-    
-    self._isPaused = isPause
-    if self._isPaused: self._pauseBuffer = []
-    else:
-      self.valsLock.acquire()
-      self.msgLog = (self._pauseBuffer + self.msgLog)[:self._config["cache.logPanel.size"]]
-      self.redraw(True)
-      self.valsLock.release()
-  
-  def draw(self, width, height):
-    """
-    Redraws message log. Entries stretch to use available space and may
-    contain up to two lines. Starts with newest entries.
-    """
-    
-    self.valsLock.acquire()
-    self._lastLoggedEvents, self._lastUpdate = list(self.msgLog), time.time()
-    
-    # draws the top label
-    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))
-    
-    # draws left-hand scroll bar if content's longer than the height
-    msgIndent, dividerIndent = 1, 0 # offsets for scroll bar
-    isScrollBarVisible = self.lastContentHeight > height - 1
-    if isScrollBarVisible:
-      msgIndent, dividerIndent = 3, 2
-      self.addScrollBar(self.scroll, self.scroll + height - 1, self.lastContentHeight, 1)
-    
-    # draws log entries
-    lineCount = 1 - self.scroll
-    seenFirstDateDivider = False
-    dividerAttr, duplicateAttr = curses.A_BOLD | uiTools.getColor("yellow"), curses.A_BOLD | uiTools.getColor("green")
-    
-    isDatesShown = self.regexFilter == None and self._config["features.log.showDateDividers"]
-    eventLog = getDaybreaks(self.msgLog, self._isPaused) if isDatesShown else list(self.msgLog)
-    if not self.showDuplicates:
-      deduplicatedLog = getDuplicates(eventLog)
-      
-      if deduplicatedLog == None:
-        msg = "Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive."
-        log.log(log.WARN, msg)
-        self.showDuplicates = True
-        deduplicatedLog = [(entry, 0) for entry in eventLog]
-    else: deduplicatedLog = [(entry, 0) for entry in eventLog]
-    
-    # determines if we have the minimum width to show date dividers
-    showDaybreaks = width - dividerIndent >= 3
-    
-    while deduplicatedLog:
-      entry, duplicateCount = deduplicatedLog.pop(0)
-      
-      if self.regexFilter and not self.regexFilter.search(entry.getDisplayMessage()):
-        continue  # filter doesn't match log message - skip
-      
-      # checks if we should be showing a divider with the date
-      if entry.type == DAYBREAK_EVENT:
-        # bottom of the divider
-        if seenFirstDateDivider:
-          if lineCount >= 1 and lineCount < height and showDaybreaks:
-            self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER,  dividerAttr)
-            self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr)
-            self.addch(lineCount, width, curses.ACS_LRCORNER, dividerAttr)
-          
-          lineCount += 1
-        
-        # top of the divider
-        if lineCount >= 1 and lineCount < height and showDaybreaks:
-          timeLabel = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp))
-          self.addch(lineCount, dividerIndent, curses.ACS_ULCORNER, dividerAttr)
-          self.addch(lineCount, dividerIndent + 1, curses.ACS_HLINE, dividerAttr)
-          self.addstr(lineCount, dividerIndent + 2, timeLabel, curses.A_BOLD | dividerAttr)
-          
-          lineLength = width - dividerIndent - len(timeLabel) - 2
-          self.hline(lineCount, dividerIndent + len(timeLabel) + 2, lineLength, dividerAttr)
-          self.addch(lineCount, dividerIndent + len(timeLabel) + 2 + lineLength, curses.ACS_URCORNER, dividerAttr)
-        
-        seenFirstDateDivider = True
-        lineCount += 1
-      else:
-        # entry contents to be displayed, tuples of the form:
-        # (msg, formatting, includeLinebreak)
-        displayQueue = []
-        
-        msgComp = entry.getDisplayMessage().split("\n")
-        for i in range(len(msgComp)):
-          font = curses.A_BOLD if "ERR" in entry.type else curses.A_NORMAL # emphasizes ERR messages
-          displayQueue.append((msgComp[i].strip(), font | uiTools.getColor(entry.color), i != len(msgComp) - 1))
-        
-        if duplicateCount:
-          pluralLabel = "s" if duplicateCount > 1 else ""
-          duplicateMsg = DUPLICATE_MSG % (duplicateCount, pluralLabel)
-          displayQueue.append((duplicateMsg, duplicateAttr, False))
-        
-        cursorLoc, lineOffset = msgIndent, 0
-        maxEntriesPerLine = self._config["features.log.maxLinesPerEntry"]
-        while displayQueue:
-          msg, format, includeBreak = displayQueue.pop(0)
-          drawLine = lineCount + lineOffset
-          if lineOffset == maxEntriesPerLine: break
-          
-          maxMsgSize = width - cursorLoc
-          if len(msg) > maxMsgSize:
-            # message is too long - break it up
-            if lineOffset == maxEntriesPerLine - 1:
-              msg = uiTools.cropStr(msg, maxMsgSize)
-            else:
-              msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True)
-              displayQueue.insert(0, (remainder.strip(), format, includeBreak))
-            
-            includeBreak = True
-          
-          if drawLine < height and drawLine >= 1:
-            if seenFirstDateDivider and width - dividerIndent >= 3 and showDaybreaks:
-              self.addch(drawLine, dividerIndent, curses.ACS_VLINE, dividerAttr)
-              self.addch(drawLine, width, curses.ACS_VLINE, dividerAttr)
-            
-            self.addstr(drawLine, cursorLoc, msg, format)
-          
-          cursorLoc += len(msg)
-          
-          if includeBreak or not displayQueue:
-            lineOffset += 1
-            cursorLoc = msgIndent + ENTRY_INDENT
-        
-        lineCount += lineOffset
-      
-      # if this is the last line and there's room, then draw the bottom of the divider
-      if not deduplicatedLog and seenFirstDateDivider:
-        if lineCount < height and showDaybreaks:
-          self.addch(lineCount, dividerIndent, curses.ACS_LLCORNER, dividerAttr)
-          self.hline(lineCount, dividerIndent + 1, width - dividerIndent - 1, dividerAttr)
-          self.addch(lineCount, width, curses.ACS_LRCORNER, dividerAttr)
-        
-        lineCount += 1
-    
-    # redraw the display if...
-    # - lastContentHeight was off by too much
-    # - we're off the bottom of the page
-    newContentHeight = lineCount + self.scroll - 1
-    contentHeightDelta = abs(self.lastContentHeight - newContentHeight)
-    forceRedraw, forceRedrawReason = True, ""
-    
-    if contentHeightDelta >= CONTENT_HEIGHT_REDRAW_THRESHOLD:
-      forceRedrawReason = "estimate was off by %i" % contentHeightDelta
-    elif newContentHeight > height and self.scroll + height - 1 > newContentHeight:
-      forceRedrawReason = "scrolled off the bottom of the page"
-    elif not isScrollBarVisible and newContentHeight > height - 1:
-      forceRedrawReason = "scroll bar wasn't previously visible"
-    elif isScrollBarVisible and newContentHeight <= height - 1:
-      forceRedrawReason = "scroll bar shouldn't be visible"
-    else: forceRedraw = False
-    
-    self.lastContentHeight = newContentHeight
-    if forceRedraw:
-      forceRedrawReason = "redrawing the log panel with the corrected content height (%s)" % forceRedrawReason
-      log.log(self._config["log.logPanel.forceDoubleRedraw"], forceRedrawReason)
-      self.redraw(True)
-    
-    self.valsLock.release()
-  
-  def redraw(self, forceRedraw=False, block=False):
-    # determines if the content needs to be redrawn or not
-    panel.Panel.redraw(self, forceRedraw, block)
-  
-  def run(self):
-    """
-    Redraws the display, coalescing updates if events are rapidly logged (for
-    instance running at the DEBUG runlevel) while also being immediately
-    responsive if additions are less frequent.
-    """
-    
-    lastDay = daysSince() # used to determine if the date has changed
-    while not self._halt:
-      currentDay = daysSince()
-      timeSinceReset = time.time() - self._lastUpdate
-      maxLogUpdateRate = self._config["features.log.maxRefreshRate"] / 1000.0
-      
-      sleepTime = 0
-      if (self.msgLog == self._lastLoggedEvents and lastDay == currentDay) or self._isPaused:
-        sleepTime = 5
-      elif timeSinceReset < maxLogUpdateRate:
-        sleepTime = max(0.05, maxLogUpdateRate - timeSinceReset)
-      
-      if sleepTime:
-        self._cond.acquire()
-        if not self._halt: self._cond.wait(sleepTime)
-        self._cond.release()
-      else:
-        lastDay = currentDay
-        self.redraw(True)
-  
-  def stop(self):
-    """
-    Halts further resolutions and terminates the thread.
-    """
-    
-    self._cond.acquire()
-    self._halt = True
-    self._cond.notifyAll()
-    self._cond.release()
-  
-  def _getTitle(self, width):
-    """
-    Provides the label used for the panel, looking like:
-      Events (ARM NOTICE - ERR, BW - filter: prepopulate):
-    
-    This truncates the attributes (with an ellipse) if too long, and condenses
-    runlevel ranges if there's three or more in a row (for instance ARM_INFO,
-    ARM_NOTICE, and ARM_WARN becomes "ARM_INFO - WARN").
-    
-    Arguments:
-      width - width constraint the label needs to fix in
-    """
-    
-    # usually the attributes used to make the label are decently static, so
-    # provide cached results if they're unchanged
-    self.valsLock.acquire()
-    currentPattern = self.regexFilter.pattern if self.regexFilter else None
-    isUnchanged = self._titleArgs[0] == self.loggedEvents
-    isUnchanged &= self._titleArgs[1] == currentPattern
-    isUnchanged &= self._titleArgs[2] == width
-    if isUnchanged:
-      self.valsLock.release()
-      return self._titleCache
-    
-    eventsList = list(self.loggedEvents)
-    if not eventsList:
-      if not currentPattern:
-        panelLabel = "Events:"
-      else:
-        labelPattern = uiTools.cropStr(currentPattern, width - 18)
-        panelLabel = "Events (filter: %s):" % labelPattern
-    else:
-      # does the following with all runlevel types (tor, arm, and torctl):
-      # - pulls to the start of the list
-      # - condenses range if there's three or more in a row (ex. "ARM_INFO - WARN")
-      # - condense further if there's identical runlevel ranges for multiple
-      #   types (ex. "NOTICE - ERR, ARM_NOTICE - ERR" becomes "TOR/ARM NOTICE - ERR")
-      tmpRunlevels = [] # runlevels pulled from the list (just the runlevel part)
-      runlevelRanges = [] # tuple of type, startLevel, endLevel for ranges to be consensed
-      
-      # reverses runlevels and types so they're appended in the right order
-      reversedRunlevels = log.Runlevel.values()
-      reversedRunlevels.reverse()
-      for prefix in ("TORCTL_", "ARM_", ""):
-        # blank ending runlevel forces the break condition to be reached at the end
-        for runlevel in reversedRunlevels + [""]:
-          eventType = prefix + runlevel
-          if runlevel and eventType in eventsList:
-            # runlevel event found, move to the tmp list
-            eventsList.remove(eventType)
-            tmpRunlevels.append(runlevel)
-          elif tmpRunlevels:
-            # adds all tmp list entries to the start of eventsList
-            if len(tmpRunlevels) >= 3:
-              # save condense sequential runlevels to be added later
-              runlevelRanges.append((prefix, tmpRunlevels[-1], tmpRunlevels[0]))
-            else:
-              # adds runlevels individaully
-              for tmpRunlevel in tmpRunlevels:
-                eventsList.insert(0, prefix + tmpRunlevel)
-            
-            tmpRunlevels = []
-      
-      # adds runlevel ranges, condensing if there's identical ranges
-      for i in range(len(runlevelRanges)):
-        if runlevelRanges[i]:
-          prefix, startLevel, endLevel = runlevelRanges[i]
-          
-          # check for matching ranges
-          matches = []
-          for j in range(i + 1, len(runlevelRanges)):
-            if runlevelRanges[j] and runlevelRanges[j][1] == startLevel and runlevelRanges[j][2] == endLevel:
-              matches.append(runlevelRanges[j])
-              runlevelRanges[j] = None
-          
-          if matches:
-            # strips underscores and replaces empty entries with "TOR"
-            prefixes = [entry[0] for entry in matches] + [prefix]
-            for k in range(len(prefixes)):
-              if prefixes[k] == "": prefixes[k] = "TOR"
-              else: prefixes[k] = prefixes[k].replace("_", "")
-            
-            eventsList.insert(0, "%s %s - %s" % ("/".join(prefixes), startLevel, endLevel))
-          else:
-            eventsList.insert(0, "%s%s - %s" % (prefix, startLevel, endLevel))
-      
-      # truncates to use an ellipsis if too long, for instance:
-      attrLabel = ", ".join(eventsList)
-      if currentPattern: attrLabel += " - filter: %s" % currentPattern
-      attrLabel = uiTools.cropStr(attrLabel, width - 10, 1)
-      if attrLabel: attrLabel = " (%s)" % attrLabel
-      panelLabel = "Events%s:" % attrLabel
-    
-    # cache results and return
-    self._titleCache = panelLabel
-    self._titleArgs = (list(self.loggedEvents), currentPattern, width)
-    self.valsLock.release()
-    return panelLabel
-  
-  def _trimEvents(self, eventListing):
-    """
-    Crops events that have either:
-    - grown beyond the cache limit
-    - outlived the configured log duration
-    
-    Argument:
-      eventListing - listing of log entries
-    """
-    
-    cacheSize = self._config["cache.logPanel.size"]
-    if len(eventListing) > cacheSize: del eventListing[cacheSize:]
-    
-    logTTL = self._config["features.log.entryDuration"]
-    if logTTL > 0:
-      currentDay = daysSince()
-      
-      breakpoint = None # index at which to crop from
-      for i in range(len(eventListing) - 1, -1, -1):
-        daysSinceEvent = currentDay - daysSince(eventListing[i].timestamp)
-        if daysSinceEvent > logTTL: breakpoint = i # older than the ttl
-        else: break
-      
-      # removes entries older than the ttl
-      if breakpoint != None: del eventListing[breakpoint:]
-
diff --git a/src/interface/torrcPanel.py b/src/interface/torrcPanel.py
deleted file mode 100644
index b7cad86..0000000
--- a/src/interface/torrcPanel.py
+++ /dev/null
@@ -1,221 +0,0 @@
-"""
-Panel displaying the torrc or armrc with the validation done against it.
-"""
-
-import math
-import curses
-import threading
-
-from util import conf, enum, panel, torConfig, uiTools
-
-DEFAULT_CONFIG = {"features.config.file.showScrollbars": True,
-                  "features.config.file.maxLinesPerEntry": 8}
-
-# TODO: The armrc use case is incomplete. There should be equivilant reloading
-# and validation capabilities to the torrc.
-Config = enum.Enum("TORRC", "ARMRC") # configuration file types that can be displayed
-
-class TorrcPanel(panel.Panel):
-  """
-  Renders the current torrc or armrc with syntax highlighting in a scrollable
-  area.
-  """
-  
-  def __init__(self, stdscr, configType, config=None):
-    panel.Panel.__init__(self, stdscr, "configFile", 0)
-    
-    self._config = dict(DEFAULT_CONFIG)
-    if config:
-      config.update(self._config, {"features.config.file.maxLinesPerEntry": 1})
-    
-    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
-    
-    # height of the content when last rendered (the cached value is invalid if
-    # _lastContentHeightArgs is None or differs from the current dimensions)
-    self._lastContentHeight = 1
-    self._lastContentHeightArgs = None
-  
-  def handleKey(self, key):
-    self.valsLock.acquire()
-    if uiTools.isScrollKey(key):
-      pageHeight = self.getPreferredSize()[0] - 1
-      newScroll = uiTools.getScrollPosition(key, self.scroll, pageHeight, self._lastContentHeight)
-      
-      if self.scroll != newScroll:
-        self.scroll = newScroll
-        self.redraw(True)
-    elif key == ord('n') or key == ord('N'):
-      self.showLineNum = not self.showLineNum
-      self._lastContentHeightArgs = None
-      self.redraw(True)
-    elif key == ord('s') or key == ord('S'):
-      self.stripComments = not self.stripComments
-      self._lastContentHeightArgs = None
-      self.redraw(True)
-    
-    self.valsLock.release()
-  
-  def draw(self, width, height):
-    self.valsLock.acquire()
-    
-    # If true, we assume that the cached value in self._lastContentHeight is
-    # still accurate, and stop drawing when there's nothing more to display.
-    # Otherwise the self._lastContentHeight is suspect, and we'll process all
-    # the content to check if it's right (and redraw again with the corrected
-    # height if not).
-    trustLastContentHeight = self._lastContentHeightArgs == (width, height)
-    
-    # restricts scroll location to valid bounds
-    self.scroll = max(0, min(self.scroll, self._lastContentHeight - height + 1))
-    
-    renderedContents, corrections, confLocation = None, {}, None
-    if self.configType == Config.TORRC:
-      loadedTorrc = torConfig.getTorrc()
-      loadedTorrc.getLock().acquire()
-      confLocation = loadedTorrc.getConfigLocation()
-      
-      if not loadedTorrc.isLoaded():
-        renderedContents = ["### Unable to load the torrc ###"]
-      else:
-        renderedContents = loadedTorrc.getDisplayContents(self.stripComments)
-        
-        # constructs a mapping of line numbers to the issue on it
-        corrections = dict((lineNum, (issue, msg)) for lineNum, issue, msg in loadedTorrc.getCorrections())
-      
-      loadedTorrc.getLock().release()
-    else:
-      loadedArmrc = conf.getConfig("arm")
-      confLocation = loadedArmrc.path
-      renderedContents = list(loadedArmrc.rawContents)
-    
-    # offset to make room for the line numbers
-    lineNumOffset = 0
-    if self.showLineNum:
-      if len(renderedContents) == 0: lineNumOffset = 2
-      else: lineNumOffset = int(math.log10(len(renderedContents))) + 2
-    
-    # draws left-hand scroll bar if content's longer than the height
-    scrollOffset = 0
-    if self._config["features.config.file.showScrollbars"] and self._lastContentHeight > height - 1:
-      scrollOffset = 3
-      self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1)
-    
-    displayLine = -self.scroll + 1 # line we're drawing on
-    
-    # draws the top label
-    if self.showLabel:
-      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)
-    
-    isMultiline = False # true if we're in the middle of a multiline torrc entry
-    for lineNumber in range(0, len(renderedContents)):
-      lineText = renderedContents[lineNumber]
-      lineText = lineText.rstrip() # remove ending whitespace
-      
-      # blank lines are hidden when stripping comments
-      if self.stripComments and not lineText: continue
-      
-      # splits the line into its component (msg, format) tuples
-      lineComp = {"option": ["", curses.A_BOLD | uiTools.getColor("green")],
-                  "argument": ["", curses.A_BOLD | uiTools.getColor("cyan")],
-                  "correction": ["", curses.A_BOLD | uiTools.getColor("cyan")],
-                  "comment": ["", uiTools.getColor("white")]}
-      
-      # parses the comment
-      commentIndex = lineText.find("#")
-      if commentIndex != -1:
-        lineComp["comment"][0] = lineText[commentIndex:]
-        lineText = lineText[:commentIndex]
-      
-      # splits the option and argument, preserving any whitespace around them
-      strippedLine = lineText.strip()
-      optionIndex = strippedLine.find(" ")
-      if isMultiline:
-        # part of a multiline entry started on a previous line so everything
-        # is part of the argument
-        lineComp["argument"][0] = lineText
-      elif optionIndex == -1:
-        # no argument provided
-        lineComp["option"][0] = lineText
-      else:
-        optionText = strippedLine[:optionIndex]
-        optionEnd = lineText.find(optionText) + len(optionText)
-        lineComp["option"][0] = lineText[:optionEnd]
-        lineComp["argument"][0] = lineText[optionEnd:]
-      
-      # flags following lines as belonging to this multiline entry if it ends
-      # with a slash
-      if strippedLine: isMultiline = strippedLine.endswith("\\")
-      
-      # gets the correction
-      if lineNumber in corrections:
-        lineIssue, lineIssueMsg = corrections[lineNumber]
-        
-        if lineIssue in (torConfig.ValidationError.DUPLICATE, torConfig.ValidationError.IS_DEFAULT):
-          lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue")
-          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue")
-        elif lineIssue == torConfig.ValidationError.MISMATCH:
-          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("red")
-          lineComp["correction"][0] = " (%s)" % lineIssueMsg
-        else:
-          # For some types of configs the correction field is simply used to
-          # provide extra data (for instance, the type for tor state fields).
-          lineComp["correction"][0] = " (%s)" % lineIssueMsg
-          lineComp["correction"][1] = curses.A_BOLD | uiTools.getColor("magenta")
-      
-      # draws the line number
-      if self.showLineNum and displayLine < height and displayLine >= 1:
-        lineNumStr = ("%%%ii" % (lineNumOffset - 1)) % (lineNumber + 1)
-        self.addstr(displayLine, scrollOffset, lineNumStr, curses.A_BOLD | uiTools.getColor("yellow"))
-      
-      # draws the rest of the components with line wrap
-      cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0
-      maxLinesPerEntry = self._config["features.config.file.maxLinesPerEntry"]
-      displayQueue = [lineComp[entry] for entry in ("option", "argument", "correction", "comment")]
-      
-      while displayQueue:
-        msg, format = displayQueue.pop(0)
-        
-        maxMsgSize, includeBreak = width - cursorLoc, False
-        if len(msg) >= maxMsgSize:
-          # message is too long - break it up
-          if lineOffset == maxLinesPerEntry - 1:
-            msg = uiTools.cropStr(msg, maxMsgSize)
-          else:
-            includeBreak = True
-            msg, remainder = uiTools.cropStr(msg, maxMsgSize, 4, 4, uiTools.Ending.HYPHEN, True)
-            displayQueue.insert(0, (remainder.strip(), format))
-        
-        drawLine = displayLine + lineOffset
-        if msg and drawLine < height and drawLine >= 1:
-          self.addstr(drawLine, cursorLoc, msg, format)
-        
-        # If we're done, and have added content to this line, then start
-        # further content on the next line.
-        cursorLoc += len(msg)
-        includeBreak |= not displayQueue and cursorLoc != lineNumOffset + scrollOffset
-        
-        if includeBreak:
-          lineOffset += 1
-          cursorLoc = lineNumOffset + scrollOffset
-      
-      displayLine += max(lineOffset, 1)
-      
-      if trustLastContentHeight and displayLine >= height: break
-    
-    if not trustLastContentHeight:
-      self._lastContentHeightArgs = (width, height)
-      newContentHeight = displayLine + self.scroll - 1
-      
-      if self._lastContentHeight != newContentHeight:
-        self._lastContentHeight = newContentHeight
-        self.redraw(True)
-    
-    self.valsLock.release()
-
diff --git a/src/starter.py b/src/starter.py
index 09fc37a..c0a0270 100644
--- a/src/starter.py
+++ b/src/starter.py
@@ -14,8 +14,8 @@ import socket
 import platform
 
 import version
-import interface.controller
-import interface.logPanel
+import cli.controller
+import cli.logPanel
 import util.conf
 import util.connections
 import util.hostnames
@@ -67,7 +67,7 @@ Terminal status monitor for Tor relays.
 Example:
 arm -b -i 1643          hide connection data, attaching to control port 1643
 arm -e we -c /tmp/cfg   use this configuration file with 'WARN'/'ERR' events
-""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], DEFAULT_CONFIG, LOG_DUMP_PATH, CONFIG["startup.events"], interface.logPanel.EVENT_LISTING)
+""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], DEFAULT_CONFIG, LOG_DUMP_PATH, CONFIG["startup.events"], cli.logPanel.EVENT_LISTING)
 
 # filename used for cached tor config descriptions
 CONFIG_DESC_FILENAME = "torConfigDesc.txt"
@@ -312,7 +312,7 @@ if __name__ == '__main__':
   
   # validates and expands log event flags
   try:
-    expandedEvents = interface.logPanel.expandEvents(param["startup.events"])
+    expandedEvents = cli.logPanel.expandEvents(param["startup.events"])
   except ValueError, exc:
     for flag in str(exc):
       print "Unrecognized event flag: %s" % flag
@@ -387,5 +387,5 @@ if __name__ == '__main__':
     util.log.log(CONFIG["log.savingDebugLog"], "Saving a debug log to '%s' (please check it for sensitive information before sharing)" % LOG_DUMP_PATH)
     _dumpConfig()
   
-  interface.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"])
+  cli.controller.startTorMonitor(time.time() - initTime, expandedEvents, param["startup.blindModeEnabled"])
 

_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits