[tor-commits] [arm/master] Using stem's conf util

atagar at torproject.org atagar at torproject.org
Wed Jan 2 01:45:27 UTC 2013


commit d0ec1e72ef3d7745b68fad7a652f2773ea7bc2b0
Author: Damian Johnson <atagar at torproject.org>
Date:   Tue Jan 1 13:58:06 2013 -0800

    Using stem's conf util
    
    Ok, let's get this out of the way first. This is a major overhaul. Arm's conf
    util had its insidious clutch over just about every file in our codebase.
    Stem's conf util introduces a new pattern for managing configurations that
    turns out to be vastly cleaner.
    
    I'll be amazed if this doesn't introduce some sort of regression but so far so
    good. In the long run this'll be a big step toward having a simpler, more
    manageable codebase.
---
 armrc.sample                       |   42 ++++--
 src/cli/configPanel.py             |   75 ++++----
 src/cli/connections/connEntry.py   |   21 +--
 src/cli/connections/connPanel.py   |   90 ++++++-----
 src/cli/controller.py              |   70 ++++----
 src/cli/graphing/bandwidthStats.py |   53 +++---
 src/cli/graphing/graphPanel.py     |   31 ++--
 src/cli/headerPanel.py             |   34 ++--
 src/cli/logPanel.py                |  137 +++++++--------
 src/cli/torrcPanel.py              |   30 ++--
 src/starter.py                     |   73 ++++-----
 src/util/conf.py                   |  337 ------------------------------------
 src/util/connections.py            |   74 ++++----
 src/util/hostnames.py              |   30 ++--
 src/util/log.py                    |   23 ++-
 src/util/panel.py                  |    9 +-
 src/util/procTools.py              |   11 +-
 src/util/sysTools.py               |   25 ++--
 src/util/torConfig.py              |   66 ++++----
 src/util/torTools.py               |   18 +-
 src/util/uiTools.py                |   33 ++--
 21 files changed, 488 insertions(+), 794 deletions(-)

diff --git a/armrc.sample b/armrc.sample
index 9072713..2c3a8ca 100644
--- a/armrc.sample
+++ b/armrc.sample
@@ -109,9 +109,17 @@ features.log.maxRefreshRate 300
 # ---------------------------
 # order
 #   three comma separated configuration attributes, options including:
-#   0 -> Category,  1 -> Option Name,   2 -> Value,       3 -> Arg Type,
-#   4 -> Arg Usage, 5 -> Summary,       6 -> Description, 7 -> Man Entry,
-#   8 -> Is Default
+#
+#     * CATEGORY
+#     * OPTION
+#     * VALUE
+#     * TYPE
+#     * ARG_USAGE
+#     * SUMMARY
+#     * DESCRIPTION
+#     * MAN_ENTRY
+#     * IS_DEFAULT
+#
 # selectionDetails.height
 #   rows of data for the panel showing details on the current selection, this
 #   is disabled entirely if zero
@@ -131,7 +139,7 @@ features.log.maxRefreshRate 300
 # file.maxLinesPerEntry
 #   max number of lines to display for a single entry in the torrc
 
-features.config.order 7, 1, 8
+features.config.order MAN_ENTRY, OPTION, IS_DEFAULT
 features.config.selectionDetails.height 6
 features.config.prepopulateEditValues true
 features.config.state.colWidth.option 25
@@ -200,13 +208,25 @@ features.graph.bw.accounting.isTimeLong false
 # ---------------------------------
 # listingType
 #   the primary category of information shown by default, options including:
-#   0 -> IP Address / Port              1 -> Hostname
-#   2 -> Fingerprint                    3 -> Nickname
+#
+#   * IP_ADDRESS
+#   * HOSTNAME
+#   * FINGERPRINT
+#   * NICKNAME
+#
 # order
 #   three comma separated configuration attributes, options including:
-#   0 -> Category,  1 -> Uptime,        2 -> Listing,     3 -> IP Address,
-#   4 -> Port,      5 -> Hostname,      6 -> Fingerprint, 7 -> Nickname,
-#   8 -> Country
+#
+#   * CATEGORY
+#   * UPTIME
+#   * LISTING
+#   * IP_ADDRESS
+#   * PORT
+#   * HOSTNAME
+#   * FINGERPRINT
+#   * NICKNAME
+#   * COUNTRY
+#
 # refreshRate
 #   rate at which the connection panel contents is redrawn (if higher than the
 #   connection resolution rate then reducing this won't casue new data to
@@ -227,8 +247,8 @@ features.graph.bw.accounting.isTimeLong false
 # showColumn.*
 #   toggles the visability of the connection table columns
 
-features.connection.listingType 0
-features.connection.order 0, 2, 1
+features.connection.listingType IP_ADDRESS
+features.connection.order CATEGORY, LISTING, UPTIME
 features.connection.refreshRate 5
 features.connection.resolveApps true
 features.connection.markInitialConnections true
diff --git a/src/cli/configPanel.py b/src/cli/configPanel.py
index 50b66b3..38df235 100644
--- a/src/cli/configPanel.py
+++ b/src/cli/configPanel.py
@@ -9,16 +9,9 @@ import threading
 import cli.controller
 import popups
 
-from util import conf, panel, sysTools, torConfig, torTools, uiTools
+from util import panel, sysTools, torConfig, torTools, uiTools
 
-from stem.util import enum
-
-DEFAULT_CONFIG = {"features.config.selectionDetails.height": 6,
-                  "features.config.prepopulateEditValues": True,
-                  "features.config.state.showPrivateOptions": False,
-                  "features.config.state.showVirtualOptions": False,
-                  "features.config.state.colWidth.option": 25,
-                  "features.config.state.colWidth.value": 15}
+from stem.util import conf, enum
 
 # TODO: The arm use cases are incomplete since they currently can't be
 # modified, have their descriptions fetched, or even get a complete listing
@@ -38,7 +31,7 @@ CATEGORY_COLOR = {torConfig.Category.GENERAL: "green",
 # 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"),
@@ -49,6 +42,26 @@ FIELD_ATTR = {Field.CATEGORY: ("Category", "red"),
               Field.MAN_ENTRY: ("Man Page Entry", "blue"),
               Field.IS_DEFAULT: ("Is Default", "magenta")}
 
+def conf_handler(key, value):
+  if key == "features.config.selectionDetails.height":
+    return max(0, value)
+  elif key == "features.config.state.colWidth.option":
+    return max(5, value)
+  elif key == "features.config.state.colWidth.value":
+    return max(5, value)
+  elif key == "features.config.order":
+    return conf.parse_enum_csv(key, value[0], Field, 3)
+
+CONFIG = conf.config_dict("arm", {
+  "features.config.order": [Field.MAN_ENTRY, Field.OPTION, Field.IS_DEFAULT],
+  "features.config.selectionDetails.height": 6,
+  "features.config.prepopulateEditValues": True,
+  "features.config.state.showPrivateOptions": False,
+  "features.config.state.showVirtualOptions": False,
+  "features.config.state.colWidth.option": 25,
+  "features.config.state.colWidth.value": 15,
+}, conf_handler)
+
 def getFieldFromLabel(fieldLabel):
   """
   Converts field labels back to their enumeration, raising a ValueError if it
@@ -173,23 +186,9 @@ class ConfigPanel(panel.Panel):
   be selected and edited.
   """
   
-  def __init__(self, stdscr, configType, config=None):
+  def __init__(self, stdscr, configType):
     panel.Panel.__init__(self, stdscr, "configuration", 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 = list(Field)
-      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.confImportantContents = []
@@ -237,16 +236,16 @@ class ConfigPanel(panel.Panel):
         confOption, confType = lineComp[0], lineComp[1]
         
         # skips private and virtual entries if not configured to show them
-        if not self._config["features.config.state.showPrivateOptions"] and confOption.startswith("__"):
+        if not CONFIG["features.config.state.showPrivateOptions"] and confOption.startswith("__"):
           continue
-        elif not self._config["features.config.state.showVirtualOptions"] and confType == "Virtual":
+        elif not 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():
+      armConf = conf.get_config("arm")
+      for key in armConf.keys():
         pass # TODO: implement
     
     # mirror listing with only the important configuration options
@@ -290,9 +289,9 @@ class ConfigPanel(panel.Panel):
     """
     
     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)))
+    if ordering: CONFIG["features.config.order"] = ordering
+    self.confContents.sort(key=lambda i: (i.getAll(CONFIG["features.config.order"])))
+    self.confImportantContents.sort(key=lambda i: (i.getAll(CONFIG["features.config.order"])))
     self.valsLock.release()
   
   def showSortDialog(self):
@@ -303,7 +302,7 @@ class ConfigPanel(panel.Panel):
     # set ordering for config options
     titleLabel = "Config Option Ordering:"
     options = [FIELD_ATTR[field][0] for field in Field]
-    oldSelection = [FIELD_ATTR[field][0] for field in self.sortOrdering]
+    oldSelection = [FIELD_ATTR[field][0] for field in CONFIG["features.config.order"]]
     optionColors = dict([FIELD_ATTR[field] for field in Field])
     results = popups.showSortDialog(titleLabel, options, oldSelection, optionColors)
     
@@ -317,7 +316,7 @@ class ConfigPanel(panel.Panel):
     isKeystrokeConsumed = True
     if uiTools.isScrollKey(key):
       pageHeight = self.getPreferredSize()[0] - 1
-      detailPanelHeight = self._config["features.config.selectionDetails.height"]
+      detailPanelHeight = CONFIG["features.config.selectionDetails.height"]
       if detailPanelHeight > 0 and detailPanelHeight + 2 <= pageHeight:
         pageHeight -= (detailPanelHeight + 1)
       
@@ -336,7 +335,7 @@ class ConfigPanel(panel.Panel):
         else: initialValue = selection.get(Field.VALUE)
         
         promptMsg = "%s Value (esc to cancel): " % configOption
-        isPrepopulated = self._config["features.config.prepopulateEditValues"]
+        isPrepopulated = CONFIG["features.config.prepopulateEditValues"]
         newValue = popups.inputPrompt(promptMsg, initialValue if isPrepopulated else "")
         
         if newValue != None and newValue != initialValue:
@@ -494,7 +493,7 @@ class ConfigPanel(panel.Panel):
     self.valsLock.acquire()
     
     # panel with details for the current selection
-    detailPanelHeight = self._config["features.config.selectionDetails.height"]
+    detailPanelHeight = CONFIG["features.config.selectionDetails.height"]
     isScrollbarVisible = False
     if detailPanelHeight == 0 or detailPanelHeight + 2 >= height:
       # no detail panel
@@ -526,8 +525,8 @@ class ConfigPanel(panel.Panel):
       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"]
+    optionWidth = CONFIG["features.config.state.colWidth.option"]
+    valueWidth = CONFIG["features.config.state.colWidth.value"]
     descriptionWidth = max(0, width - scrollOffset - optionWidth - valueWidth - 2)
     
     # if the description column is overly long then use its space for the
diff --git a/src/cli/connections/connEntry.py b/src/cli/connections/connEntry.py
index 2f57b51..73c3ece 100644
--- a/src/cli/connections/connEntry.py
+++ b/src/cli/connections/connEntry.py
@@ -9,7 +9,7 @@ import curses
 from util import connections, torTools, uiTools
 from cli.connections import entries
 
-from stem.util import enum
+from stem.util import conf, enum
 
 # Connection Categories:
 #   Inbound      Relay connection, coming to us.
@@ -35,16 +35,15 @@ 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.showIps": 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)
+CONFIG = conf.config_dict("arm", {
+  "features.connection.markInitialConnections": True,
+  "features.connection.showIps": 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,
+})
 
 class Endpoint:
   """
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 50ba2e8..9f3a5ed 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -12,12 +12,7 @@ import cli.popups
 from cli.connections import countPopup, descriptorPopup, entries, connEntry, circEntry
 from util import connections, panel, torTools, uiTools
 
-from stem.util import enum
-
-DEFAULT_CONFIG = {"features.connection.resolveApps": True,
-                  "features.connection.listingType": 0,
-                  "features.connection.refreshRate": 5,
-                  "features.connection.showIps": True}
+from stem.util import conf, enum
 
 # height of the detail panel content, not counting top and bottom border
 DETAILS_HEIGHT = 7
@@ -25,7 +20,24 @@ 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)
+def conf_handler(key, value):
+  if key == "features.connection.listingType":
+    return conf.parse_enum(key, value, Listing)
+  elif key == "features.connection.refreshRate":
+    return max(1, value)
+  elif key == "features.connection.order":
+    return conf.parse_enum_csv(key, value[0], entries.SortAttr, 3)
+
+CONFIG = conf.config_dict("arm", {
+  "features.connection.resolveApps": True,
+  "features.connection.listingType": Listing.IP_ADDRESS,
+  "features.connection.order": [
+    entries.SortAttr.CATEGORY,
+    entries.SortAttr.LISTING,
+    entries.SortAttr.UPTIME],
+  "features.connection.refreshRate": 5,
+  "features.connection.showIps": True,
+}, conf_handler)
 
 class ConnectionPanel(panel.Panel, threading.Thread):
   """
@@ -33,31 +45,21 @@ class ConnectionPanel(panel.Panel, threading.Thread):
   the current consensus and other data sources.
   """
   
-  def __init__(self, stdscr, config=None):
+  def __init__(self, stdscr):
     panel.Panel.__init__(self, stdscr, "connections", 0)
     threading.Thread.__init__(self)
     self.setDaemon(True)
     
-    self._sortOrdering = DEFAULT_SORT_ORDER
-    self._config = dict(DEFAULT_CONFIG)
+    # defaults our listing selection to fingerprints if ip address
+    # displaying is disabled
+    #
+    # TODO: This is a little sucky in that it won't work if showIps changes
+    # while we're running (... but arm doesn't allow for that atm)
     
-    if config:
-      config.update(self._config, {
-        "features.connection.listingType": (0, len(list(Listing)) - 1),
-        "features.connection.refreshRate": 1})
-      
-      # defaults our listing selection to fingerprints if ip address
-      # displaying is disabled
-      if not self._config["features.connection.showIps"] and self._config["features.connection.listingType"] == 0:
-        self._config["features.connection.listingType"] = 2
-      
-      sortFields = list(entries.SortAttr)
-      customOrdering = config.getIntCSV("features.connection.order", None, 3, 0, len(sortFields))
-      
-      if customOrdering:
-        self._sortOrdering = [sortFields[i] for i in customOrdering]
+    if not CONFIG["features.connection.showIps"] and CONFIG["features.connection.listingType"] == 0:
+      armConf = conf.get_config("arm")
+      armConf.set("features.connection.listingType", enumeration.keys()[Listing.index_of(Listing.FINGERPRINT)])
     
-    self._listingType = list(Listing)[self._config["features.connection.listingType"]]
     self._scroller = uiTools.Scroller(True)
     self._title = "Connections:" # title line of the panel
     self._entries = []          # last fetched display entries
@@ -150,8 +152,14 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     """
     
     self.valsLock.acquire()
-    if ordering: self._sortOrdering = ordering
-    self._entries.sort(key=lambda i: (i.getSortValues(self._sortOrdering, self._listingType)))
+    
+    if ordering:
+      armConf = conf.get_config("arm")
+      
+      ordering_keys = [entries.SortAttr.keys()[entries.SortAttr.index_of(v)] for v in ordering]
+      armConf.set("features.connection.order", ", ".join(ordering_keys))
+    
+    self._entries.sort(key=lambda i: (i.getSortValues(CONFIG["features.connection.order"], self.getListingType())))
     
     self._entryLines = []
     for entry in self._entries:
@@ -163,7 +171,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     Provides the priority content we list connections by.
     """
     
-    return self._listingType
+    return CONFIG["features.connection.listingType"]
   
   def setListingType(self, listingType):
     """
@@ -173,13 +181,15 @@ class ConnectionPanel(panel.Panel, threading.Thread):
       listingType - Listing instance for the primary information to be shown
     """
     
-    if self._listingType == listingType: return
+    if self.getListingType() == listingType: return
     
     self.valsLock.acquire()
-    self._listingType = listingType
+    
+    armConf = conf.get_config("arm")
+    armConf.set("features.connection.listingType", enumeration.keys()[Listing.index_of(listingType)])
     
     # if we're sorting by the listing then we need to resort
-    if entries.SortAttr.LISTING in self._sortOrdering:
+    if entries.SortAttr.LISTING in CONFIG["features.connection.order"]:
       self.setSortOrder()
     
     self.valsLock.release()
@@ -208,7 +218,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     # set ordering for connection options
     titleLabel = "Connection Ordering:"
     options = list(entries.SortAttr)
-    oldSelection = self._sortOrdering
+    oldSelection = CONFIG["features.connection.order"]
     optionColors = dict([(attr, entries.SORT_COLORS[attr]) for attr in options])
     results = cli.popups.showSortDialog(titleLabel, options, oldSelection, optionColors)
     if results: self.setSortOrder(results)
@@ -251,7 +261,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
       # dropping the HOSTNAME listing type until we support displaying that content
       options.remove(cli.connections.entries.ListingType.HOSTNAME)
       
-      oldSelection = options.index(self._listingType)
+      oldSelection = options.index(self.getListingType())
       selection = cli.popups.showMenu(title, options, oldSelection)
       
       # applies new setting
@@ -287,7 +297,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     while not self._halt:
       currentTime = time.time()
       
-      if self.isPaused() or not self._isTorRunning or currentTime - lastDraw < self._config["features.connection.refreshRate"]:
+      if self.isPaused() or not self._isTorRunning or currentTime - lastDraw < CONFIG["features.connection.refreshRate"]:
         self._cond.acquire()
         if not self._halt: self._cond.wait(0.2)
         self._cond.release()
@@ -298,8 +308,8 @@ class ConnectionPanel(panel.Panel, threading.Thread):
         
         # 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
+        drawTicks = (time.time() - lastDraw) / CONFIG["features.connection.refreshRate"]
+        lastDraw += CONFIG["features.connection.refreshRate"] * drawTicks
   
   def getHelp(self):
     resolverUtil = connections.getResolver("tor").overwriteResolver
@@ -319,7 +329,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     if self.isExitsAllowed():
       options.append(("e", "exit port usage summary", None))
     
-    options.append(("l", "listed identity", self._listingType.lower()))
+    options.append(("l", "listed identity", self.getListingType().lower()))
     options.append(("s", "sort ordering", None))
     options.append(("u", "resolving utility", resolverUtil))
     return options
@@ -387,7 +397,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
         self.addch(drawLine, scrollOffset + i, prefix[i])
       
       xOffset = scrollOffset + len(prefix)
-      drawEntry = entryLine.getListingEntry(width - scrollOffset - len(prefix), currentTime, self._listingType)
+      drawEntry = entryLine.getListingEntry(width - scrollOffset - len(prefix), currentTime, self.getListingType())
       
       for msg, attr in drawEntry:
         attr |= extraFormat
@@ -530,7 +540,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
                   until the next update if true
     """
     
-    if self.appResolveSinceUpdate or not self._config["features.connection.resolveApps"]: return
+    if self.appResolveSinceUpdate or not 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
diff --git a/src/cli/controller.py b/src/cli/controller.py
index af3fdac..a41cc83 100644
--- a/src/cli/controller.py
+++ b/src/cli/controller.py
@@ -22,29 +22,37 @@ import cli.connections.connPanel
 
 from stem.control import Controller
 
-from util import connections, conf, hostnames, log, panel, sysTools, torConfig, torTools
+from util import connections, hostnames, log, panel, sysTools, torConfig, torTools
 
-from stem.util import enum
+from stem.util import conf, enum
 
 ARM_CONTROLLER = None
 
-CONFIG = {"startup.events": "N3",
-          "startup.dataDirectory": "~/.arm",
-          "startup.blindModeEnabled": False,
-          "features.panels.show.graph": True,
-          "features.panels.show.log": True,
-          "features.panels.show.connection": True,
-          "features.panels.show.config": True,
-          "features.panels.show.torrc": True,
-          "features.redrawRate": 5,
-          "features.refreshRate": 5,
-          "features.confirmQuit": True,
-          "features.graph.type": 1,
-          "features.graph.bw.prepopulate": True,
-          "log.startTime": log.INFO,
-          "log.torEventTypeUnrecognized": log.INFO,
-          "log.configEntryUndefined": log.NOTICE,
-          "log.unknownTorPid": log.WARN}
+def conf_handler(key, value):
+  if key == "features.redrawRate":
+    return max(1, value)
+  elif key == "features.refreshRate":
+    return max(0, value)
+
+CONFIG = conf.config_dict("arm", {
+  "startup.events": "N3",
+  "startup.dataDirectory": "~/.arm",
+  "startup.blindModeEnabled": False,
+  "features.panels.show.graph": True,
+  "features.panels.show.log": True,
+  "features.panels.show.connection": True,
+  "features.panels.show.config": True,
+  "features.panels.show.torrc": True,
+  "features.redrawRate": 5,
+  "features.refreshRate": 5,
+  "features.confirmQuit": True,
+  "features.graph.type": 1,
+  "features.graph.bw.prepopulate": True,
+  "log.startTime": log.INFO,
+  "log.torEventTypeUnrecognized": log.INFO,
+  "log.configEntryUndefined": log.NOTICE,
+  "log.unknownTorPid": log.WARN,
+}, conf_handler)
 
 GraphStat = enum.Enum("BANDWIDTH", "CONNECTIONS", "SYSTEM_RESOURCES")
 
@@ -67,10 +75,9 @@ def initController(stdscr, startTime):
   """
   
   global ARM_CONTROLLER
-  config = conf.getConfig("arm")
   
   # initializes the panels
-  stickyPanels = [cli.headerPanel.HeaderPanel(stdscr, startTime, config),
+  stickyPanels = [cli.headerPanel.HeaderPanel(stdscr, startTime),
                   LabelPanel(stdscr)]
   pagePanels, firstPagePanels = [], []
   
@@ -80,21 +87,21 @@ def initController(stdscr, startTime):
   
   if CONFIG["features.panels.show.log"]:
     expandedEvents = cli.logPanel.expandEvents(CONFIG["startup.events"])
-    firstPagePanels.append(cli.logPanel.LogPanel(stdscr, expandedEvents, config))
+    firstPagePanels.append(cli.logPanel.LogPanel(stdscr, expandedEvents))
   
   if firstPagePanels: pagePanels.append(firstPagePanels)
   
   # second page: connections
   if not CONFIG["startup.blindModeEnabled"] and CONFIG["features.panels.show.connection"]:
-    pagePanels.append([cli.connections.connPanel.ConnectionPanel(stdscr, config)])
+    pagePanels.append([cli.connections.connPanel.ConnectionPanel(stdscr)])
   
   # third page: config
   if CONFIG["features.panels.show.config"]:
-    pagePanels.append([cli.configPanel.ConfigPanel(stdscr, cli.configPanel.State.TOR, config)])
+    pagePanels.append([cli.configPanel.ConfigPanel(stdscr, cli.configPanel.State.TOR)])
   
   # fourth page: torrc
   if CONFIG["features.panels.show.torrc"]:
-    pagePanels.append([cli.torrcPanel.TorrcPanel(stdscr, cli.torrcPanel.Config.TORRC, config)])
+    pagePanels.append([cli.torrcPanel.TorrcPanel(stdscr, cli.torrcPanel.Config.TORRC)])
   
   # initializes the controller
   ARM_CONTROLLER = Controller(stdscr, stickyPanels, pagePanels)
@@ -104,7 +111,7 @@ def initController(stdscr, startTime):
   
   if graphPanel:
     # statistical monitors for graph
-    bwStats = cli.graphing.bandwidthStats.BandwidthStats(config)
+    bwStats = cli.graphing.bandwidthStats.BandwidthStats()
     graphPanel.addStats(GraphStat.BANDWIDTH, bwStats)
     graphPanel.addStats(GraphStat.SYSTEM_RESOURCES, cli.graphing.resourceStats.ResourceStats())
     if not CONFIG["startup.blindModeEnabled"]:
@@ -509,15 +516,6 @@ def startTorMonitor(startTime):
     startTime - unix time for when arm was started
   """
   
-  # initializes interface configs
-  config = conf.getConfig("arm")
-  config.update(CONFIG, {
-    "features.redrawRate": 1,
-    "features.refreshRate": 0})
-  
-  cli.graphing.graphPanel.loadConfig(config)
-  cli.connections.connEntry.loadConfig(config)
-  
   # attempts to fetch the tor pid, warning if unsuccessful (this is needed for
   # checking its resource usage, among other things)
   conn = torTools.getConn()
@@ -592,7 +590,7 @@ def drawTorMonitor(stdscr, startTime):
   control = getController()
   
   # provides notice about any unused config keys
-  for key in conf.getConfig("arm").getUnusedKeys():
+  for key in conf.get_config("arm").unused_keys():
     log.log(CONFIG["log.configEntryUndefined"], "Unused configuration entry: %s" % key)
   
   # tells daemon panels to start
diff --git a/src/cli/graphing/bandwidthStats.py b/src/cli/graphing/bandwidthStats.py
index 2b60c7e..9aecf96 100644
--- a/src/cli/graphing/bandwidthStats.py
+++ b/src/cli/graphing/bandwidthStats.py
@@ -11,6 +11,21 @@ import cli.controller
 from cli.graphing import graphPanel
 from util import log, sysTools, torTools, uiTools
 
+from stem.util import conf
+
+def conf_handler(key, value):
+  if key == "features.graph.bw.accounting.rate":
+    return max(1, value)
+
+CONFIG = conf.config_dict("arm", {
+  "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,
+}, conf_handler)
+
 DL_COLOR, UL_COLOR = "green", "cyan"
 
 # width at which panel abandons placing optional stats (avg and total) with
@@ -23,26 +38,14 @@ ACCOUNTING_ARGS = ("status", "resetTime", "read", "written", "readLimit", "writt
 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, isPauseBuffer=False):
+  def __init__(self, isPauseBuffer=False):
     graphPanel.GraphStats.__init__(self)
     
-    self.inputConfig = config
-    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
@@ -78,7 +81,7 @@ class BandwidthStats(graphPanel.GraphStats):
       self.initialSecondaryTotal = int(writeTotal) / 1024 # Bytes -> KB
   
   def clone(self, newCopy=None):
-    if not newCopy: newCopy = BandwidthStats(self.inputConfig, True)
+    if not newCopy: newCopy = BandwidthStats(True)
     newCopy.accountingLastUpdated = self.accountingLastUpdated
     newCopy.accountingInfo = self.accountingInfo
     
@@ -93,7 +96,7 @@ class BandwidthStats(graphPanel.GraphStats):
     self._titleStats = []     # force reset of title
     self.new_desc_event(None) # updates title params
     
-    if eventType in (torTools.State.INIT, torTools.State.RESET) and self._config["features.graph.bw.accounting.show"]:
+    if eventType in (torTools.State.INIT, torTools.State.RESET) and CONFIG["features.graph.bw.accounting.show"]:
       isAccountingEnabled = conn.getInfo('accounting/enabled', None) == '1'
       
       if isAccountingEnabled != self.isAccounting:
@@ -136,21 +139,21 @@ class BandwidthStats(graphPanel.GraphStats):
     # 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)
+      log.log(CONFIG["log.graph.bw.prepopulateFailure"], msg)
       return False
     
     # get the user's data directory (usually '~/.tor')
     dataDir = conn.getOption("DataDirectory", None)
     if not dataDir:
       msg = PREPOPULATE_FAILURE_MSG % "data directory not found"
-      log.log(self._config["log.graph.bw.prepopulateFailure"], msg)
+      log.log(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)
+      log.log(CONFIG["log.graph.bw.prepopulateFailure"], msg)
       return False
     
     # get the BWHistory entries (ordered oldest to newest) and number of
@@ -189,7 +192,7 @@ class BandwidthStats(graphPanel.GraphStats):
     
     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)
+      log.log(CONFIG["log.graph.bw.prepopulateFailure"], msg)
       return False
     
     # fills missing entries with the last value
@@ -228,13 +231,13 @@ class BandwidthStats(graphPanel.GraphStats):
     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)
+    log.log(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"]:
+      if time.time() - self.accountingLastUpdated >= CONFIG["features.graph.bw.accounting.rate"]:
         self._updateAccountingInfo()
     
     # scales units from B to KB for graphing
@@ -309,7 +312,7 @@ class BandwidthStats(graphPanel.GraphStats):
       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"]))
+    stats[0] = "%-14s" % ("%s/sec" % uiTools.getSizeLabel((self.lastPrimary if isPrimary else self.lastSecondary) * 1024, 1, False, CONFIG["features.graph.bw.transferInBytes"]))
     
     # drops label's components if there's not enough space
     labeling = graphType + " (" + "".join(stats).strip() + "):"
@@ -342,7 +345,7 @@ class BandwidthStats(graphPanel.GraphStats):
       bwBurst = conn.getMyBandwidthBurst()
       bwObserved = conn.getMyBandwidthObserved()
       bwMeasured = conn.getMyBandwidthMeasured()
-      labelInBytes = self._config["features.graph.bw.transferInBytes"]
+      labelInBytes = CONFIG["features.graph.bw.transferInBytes"]
       
       if bwRate and bwBurst:
         bwRateLabel = uiTools.getSizeLabel(bwRate, 1, False, labelInBytes)
@@ -369,7 +372,7 @@ class BandwidthStats(graphPanel.GraphStats):
   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"])
+    return "avg: %s/sec" % uiTools.getSizeLabel((total / max(1, self.tick + self.prepopulateTicks)) * 1024, 1, False, CONFIG["features.graph.bw.transferInBytes"])
   
   def _getTotalLabel(self, isPrimary):
     total = self.primaryTotal if isPrimary else self.secondaryTotal
@@ -396,7 +399,7 @@ class BandwidthStats(graphPanel.GraphStats):
       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"]:
+      if CONFIG["features.graph.bw.accounting.isTimeLong"]:
         queried["resetTime"] = ", ".join(uiTools.getTimeLabels(sec, True))
       else:
         days = sec / 86400
diff --git a/src/cli/graphing/graphPanel.py b/src/cli/graphing/graphPanel.py
index 16efe8f..b989ef5 100644
--- a/src/cli/graphing/graphPanel.py
+++ b/src/cli/graphing/graphPanel.py
@@ -26,7 +26,7 @@ import stem.control
 
 from util import panel, torTools, uiTools
 
-from stem.util import enum
+from stem.util import conf, enum
 
 # time intervals at which graphs can be updated
 UPDATE_INTERVALS = [("each second", 1), ("5 seconds", 5),   ("30 seconds", 30),
@@ -45,19 +45,24 @@ 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 conf_handler(key, value):
+  if key == "features.graph.height":
+    return max(MIN_GRAPH_HEIGHT, value)
+  elif key == "features.graph.maxWidth":
+    return max(1, value)
+  elif key == "features.graph.interval":
+    return max(0, min(len(UPDATE_INTERVALS) - 1, value))
+  elif key == "features.graph.bound":
+    return max(0, min(2, value))
 
-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)})
+# used for setting defaults when initializing GraphStats and GraphPanel instances
+CONFIG = conf.config_dict("arm", {
+  "features.graph.height": 7,
+  "features.graph.interval": 0,
+  "features.graph.bound": 1,
+  "features.graph.maxWidth": 150,
+  "features.graph.showIntermediateBounds": True,
+}, conf_handler)
 
 class GraphStats:
   """
diff --git a/src/cli/headerPanel.py b/src/cli/headerPanel.py
index 4ab3daf..07937e0 100644
--- a/src/cli/headerPanel.py
+++ b/src/cli/headerPanel.py
@@ -23,6 +23,7 @@ import stem
 import stem.connection
 
 from stem.control import Controller
+from stem.util import conf
 
 import starter
 import cli.popups
@@ -42,12 +43,14 @@ FLAG_COLORS = {"Authority": "white",  "BadExit": "red",     "BadDirectory": "red
 VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "red", "recommended": "green",  
                          "old": "red",  "unrecommended": "red",  "unknown": "cyan"}
 
-DEFAULT_CONFIG = {"startup.interface.ipAddress": "127.0.0.1",
-                  "startup.interface.port": 9051,
-                  "startup.interface.socket": "/var/run/tor/control",
-                  "features.showFdUsage": False,
-                  "log.fdUsageSixtyPercent": log.NOTICE,
-                  "log.fdUsageNinetyPercent": log.WARN}
+CONFIG = conf.config_dict("arm", {
+  "startup.interface.ipAddress": "127.0.0.1",
+  "startup.interface.port": 9051,
+  "startup.interface.socket": "/var/run/tor/control",
+  "features.showFdUsage": False,
+  "log.fdUsageSixtyPercent": log.NOTICE,
+  "log.fdUsageNinetyPercent": log.WARN,
+})
 
 class HeaderPanel(panel.Panel, threading.Thread):
   """
@@ -63,14 +66,11 @@ class HeaderPanel(panel.Panel, threading.Thread):
   * volatile parameter that'll be reset on each update
   """
   
-  def __init__(self, stdscr, startTime, config = None):
+  def __init__(self, stdscr, startTime):
     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 = torTools.getConn().isAlive()
     self._lastUpdate = -1       # time the content was last revised
     self._halt = False          # terminates thread if true
@@ -140,10 +140,10 @@ class HeaderPanel(panel.Panel, threading.Thread):
       controller = None
       allowPortConnection, allowSocketConnection, _ = starter.allowConnectionTypes()
       
-      if os.path.exists(self._config["startup.interface.socket"]) and allowSocketConnection:
+      if os.path.exists(CONFIG["startup.interface.socket"]) and allowSocketConnection:
         try:
           # TODO: um... what about passwords?
-          controller = Controller.from_socket_file(self._config["startup.interface.socket"])
+          controller = Controller.from_socket_file(CONFIG["startup.interface.socket"])
           controller.authenticate()
         except (IOError, stem.SocketError), exc:
           controller = None
@@ -151,7 +151,7 @@ class HeaderPanel(panel.Panel, threading.Thread):
           if not allowPortConnection:
             cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3)
       elif not allowPortConnection:
-        cli.popups.showMsg("Unable to reconnect (socket '%s' doesn't exist)" % self._config["startup.interface.socket"], 3)
+        cli.popups.showMsg("Unable to reconnect (socket '%s' doesn't exist)" % CONFIG["startup.interface.socket"], 3)
       
       if not controller and allowPortConnection:
         # TODO: This has diverged from starter.py's connection, for instance it
@@ -161,7 +161,7 @@ class HeaderPanel(panel.Panel, threading.Thread):
         # manageable.
         
         try:
-          ctlAddr, ctlPort = self._config["startup.interface.ipAddress"], self._config["startup.interface.port"]
+          ctlAddr, ctlPort = CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"]
           controller = Controller.from_port(ctlAddr, ctlPort)
           
           try:
@@ -297,7 +297,7 @@ class HeaderPanel(panel.Panel, threading.Thread):
         
         fdPercent = 100 * self.vals["tor/fdUsed"] / self.vals["tor/fdLimit"]
         
-        if fdPercent >= 60 or self._config["features.showFdUsage"]:
+        if fdPercent >= 60 or CONFIG["features.showFdUsage"]:
           fdPercentLabel, fdPercentFormat = "%i%%" % fdPercent, curses.A_NORMAL
           if fdPercent >= 95:
             fdPercentFormat = curses.A_BOLD | uiTools.getColor("red")
@@ -559,10 +559,10 @@ class HeaderPanel(panel.Panel, threading.Thread):
       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)
+        log.log(CONFIG["log.fdUsageNinetyPercent"], msg)
       elif fdPercent >= 60 and not self._isFdSixtyPercentWarned:
         self._isFdSixtyPercentWarned = True
-        log.log(self._config["log.fdUsageSixtyPercent"], msg)
+        log.log(CONFIG["log.fdUsageSixtyPercent"], msg)
     
     # ps or proc derived resource usage stats
     if self.vals["tor/pid"]:
diff --git a/src/cli/logPanel.py b/src/cli/logPanel.py
index d03973d..e196544 100644
--- a/src/cli/logPanel.py
+++ b/src/cli/logPanel.py
@@ -11,12 +11,13 @@ import curses
 import logging
 import threading
 
-import stem.util.log
+import stem.util.log # move to a 'from' import when we've dropped the other log util
 from stem.response import events
+from stem.util import conf
 
 import popups
 from version import VERSION
-from util import conf, log, panel, sysTools, torTools, uiTools
+from util import log, panel, sysTools, torTools, uiTools
 
 TOR_EVENT_TYPES = {
   "d": "DEBUG",   "a": "ADDRMAP",          "k": "DESCCHANGED",  "s": "STREAM",
@@ -42,22 +43,35 @@ 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": 6,
-                  "features.log.prepopulate": True,
-                  "features.log.prepopulateReadLimit": 5000,
-                  "features.log.maxRefreshRate": 300,
-                  "features.log.regex": [],
-                  "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,
-                  "log.configEntryTypeError": log.NOTICE}
+
+def conf_handler(key, value):
+  if key == "features.log.maxLinesPerEntry":
+    return max(1, value)
+  elif key == "features.log.prepopulateReadLimit":
+    return max(0, value)
+  elif key == "features.log.maxRefreshRate":
+    return max(10, value)
+  elif key == "cache.logPanel.size":
+    return max(1000, value)
+
+CONFIG = conf.config_dict("arm", {
+  "features.logFile": "",
+  "features.log.showDateDividers": True,
+  "features.log.showDuplicateEntries": False,
+  "features.log.entryDuration": 7,
+  "features.log.maxLinesPerEntry": 6,
+  "features.log.prepopulate": True,
+  "features.log.prepopulateReadLimit": 5000,
+  "features.log.maxRefreshRate": 300,
+  "features.log.regex": [],
+  "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,
+  "log.configEntryTypeError": log.NOTICE,
+}, conf_handler)
 
 DUPLICATE_MSG = " [%i duplicate%s hidden]"
 
@@ -173,16 +187,16 @@ def loadLogMessages():
   """
   
   global COMMON_LOG_MESSAGES
-  armConf = conf.getConfig("arm")
+  armConf = conf.get_config("arm")
   
   COMMON_LOG_MESSAGES = {}
-  for confKey in armConf.getKeys():
+  for confKey in armConf.keys():
     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):
+def getLogFileEntries(runlevels, readLimit = None, addLimit = 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
@@ -193,15 +207,11 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = Non
     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):
@@ -252,7 +262,7 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = Non
       logFile.close()
   except IOError:
     msg = "Unable to read tor's log file: %s" % loggingLocation
-    log.log(config["log.logPanel.prepopulateFailed"], msg)
+    log.log(CONFIG["log.logPanel.prepopulateFailed"], msg)
   
   if not lines: return []
   
@@ -308,7 +318,7 @@ def getLogFileEntries(runlevels, readLimit = None, addLimit = None, config = Non
   
   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)
+  log.log(CONFIG["log.logPanel.prepopulateSuccess"], msg)
   return loggedEvents
 
 def getDaybreaks(events, ignoreTimeForCache = False):
@@ -474,7 +484,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
   from tor's log file if it exists.
   """
   
-  def __init__(self, stdscr, loggedEvents, config=None):
+  def __init__(self, stdscr, loggedEvents):
     panel.Panel.__init__(self, stdscr, "log", 0)
     logging.Handler.__init__(self, level = stem.util.log.logging_level(stem.util.log.DEBUG))
     
@@ -494,28 +504,16 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
     # regex filters the user has defined
     self.filterOptions = []
     
-    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})
+    for filter in CONFIG["features.log.regex"]:
+      # checks if we can't have more filters
+      if len(self.filterOptions) >= MAX_REGEX_FILTERS: break
       
-      for filter in self._config["features.log.regex"]:
-        # checks if we can't have more filters
-        if len(self.filterOptions) >= MAX_REGEX_FILTERS: break
-        
-        try:
-          re.compile(filter)
-          self.filterOptions.append(filter)
-        except re.error, exc:
-          msg = "Invalid regular expression pattern (%s): %s" % (exc, filter)
-          log.log(self._config["log.configEntryTypeError"], msg)
-    
-    # collapses duplicate log entries if false, showing only the most recent
-    self.showDuplicates = self._config["features.log.showDuplicateEntries"]
+      try:
+        re.compile(filter)
+        self.filterOptions.append(filter)
+      except re.error, exc:
+        msg = "Invalid regular expression pattern (%s): %s" % (exc, filter)
+        log.log(CONFIG["log.configEntryTypeError"], msg)
     
     self.loggedEvents = [] # needs to be set before we receive any events
     
@@ -563,8 +561,8 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
     conn.addStatusListener(self._resetListener)
     
     # opens log file if we'll be saving entries
-    if self._config["features.logFile"]:
-      logPath = self._config["features.logFile"]
+    if CONFIG["features.logFile"]:
+      logPath = CONFIG["features.logFile"]
       
       try:
         # make dir if the path doesn't already exist
@@ -572,9 +570,9 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
         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))
+        log.log(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))
+        log.log(CONFIG["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
         self.logFile = None
     
     stem_logger = stem.util.log.get_logger()
@@ -596,11 +594,11 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
     
     # fetches past tor events from log file, if available
     torEventBacklog = []
-    if self._config["features.log.prepopulate"]:
+    if CONFIG["features.log.prepopulate"]:
       setRunlevels = list(set.intersection(set(self.loggedEvents), set(list(log.Runlevel))))
-      readLimit = self._config["features.log.prepopulateReadLimit"]
-      addLimit = self._config["cache.logPanel.size"]
-      torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit, self._config)
+      readLimit = CONFIG["features.log.prepopulateReadLimit"]
+      addLimit = CONFIG["cache.logPanel.size"]
+      torEventBacklog = getLogFileEntries(setRunlevels, readLimit, addLimit)
     
     # gets the set of arm events we're logging
     setRunlevels = []
@@ -639,7 +637,8 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
                   deduplicated
     """
     
-    self.showDuplicates = isVisible
+    armConf = conf.get_config("arm")
+    armConf.set("features.log.showDuplicateEntries", str(isVisible))
   
   def registerTorEvent(self, event):
     """
@@ -687,7 +686,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
         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))
+        log.log(CONFIG["log.logPanel.logFileWriteFailed"], "Unable to write to log file: %s" % sysTools.getFileErrorMsg(exc))
         self.logFile = None
     
     self.valsLock.acquire()
@@ -883,7 +882,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
         self.valsLock.release()
     elif key in (ord('u'), ord('U')):
       self.valsLock.acquire()
-      self.showDuplicates = not self.showDuplicates
+      self.setDuplicateVisability(not CONFIG["features.log.showDuplicateEntries"])
       self.redraw(True)
       self.valsLock.release()
     elif key == ord('c') or key == ord('C'):
@@ -929,7 +928,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
     options.append(("a", "save snapshot of the log", None))
     options.append(("e", "change logged events", None))
     options.append(("f", "log regex filter", "enabled" if self.regexFilter else "disabled"))
-    options.append(("u", "duplicate log entries", "visible" if self.showDuplicates else "hidden"))
+    options.append(("u", "duplicate log entries", "visible" if CONFIG["features.log.showDuplicateEntries"] else "hidden"))
     options.append(("c", "clear event log", None))
     return options
   
@@ -963,15 +962,15 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
     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"]
+    isDatesShown = self.regexFilter == None and CONFIG["features.log.showDateDividers"]
     eventLog = getDaybreaks(currentLog, self.isPaused()) if isDatesShown else list(currentLog)
-    if not self.showDuplicates:
+    if not CONFIG["features.log.showDuplicateEntries"]:
       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
+        self.setDuplicateVisability(True)
         deduplicatedLog = [(entry, 0) for entry in eventLog]
     else: deduplicatedLog = [(entry, 0) for entry in eventLog]
     
@@ -1024,7 +1023,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
           displayQueue.append((duplicateMsg, duplicateAttr, False))
         
         cursorLoc, lineOffset = msgIndent, 0
-        maxEntriesPerLine = self._config["features.log.maxLinesPerEntry"]
+        maxEntriesPerLine = CONFIG["features.log.maxLinesPerEntry"]
         while displayQueue:
           msg, format, includeBreak = displayQueue.pop(0)
           drawLine = lineCount + lineOffset
@@ -1085,7 +1084,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
     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)
+      log.log(CONFIG["log.logPanel.forceDoubleRedraw"], forceRedrawReason)
       self.redraw(True)
     
     self.valsLock.release()
@@ -1105,7 +1104,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
     while not self._halt:
       currentDay = daysSince()
       timeSinceReset = time.time() - self._lastUpdate
-      maxLogUpdateRate = self._config["features.log.maxRefreshRate"] / 1000.0
+      maxLogUpdateRate = CONFIG["features.log.maxRefreshRate"] / 1000.0
       
       sleepTime = 0
       if (self.msgLog == self._lastLoggedEvents and lastDay == currentDay) or self.isPaused():
@@ -1281,10 +1280,10 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
       eventListing - listing of log entries
     """
     
-    cacheSize = self._config["cache.logPanel.size"]
+    cacheSize = CONFIG["cache.logPanel.size"]
     if len(eventListing) > cacheSize: del eventListing[cacheSize:]
     
-    logTTL = self._config["features.log.entryDuration"]
+    logTTL = CONFIG["features.log.entryDuration"]
     if logTTL > 0:
       currentDay = daysSince()
       
diff --git a/src/cli/torrcPanel.py b/src/cli/torrcPanel.py
index 983768f..635fc85 100644
--- a/src/cli/torrcPanel.py
+++ b/src/cli/torrcPanel.py
@@ -8,12 +8,18 @@ import threading
 
 import popups
 
-from util import conf, panel, torConfig, torTools, uiTools
+from util import panel, torConfig, torTools, uiTools
 
-from stem.util import enum
+from stem.util import conf, enum
 
-DEFAULT_CONFIG = {"features.config.file.showScrollbars": True,
-                  "features.config.file.maxLinesPerEntry": 8}
+def conf_handler(key, value):
+  if key == "features.config.file.maxLinesPerEntry":
+    return max(1, value)
+
+CONFIG = conf.config_dict("arm", {
+  "features.config.file.showScrollbars": True,
+  "features.config.file.maxLinesPerEntry": 8,
+}, conf_handler)
 
 # TODO: The armrc use case is incomplete. There should be equivilant reloading
 # and validation capabilities to the torrc.
@@ -25,13 +31,9 @@ class TorrcPanel(panel.Panel):
   area.
   """
   
-  def __init__(self, stdscr, configType, config=None):
+  def __init__(self, stdscr, configType):
     panel.Panel.__init__(self, stdscr, "torrc", 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
@@ -180,9 +182,9 @@ class TorrcPanel(panel.Panel):
       
       loadedTorrc.getLock().release()
     else:
-      loadedArmrc = conf.getConfig("arm")
-      confLocation = loadedArmrc.path
-      renderedContents = list(loadedArmrc.rawContents)
+      loadedArmrc = conf.get_config("arm")
+      confLocation = loadedArmrc._path
+      renderedContents = list(loadedArmrc._raw_contents)
     
     # offset to make room for the line numbers
     lineNumOffset = 0
@@ -192,7 +194,7 @@ class TorrcPanel(panel.Panel):
     
     # 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:
+    if CONFIG["features.config.file.showScrollbars"] and self._lastContentHeight > height - 1:
       scrollOffset = 3
       self.addScrollBar(self.scroll, self.scroll + height - 1, self._lastContentHeight, 1)
     
@@ -267,7 +269,7 @@ class TorrcPanel(panel.Panel):
       
       # draws the rest of the components with line wrap
       cursorLoc, lineOffset = lineNumOffset + scrollOffset, 0
-      maxLinesPerEntry = self._config["features.config.file.maxLinesPerEntry"]
+      maxLinesPerEntry = CONFIG["features.config.file.maxLinesPerEntry"]
       displayQueue = [lineComp[entry] for entry in ("option", "argument", "correction", "comment")]
       
       while displayQueue:
diff --git a/src/starter.py b/src/starter.py
index ccf7aad..3cf4dff 100644
--- a/src/starter.py
+++ b/src/starter.py
@@ -17,12 +17,8 @@ import platform
 import version
 import cli.controller
 import cli.logPanel
-import util.conf
 import util.connections
-import util.hostnames
 import util.log
-import util.panel
-import util.procTools
 import util.sysTools
 import util.torConfig
 import util.torTools
@@ -30,28 +26,32 @@ import util.uiTools
 
 from stem.control import Controller
 import stem.connection
+import stem.util.conf
 
 LOG_DUMP_PATH = os.path.expanduser("~/.arm/log")
 DEFAULT_CONFIG = os.path.expanduser("~/.arm/armrc")
-CONFIG = {"startup.controlPassword": None,
-          "startup.interface.ipAddress": "127.0.0.1",
-          "startup.interface.port": 9051,
-          "startup.interface.socket": "/var/run/tor/control",
-          "startup.blindModeEnabled": False,
-          "startup.events": "N3",
-          "startup.dataDirectory": "~/.arm",
-          "features.allowDetachedStartup": False,
-          "features.config.descriptions.enabled": True,
-          "features.config.descriptions.persist": True,
-          "log.configDescriptions.readManPageSuccess": util.log.INFO,
-          "log.configDescriptions.readManPageFailed": util.log.NOTICE,
-          "log.configDescriptions.internalLoadSuccess": util.log.NOTICE,
-          "log.configDescriptions.internalLoadFailed": util.log.ERR,
-          "log.configDescriptions.persistance.loadSuccess": util.log.INFO,
-          "log.configDescriptions.persistance.loadFailed": util.log.INFO,
-          "log.configDescriptions.persistance.saveSuccess": util.log.INFO,
-          "log.configDescriptions.persistance.saveFailed": util.log.NOTICE,
-          "log.savingDebugLog": util.log.NOTICE}
+
+CONFIG = stem.util.conf.config_dict("arm", {
+  "startup.controlPassword": None,
+  "startup.interface.ipAddress": "127.0.0.1",
+  "startup.interface.port": 9051,
+  "startup.interface.socket": "/var/run/tor/control",
+  "startup.blindModeEnabled": False,
+  "startup.events": "N3",
+  "startup.dataDirectory": "~/.arm",
+  "features.allowDetachedStartup": False,
+  "features.config.descriptions.enabled": True,
+  "features.config.descriptions.persist": True,
+  "log.configDescriptions.readManPageSuccess": util.log.INFO,
+  "log.configDescriptions.readManPageFailed": util.log.NOTICE,
+  "log.configDescriptions.internalLoadSuccess": util.log.NOTICE,
+  "log.configDescriptions.internalLoadFailed": util.log.ERR,
+  "log.configDescriptions.persistance.loadSuccess": util.log.INFO,
+  "log.configDescriptions.persistance.loadFailed": util.log.INFO,
+  "log.configDescriptions.persistance.saveSuccess": util.log.INFO,
+  "log.configDescriptions.persistance.saveFailed": util.log.NOTICE,
+  "log.savingDebugLog": util.log.NOTICE,
+})
 
 OPT = "gi:s:c:dbe:vh"
 OPT_EXPANDED = ["interface=", "socket=", "config=", "debug", "blind", "event=", "version", "help"]
@@ -113,7 +113,7 @@ def allowConnectionTypes():
   (allowPortConnection, allowSocketConnection, allowDetachedStart)
   """
   
-  confKeys = util.conf.getConfig("arm").getKeys()
+  confKeys = stem.util.conf.get_config("arm").keys()
   
   isPortArgPresent = "startup.interface.ipAddress" in confKeys or "startup.interface.port" in confKeys
   isSocketArgPresent = "startup.interface.socket" in confKeys
@@ -242,12 +242,12 @@ def _dumpConfig():
   check that I didn't miss anything.
   """
   
-  config = util.conf.getConfig("arm")
+  config = stem.util.conf.get_config("arm")
   conn = util.torTools.getConn()
   
   # dumps arm's configuration
   armConfigEntry = ""
-  armConfigKeys = list(config.getKeys())
+  armConfigKeys = list(config.keys())
   armConfigKeys.sort()
   
   for configKey in armConfigKeys:
@@ -256,7 +256,7 @@ def _dumpConfig():
     # arm.
     
     if not configKey.startswith("config.summary.") and not configKey.startswith("torrc.") and not configKey.startswith("msg."):
-      armConfigEntry += "%s -> %s\n" % (configKey, config.contents[configKey])
+      armConfigEntry += "%s -> %s\n" % (configKey, config.get_value(configKey))
   
   if armConfigEntry: armConfigEntry = "Arm Configuration:\n%s" % armConfigEntry
   else: armConfigEntry = "Arm Configuration: None"
@@ -338,7 +338,7 @@ if __name__ == '__main__':
     except (OSError, IOError), exc:
       print "Unable to write to debug log file: %s" % util.sysTools.getFileErrorMsg(exc)
   
-  config = util.conf.getConfig("arm")
+  config = stem.util.conf.get_config("arm")
   
   # attempts to fetch attributes for parsing tor's logs, configuration, etc
   pathPrefix = os.path.dirname(sys.argv[0])
@@ -371,13 +371,6 @@ if __name__ == '__main__':
   if util.torTools.isTorRunning():
     config.set("features.allowDetachedStartup", "false")
   
-  # revises defaults to match user's configuration
-  config.update(CONFIG)
-  
-  # loads user preferences for utilities
-  for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.procTools, util.sysTools, util.torConfig, util.torTools, util.uiTools):
-    utilModule.loadConfig(config)
-  
   # syncs config and parameters, saving changed config options and overwriting
   # undefined parameters with defaults
   for key in param.keys():
@@ -435,17 +428,17 @@ if __name__ == '__main__':
     # (unfortunately python does allow for direct access to the memory so this
     # is the best we can do)
     del authPassword
-    if "startup.controlPassword" in config.contents:
-      del config.contents["startup.controlPassword"]
+    if "startup.controlPassword" in config._contents:
+      del config._contents["startup.controlPassword"]
       
       pwLineNum = None
-      for i in range(len(config.rawContents)):
-        if config.rawContents[i].strip().startswith("startup.controlPassword"):
+      for i in range(len(config._raw_contents)):
+        if config._raw_contents[i].strip().startswith("startup.controlPassword"):
           pwLineNum = i
           break
       
       if pwLineNum != None:
-        del config.rawContents[i]
+        del config._raw_contents[i]
   
   if controller is None and not allowDetachedStart: sys.exit(1)
   
diff --git a/src/util/conf.py b/src/util/conf.py
deleted file mode 100644
index a8ca6de..0000000
--- a/src/util/conf.py
+++ /dev/null
@@ -1,337 +0,0 @@
-"""
-This provides handlers for specially formatted configuration files. Entries are
-expected to consist of simple key/value pairs, and anything after "#" is
-stripped as a comment. Excess whitespace is trimmed and empty lines are
-ignored. For instance:
-# This is my sample config
-
-user.name Galen
-user.password yabba1234 # here's an inline comment
-user.notes takes a fancy to pepperjack chese
-blankEntry.example
-
-would be loaded as four entries (the last one's value being an empty string).
-If a key's defined multiple times then the last instance of it is used.
-"""
-
-import threading
-
-from util import log
-
-CONFS = {}  # mapping of identifier to singleton instances of configs
-CONFIG = {"log.configEntryNotFound": None,
-          "log.configEntryTypeError": log.NOTICE}
-
-def loadConfig(config):
-  config.update(CONFIG)
-
-def getConfig(handle):
-  """
-  Singleton constructor for configuration file instances. If a configuration
-  already exists for the handle then it's returned. Otherwise a fresh instance
-  is constructed.
-  
-  Arguments:
-    handle - unique identifier used to access this config instance
-  """
-  
-  if not handle in CONFS: CONFS[handle] = Config()
-  return CONFS[handle]
-
-class Config():
-  """
-  Handler for easily working with custom configurations, providing persistence
-  to and from files. All operations are thread safe.
-  
-  Parameters:
-    path        - location from which configurations are saved and loaded
-    contents    - mapping of current key/value pairs
-    rawContents - last read/written config (initialized to an empty string)
-  """
-  
-  def __init__(self):
-    """
-    Creates a new configuration instance.
-    """
-    
-    self.path = None        # location last loaded from
-    self.contents = {}      # configuration key/value pairs
-    self.contentsLock = threading.RLock()
-    self.requestedKeys = set()
-    self.rawContents = []   # raw contents read from configuration file
-  
-  def getValue(self, key, default=None, multiple=False):
-    """
-    This provides the currently value associated with a given key. If no such
-    key exists then this provides the default.
-    
-    Arguments:
-      key      - config setting to be fetched
-      default  - value provided if no such key exists
-      multiple - provides back a list of all values if true, otherwise this
-                 returns the last loaded configuration value
-    """
-    
-    self.contentsLock.acquire()
-    
-    if key in self.contents:
-      val = self.contents[key]
-      if not multiple: val = val[-1]
-      self.requestedKeys.add(key)
-    else:
-      msg = "config entry '%s' not found, defaulting to '%s'" % (key, str(default))
-      log.log(CONFIG["log.configEntryNotFound"], msg)
-      val = default
-    
-    self.contentsLock.release()
-    
-    return val
-  
-  def get(self, key, default=None):
-    """
-    Fetches the given configuration, using the key and default value to hint
-    the type it should be. Recognized types are:
-    - logging runlevel if key starts with "log."
-    - boolean if default is a boolean (valid values are 'true' and 'false',
-      anything else provides the default)
-    - integer or float if default is a number (provides default if fails to
-      cast)
-    - list of all defined values default is a list
-    - mapping of all defined values (key/value split via "=>") if the default
-      is a dict
-    
-    Arguments:
-      key      - config setting to be fetched
-      default  - value provided if no such key exists
-    """
-    
-    isMultivalue = isinstance(default, list) or isinstance(default, dict)
-    val = self.getValue(key, default, isMultivalue)
-    if val == default: return val
-    
-    if key.startswith("log."):
-      if val.upper() == "NONE": val = None
-      elif val.upper() in list(log.Runlevel): val = val.upper()
-      else:
-        msg = "Config entry '%s' is expected to be a runlevel" % key
-        if default != None: msg += ", defaulting to '%s'" % default
-        log.log(CONFIG["log.configEntryTypeError"], msg)
-        val = default
-    elif isinstance(default, bool):
-      if val.lower() == "true": val = True
-      elif val.lower() == "false": val = False
-      else:
-        msg = "Config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default))
-        log.log(CONFIG["log.configEntryTypeError"], msg)
-        val = default
-    elif isinstance(default, int):
-      try: val = int(val)
-      except ValueError:
-        msg = "Config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default)
-        log.log(CONFIG["log.configEntryTypeError"], msg)
-        val = default
-    elif isinstance(default, float):
-      try: val = float(val)
-      except ValueError:
-        msg = "Config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default)
-        log.log(CONFIG["log.configEntryTypeError"], msg)
-        val = default
-    elif isinstance(default, list):
-      pass # nothing special to do (already a list)
-    elif isinstance(default, dict):
-      valMap = {}
-      for entry in val:
-        if "=>" in entry:
-          entryKey, entryVal = entry.split("=>", 1)
-          valMap[entryKey.strip()] = entryVal.strip()
-        else:
-          msg = "Ignoring invalid %s config entry (expected a mapping, but \"%s\" was missing \"=>\")" % (key, entry)
-          log.log(CONFIG["log.configEntryTypeError"], msg)
-      val = valMap
-    
-    return val
-  
-  def getStrCSV(self, key, default = None, count = None):
-    """
-    Fetches the given key as a comma separated value. This provides back a list
-    with the stripped values.
-    
-    Arguments:
-      key     - config setting to be fetched
-      default - value provided if no such key exists or doesn't match the count
-      count   - if set, then a TypeError is logged (and default returned) if
-                the number of elements doesn't match the count
-    """
-    
-    confValue = self.getValue(key)
-    if confValue == None: return default
-    else:
-      confComp = [entry.strip() for entry in confValue.split(",")]
-      
-      # check if the count doesn't match
-      if count != None and len(confComp) != count:
-        msg = "Config entry '%s' is expected to be %i comma separated values" % (key, count)
-        if default != None and (isinstance(default, list) or isinstance(default, tuple)):
-          defaultStr = ", ".join([str(i) for i in default])
-          msg += ", defaulting to '%s'" % defaultStr
-        
-        log.log(CONFIG["log.configEntryTypeError"], msg)
-        return default
-      
-      return confComp
-  
-  def getIntCSV(self, key, default = None, count = None, minValue = None, maxValue = None):
-    """
-    Fetches the given comma separated value, logging a TypeError (and returning
-    the default) if the values arne't ints or aren't constrained to the given
-    bounds.
-    
-    Arguments:
-      key      - config setting to be fetched
-      default  - value provided if no such key exists, doesn't match the count,
-                 values aren't all integers, or doesn't match the bounds
-      count    - checks that the number of values matches this if set
-      minValue - checks that all values are over this if set
-      maxValue - checks that all values are less than this if set
-    """
-    
-    confComp = self.getStrCSV(key, default, count)
-    if confComp == default: return default
-    
-    # validates the input, setting the errorMsg if there's a problem
-    errorMsg = None
-    baseErrorMsg = "Config entry '%s' is expected to %%s" % key
-    if default != None and (isinstance(default, list) or isinstance(default, tuple)):
-      defaultStr = ", ".join([str(i) for i in default])
-      baseErrorMsg += ", defaulting to '%s'" % defaultStr
-    
-    for val in confComp:
-      if not val.isdigit():
-        errorMsg = baseErrorMsg % "only have integer values"
-        break
-      else:
-        if minValue != None and int(val) < minValue:
-          errorMsg = baseErrorMsg % "only have values over %i" % minValue
-          break
-        elif maxValue != None and int(val) > maxValue:
-          errorMsg = baseErrorMsg % "only have values less than %i" % maxValue
-          break
-    
-    if errorMsg:
-      log.log(CONFIG["log.configEntryTypeError"], errorMsg)
-      return default
-    else: return [int(val) for val in confComp]
-
-  def update(self, confMappings, limits = {}):
-    """
-    Revises a set of key/value mappings to reflect the current configuration.
-    Undefined values are left with their current values.
-    
-    Arguments:
-      confMappings - configuration key/value mappings to be revised
-      limits       - mappings of limits on numeric values, expected to be of
-                     the form "configKey -> min" or "configKey -> (min, max)"
-    """
-    
-    for entry in confMappings.keys():
-      val = self.get(entry, confMappings[entry])
-      
-      if entry in limits and (isinstance(val, int) or isinstance(val, float)):
-        if isinstance(limits[entry], tuple):
-          val = max(val, limits[entry][0])
-          val = min(val, limits[entry][1])
-        else: val = max(val, limits[entry])
-      
-      confMappings[entry] = val
-  
-  def getKeys(self):
-    """
-    Provides all keys in the currently loaded configuration.
-    """
-    
-    return self.contents.keys()
-  
-  def getUnusedKeys(self):
-    """
-    Provides the set of keys that have never been requested.
-    """
-    
-    return set(self.getKeys()).difference(self.requestedKeys)
-  
-  def set(self, key, value):
-    """
-    Stores the given configuration value.
-    
-    Arguments:
-      key   - config key to be set
-      value - config value to be set
-    """
-    
-    self.contentsLock.acquire()
-    self.contents[key] = [value]
-    self.contentsLock.release()
-  
-  def clear(self):
-    """
-    Drops all current key/value mappings.
-    """
-    
-    self.contentsLock.acquire()
-    self.contents.clear()
-    self.contentsLock.release()
-  
-  def load(self, path):
-    """
-    Reads in the contents of the given path, adding its configuration values
-    and overwriting any that already exist. If the file's empty then this
-    doesn't do anything. Other issues (like having insufficient permissions or
-    if the file doesn't exist) result in an IOError.
-    
-    Arguments:
-      path - file path to be loaded
-    """
-    
-    configFile = open(path, "r")
-    self.rawContents = configFile.readlines()
-    configFile.close()
-    
-    self.contentsLock.acquire()
-    
-    for line in self.rawContents:
-      # strips any commenting or excess whitespace
-      commentStart = line.find("#")
-      if commentStart != -1: line = line[:commentStart]
-      line = line.strip()
-      
-      # parse the key/value pair
-      if line and " " in line:
-        key, value = line.split(" ", 1)
-        value = value.strip()
-        
-        if key in self.contents: self.contents[key].append(value)
-        else: self.contents[key] = [value]
-    
-    self.path = path
-    self.contentsLock.release()
-  
-  def save(self, saveBackup=True):
-    """
-    Writes the contents of the current configuration. If a configuration file
-    already exists then merges as follows:
-    - comments and file contents not in this config are left unchanged
-    - lines with duplicate keys are stripped (first instance is kept)
-    - existing entries are overwritten with their new values, preserving the
-      positioning of in-line comments if able
-    - config entries not in the file are appended to the end in alphabetical
-      order
-    
-    If problems arise in writing (such as an unset path or insufficient
-    permissions) result in an IOError.
-    
-    Arguments:
-      saveBackup - if true and a file already exists then it's saved (with
-                   '.backup' appended to its filename)
-    """
-    
-    pass # TODO: implement when persistence is needed
-
diff --git a/src/util/connections.py b/src/util/connections.py
index de128c2..5b3c9bb 100644
--- a/src/util/connections.py
+++ b/src/util/connections.py
@@ -23,7 +23,7 @@ import threading
 
 from util import log, procTools, sysTools
 
-from stem.util import enum
+from stem.util import conf, enum
 
 # enums for connection resolution utilities
 Resolver = enum.Enum(("PROC", "proc"),
@@ -76,45 +76,43 @@ RESOLVERS = []                      # connection resolvers available via the sin
 RESOLVER_FAILURE_TOLERANCE = 3      # number of subsequent failures before moving on to another resolver
 RESOLVER_SERIAL_FAILURE_MSG = "Unable to query connections with %s, trying %s"
 RESOLVER_FINAL_FAILURE_MSG = "All connection resolvers failed"
-CONFIG = {"queries.connections.minRate": 5,
-          "log.connResolverOptions": log.INFO,
-          "log.connLookupFailed": log.INFO,
-          "log.connLookupFailover": log.NOTICE,
-          "log.connLookupAbandon": log.NOTICE,
-          "log.connLookupRateGrowing": None,
-          "log.configEntryTypeError": log.NOTICE}
 
-PORT_USAGE = {}
-
-def loadConfig(config):
-  config.update(CONFIG)
-  
-  for configKey in config.getKeys():
-    # fetches any port.label.* values
-    if configKey.startswith("port.label."):
-      portEntry = configKey[11:]
-      purpose = config.get(configKey)
-      
-      divIndex = portEntry.find("-")
-      if divIndex == -1:
-        # single port
-        if portEntry.isdigit():
-          PORT_USAGE[portEntry] = purpose
-        else:
-          msg = "Port value isn't numeric for entry: %s" % configKey
-          log.log(CONFIG["log.configEntryTypeError"], msg)
+def conf_handler(key, value):
+  if key.startswith("port.label."):
+    portEntry = key[11:]
+    
+    divIndex = portEntry.find("-")
+    if divIndex == -1:
+      # single port
+      if portEntry.isdigit():
+        PORT_USAGE[portEntry] = value
       else:
-        try:
-          # range of ports (inclusive)
-          minPort = int(portEntry[:divIndex])
-          maxPort = int(portEntry[divIndex + 1:])
-          if minPort > maxPort: raise ValueError()
-          
-          for port in range(minPort, maxPort + 1):
-            PORT_USAGE[str(port)] = purpose
-        except ValueError:
-          msg = "Unable to parse port range for entry: %s" % configKey
-          log.log(CONFIG["log.configEntryTypeError"], msg)
+        msg = "Port value isn't numeric for entry: %s" % key
+        log.log(CONFIG["log.configEntryTypeError"], msg)
+    else:
+      try:
+        # range of ports (inclusive)
+        minPort = int(portEntry[:divIndex])
+        maxPort = int(portEntry[divIndex + 1:])
+        if minPort > maxPort: raise ValueError()
+        
+        for port in range(minPort, maxPort + 1):
+          PORT_USAGE[str(port)] = value
+      except ValueError:
+        msg = "Unable to parse port range for entry: %s" % key
+        log.log(CONFIG["log.configEntryTypeError"], msg)
+
+CONFIG = conf.config_dict("arm", {
+  "queries.connections.minRate": 5,
+  "log.connResolverOptions": log.INFO,
+  "log.connLookupFailed": log.INFO,
+  "log.connLookupFailover": log.NOTICE,
+  "log.connLookupAbandon": log.NOTICE,
+  "log.connLookupRateGrowing": None,
+  "log.configEntryTypeError": log.NOTICE,
+}, conf_handler)
+
+PORT_USAGE = {}
 
 def isValidIpAddress(ipStr):
   """
diff --git a/src/util/hostnames.py b/src/util/hostnames.py
index 9ae856a..d12f5cb 100644
--- a/src/util/hostnames.py
+++ b/src/util/hostnames.py
@@ -34,25 +34,31 @@ import distutils.sysconfig
 
 from util import log, sysTools
 
+from stem.util import conf
+
 RESOLVER = None                       # hostname resolver (service is stopped if None)
 RESOLVER_LOCK = threading.RLock()     # regulates assignment to the RESOLVER
 RESOLVER_COUNTER = itertools.count()  # atomic counter, providing the age for new entries (for trimming)
 DNS_ERROR_CODES = ("1(FORMERR)", "2(SERVFAIL)", "3(NXDOMAIN)", "4(NOTIMP)", "5(REFUSED)", "6(YXDOMAIN)",
                    "7(YXRRSET)", "8(NXRRSET)", "9(NOTAUTH)", "10(NOTZONE)", "16(BADVERS)")
 
-CONFIG = {"queries.hostnames.poolSize": 5,
-          "queries.hostnames.useSocketModule": False,
-          "cache.hostnames.size": 700000,
-          "cache.hostnames.trimSize": 200000,
-          "log.hostnameCacheTrimmed": log.INFO}
+def conf_handler(key, value):
+  if key == "queries.hostnames.poolSize":
+    return max(1, value)
+  elif key == "cache.hostnames.size":
+    return max(100, value)
+  elif key == "cache.hostnames.trimSize":
+    return max(10, value)
+  elif key == "cache.hostnames.trimSize":
+    return min(value, CONFIG["cache.hostnames.size"] / 2)
 
-def loadConfig(config):
-  config.update(CONFIG, {
-    "queries.hostnames.poolSize": 1,
-    "cache.hostnames.size": 100,
-    "cache.hostnames.trimSize": 10})
-  
-  CONFIG["cache.hostnames.trimSize"] = min(CONFIG["cache.hostnames.trimSize"], CONFIG["cache.hostnames.size"] / 2)
+CONFIG = conf.config_dict("arm", {
+  "queries.hostnames.poolSize": 5,
+  "queries.hostnames.useSocketModule": False,
+  "cache.hostnames.size": 700000,
+  "cache.hostnames.trimSize": 200000,
+  "log.hostnameCacheTrimmed": log.INFO,
+}, conf_handler)
 
 def start():
   """
diff --git a/src/util/log.py b/src/util/log.py
index 56a6636..60572d5 100644
--- a/src/util/log.py
+++ b/src/util/log.py
@@ -11,7 +11,7 @@ import time
 from sys import maxint
 from threading import RLock
 
-from stem.util import enum
+from stem.util import conf, enum
 
 # Logging runlevels. These are *very* commonly used so including shorter
 # aliases (so they can be referenced as log.DEBUG, log.WARN, etc).
@@ -28,17 +28,20 @@ _backlog = dict([(level, []) for level in Runlevel])
 # mapping of runlevels to the listeners interested in receiving events from it
 _listeners = dict([(level, []) for level in Runlevel])
 
-CONFIG = {"cache.armLog.size": 1000,
-          "cache.armLog.trimSize": 200}
+def conf_handler(key, value):
+  if key == "cache.armLog.size":
+    return max(10, value)
+  elif key == "cache.armLog.trimSize":
+    return max(5, value)
+  elif key == "cache.armLog.trimSize":
+    return min(value, CONFIG["cache.armLog.size"] / 2)
 
-DUMP_FILE = None
+CONFIG = conf.config_dict("arm", {
+  "cache.armLog.size": 1000,
+  "cache.armLog.trimSize": 200,
+}, conf_handler)
 
-def loadConfig(config):
-  config.update(CONFIG, {
-    "cache.armLog.size": 10,
-    "cache.armLog.trimSize": 5})
-  
-  CONFIG["cache.armLog.trimSize"] = min(CONFIG["cache.armLog.trimSize"], CONFIG["cache.armLog.size"] / 2)
+DUMP_FILE = None
 
 def setDumpFile(logPath):
   """
diff --git a/src/util/panel.py b/src/util/panel.py
index 50ae2c0..6f69881 100644
--- a/src/util/panel.py
+++ b/src/util/panel.py
@@ -11,6 +11,8 @@ from threading import RLock
 
 from util import log, textInput, uiTools
 
+from stem.util import conf
+
 # global ui lock governing all panel instances (curses isn't thread save and 
 # concurrency bugs produce especially sinister glitches)
 CURSES_LOCK = RLock()
@@ -23,14 +25,13 @@ FORMAT_TAGS = {"<b>": (_noOp, curses.A_BOLD),
                "<h>": (_noOp, curses.A_STANDOUT)}
 for colorLabel in uiTools.COLOR_LIST: FORMAT_TAGS["<%s>" % colorLabel] = (uiTools.getColor, colorLabel)
 
-CONFIG = {"log.panelRecreated": log.DEBUG}
+CONFIG = conf.config_dict("arm", {
+  "log.panelRecreated": log.DEBUG,
+})
 
 # prevents curses redraws if set
 HALT_ACTIVITY = False
 
-def loadConfig(config):
-  config.update(CONFIG)
-
 class Panel():
   """
   Wrapper for curses subwindows. This hides most of the ugliness in common
diff --git a/src/util/procTools.py b/src/util/procTools.py
index 835814e..6f3c31a 100644
--- a/src/util/procTools.py
+++ b/src/util/procTools.py
@@ -22,18 +22,17 @@ import base64
 
 from util import log
 
-from stem.util import enum
+from stem.util import conf, enum
 
 # cached system values
 SYS_START_TIME, SYS_PHYSICAL_MEMORY = None, None
 CLOCK_TICKS = os.sysconf(os.sysconf_names["SC_CLK_TCK"])
 Stat = enum.Enum("COMMAND", "CPU_UTIME", "CPU_STIME", "START_TIME")
 
-CONFIG = {"queries.useProc": True,
-          "log.procCallMade": log.DEBUG}
-
-def loadConfig(config):
-  config.update(CONFIG)
+CONFIG = conf.config_dict("arm", {
+  "queries.useProc": True,
+  "log.procCallMade": log.DEBUG,
+})
 
 def isProcAvailable():
   """
diff --git a/src/util/sysTools.py b/src/util/sysTools.py
index 1c84462..e8acf86 100644
--- a/src/util/sysTools.py
+++ b/src/util/sysTools.py
@@ -8,6 +8,8 @@ import threading
 
 from util import log, procTools, uiTools
 
+from stem.util import conf
+
 # Mapping of commands to if they're available or not. This isn't always
 # reliable, failing for some special commands. For these the cache is
 # prepopulated to skip lookups.
@@ -28,18 +30,17 @@ RESOURCE_TRACKERS = {}  # mapping of pids to their resource tracker instances
 RUNTIMES = []
 SAMPLING_PERIOD = 5 # time of the sampling period
 
-CONFIG = {"queries.resourceUsage.rate": 5,
-          "cache.sysCalls.size": 600,
-          "log.sysCallMade": log.DEBUG,
-          "log.sysCallCached": None,
-          "log.sysCallFailed": log.INFO,
-          "log.sysCallCacheGrowing": log.INFO,
-          "log.stats.failedProcResolution": log.DEBUG,
-          "log.stats.procResolutionFailover": log.INFO,
-          "log.stats.failedPsResolution": log.INFO}
-
-def loadConfig(config):
-  config.update(CONFIG)
+CONFIG = conf.config_dict("arm", {
+  "queries.resourceUsage.rate": 5,
+  "cache.sysCalls.size": 600,
+  "log.sysCallMade": log.DEBUG,
+  "log.sysCallCached": None,
+  "log.sysCallFailed": log.INFO,
+  "log.sysCallCacheGrowing": log.INFO,
+  "log.stats.failedProcResolution": log.DEBUG,
+  "log.stats.procResolutionFailover": log.INFO,
+  "log.stats.failedPsResolution": log.INFO,
+})
 
 def getSysCpuUsage():
   """
diff --git a/src/util/torConfig.py b/src/util/torConfig.py
index 5566e9d..27636b3 100644
--- a/src/util/torConfig.py
+++ b/src/util/torConfig.py
@@ -11,25 +11,38 @@ import stem.version
 
 from util import log, sysTools, torTools, uiTools
 
-from stem.util import enum
+from stem.util import conf, enum
 
-CONFIG = {"features.torrc.validate": True,
-          "config.important": [],
-          "torrc.alias": {},
-          "torrc.label.size.b": [],
-          "torrc.label.size.kb": [],
-          "torrc.label.size.mb": [],
-          "torrc.label.size.gb": [],
-          "torrc.label.size.tb": [],
-          "torrc.label.time.sec": [],
-          "torrc.label.time.min": [],
-          "torrc.label.time.hour": [],
-          "torrc.label.time.day": [],
-          "torrc.label.time.week": [],
-          "log.torrc.readFailed": log.WARN,
-          "log.configDescriptions.unrecognizedCategory": log.NOTICE,
-          "log.torrc.validation.unnecessaryTorrcEntries": log.NOTICE,
-          "log.torrc.validation.torStateDiffers": log.WARN}
+def conf_handler(key, value):
+  if key == "config.important":
+    # stores lowercase entries to drop case sensitivity
+    return [entry.lower() for entry in value]
+  elif key.startswith("config.summary."):
+    # we'll look for summary keys with a lowercase config name
+    CONFIG[key.lower()] = value
+  elif key.startswith("torrc.label.") and value:
+    # all the torrc.label.* values are comma separated lists
+    return [entry.strip() for entry in value[0].split(",")]
+
+CONFIG = conf.config_dict("arm", {
+  "features.torrc.validate": True,
+  "config.important": [],
+  "torrc.alias": {},
+  "torrc.label.size.b": [],
+  "torrc.label.size.kb": [],
+  "torrc.label.size.mb": [],
+  "torrc.label.size.gb": [],
+  "torrc.label.size.tb": [],
+  "torrc.label.time.sec": [],
+  "torrc.label.time.min": [],
+  "torrc.label.time.hour": [],
+  "torrc.label.time.day": [],
+  "torrc.label.time.week": [],
+  "log.torrc.readFailed": log.WARN,
+  "log.configDescriptions.unrecognizedCategory": log.NOTICE,
+  "log.torrc.validation.unnecessaryTorrcEntries": log.NOTICE,
+  "log.torrc.validation.torStateDiffers": log.WARN,
+}, conf_handler)
 
 # enums and values for numeric torrc entries
 ValueType = enum.Enum("UNRECOGNIZED", "SIZE", "TIME")
@@ -59,23 +72,6 @@ MULTILINE_PARAM = None # cached multiline parameters (lazily loaded)
 # torrc options that bind to ports
 PORT_OPT = ("SocksPort", "ORPort", "DirPort", "ControlPort", "TransPort")
 
-def loadConfig(config):
-  config.update(CONFIG)
-  
-  # stores lowercase entries to drop case sensitivity
-  CONFIG["config.important"] = [entry.lower() for entry in CONFIG["config.important"]]
-  
-  for configKey in config.getKeys():
-    # fetches any config.summary.* values
-    if configKey.startswith("config.summary."):
-      CONFIG[configKey.lower()] = config.get(configKey)
-  
-  # all the torrc.label.* values are comma separated lists
-  for configKey in CONFIG.keys():
-    if configKey.startswith("torrc.label."):
-      configValues = config.get(configKey, "").split(",")
-      if configValues: CONFIG[configKey] = [val.strip() for val in configValues]
-
 class ManPageEntry:
   """
   Information provided about a tor configuration option in its man page entry.
diff --git a/src/util/torTools.py b/src/util/torTools.py
index 7aea8fa..ff85496 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -18,7 +18,7 @@ import stem.descriptor
 
 from util import connections, log, procTools, sysTools, uiTools
 
-from stem.util import enum
+from stem.util import conf, enum
 
 # enums for tor's controller state:
 # INIT - attached to a new controller
@@ -46,11 +46,14 @@ CONTROLLER = None # singleton Controller instance
 UNDEFINED = "<Undefined_ >"
 
 UNKNOWN = "UNKNOWN" # value used by cached information if undefined
-CONFIG = {"features.pathPrefix": "",
-          "log.stemPortClosed": log.NOTICE,
-          "log.torPrefixPathInvalid": log.NOTICE,
-          "log.bsdJailFound": log.INFO,
-          "log.unknownBsdJailId": log.WARN}
+
+CONFIG = conf.config_dict("arm", {
+  "features.pathPrefix": "",
+  "log.stemPortClosed": log.NOTICE,
+  "log.torPrefixPathInvalid": log.NOTICE,
+  "log.bsdJailFound": log.INFO,
+  "log.unknownBsdJailId": log.WARN,
+})
 
 # events used for controller functionality:
 # NOTICE - used to detect when tor is shut down
@@ -73,9 +76,6 @@ NO_SPAWN = False
 # startup performance so we don't introduce a sleep while initializing.
 IS_STARTUP_SIGNAL = True
 
-def loadConfig(config):
-  config.update(CONFIG)
-
 def getPid(controlPort=9051, pidFilePath=None):
   """
   Attempts to determine the process id for a running tor process, using the
diff --git a/src/util/uiTools.py b/src/util/uiTools.py
index c1f1a86..10f922b 100644
--- a/src/util/uiTools.py
+++ b/src/util/uiTools.py
@@ -12,7 +12,7 @@ import curses
 from curses.ascii import isprint
 from util import log
 
-from stem.util import enum
+from stem.util import conf, enum
 
 # colors curses can handle
 COLOR_LIST = {"red": curses.COLOR_RED,        "green": curses.COLOR_GREEN,
@@ -40,27 +40,26 @@ TIME_UNITS = [(86400.0, "d", " day"), (3600.0, "h", " hour"),
 
 Ending = enum.Enum("ELLIPSE", "HYPHEN")
 SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END)
-CONFIG = {"features.colorInterface": True,
-          "features.acsSupport": True,
-          "features.printUnicode": True,
-          "log.cursesColorSupport": log.INFO,
-          "log.configEntryTypeError": log.NOTICE}
+
+def conf_handler(key, value):
+  if key == "features.colorOverride" and value != "none":
+    try: setColorOverride(value)
+    except ValueError, exc:
+      log.log(CONFIG["log.configEntryTypeError"], exc)
+
+CONFIG = conf.config_dict("arm", {
+  "features.colorOverride": "none",
+  "features.colorInterface": True,
+  "features.acsSupport": True,
+  "features.printUnicode": True,
+  "log.cursesColorSupport": log.INFO,
+  "log.configEntryTypeError": log.NOTICE,
+}, conf_handler)
 
 # Flag indicating if unicode is supported by curses. If None then this has yet
 # to be determined.
 IS_UNICODE_SUPPORTED = None
 
-def loadConfig(config):
-  config.update(CONFIG)
-  
-  CONFIG["features.colorOverride"] = "none"
-  colorOverride = config.get("features.colorOverride", "none")
-  
-  if colorOverride != "none":
-    try: setColorOverride(colorOverride)
-    except ValueError, exc:
-      log.log(CONFIG["log.configEntryTypeError"], exc)
-
 def demoGlyphs():
   """
   Displays all ACS options with their corresponding representation. These are



More information about the tor-commits mailing list