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

[or-cvs] r23865: {arm} Renaming new config panels. (arm/trunk/src/interface)



Author: atagar
Date: 2010-11-27 21:55:13 +0000 (Sat, 27 Nov 2010)
New Revision: 23865

Added:
   arm/trunk/src/interface/configPanel.py
   arm/trunk/src/interface/torrcPanel.py
Removed:
   arm/trunk/src/interface/configFilePanel.py
   arm/trunk/src/interface/configStatePanel.py
Modified:
   arm/trunk/src/interface/__init__.py
   arm/trunk/src/interface/controller.py
Log:
Renaming new config panels.



Modified: arm/trunk/src/interface/__init__.py
===================================================================
--- arm/trunk/src/interface/__init__.py	2010-11-27 20:49:44 UTC (rev 23864)
+++ arm/trunk/src/interface/__init__.py	2010-11-27 21:55:13 UTC (rev 23865)
@@ -2,5 +2,5 @@
 Panels, popups, and handlers comprising the arm user interface.
 """
 
-__all__ = ["configFilePanel", "configStatePanel", "connPanel", "controller", "descriptorPopup", "fileDescriptorPopup", "headerPanel", "logPanel"]
+__all__ = ["configPanel", "connPanel", "controller", "descriptorPopup", "fileDescriptorPopup", "headerPanel", "logPanel", "torrcPanel"]
 

Deleted: arm/trunk/src/interface/configFilePanel.py
===================================================================
--- arm/trunk/src/interface/configFilePanel.py	2010-11-27 20:49:44 UTC (rev 23864)
+++ arm/trunk/src/interface/configFilePanel.py	2010-11-27 21:55:13 UTC (rev 23865)
@@ -1,212 +0,0 @@
-"""
-Panel displaying the torrc or armrc with the validation done against it.
-"""
-
-import math
-import curses
-import threading
-
-from util import conf, panel, torConfig, uiTools
-
-DEFAULT_CONFIG = {"features.config.file.showScrollbars": True,
-                  "features.config.file.maxLinesPerEntry": 8}
-
-TORRC, ARMRC = range(1, 3) # configuration file types that can  be displayed
-
-class ConfigFilePanel(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, subwindow, 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 == 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)
-        corrections = loadedTorrc.getCorrections()
-      
-      loadedTorrc.getLock().release()
-    else:
-      # TODO: The armrc use case is incomplete. There should be equivilant
-      # reloading and validation capabilities to the torrc.
-      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 == TORRC else "Arm"
-      locationLabel = " (%s)" % confLocation if confLocation else ""
-      self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT)
-    
-    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 optionIndex == -1:
-        lineComp["option"][0] = lineText # no argument provided
-      else:
-        optionText = strippedLine[:optionIndex]
-        optionEnd = lineText.find(optionText) + len(optionText)
-        lineComp["option"][0] = lineText[:optionEnd]
-        lineComp["argument"][0] = lineText[optionEnd:]
-      
-      # gets the correction
-      if lineNumber in corrections:
-        lineIssue, lineIssueMsg = corrections[lineNumber]
-        
-        if lineIssue == torConfig.VAL_DUPLICATE:
-          lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue")
-          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue")
-        elif lineIssue == torConfig.VAL_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.END_WITH_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()
-  
-  def redraw(self, forceRedraw=False, block=False):
-    panel.Panel.redraw(self, forceRedraw, block)
-

Copied: arm/trunk/src/interface/configPanel.py (from rev 23862, arm/trunk/src/interface/configStatePanel.py)
===================================================================
--- arm/trunk/src/interface/configPanel.py	                        (rev 0)
+++ arm/trunk/src/interface/configPanel.py	2010-11-27 21:55:13 UTC (rev 23865)
@@ -0,0 +1,336 @@
+"""
+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, 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": 10}
+
+TOR_STATE, ARM_STATE = range(1, 3) # state to be presented
+
+# mappings of option categories to the color for their entries
+CATEGORY_COLOR = {torConfig.GENERAL: "green",
+                  torConfig.CLIENT: "blue",
+                  torConfig.SERVER: "yellow",
+                  torConfig.DIRECTORY: "magenta",
+                  torConfig.AUTHORITY: "red",
+                  torConfig.HIDDEN_SERVICE: "cyan",
+                  torConfig.TESTING: "white",
+                  torConfig.UNKNOWN: "white"}
+
+# attributes of a ConfigEntry
+FIELD_CATEGORY, FIELD_OPTION, FIELD_VALUE, FIELD_TYPE, FIELD_ARG_USAGE, FIELD_DESCRIPTION, FIELD_MAN_ENTRY, FIELD_IS_DEFAULT = range(8)
+DEFAULT_SORT_ORDER = (FIELD_CATEGORY, FIELD_MAN_ENTRY, 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_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, manEntry):
+    self.fields = {}
+    self.fields[FIELD_OPTION] = option
+    self.fields[FIELD_TYPE] = type
+    self.fields[FIELD_IS_DEFAULT] = isDefault
+    
+    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.UNKNOWN
+      self.fields[FIELD_ARG_USAGE] = ""
+      self.fields[FIELD_DESCRIPTION] = ""
+  
+  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 _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
+  
+  def getAttr(self, argTypes):
+    """
+    Provides back a list with the given parameters.
+    
+    Arguments:
+      argTypes - list of enums for the arguments to be provided back
+    """
+    
+    return [self.get(field) for field in argTypes]
+
+class ConfigStatePanel(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})
+      
+      self.sortOrdering = config.getIntCSV("features.config.order", self.sortOrdering, 3, 0, 6)
+    
+    self.configType = configType
+    self.confContents = []
+    self.scroller = uiTools.Scroller(True)
+    self.valsLock = threading.RLock()
+    
+    # TODO: this will need to be able to listen for SETCONF events (arg!)
+    
+    if self.configType == TOR_STATE:
+      conn = torTools.getConn()
+      
+      # gets options that differ from their default
+      setOptions = set()
+      configTextQuery = conn.getInfo("config-text", "").strip().split("\n")
+      for entry in configTextQuery: setOptions.add(entry[:entry.find(" ")])
+      
+      # for all recognized tor config options, provide their current value
+      configOptionQuery = conn.getInfo("config/names", "").strip().split("\n")
+      
+      for lineNum in range(len(configOptionQuery)):
+        # lines are of the form "<option> <type>", like:
+        # UseEntryGuards Boolean
+        line = configOptionQuery[lineNum]
+        confOption, confType = line.strip().split(" ", 1)
+        
+        # skips private and virtual entries if not set 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
+        
+        manEntry = torConfig.getConfigDescription(confOption)
+        self.confContents.append(ConfigEntry(confOption, confType, not confOption in setOptions, manEntry))
+      
+      
+      self.setSortOrder() # initial sorting of the contents
+    elif self.configType == ARM_STATE:
+      # loaded via the conf utility
+      armConf = conf.getConfig("arm")
+      for key in armConf.getKeys():
+        self.confContents.append(ConfigEntry("", key, ", ".join(armConf.getValue(key, [], True)), "", "", True))
+  
+  def getSelection(self):
+    """
+    Provides the currently selected entry.
+    """
+    
+    return self.scroller.getCursorSelection(self.confContents)
+  
+  def setSortOrder(self, ordering = None):
+    """
+    Sets the configuration attributes we're sorting by and resorts the
+    contents. If the ordering isn't defined then this resorts based on the
+    last set ordering.
+    """
+    
+    self.valsLock.acquire()
+    if ordering: self.sortOrdering = ordering
+    self.confContents.sort(key=lambda i: (i.getAttr(self.sortOrdering)))
+    self.valsLock.release()
+  
+  def getSortOrder(self):
+    """
+    Provides the current configuration attributes we're sorting by.
+    """
+    
+    return self.sortOrdering
+  
+  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.confContents, pageHeight)
+      if isChanged: self.redraw(True)
+    self.valsLock.release()
+  
+  def draw(self, subwindow, width, height):
+    self.valsLock.acquire()
+    
+    # draws the top label
+    titleLabel = "%s Configuration:" % ("Tor" if self.configType == TOR_STATE else "Arm")
+    self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
+    
+    # panel with details for the current selection
+    detailPanelHeight = self._config["features.config.selectionDetails.height"]
+    if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
+      # no detail panel
+      detailPanelHeight = 0
+      scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1)
+      cursorSelection = self.scroller.getCursorSelection(self.confContents)
+    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.confContents, height - 1 - detailPanelHeight)
+      cursorSelection = self.scroller.getCursorSelection(self.confContents)
+      
+      self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, titleLabel)
+    
+    # draws left-hand scroll bar if content's longer than the height
+    scrollOffset = 0
+    if len(self.confContents) > height - detailPanelHeight - 1:
+      scrollOffset = 3
+      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self.confContents), 1 + detailPanelHeight)
+    
+    # determines the width for the columns
+    optionColWidth, valueColWidth = 0, 0
+    
+    # constructs a mapping of entries to their current values
+    # TODO: just skip dynamic widths entirely?
+    entryToValues = {}
+    for entry in self.confContents:
+      entryToValues[entry] = entry.get(FIELD_VALUE)
+      #optionColWidth = max(optionColWidth, len(entry.get(FIELD_OPTION)))
+      #valueColWidth = max(valueColWidth, len(entryToValues[entry]))
+    
+    #optionColWidth = min(self._config["features.config.state.colWidth.option"], optionColWidth)
+    #valueColWidth = min(self._config["features.config.state.colWidth.value"], valueColWidth)
+    optionColWidth = self._config["features.config.state.colWidth.option"]
+    valueColWidth = self._config["features.config.state.colWidth.value"]
+    descriptionColWidth = max(0, width - scrollOffset - optionColWidth - valueColWidth - 2)
+    
+    for lineNum in range(scrollLoc, len(self.confContents)):
+      entry = self.confContents[lineNum]
+      drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
+      
+      # TODO: need to cut off description at the first newline
+      optionLabel = uiTools.cropStr(entry.get(FIELD_OPTION), optionColWidth)
+      valueLabel = uiTools.cropStr(entryToValues[entry], valueColWidth)
+      descriptionLabel = uiTools.cropStr(entry.get(FIELD_DESCRIPTION), descriptionColWidth, None)
+      
+      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)])
+      #lineFormat = uiTools.getColor("green") if entry.isDefault else curses.A_BOLD | uiTools.getColor("yellow")
+      if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
+      
+      lineTextLayout = "%%-%is %%-%is %%-%is" % (optionColWidth, valueColWidth, descriptionColWidth)
+      lineText = lineTextLayout % (optionLabel, valueLabel, descriptionLabel)
+      self.addstr(drawLine, scrollOffset, lineText, lineFormat)
+      
+      if drawLine >= height: break
+    
+    self.valsLock.release()
+  
+  def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, titleLabel):
+    """
+    Renders a panel for the selected configuration option.
+    """
+    
+    # border (top)
+    if width >= len(titleLabel):
+      self.win.hline(0, len(titleLabel), curses.ACS_HLINE, width - len(titleLabel))
+      self.win.addch(0, width, curses.ACS_URCORNER)
+    
+    # border (sides)
+    self.win.vline(1, 0, curses.ACS_VLINE, detailPanelHeight - 1)
+    self.win.vline(1, width, curses.ACS_VLINE, detailPanelHeight - 1)
+    
+    # border (bottom)
+    self.win.addch(detailPanelHeight, 0, curses.ACS_LLCORNER)
+    if width >= 2: self.win.addch(detailPanelHeight, 1, curses.ACS_TTEE)
+    if width >= 3: self.win.hline(detailPanelHeight, 2, curses.ACS_HLINE, width - 2)
+    self.win.addch(detailPanelHeight, width, curses.ACS_LRCORNER)
+    
+    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[cursorSelection.get(FIELD_CATEGORY)])
+    
+    # first entry:
+    # <option> (<category> Option)
+    optionLabel =" (%s Option)" % torConfig.OPTION_CATEGORY_STR[cursorSelection.get(FIELD_CATEGORY)]
+    self.addstr(1, 2, cursorSelection.get(FIELD_OPTION) + optionLabel, selectionFormat)
+    
+    # second entry:
+    # Value: <value> ([default|custom], <type>, usage: <argument usage>)
+    if detailPanelHeight >= 3:
+      valueAttr = []
+      valueAttr.append("default" if cursorSelection.get(FIELD_IS_DEFAULT) else "custom")
+      valueAttr.append(cursorSelection.get(FIELD_TYPE))
+      valueAttr.append("usage: %s" % (cursorSelection.get(FIELD_ARG_USAGE)))
+      valueAttrLabel = ", ".join(valueAttr)
+      
+      valueLabelWidth = width - 12 - len(valueAttrLabel)
+      valueLabel = uiTools.cropStr(cursorSelection.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: " + cursorSelection.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.END_WITH_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)
+

Deleted: arm/trunk/src/interface/configStatePanel.py
===================================================================
--- arm/trunk/src/interface/configStatePanel.py	2010-11-27 20:49:44 UTC (rev 23864)
+++ arm/trunk/src/interface/configStatePanel.py	2010-11-27 21:55:13 UTC (rev 23865)
@@ -1,336 +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, 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": 10}
-
-TOR_STATE, ARM_STATE = range(1, 3) # state to be presented
-
-# mappings of option categories to the color for their entries
-CATEGORY_COLOR = {torConfig.GENERAL: "green",
-                  torConfig.CLIENT: "blue",
-                  torConfig.SERVER: "yellow",
-                  torConfig.DIRECTORY: "magenta",
-                  torConfig.AUTHORITY: "red",
-                  torConfig.HIDDEN_SERVICE: "cyan",
-                  torConfig.TESTING: "white",
-                  torConfig.UNKNOWN: "white"}
-
-# attributes of a ConfigEntry
-FIELD_CATEGORY, FIELD_OPTION, FIELD_VALUE, FIELD_TYPE, FIELD_ARG_USAGE, FIELD_DESCRIPTION, FIELD_MAN_ENTRY, FIELD_IS_DEFAULT = range(8)
-DEFAULT_SORT_ORDER = (FIELD_CATEGORY, FIELD_MAN_ENTRY, 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_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, manEntry):
-    self.fields = {}
-    self.fields[FIELD_OPTION] = option
-    self.fields[FIELD_TYPE] = type
-    self.fields[FIELD_IS_DEFAULT] = isDefault
-    
-    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.UNKNOWN
-      self.fields[FIELD_ARG_USAGE] = ""
-      self.fields[FIELD_DESCRIPTION] = ""
-  
-  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 _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
-  
-  def getAttr(self, argTypes):
-    """
-    Provides back a list with the given parameters.
-    
-    Arguments:
-      argTypes - list of enums for the arguments to be provided back
-    """
-    
-    return [self.get(field) for field in argTypes]
-
-class ConfigStatePanel(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})
-      
-      self.sortOrdering = config.getIntCSV("features.config.order", self.sortOrdering, 3, 0, 6)
-    
-    self.configType = configType
-    self.confContents = []
-    self.scroller = uiTools.Scroller(True)
-    self.valsLock = threading.RLock()
-    
-    # TODO: this will need to be able to listen for SETCONF events (arg!)
-    
-    if self.configType == TOR_STATE:
-      conn = torTools.getConn()
-      
-      # gets options that differ from their default
-      setOptions = set()
-      configTextQuery = conn.getInfo("config-text", "").strip().split("\n")
-      for entry in configTextQuery: setOptions.add(entry[:entry.find(" ")])
-      
-      # for all recognized tor config options, provide their current value
-      configOptionQuery = conn.getInfo("config/names", "").strip().split("\n")
-      
-      for lineNum in range(len(configOptionQuery)):
-        # lines are of the form "<option> <type>", like:
-        # UseEntryGuards Boolean
-        line = configOptionQuery[lineNum]
-        confOption, confType = line.strip().split(" ", 1)
-        
-        # skips private and virtual entries if not set 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
-        
-        manEntry = torConfig.getConfigDescription(confOption)
-        self.confContents.append(ConfigEntry(confOption, confType, not confOption in setOptions, manEntry))
-      
-      
-      self.setSortOrder() # initial sorting of the contents
-    elif self.configType == ARM_STATE:
-      # loaded via the conf utility
-      armConf = conf.getConfig("arm")
-      for key in armConf.getKeys():
-        self.confContents.append(ConfigEntry("", key, ", ".join(armConf.getValue(key, [], True)), "", "", True))
-  
-  def getSelection(self):
-    """
-    Provides the currently selected entry.
-    """
-    
-    return self.scroller.getCursorSelection(self.confContents)
-  
-  def setSortOrder(self, ordering = None):
-    """
-    Sets the configuration attributes we're sorting by and resorts the
-    contents. If the ordering isn't defined then this resorts based on the
-    last set ordering.
-    """
-    
-    self.valsLock.acquire()
-    if ordering: self.sortOrdering = ordering
-    self.confContents.sort(key=lambda i: (i.getAttr(self.sortOrdering)))
-    self.valsLock.release()
-  
-  def getSortOrder(self):
-    """
-    Provides the current configuration attributes we're sorting by.
-    """
-    
-    return self.sortOrdering
-  
-  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.confContents, pageHeight)
-      if isChanged: self.redraw(True)
-    self.valsLock.release()
-  
-  def draw(self, subwindow, width, height):
-    self.valsLock.acquire()
-    
-    # draws the top label
-    titleLabel = "%s Configuration:" % ("Tor" if self.configType == TOR_STATE else "Arm")
-    self.addstr(0, 0, titleLabel, curses.A_STANDOUT)
-    
-    # panel with details for the current selection
-    detailPanelHeight = self._config["features.config.selectionDetails.height"]
-    if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
-      # no detail panel
-      detailPanelHeight = 0
-      scrollLoc = self.scroller.getScrollLoc(self.confContents, height - 1)
-      cursorSelection = self.scroller.getCursorSelection(self.confContents)
-    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.confContents, height - 1 - detailPanelHeight)
-      cursorSelection = self.scroller.getCursorSelection(self.confContents)
-      
-      self._drawSelectionPanel(cursorSelection, width, detailPanelHeight, titleLabel)
-    
-    # draws left-hand scroll bar if content's longer than the height
-    scrollOffset = 0
-    if len(self.confContents) > height - detailPanelHeight - 1:
-      scrollOffset = 3
-      self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelHeight - 1, len(self.confContents), 1 + detailPanelHeight)
-    
-    # determines the width for the columns
-    optionColWidth, valueColWidth = 0, 0
-    
-    # constructs a mapping of entries to their current values
-    # TODO: just skip dynamic widths entirely?
-    entryToValues = {}
-    for entry in self.confContents:
-      entryToValues[entry] = entry.get(FIELD_VALUE)
-      #optionColWidth = max(optionColWidth, len(entry.get(FIELD_OPTION)))
-      #valueColWidth = max(valueColWidth, len(entryToValues[entry]))
-    
-    #optionColWidth = min(self._config["features.config.state.colWidth.option"], optionColWidth)
-    #valueColWidth = min(self._config["features.config.state.colWidth.value"], valueColWidth)
-    optionColWidth = self._config["features.config.state.colWidth.option"]
-    valueColWidth = self._config["features.config.state.colWidth.value"]
-    descriptionColWidth = max(0, width - scrollOffset - optionColWidth - valueColWidth - 2)
-    
-    for lineNum in range(scrollLoc, len(self.confContents)):
-      entry = self.confContents[lineNum]
-      drawLine = lineNum + detailPanelHeight + 1 - scrollLoc
-      
-      # TODO: need to cut off description at the first newline
-      optionLabel = uiTools.cropStr(entry.get(FIELD_OPTION), optionColWidth)
-      valueLabel = uiTools.cropStr(entryToValues[entry], valueColWidth)
-      descriptionLabel = uiTools.cropStr(entry.get(FIELD_DESCRIPTION), descriptionColWidth, None)
-      
-      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)])
-      #lineFormat = uiTools.getColor("green") if entry.isDefault else curses.A_BOLD | uiTools.getColor("yellow")
-      if entry == cursorSelection: lineFormat |= curses.A_STANDOUT
-      
-      lineTextLayout = "%%-%is %%-%is %%-%is" % (optionColWidth, valueColWidth, descriptionColWidth)
-      lineText = lineTextLayout % (optionLabel, valueLabel, descriptionLabel)
-      self.addstr(drawLine, scrollOffset, lineText, lineFormat)
-      
-      if drawLine >= height: break
-    
-    self.valsLock.release()
-  
-  def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, titleLabel):
-    """
-    Renders a panel for the selected configuration option.
-    """
-    
-    # border (top)
-    if width >= len(titleLabel):
-      self.win.hline(0, len(titleLabel), curses.ACS_HLINE, width - len(titleLabel))
-      self.win.addch(0, width, curses.ACS_URCORNER)
-    
-    # border (sides)
-    self.win.vline(1, 0, curses.ACS_VLINE, detailPanelHeight - 1)
-    self.win.vline(1, width, curses.ACS_VLINE, detailPanelHeight - 1)
-    
-    # border (bottom)
-    self.win.addch(detailPanelHeight, 0, curses.ACS_LLCORNER)
-    if width >= 2: self.win.addch(detailPanelHeight, 1, curses.ACS_TTEE)
-    if width >= 3: self.win.hline(detailPanelHeight, 2, curses.ACS_HLINE, width - 2)
-    self.win.addch(detailPanelHeight, width, curses.ACS_LRCORNER)
-    
-    selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[cursorSelection.get(FIELD_CATEGORY)])
-    
-    # first entry:
-    # <option> (<category> Option)
-    optionLabel =" (%s Option)" % torConfig.OPTION_CATEGORY_STR[cursorSelection.get(FIELD_CATEGORY)]
-    self.addstr(1, 2, cursorSelection.get(FIELD_OPTION) + optionLabel, selectionFormat)
-    
-    # second entry:
-    # Value: <value> ([default|custom], <type>, usage: <argument usage>)
-    if detailPanelHeight >= 3:
-      valueAttr = []
-      valueAttr.append("default" if cursorSelection.get(FIELD_IS_DEFAULT) else "custom")
-      valueAttr.append(cursorSelection.get(FIELD_TYPE))
-      valueAttr.append("usage: %s" % (cursorSelection.get(FIELD_ARG_USAGE)))
-      valueAttrLabel = ", ".join(valueAttr)
-      
-      valueLabelWidth = width - 12 - len(valueAttrLabel)
-      valueLabel = uiTools.cropStr(cursorSelection.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: " + cursorSelection.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.END_WITH_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)
-

Modified: arm/trunk/src/interface/controller.py
===================================================================
--- arm/trunk/src/interface/controller.py	2010-11-27 20:49:44 UTC (rev 23864)
+++ arm/trunk/src/interface/controller.py	2010-11-27 21:55:13 UTC (rev 23865)
@@ -20,8 +20,8 @@
 import graphing.graphPanel
 import logPanel
 import connPanel
-import configStatePanel
-import configFilePanel
+import configPanel
+import torrcPanel
 import descriptorPopup
 import fileDescriptorPopup
 
@@ -517,8 +517,8 @@
   
   panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
   panels["control"] = ControlPanel(stdscr, isBlindMode)
-  panels["config"] = configStatePanel.ConfigStatePanel(stdscr, configStatePanel.TOR_STATE, config)
-  panels["torrc"] = configFilePanel.ConfigFilePanel(stdscr, configFilePanel.TORRC, config)
+  panels["config"] = configPanel.ConfigStatePanel(stdscr, configPanel.TOR_STATE, config)
+  panels["torrc"] = torrcPanel.ConfigFilePanel(stdscr, torrcPanel.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")
@@ -623,7 +623,7 @@
         if panels["graph"].currentDisplay == "bandwidth":
           panels["graph"].setHeight(panels["graph"].stats["bandwidth"].getContentHeight())
         
-        # TODO: should redraw the configFilePanel
+        # TODO: should redraw the torrcPanel
         #panels["torrc"].loadConfig()
         sighupTracker.isReset = False
       
@@ -1582,9 +1582,9 @@
     elif page == 2 and (key == ord('s') or key == ord('S')):
       # set ordering for config options
       titleLabel = "Config Option Ordering:"
-      options = [configStatePanel.FIELD_ATTR[i][0] for i in range(8)]
-      oldSelection = [configStatePanel.FIELD_ATTR[entry][0] for entry in panels["config"].sortOrdering]
-      optionColors = dict([configStatePanel.FIELD_ATTR[i] for i in range(8)])
+      options = [configPanel.FIELD_ATTR[i][0] for i in range(8)]
+      oldSelection = [configPanel.FIELD_ATTR[entry][0] for entry in panels["config"].sortOrdering]
+      optionColors = dict([configPanel.FIELD_ATTR[i] for i in range(8)])
       results = showSortDialog(stdscr, panels, isPaused, page, titleLabel, options, oldSelection, optionColors)
       
       if results:
@@ -1592,8 +1592,8 @@
         resultEnums = []
         
         for label in results:
-          for entryEnum in configStatePanel.FIELD_ATTR:
-            if label == configStatePanel.FIELD_ATTR[entryEnum][0]:
+          for entryEnum in configPanel.FIELD_ATTR:
+            if label == configPanel.FIELD_ATTR[entryEnum][0]:
               resultEnums.append(entryEnum)
               break
         
@@ -1608,13 +1608,13 @@
         
         # provides prompt
         selection = panels["config"].getSelection()
-        configOption = selection.get(configStatePanel.FIELD_OPTION)
+        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(configStatePanel.FIELD_VALUE)
+        initialValue = selection.get(configPanel.FIELD_VALUE)
         
         # initial input for the text field
         initialText = ""
@@ -1628,12 +1628,12 @@
           conn = torTools.getConn()
           
           # if the value's a boolean then allow for 'true' and 'false' inputs
-          if selection.get(configStatePanel.FIELD_TYPE) == "Boolean":
+          if selection.get(configPanel.FIELD_TYPE) == "Boolean":
             if newConfigValue.lower() == "true": newConfigValue = "1"
             elif newConfigValue.lower() == "false": newConfigValue = "0"
           
           try:
-            if selection.get(configStatePanel.FIELD_TYPE) == "LineList":
+            if selection.get(configPanel.FIELD_TYPE) == "LineList":
               newConfigValue = newConfigValue.split(",")
             
             conn.setOption(configOption, newConfigValue)
@@ -1643,7 +1643,7 @@
             configTextQuery = conn.getInfo("config-text", "").strip().split("\n")
             for entry in configTextQuery: setOptions.add(entry[:entry.find(" ")])
             
-            selection.fields[configStatePanel.FIELD_IS_DEFAULT] = not configOption in setOptions
+            selection.fields[configPanel.FIELD_IS_DEFAULT] = not configOption in setOptions
             panels["config"].redraw(True)
           except Exception, exc:
             errorMsg = "%s (press any key)" % exc

Copied: arm/trunk/src/interface/torrcPanel.py (from rev 23853, arm/trunk/src/interface/configFilePanel.py)
===================================================================
--- arm/trunk/src/interface/torrcPanel.py	                        (rev 0)
+++ arm/trunk/src/interface/torrcPanel.py	2010-11-27 21:55:13 UTC (rev 23865)
@@ -0,0 +1,212 @@
+"""
+Panel displaying the torrc or armrc with the validation done against it.
+"""
+
+import math
+import curses
+import threading
+
+from util import conf, panel, torConfig, uiTools
+
+DEFAULT_CONFIG = {"features.config.file.showScrollbars": True,
+                  "features.config.file.maxLinesPerEntry": 8}
+
+TORRC, ARMRC = range(1, 3) # configuration file types that can  be displayed
+
+class ConfigFilePanel(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, subwindow, 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 == 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)
+        corrections = loadedTorrc.getCorrections()
+      
+      loadedTorrc.getLock().release()
+    else:
+      # TODO: The armrc use case is incomplete. There should be equivilant
+      # reloading and validation capabilities to the torrc.
+      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 == TORRC else "Arm"
+      locationLabel = " (%s)" % confLocation if confLocation else ""
+      self.addstr(0, 0, "%s Configuration File%s:" % (sourceLabel, locationLabel), curses.A_STANDOUT)
+    
+    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 optionIndex == -1:
+        lineComp["option"][0] = lineText # no argument provided
+      else:
+        optionText = strippedLine[:optionIndex]
+        optionEnd = lineText.find(optionText) + len(optionText)
+        lineComp["option"][0] = lineText[:optionEnd]
+        lineComp["argument"][0] = lineText[optionEnd:]
+      
+      # gets the correction
+      if lineNumber in corrections:
+        lineIssue, lineIssueMsg = corrections[lineNumber]
+        
+        if lineIssue == torConfig.VAL_DUPLICATE:
+          lineComp["option"][1] = curses.A_BOLD | uiTools.getColor("blue")
+          lineComp["argument"][1] = curses.A_BOLD | uiTools.getColor("blue")
+        elif lineIssue == torConfig.VAL_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.END_WITH_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()
+  
+  def redraw(self, forceRedraw=False, block=False):
+    panel.Panel.redraw(self, forceRedraw, block)
+