[tor-commits] [arm/master] Renaming project to 'seth'

atagar at torproject.org atagar at torproject.org
Sun Mar 15 03:36:33 UTC 2015


commit c636ecfe5f585d79e4822faf6a5c211084057e23
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Mar 14 17:07:48 2015 -0700

    Renaming project to 'seth'
    
    As discussed on tor-dev@ I'm scheming to rename this project from arm to
    seth...
    
      https://lists.torproject.org/pipermail/tor-dev/2015-March/008398.html
    
    I'm taking this slow with the plan to wait another week before renaming the
    repository, but I can move forward this much to get a feel of working with the
    new name. We can revert later this week if we find a better name.
---
 ChangeLog                               |   86 +-
 README                                  |   41 +-
 arm/__init__.py                         |   17 -
 arm/arguments.py                        |  254 ------
 arm/config/attributes.cfg               |   41 -
 arm/config/dedup.cfg                    |  107 ---
 arm/config/strings.cfg                  |  100 ---
 arm/config/torrc.cfg                    |  313 -------
 arm/config_panel.py                     |  721 ----------------
 arm/connections/__init__.py             |   12 -
 arm/connections/circ_entry.py           |  253 ------
 arm/connections/conn_entry.py           | 1258 ----------------------------
 arm/connections/conn_panel.py           |  674 ---------------
 arm/connections/count_popup.py          |  111 ---
 arm/connections/descriptor_popup.py     |  273 ------
 arm/connections/entries.py              |  179 ----
 arm/controller.py                       |  657 ---------------
 arm/demo_glyphs.py                      |   66 --
 arm/graph_panel.py                      |  734 -----------------
 arm/header_panel.py                     |  480 -----------
 arm/log_panel.py                        | 1369 -------------------------------
 arm/menu/__init__.py                    |    9 -
 arm/menu/actions.py                     |  327 --------
 arm/menu/item.py                        |  207 -----
 arm/menu/menu.py                        |  192 -----
 arm/popups.py                           |  392 ---------
 arm/resources/arm.1                     |   69 --
 arm/resources/tor-arm.desktop           |   12 -
 arm/resources/tor-arm.svg               | 1074 ------------------------
 arm/resources/torConfigDesc.txt         | 1123 -------------------------
 arm/starter.py                          |  297 -------
 arm/torrc_panel.py                      |  351 --------
 arm/uninstall                           |   16 -
 arm/util/__init__.py                    |  203 -----
 arm/util/log.py                         |   44 -
 arm/util/panel.py                       |  864 -------------------
 arm/util/text_input.py                  |  213 -----
 arm/util/tor_config.py                  | 1116 -------------------------
 arm/util/tracker.py                     |  666 ---------------
 arm/util/ui_tools.py                    |  400 ---------
 armrc.sample                            |  244 ------
 install                                 |    2 +-
 run_arm                                 |   47 --
 run_seth                                |   47 ++
 run_tests.py                            |    8 +-
 seth/__init__.py                        |   17 +
 seth/arguments.py                       |  254 ++++++
 seth/config/attributes.cfg              |   41 +
 seth/config/dedup.cfg                   |  107 +++
 seth/config/strings.cfg                 |  100 +++
 seth/config/torrc.cfg                   |  313 +++++++
 seth/config_panel.py                    |  721 ++++++++++++++++
 seth/connections/__init__.py            |   12 +
 seth/connections/circ_entry.py          |  253 ++++++
 seth/connections/conn_entry.py          | 1258 ++++++++++++++++++++++++++++
 seth/connections/conn_panel.py          |  674 +++++++++++++++
 seth/connections/count_popup.py         |  111 +++
 seth/connections/descriptor_popup.py    |  273 ++++++
 seth/connections/entries.py             |  179 ++++
 seth/controller.py                      |  657 +++++++++++++++
 seth/demo_glyphs.py                     |   66 ++
 seth/graph_panel.py                     |  734 +++++++++++++++++
 seth/header_panel.py                    |  480 +++++++++++
 seth/log_panel.py                       | 1369 +++++++++++++++++++++++++++++++
 seth/menu/__init__.py                   |    9 +
 seth/menu/actions.py                    |  327 ++++++++
 seth/menu/item.py                       |  207 +++++
 seth/menu/menu.py                       |  192 +++++
 seth/popups.py                          |  392 +++++++++
 seth/resources/arm.1                    |   69 ++
 seth/resources/tor-arm.desktop          |   12 +
 seth/resources/tor-arm.svg              | 1074 ++++++++++++++++++++++++
 seth/resources/torConfigDesc.txt        | 1123 +++++++++++++++++++++++++
 seth/starter.py                         |  297 +++++++
 seth/torrc_panel.py                     |  351 ++++++++
 seth/uninstall                          |   16 +
 seth/util/__init__.py                   |  203 +++++
 seth/util/log.py                        |   44 +
 seth/util/panel.py                      |  864 +++++++++++++++++++
 seth/util/text_input.py                 |  213 +++++
 seth/util/tor_config.py                 | 1116 +++++++++++++++++++++++++
 seth/util/tracker.py                    |  666 +++++++++++++++
 seth/util/ui_tools.py                   |  400 +++++++++
 sethrc.sample                           |  244 ++++++
 setup.py                                |   40 +-
 test/__init__.py                        |    2 +-
 test/arguments.py                       |    8 +-
 test/settings.cfg                       |    4 +-
 test/util/__init__.py                   |    2 +-
 test/util/bandwidth_from_state.py       |   18 +-
 test/util/tracker/__init__.py           |    2 +-
 test/util/tracker/connection_tracker.py |   18 +-
 test/util/tracker/daemon.py             |   22 +-
 test/util/tracker/port_usage_tracker.py |   18 +-
 test/util/tracker/resource_tracker.py   |   38 +-
 95 files changed, 15639 insertions(+), 15640 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 072be66..ebd8f69 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,9 +1,9 @@
 CHANGE LOG
 
 4/28/12 - version 1.4.5 (e249dc8)
-Software isn't perfect and arm is no exception. This is a bugfix release that corrects most issues that users have reported over the last several months. This does not include any new features, but includes changes that are important for continued interoperability with tor.
+Software isn't perfect and seth is no exception. This is a bugfix release that corrects most issues that users have reported over the last several months. This does not include any new features, but includes changes that are important for continued interoperability with tor.
 
-    * fix: unrecognized authentication methods (like 'SAFECOOKIE') would make arm crash (caught by E)
+    * fix: unrecognized authentication methods (like 'SAFECOOKIE') would make seth crash (caught by E)
     * fix: providing users a log message when DisableDebuggerAttachment breaks us
     * fix: crashing issue when parsing tor log entries from leap years (fix by Sebastian, https://trac.torproject.org/projects/tor/ticket/5265)
     * fix: major terminal glitches related to the import of the readline module (caught by Stephan Seitz)
@@ -38,21 +38,21 @@ Besides the normal bug fixes and minor features, this release introduces the con
     * added: optional system wide torrc integration (thanks to ioerror, https://trac.torproject.org/projects/tor/ticket/3629)
     * added: dialog for guards, bridges, and exits with exit port usage and client locales (feature request by waltman and ioerror)
     * added: configuration editor for the gui interface (implemented by krkhan)
-    * added: notice that warns the user against running tor or arm as root
+    * added: notice that warns the user against running tor or seth as root
     * change: doing a manual redraw when the user presses ctrl+L (https://trac.torproject.org/projects/tor/ticket/2830)
     * change: moving download location to archive.torproject.org so downloads will have ssl (suggested by arma)
     * change: rewrite of the descriptor popup, cleaning up the code and minor performance improvements
-    * fix: preventing arm from starting if there's a running tor instance that we can't connect to
+    * fix: preventing seth from starting if there's a running tor instance that we can't connect to
     * fix: when the config-text GETINFO option was unavailable we'd write a blank torrc (caught by Runa)
     * fix: appending path prefix to auth cookie path (caught by sid77)
     * fix: skipping log parsing if misformatted (caught by Sjon)
-    * fix: incorrect armrc path in man page (caught by dererk)
+    * fix: incorrect sethrc path in man page (caught by dererk)
     * fix: trying all authentication methods rather than just the first (caught by arma, https://trac.torproject.org/projects/tor/ticket/3958)
     * fix: toning down the warning when all connection resolvers fail (suggested by Sebastian)
     * fix: quitting wizard when the user presses 'q' instead of just esc (suggested by monochromec, https://trac.torproject.org/projects/tor/ticket/3995)
     * fix: quitting could cause unclean curses shutdown
     * fix: relay address fetching would ignore default values at shutdown, causing a stacktrace
-    * fix: concurrency bug could crash arm if a CIRC event occurs while caching attached relays
+    * fix: concurrency bug could crash seth if a CIRC event occurs while caching attached relays
     * fix: crashing issue from use of an uninitialized buffer when paused with accounting stats
     * fix: periodically redrawing content to prevent weird terminal glitches from persisting
     * fix: pressing 'enter' on config panel when never attached to tor would crash
@@ -70,7 +70,7 @@ Besides the normal bug fixes and minor features, this release introduces the con
 This completes the codebase refactoring project that's been a year in the works and provides numerous performance and usability improvements. Most notably this includes a setup wizard for new relays, menuing interface, gui prototype, substantial performance improvements, and support for Mac OSX.
 
     * added: relay setup wizard for autogenerating a torrc configuration
-    * added: menu interface with all of arm's functionality (thanks to help from krkhan)
+    * added: menu interface with all of seth's functionality (thanks to help from krkhan)
     * added: option for reconnecting to tor if it's been shut down and restarted
     * added: fetching the download and upload bandwidth totals from Tor if it's available
     * added: option for requesting a new identity
@@ -78,10 +78,10 @@ This completes the codebase refactoring project that's been a year in the works
     * added: logging a notice when file descriptor usage is high
     * added: making interface components optionally excludeable
     * added: reintroduced the descriptor popup for the new connection panel
-    * added: moved arm's codebase to git, with helper scripts to replace svn:externals and export
+    * added: moved seth's codebase to git, with helper scripts to replace svn:externals and export
     * added: option for overriding all displayed color
-    * change: renaming our process from "python src/starter.py <args>" to "arm <args>" (thanks to help from ioerror)
-    * change: hiding connection init latency, dropping arm's startup time from 0.84 seconds to 0.14 (83% improvement)
+    * change: renaming our process from "python src/starter.py <args>" to "seth <args>" (thanks to help from ioerror)
+    * change: hiding connection init latency, dropping seth's startup time from 0.84 seconds to 0.14 (83% improvement)
     * change: using blank space to display the nickname for circuit connections
     * change: dropping the file descriptor popup (it was both unused and inaccurate)
     * change: dropping deprecated connection panel from the codebase
@@ -89,13 +89,13 @@ This completes the codebase refactoring project that's been a year in the works
     * change: closing all message prompts when a key is pressed
     * change: using a more intuitive mode toggling for resizing the graph
     * change: added summaries for new tor config options
-    * fix: avoiding mass memory allocation in torctl, lowering arm's perceived base memory footprint by 2.5 MB (12%) (https://trac.torproject.org/projects/tor/ticket/3406)
-    * fix: fixing shutdown concurrency in torctl, lowering arm's shutdown time from a second to being almost instantaneous (https://trac.torproject.org/projects/tor/ticket/2412)
-    * fix: sighups that caused tor to crash would also crash torctl and arm (https://trac.torproject.org/projects/tor/ticket/1329)
+    * fix: avoiding mass memory allocation in torctl, lowering seth's perceived base memory footprint by 2.5 MB (12%) (https://trac.torproject.org/projects/tor/ticket/3406)
+    * fix: fixing shutdown concurrency in torctl, lowering seth's shutdown time from a second to being almost instantaneous (https://trac.torproject.org/projects/tor/ticket/2412)
+    * fix: sighups that caused tor to crash would also crash torctl and seth (https://trac.torproject.org/projects/tor/ticket/1329)
     * fix: crash when connecting to tor's socks port rather than control port (https://trac.torproject.org/projects/tor/ticket/2580)
     * fix: shutting down torctl zombie connections to the control port (https://trac.torproject.org/projects/tor/ticket/2812)
     * fix: moving connection component negotiation into torctl (https://trac.torproject.org/projects/tor/ticket/3409)
-    * fix: all pid resolution was failing on macs, causing much of arm's functionality to fail
+    * fix: all pid resolution was failing on macs, causing much of seth's functionality to fail
     * fix: dropping straight to lsof on macs to avoid fallback process, which was both lengthy and raised warnings
     * fix: misparsing circuit paths for Tor < 0.2.2.1 (caught by asn)
     * fix: pressing enter on an empty connection page caused crash (caught by asn, https://trac.torproject.org/projects/tor/ticket/3128)
@@ -124,11 +124,11 @@ This release chiefly consists of a fully reimplemented connection panel. Besides
 
     * added: full rewrite of the connection panel, providing:
           o listing the full paths involved in active circuits
-          o identification of socks, hidden service, and controller applications (arm, vidalia, polipo, etc)
+          o identification of socks, hidden service, and controller applications (seth, vidalia, polipo, etc)
           o identification of exit connections with the common usage for the port they're using
           o display of the local -> internal -> external address when room is available (original patch by Fabian Keil)
           o better accuracy and performance in identifying client and directory connections
-          o marking the uptimes for initial connections (arm only tracks connection uptimes since starting, so these entries are just minimum durations)
+          o marking the uptimes for initial connections (seth only tracks connection uptimes since starting, so these entries are just minimum durations)
           o lazily loading the initial IP -> fingerprint mappings to improve the startup time
           o using the circuit-status to disambiguating multiple relays on the same IP address
           o smarter space utilization, filling in smaller columns if there isn't room for higher priority but larger entries
@@ -173,20 +173,20 @@ This release chiefly consists of a fully reimplemented connection panel. Besides
 1/7/11 - version 1.4.1 (r24054)
 Platform specific enhancements including BSD compatibility and vastly improved performance on Linux.
 
-    * added: querying the proc contents when able for tor resource and connection samplings to *greatly* reduce arm's resource usage (many thanks to psutil)
+    * added: querying the proc contents when able for tor resource and connection samplings to *greatly* reduce seth's resource usage (many thanks to psutil)
     * added: vastly improved BSD compatibility, thanks to patches by Fabian Keil
           o pid resolution via pgrep (all platforms) and sockstat (bsd only)
           o connection resolution via sockstat (all platforms) and procstat (bsd only)
           o autodetecting the path prefix for FreeBSD jails
     * added: displaying summaries of the options on the configuration panel (idea by Sebastian)
-    * added: arm cpu usage to the header panel and logs (with an estimate for system call usages)
+    * added: seth cpu usage to the header panel and logs (with an estimate for system call usages)
     * added: testing script for checking connection resolution performance, connection dumps, and the glyph demo
-    * added: option to dump arm debug logs (better failsafe option)
+    * added: option to dump seth debug logs (better failsafe option)
     * change: incrementing the uptime field of the header panel each second
-    * change: centralizing arm resources in ~/.arm (suggested by Sebastian and also thanks to feedback from rransom)
+    * change: centralizing seth resources in ~/.seth (suggested by Sebastian and also thanks to feedback from rransom)
     * change: using exponential backoff of ps/proc resource resolutions when calls fail or tor isn't running
     * change: reordered resolvers by order of performance
-    * change: when tor's man page is unavailable falling back to descriptions provided with arm (often the case with tbb)
+    * change: when tor's man page is unavailable falling back to descriptions provided with seth (often the case with tbb)
     * change: dropping support for graphing of custom ps attributes (feature was never used, kinda pointless, and incompatible with the proc enhancement)
     * fix: providing proper cpu samplings rather than an average over the life of the process
     * fix: expanding relative paths for the authentication cookie (mostly a problem for tbb instances)
@@ -211,7 +211,7 @@ Platform specific enhancements including BSD compatibility and vastly improved p
     * fix: connection resolution wasn't finding results if tor was running under a different name
     * fix: brought all Linux connection resolvers into parity (established tcp connections only)
     * fix: commands with quoted pipes were being mis-parsed by the sysTools' call function
-    * fix (1/11/11, r24064): including platform, python version, and arm/tor configurations in debug dumps
+    * fix (1/11/11, r24064): including platform, python version, and seth/tor configurations in debug dumps
     * fix (1/11/11, r24064): properly parse the ps field when displaying decimal seconds (patch by Fabian)
     * fix (1/11/11, r24064): error when initial resource lookups fail (caught by Trystero)
     * fix (1/12/11, r24075): decimal seconds in the ps uptime field were being misparsed (patch by Fabian)
@@ -227,7 +227,7 @@ Introducing a new page for managing tor's configuration, along with several othe
           o sorting by any of the config attributes
     * change: numerous revisions in preparation for being included in debian, thanks to weasel
           o moved deb/rpm build resources out of the source repository and added helper scripts
-          o moved the arm install location to /usr/share/arm
+          o moved the seth install location to /usr/share/seth
           o purging the autogenerated egg file from the deb build
           o using temporary file utility for man page compression to avoid potential security issues (thanks to asn)
           o including dh_pysupport flag so it'll recognize the private python module (thanks to Emilio Pozuelo Monfort)
@@ -242,18 +242,18 @@ Introducing a new page for managing tor's configuration, along with several othe
           o fix: scrolling was buggy if comments were being stripped
           o fix: more helpful messages for validation errors
           o fix: unnecessary whitespace was being stripped
-    * added: INFO level logging for the arm startup time
+    * added: INFO level logging for the seth startup time
     * change: removing all references to the controller password after we've connected to tor (request by ioerror)
     * change: using curses.textpad to improve text fields (supports arrow keys, emacs keybindings, etc)
-    * change: revised the arm config interface (simplified and expanded to include maps)
+    * change: revised the seth config interface (simplified and expanded to include maps)
     * fix: verbose logging was causing the application to freeze due to an n^2 deduplication implementation, disabling this feature for now when it takes too long (caught by NightMonkey)
     * fix: wasn't loading the settings.cfg if starting starter from the src directory (caught by NightMonkey)
     * fix: displaying empty conf contents caused crashes when calling math.log10(0) (caught by NightMonkey)
     * fix: persisting results from scraping the man page to greatly reduce startup time (idea by nickm)
-    * fix: path for the sample armrc was wrong in the man page (caught by weasel)
-    * fix: the arm starter was only executable from the arm directory
+    * fix: path for the sample sethrc was wrong in the man page (caught by weasel)
+    * fix: the seth starter was only executable from the seth directory
     * fix: not all worker threads were daemons, causing the process to persist in a broken state after exceptions and when quitting via ctrl+c
-    * fix: custom armrcs resulted in the parsing config options being unavailable
+    * fix: custom sethrcs resulted in the parsing config options being unavailable
     * fix: rounding error in rendering the scrollbar, causing it to shrink a line when at the bottom
     * fix: crashing issue when the 'queries.ps.rate' config value was undefined and the stats graph was displayed
     * fix: making the interface more resilient to being resized while popups are visible
@@ -276,7 +276,7 @@ Numerous improvements, most notably being an expanded log panel, installer, and
           o dividers for the date, bordering all events that occurred on the same day
           o hiding duplicate log entries (feature request by asn)
           o coalescing updates if they're numerous, such as running at the DEBUG runlevel
-          o providing a notice if tor supports event types that arm doesn't, and logging them as the 'UNKNOWN' type
+          o providing a notice if tor supports event types that seth doesn't, and logging them as the 'UNKNOWN' type
           o condensing the label for runlevel event ranges further if they're identical for multiple types
           o options for:
                 + saving logged events to a file, either via snapshots or running persistence
@@ -288,7 +288,7 @@ Numerous improvements, most notably being an expanded log panel, installer, and
           o minor bug fixes including:
                 + added handling for BUILDTIMEOUT_SET events
                 + dropping brackets from label if no events are being logged
-                + merging tor and arm backlogs according to timestamps
+                + merging tor and seth backlogs according to timestamps
                 + regex matches were failing for multiline log entries
     * change: using PidFile entry to fetch pid if available (idea by arma)
     * change: dropping irrelevant information from the header when not running as a relay
@@ -317,12 +317,12 @@ Numerous improvements, most notably being an expanded log panel, installer, and
 6/7/10 - version 1.3.6 (r22617)
 Rewrite of the first third of the interface, providing vastly improved performance, maintainability, and a few very nice features. This improved the refresh rate (which is also related to system resource usage) from 30ms to 4ms (an 87% improvement).
 
-    * added: settings are fetched from an optional armrc (update rates, controller password, caching, runlevels, etc)
+    * added: settings are fetched from an optional sethrc (update rates, controller password, caching, runlevels, etc)
     * added: system tools util providing simplified usage, suppression of leaks to stdout, logging, and optional caching
     * added: wrapper for accessing TorCtl providing:
           o client side caching for commonly fetched relay information (fingerprint, descriptor, etc)
           o singleton accessor and convenience functions, simplifying interface code
-          o wrapper allowing reattachment to new controllers (ie, arm still works if tor's stopped then restarted - still in the works)
+          o wrapper allowing reattachment to new controllers (ie, seth still works if tor's stopped then restarted - still in the works)
     * change: full rewrite of the header panel, providing:
           o notice for when tor's disconnected (with time-stamp)
           o lightweight redrawing (smarter caching and moved updating into a daemon thread)
@@ -349,7 +349,7 @@ Rewrite of the first third of the interface, providing vastly improved performan
 4/8/10 - version 1.3.5 (r22148)
 Utility and service rewrite (refactored roughly a third of the codebase, including revised APIs and much better documentation).
 
-    * added: centralized logging utility for handling arm events, simplifying several parts of the interface
+    * added: centralized logging utility for handling seth events, simplifying several parts of the interface
     * added: rewrote connection resolver, including:
           o fallback support for 'ss' and 'lsof' (requested by dun, John Case, and Christopher Davis)
           o readjusts resolution rate if calls prove burdensome
@@ -365,7 +365,7 @@ Utility and service rewrite (refactored roughly a third of the codebase, includi
     * fix: removed workaround for mysterious torrc validation bug (was accidentally already fixed - thanks to dun for lending a test environment)
     * fix: size and time labels weren't doing integer truncation (rounding was unintended and frustratingly difficult to get rid of)
     * fix: hack to prevent log panel from drawing before being positioned
-    * fix: arm crashed if torrc was an empty file
+    * fix: seth crashed if torrc was an empty file
     * fix: wasn't consistently bolding help keys
 
 3/7/10 - version 1.3.4 (r21852)
@@ -413,7 +413,7 @@ Had enough of a siesta - getting back into development beginning with a rewrite
 
     * added: made authentication a little smarter, using PROTOCOLINFO to autodetect authentication type and cookie location
     * change: made 'blind mode' (disables connection queries) a startup option rather than flag in source (request by Sebastian)
-    * change: all log events (including arm) are now set via character flags, with TorCtl events log as their own toggleable type
+    * change: all log events (including seth) are now set via character flags, with TorCtl events log as their own toggleable type
     * change: starting log label with runlevel events, condensing if logging a range
     * change: simplifying command line parsing via getopt
     * fix: blind mode now prevents all netstats (including connection counts and halting resolver thread), improving performance
@@ -440,12 +440,12 @@ This will be the last update for a while since I'm about to start a new job.
     * fix: stretching connection lines to fill full screen
 
 10/21/09 - version 1.2.1 (r20814)
-Substantial bundle of changes including torrc validation, improved arm event logging, and numerous bug fixes.
+Substantial bundle of changes including torrc validation, improved seth event logging, and numerous bug fixes.
 
     * added: verifies loaded torrc consistency against tor's actual state (gives warning and providing corrections)
     * added: checks for torrc entries that are irrelevant due to duplication (gives notices and highlights)
     * added: log provides TorCtl events (hack... so ugly...)
-    * added: option for logging runlevel events of arm, tor, or both
+    * added: option for logging runlevel events of seth, tor, or both
     * added: ARM-DEBUG event for netstat query time
     * added: providing progress bar when resolving a batch of hostnames
     * change: providing prompt notice when tor's control port is closed
@@ -490,7 +490,7 @@ Bundle of semi-low hanging fruit, including a few issues discussed on irc.
     * added: showing extra parameters in connection listings if room's available
     * added: identifying directory server connections
     * change: providing an error message if running an incompatible python version (issue spotted by arma)
-    * change: giving arm a version to help in bug reports
+    * change: giving seth a version to help in bug reports
     * change: minor tweak to the wording of a faq entry (requested by Sebastian)
     * fix: wasn't accounting for RelayBandwidthRate/Burst in effective bandwidth (caught by hexa and arma)
     * fix: timing issue when shutting down (caught by arma)
@@ -498,7 +498,7 @@ Bundle of semi-low hanging fruit, including a few issues discussed on irc.
     * fix: preserving old results when netstat fails
 
 9/6/09 - r20493
-Several substantial features (last tasks for arm's todo list).
+Several substantial features (last tasks for seth's todo list).
 
     * added: scroll bars for connections listing and event log
     * added: made log scrollable (feature request by StrangeCharm)
@@ -581,7 +581,7 @@ Quick fixes based on discussion on irc.
     * fix: missing import for the socket module
 
 7/20/09 - r20096, r20097, r20098
-Couple fixes so arm plays nicely in the case of multiple running tor instances.
+Couple fixes so seth plays nicely in the case of multiple running tor instances.
 
     * fix: can now deal with multiple tor instances: checks pid of process with the open control port
     * fix: if only one tor process is running use that pid (netstat fails if running as a different user
@@ -599,7 +599,7 @@ Miscellaneous fix and feature batch.
     * added: relay's flags to the header
     * added: listing by relay nickname
     * added: additional event aliases and option for NEWCONSENSUS
-    * added (phobos): screenshot of arm in action so people can see what it looks like
+    * added (phobos): screenshot of seth in action so people can see what it looks like
     * change: use constant "Listing" label for sorting rather than current view
     * change: removed 'reload torrc' option (deceptive and useless)
     * fix: updates cached consensus mappings with NEWDESC and NEWCONSENSUS events
@@ -609,7 +609,7 @@ Resolved a few quick bugs:
 
     * fix: added fingerprint lookup cache to resolve substantial performance issue
     * fix: hostname resolution progress accounts for newly added entries (no more negative progress)
-    * fix: resolved bug that prevented arm from starting if too small
+    * fix: resolved bug that prevented seth from starting if too small
     * fix: ordering issue when sorting unresolved ip addresses
 
 7/11/09 - r19975
@@ -693,7 +693,7 @@ Few small tweaks including:
     * change: make inclusion of 'unknown' events toggleable
 
 5/24/09 - r19548, r19549, r19550, r19551
-Initial version of arm (terminal relay status monitor). Repository set up by arma.
+Initial version of seth (terminal relay status monitor). Repository set up by arma.
 
     * fix: bug concerning undefined exit policy
     * fix: resolved issue that prevented monitor from functioning in terminals without curs_set support
diff --git a/README b/README
index 8050657..10a5a6e 100644
--- a/README
+++ b/README
@@ -1,4 +1,4 @@
-arm (anonymizing relay monitor) - Terminal status monitor for Tor relays.
+seth - Terminal status monitor for Tor relays.
 Developed by Damian Johnson (www.atagar.com - atagar at torproject.org)
 All code under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
 Project page: www.atagar.com/arm
@@ -20,7 +20,7 @@ An interview by Brenno Winter discussing the project is available at:
 
 Requirements:
 Python 2.5
-Stem (this is included with arm)
+Stem
 Tor is running with an available control port. This means either...
   ... starting Tor with '--controlport <PORT>'
   ... or including 'ControlPort <PORT>' in your torrc
@@ -31,15 +31,15 @@ This can be done either with a cookie or password:
     contents of the control_auth_cookie file. To set this up...
     - add "CookieAuthentication 1" to your torrc
     - either restart Tor or run "pkill -sighup tor"
-    - this method of authentication is automatically handled by arm, so you
-      can still start arm as you normally would
+    - this method of authentication is automatically handled by seth, so you
+      can still start seth as you normally would
   
   * Password Authentication - Attaching to the control port requires a
     password. To set this up...
     - run "tor --hash-password <your password>"
     - add "HashedControlPassword <hashed password>" to your torrc
     - either restart Tor or run "pkill -sighup tor"
-    - when starting up arm will prompt you for this password
+    - when starting up seth will prompt you for this password
 
 For full functionality this also needs:
 - To be ran with the same user as tor to avoid permission issues with
@@ -59,17 +59,16 @@ For full functionality this also needs:
     * pgrep or pidof
     * host (if dns lookups are enabled)
 
-This is started via 'arm' (use the '--help' argument for usage).
+This is started via 'seth' (use the '--help' argument for usage).
 
 -------------------------------------------------------------------------------
 
 FAQ:
-> Why is it called 'arm'?
+> Why is it called 'seth'?
 
 Simple - because it makes the command short and memorable. Terminal
 applications need to be easy to type (like 'top', 'ssh', etc), and anything
-longer is just begging command-line aficionados to alias it down. I chose the
-meaning of the acronym ('anonymizing relay monitor') afterward.
+longer is just begging command-line aficionados to alias it down.
 
 > If you're listing connections then what about exit nodes? Won't this include 
 people's traffic?
@@ -78,23 +77,23 @@ No. Potential client and exit connections are specifically scrubbed of
 identifying information. Be aware that it's highly discouraged for relay
 operators to fetch this data, so please don't.
 
-> Is it harmful to share the information provided by arm?
+> Is it harmful to share the information provided by seth?
 
-Not really, but it's discouraged. The original plan for arm included a special
+Not really, but it's discouraged. The original plan for seth included a special
 emphasis that it wouldn't log any data. The reason is that if a large number of
 relay operators published the details of their connections then correlation
 attacks could break Tor user's anonymity. Just show some moderation in what you
 share and it should be fine.
 
-> Is there any chance that arm will leak data?
+> Is there any chance that seth will leak data?
 
 No. Arm is a completely passive listener, fetching all information from either
 Tor or the local system.
 
-> When arm starts it gives "Unable to resolve tor pid, abandoning connection 
+> When seth starts it gives "Unable to resolve tor pid, abandoning connection 
 listing"... why?
 
-If you're running multiple instances of Tor then arm needs to figure out which
+If you're running multiple instances of Tor then seth needs to figure out which
 pid belongs to the open control port. If it's running as a different user (such
 as being in a chroot jail) then it's probably failing due to permission issues.
 Arm still runs, just no connection listing or ps stats.
@@ -114,8 +113,8 @@ then you're encountering a bug between ncurses and your terminal where
 alternate character support (ACS) is unavailable. For more information see...
 http://invisible-island.net/ncurses/ncurses.faq.html#no_line_drawing
 
-Unfortunately there doesn't seem to be a way for arm to automatically detect
-and correct this. To work around some of the issues set this in your armrc...
+Unfortunately there doesn't seem to be a way for seth to automatically detect
+and correct this. To work around some of the issues set this in your sethrc...
 features.acsSupport false
 
 > When I press enter in the connection panel to get details some of the
@@ -154,15 +153,15 @@ information then this is simple to fix with the above.
 Layout:
 
 ./
-  arm     - startup script
+  seth     - startup script
   install - installation script
   
-  arm.1        - man page
-  armrc.sample - example arm configuration file with defaults
+  seth.1        - man page
+  sethrc.sample - example seth configuration file with defaults
   ChangeLog    - revision history
   LICENSE      - copy of the gpl v3
   README       - um... guess you figured this one out
-  setup.py     - distutils installation script for arm
+  setup.py     - distutils installation script for seth
   
   src/
     __init__.py
@@ -202,7 +201,7 @@ Layout:
       headerPanel.py         - top of all pages, providing general information
       popups.py              - toolkit providing display popups
       
-      logPanel.py            - (page 1) displays tor, arm, and stem events
+      logPanel.py            - (page 1) displays tor, seth, and stem events
       configPanel.py         - (page 3) editor panel for the tor configuration
       torrcPanel.py          - (page 4) displays torrc and validation
     
diff --git a/arm/__init__.py b/arm/__init__.py
deleted file mode 100644
index b81c0fb..0000000
--- a/arm/__init__.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""
-Tor curses monitoring application.
-"""
-
-__all__ = [
-  'arguments',
-  'config_panel',
-  'controller',
-  'header_panel',
-  'log_panel',
-  'popups',
-  'starter',
-  'torrc_panel',
-]
-
-__version__ = '1.4.6_dev'
-__release_date__ = 'April 28, 2011'
diff --git a/arm/arguments.py b/arm/arguments.py
deleted file mode 100644
index b404486..0000000
--- a/arm/arguments.py
+++ /dev/null
@@ -1,254 +0,0 @@
-"""
-Commandline argument parsing for arm.
-"""
-
-import collections
-import getopt
-import os
-
-import arm
-
-import stem.util.connection
-
-from arm.util import tor_controller, msg
-
-DEFAULT_ARGS = {
-  'control_address': '127.0.0.1',
-  'control_port': 9051,
-  'user_provided_port': False,
-  'control_socket': '/var/run/tor/control',
-  'user_provided_socket': False,
-  'config': os.path.expanduser("~/.arm/armrc"),
-  'debug_path': None,
-  'logged_events': 'N3',
-  'print_version': False,
-  'print_help': False,
-}
-
-OPT = 'i:s:c:d:l:vh'
-
-OPT_EXPANDED = [
-  'interface=',
-  'socket=',
-  'config=',
-  'debug=',
-  'log=',
-  'version',
-  'help',
-]
-
-TOR_EVENT_TYPES = {
-  'd': 'DEBUG',
-  'i': 'INFO',
-  'n': 'NOTICE',
-  'w': 'WARN',
-  'e': 'ERR',
-
-  'a': 'ADDRMAP',
-  'f': 'AUTHDIR_NEWDESCS',
-  'h': 'BUILDTIMEOUT_SET',
-  'b': 'BW',
-  'c': 'CIRC',
-  'j': 'CLIENTS_SEEN',
-  'k': 'DESCCHANGED',
-  'g': 'GUARD',
-  'l': 'NEWCONSENSUS',
-  'm': 'NEWDESC',
-  'p': 'NS',
-  'q': 'ORCONN',
-  's': 'STREAM',
-  'r': 'STREAM_BW',
-  't': 'STATUS_CLIENT',
-  'u': 'STATUS_GENERAL',
-  'v': 'STATUS_SERVER',
-}
-
-
-def parse(argv):
-  """
-  Parses our arguments, providing a named tuple with their values.
-
-  :param list argv: input arguments to be parsed
-
-  :returns: a **named tuple** with our parsed arguments
-
-  :raises: **ValueError** if we got an invalid argument
-  """
-
-  args = dict(DEFAULT_ARGS)
-
-  try:
-    recognized_args, unrecognized_args = getopt.getopt(argv, OPT, OPT_EXPANDED)
-
-    if unrecognized_args:
-      error_msg = "aren't recognized arguments" if len(unrecognized_args) > 1 else "isn't a recognized argument"
-      raise getopt.GetoptError("'%s' %s" % ("', '".join(unrecognized_args), error_msg))
-  except getopt.GetoptError as exc:
-    raise ValueError(msg('usage.invalid_arguments', error = exc))
-
-  for opt, arg in recognized_args:
-    if opt in ('-i', '--interface'):
-      if ':' in arg:
-        address, port = arg.split(':', 1)
-      else:
-        address, port = None, arg
-
-      if address is not None:
-        if not stem.util.connection.is_valid_ipv4_address(address):
-          raise ValueError(msg('usage.not_a_valid_address', address_input = address))
-
-        args['control_address'] = address
-
-      if not stem.util.connection.is_valid_port(port):
-        raise ValueError(msg('usage.not_a_valid_port', port_input = port))
-
-      args['control_port'] = int(port)
-      args['user_provided_port'] = True
-    elif opt in ('-s', '--socket'):
-      args['control_socket'] = arg
-      args['user_provided_socket'] = True
-    elif opt in ('-c', '--config'):
-      args['config'] = arg
-    elif opt in ('-d', '--debug'):
-      args['debug_path'] = os.path.expanduser(arg)
-    elif opt in ('-l', '--log'):
-      try:
-        expand_events(arg)
-      except ValueError as exc:
-        raise ValueError(msg('usage.unrecognized_log_flags', flags = exc))
-
-      args['logged_events'] = arg
-    elif opt in ('-v', '--version'):
-      args['print_version'] = True
-    elif opt in ('-h', '--help'):
-      args['print_help'] = True
-
-  # translates our args dict into a named tuple
-
-  Args = collections.namedtuple('Args', args.keys())
-  return Args(**args)
-
-
-def get_help():
-  """
-  Provides our --help usage information.
-
-  :returns: **str** with our usage information
-  """
-
-  return msg(
-    'usage.help_output',
-    address = DEFAULT_ARGS['control_address'],
-    port = DEFAULT_ARGS['control_port'],
-    socket = DEFAULT_ARGS['control_socket'],
-    config_path = DEFAULT_ARGS['config'],
-    events = DEFAULT_ARGS['logged_events'],
-    event_flags = msg('misc.event_types'),
-  )
-
-
-def get_version():
-  """
-  Provides our --version information.
-
-  :returns: **str** with our versioning information
-  """
-
-  return msg(
-    'usage.version_output',
-    version = arm.__version__,
-    date = arm.__release_date__,
-  )
-
-
-def expand_events(flags):
-  """
-  Expands event abbreviations to their full names. Beside mappings provided in
-  TOR_EVENT_TYPES this recognizes the following special events and aliases:
-
-  * A - all events
-  * X - no events
-  * U - UKNOWN events
-  * DINWE - runlevel and higher
-  * 12345 - arm/stem runlevel and higher (ARM_DEBUG - ARM_ERR)
-
-  For example...
-
-  ::
-
-    >>> expand_events('inUt')
-    set(['INFO', 'NOTICE', 'UNKNOWN', 'STATUS_CLIENT'])
-
-    >>> expand_events('N4')
-    set(['NOTICE', 'WARN', 'ERR', 'ARM_WARN', 'ARM_ERR'])
-
-    >>> expand_events('cfX')
-    set([])
-
-  :param str flags: character flags to be expanded
-
-  :returns: **set** of the expanded event types
-
-  :raises: **ValueError** with invalid input if any flags are unrecognized
-  """
-
-  expanded_events, invalid_flags = set(), ''
-
-  tor_runlevels = ['DEBUG', 'INFO', 'NOTICE', 'WARN', 'ERR']
-  arm_runlevels = ['ARM_' + runlevel for runlevel in tor_runlevels]
-
-  for flag in flags:
-    if flag == 'A':
-      return set(list(TOR_EVENT_TYPES) + arm_runlevels + ['UNKNOWN'])
-    elif flag == 'X':
-      return set()
-    elif flag in 'DINWE12345':
-      # all events for a runlevel and higher
-
-      if flag in 'D1':
-        runlevel_index = 0
-      elif flag in 'I2':
-        runlevel_index = 1
-      elif flag in 'N3':
-        runlevel_index = 2
-      elif flag in 'W4':
-        runlevel_index = 3
-      elif flag in 'E5':
-        runlevel_index = 4
-
-      if flag in 'DINWE':
-        runlevels = tor_runlevels[runlevel_index:]
-      elif flag in '12345':
-        runlevels = arm_runlevels[runlevel_index:]
-
-      expanded_events.update(set(runlevels))
-    elif flag == 'U':
-      expanded_events.add('UNKNOWN')
-    elif flag in TOR_EVENT_TYPES:
-      expanded_events.add(TOR_EVENT_TYPES[flag])
-    else:
-      invalid_flags += flag
-
-  if invalid_flags:
-    raise ValueError(''.join(set(invalid_flags)))
-  else:
-    return expanded_events
-
-
-def missing_event_types():
-  """
-  Provides the event types the current tor connection supports but arm
-  doesn't. This provides an empty list if no event types are missing or the
-  GETINFO query fails.
-
-  :returns: **list** of missing event types
-  """
-
-  response = tor_controller().get_info('events/names', None)
-
-  if response is None:
-    return []  # GETINFO query failed
-
-  tor_event_types = response.split(' ')
-  recognized_types = TOR_EVENT_TYPES.values()
-  return filter(lambda x: x not in recognized_types, tor_event_types)
diff --git a/arm/config/attributes.cfg b/arm/config/attributes.cfg
deleted file mode 100644
index b592e11..0000000
--- a/arm/config/attributes.cfg
+++ /dev/null
@@ -1,41 +0,0 @@
-# General configuration data used by arm.
-
-attr.flag_colors Authority => white
-attr.flag_colors BadExit => red
-attr.flag_colors BadDirectory => red
-attr.flag_colors Exit => cyan
-attr.flag_colors Fast => yellow
-attr.flag_colors Guard => green
-attr.flag_colors HSDir => magenta
-attr.flag_colors Named => blue
-attr.flag_colors Stable => blue
-attr.flag_colors Running => yellow
-attr.flag_colors Unnamed => magenta
-attr.flag_colors Valid => green
-attr.flag_colors V2Dir => cyan
-attr.flag_colors V3Dir => white
-
-attr.version_status_colors new => blue
-attr.version_status_colors new in series => blue
-attr.version_status_colors obsolete => red
-attr.version_status_colors recommended => green
-attr.version_status_colors old => red
-attr.version_status_colors unrecommended => red
-attr.version_status_colors unknown => cyan
-
-attr.hibernate_color awake => green
-attr.hibernate_color soft => yellow
-attr.hibernate_color hard => red
-
-attr.graph.title bandwidth => Bandwidth
-attr.graph.title connections => Connection Count
-attr.graph.title resources => System Resources
-
-attr.graph.header.primary bandwidth => Download
-attr.graph.header.primary connections => Inbound
-attr.graph.header.primary resources => CPU
-
-attr.graph.header.secondary bandwidth => Upload
-attr.graph.header.secondary connections => Outbound
-attr.graph.header.secondary resources => Memory
-
diff --git a/arm/config/dedup.cfg b/arm/config/dedup.cfg
deleted file mode 100644
index ce8afcb..0000000
--- a/arm/config/dedup.cfg
+++ /dev/null
@@ -1,107 +0,0 @@
-################################################################################
-#
-# Snippets from common log messages. These are used to determine when entries
-# with dynamic content (hostnames, numbers, etc) are the same. If this matches
-# the start of both messages then the entries are flagged as duplicates. If the
-# entry begins with an asterisk (*) then it checks if the substrings exist
-# anywhere in the messages.
-#
-# Examples for the complete messages:
-#
-# [BW] READ: 0, WRITTEN: 0
-# [DEBUG] connection_handle_write(): After TLS write of 512: 0 read, 586 written
-# [DEBUG] flush_chunk_tls(): flushed 512 bytes, 0 ready to flush, 0 remain.
-# [DEBUG] conn_read_callback(): socket 7 wants to read.
-# [DEBUG] conn_write_callback(): socket 51 wants to write.
-# [DEBUG] connection_remove(): removing socket -1 (type OR), n_conns now 50
-# [DEBUG] connection_or_process_cells_from_inbuf(): 7: starting, inbuf_datalen
-#         0 (0 pending in tls object).
-# [DEBUG] connection_read_to_buf(): 38: starting, inbuf_datalen 0 (0 pending in
-#         tls object). at_most 12800.
-# [DEBUG] connection_read_to_buf(): TLS connection closed on read. Closing.
-#         (Nickname moria1, address 128.31.0.34)
-# [INFO] run_connection_housekeeping(): Expiring non-open OR connection to fd
-#        16 (79.193.61.171:443).
-# [INFO] rep_hist_downrate_old_runs(): Discounting all old stability info by a
-#        factor of 0.950000
-# [NOTICE] Circuit build timeout of 96803ms is beyond the maximum build time we 
-#          have ever observed. Capping it to 96107ms.
-#   The above NOTICE changes to an INFO message in maint-0.2.2
-# [NOTICE] Based on 1000 circuit times, it looks like we don't need to wait so 
-#          long for circuits to finish. We will now assume a circuit is too slow
-#          to use after waiting 65 seconds.
-# [NOTICE] We stalled too much while trying to write 150 bytes to address
-#          [scrubbed].  If this happens a lot, either something is wrong with
-#          your network connection, or something is wrong with theirs. (fd 238,
-#          type Directory, state 1, marked at main.c:702).
-# [NOTICE] I learned some more directory information, but not enough to build a
-#          circuit: We have only 469/2027 usable descriptors.
-# [NOTICE] Attempt by %s to open a stream from unknown relay. Closing.
-# [NOTICE] Bootstrapped 72%: Loading relay descriptors.
-# [WARN] You specified a server "Amunet8" by name, but this name is not
-#        registered
-# [WARN] I have no descriptor for the router named "Amunet8" in my declared
-#        family; I'll use the nickname as is, but this   may confuse clients.
-# [WARN] Controller gave us config lines that didn't validate: Value
-#        'BandwidthRate  ' is malformed or out of bounds.
-# [WARN] Problem bootstrapping. Stuck at 80%: Connecting to the Tor network.
-#        (Network is unreachable; NOROUTE; count 47;    recommendation warn)
-# [WARN] 4 unknown, 1 missing key, 3 good, 0 bad, 1 no signature, 4 required
-# [ARM_DEBUG] refresh rate: 0.001 seconds
-# [ARM_DEBUG] proc call (process connections): /proc/net/[tcp|udp] (runtime: 0.0018)
-# [ARM_DEBUG] system call: ps -p 2354 -o %cpu,rss,%mem,etime (runtime: 0.02)
-# [ARM_DEBUG] system call: netstat -npt | grep 2354/tor (runtime: 0.02)
-# [ARM_DEBUG] recreating panel 'graph' with the dimensions of 14/124
-# [ARM_DEBUG] redrawing the log panel with the corrected content height (estimat was off by 4)
-# [ARM_DEBUG] GETINFO accounting/bytes-left (runtime: 0.0006)
-# [ARM_DEBUG] GETINFO traffic/read (runtime: 0.0004)
-# [ARM_DEBUG] GETINFO traffic/written (runtime: 0.0002)
-# [ARM_DEBUG] GETCONF MyFamily (runtime: 0.0007)
-# [ARM_DEBUG] Unable to query process resource usage from ps, waiting 6.25 seconds (unrecognized output from ps: ...)
-#
-################################################################################
-
-dedup.BW READ:
-dedup.DEBUG connection_handle_write(): After TLS write of
-dedup.DEBUG flush_chunk_tls(): flushed
-dedup.DEBUG conn_read_callback(): socket
-dedup.DEBUG conn_write_callback(): socket
-dedup.DEBUG connection_remove(): removing socket
-dedup.DEBUG connection_or_process_cells_from_inbuf():
-dedup.DEBUG *pending in tls object). at_most
-dedup.DEBUG connection_read_to_buf(): TLS connection closed on read. Closing.
-dedup.INFO run_connection_housekeeping(): Expiring
-dedup.INFO rep_hist_downrate_old_runs(): Discounting all old stability info by a factor of
-dedup.INFO *build time we have ever observed. Capping it to
-dedup.NOTICE *build time we have ever observed. Capping it to
-dedup.NOTICE *We will now assume a circuit is too slow to use after waiting
-dedup.NOTICE We stalled too much while trying to write
-dedup.NOTICE I learned some more directory information, but not enough to build a circuit
-dedup.NOTICE Attempt by
-dedup.NOTICE *Loading relay descriptors.
-dedup.WARN You specified a server
-dedup.WARN I have no descriptor for the router named
-dedup.WARN Controller gave us config lines that didn't validate
-dedup.WARN Problem bootstrapping. Stuck at
-dedup.WARN *missing key,
-dedup.ARM_DEBUG refresh rate:
-dedup.ARM_DEBUG proc call (cwd):
-dedup.ARM_DEBUG proc call (memory usage):
-dedup.ARM_DEBUG proc call (process command
-dedup.ARM_DEBUG proc call (process utime
-dedup.ARM_DEBUG proc call (process stime
-dedup.ARM_DEBUG proc call (process start time
-dedup.ARM_DEBUG proc call (process connections):
-dedup.ARM_DEBUG system call: ps
-dedup.ARM_DEBUG system call: netstat
-dedup.ARM_DEBUG recreating panel '
-dedup.ARM_DEBUG redrawing the log panel with the corrected content height (
-dedup.ARM_DEBUG GETINFO accounting/bytes
-dedup.ARM_DEBUG GETINFO accounting/bytes-left
-dedup.ARM_DEBUG GETINFO accounting/interval-end
-dedup.ARM_DEBUG GETINFO accounting/hibernating
-dedup.ARM_DEBUG GETINFO traffic/read
-dedup.ARM_DEBUG GETINFO traffic/written
-dedup.ARM_DEBUG GETCONF
-dedup.ARM_DEBUG Unable to query process resource usage from ps
-
diff --git a/arm/config/strings.cfg b/arm/config/strings.cfg
deleted file mode 100644
index 465e208..0000000
--- a/arm/config/strings.cfg
+++ /dev/null
@@ -1,100 +0,0 @@
-################################################################################
-#
-# User facing strings. These are sorted into the following namespaces...
-#
-#   * config    parsing or handling configuration options
-#   * debug     concerns the --debug argument
-#   * misc      anything that doesn't fit into a present namespace
-#   * panel     used after startup by our curses panels
-#   * setup     notificaitons or issues arising while starting arm
-#   * tracker   related to tracking resource usage or connections
-#   * usage     usage information about starting and running arm
-#
-################################################################################
-
-msg.wrap {text}
-
-msg.config.unable_to_read_file Failed to load configuration (using defaults): "{error}"
-msg.config.nothing_loaded No armrc loaded, using defaults. You can customize arm by placing a configuration file at {path} (see the armrc.sample for its options).
-
-msg.debug.saving_to_path Saving a debug log to {path}, please check it for sensitive information before sharing it.
-msg.debug.unable_to_write_file Unable to write to our debug log file ({path}): {error}
-
-msg.panel.graphing.prepopulation_all_successful Read the last day of bandwidth history from the state file
-msg.panel.graphing.prepopulation_successful Read the last day of bandwidth history from the state file ({duration} is missing)
-msg.panel.graphing.prepopulation_failure Unable to prepopulate bandwidth information ({error})
-msg.panel.header.fd_used_at_sixty_percent Tor's file descriptor usage is at {percentage}%.
-msg.panel.header.fd_used_at_ninety_percent Tor's file descriptor usage is at {percentage}%. If you run out Tor will be unable to continue functioning.
-
-msg.setup.arm_is_running_as_root Arm is currently running with root permissions. This isn't a good idea, nor should it be necessary. Try starting arm with "sudo -u {tor_user} arm" instead.
-msg.setup.chroot_doesnt_exist The chroot path set in your config ({path}) doesn't exist.
-msg.setup.set_freebsd_chroot Adjusting paths to account for Tor running in a FreeBSD jail at: {path}
-msg.setup.tor_is_running_as_root Tor is currently running with root permissions. This isn't a good idea, nor should it be necessary. See the 'User UID' option on Tor's man page for an easy method of reducing its permissions after startup.
-msg.setup.unable_to_determine_pid Unable to determine Tor's pid. Some information, like its resource usage will be unavailable.
-msg.setup.unknown_event_types arm doesn't recognize the following event types: {event_types} (log 'UNKNOWN' events to see them)
-msg.setup.color_support_available Terminal color support detected and enabled
-msg.setup.color_support_unavailable Terminal color support unavailable
-
-msg.tracker.abort_getting_resources Failed three attempts to get process resource usage from {resolver}, {response} ({exc})
-msg.tracker.abort_getting_port_usage Failed three attempts to determine the process using active ports ({exc})
-msg.tracker.lookup_rate_increased connection lookup time increasing to {seconds} seconds per call
-msg.tracker.unable_to_get_port_usages Unable to query the processes using ports usage lsof ({exc})
-msg.tracker.unable_to_get_resources Unable to query process resource usage from {resolver} ({exc})
-msg.tracker.unable_to_use_all_resolvers We were unable to use any of your system's resolvers to get tor's connections. This is fine, but means that the connections page will be empty. This is usually permissions related so if you would like to fix this then run arm with the same user as tor (ie, "sudo -u <tor user> arm").
-msg.tracker.unable_to_use_resolver Unable to query connections with {old_resolver}, trying {new_resolver}
-
-msg.usage.invalid_arguments {error} (for usage provide --help)
-msg.usage.not_a_valid_address '{address_input}' isn't a valid IPv4 address
-msg.usage.not_a_valid_port '{port_input}' isn't a valid port number
-msg.usage.unrecognized_log_flags Unrecognized event flags: {flags}
-msg.usage.unable_to_set_color_override "{color}" isn't a valid color
-
-msg.debug.header
-|Arm {arm_version} Debug Dump
-|Stem Version: {stem_version}
-|Python Version: {python_version}
-|Platform: {system} ({platform})
-|--------------------------------------------------------------------------------
-|Arm Configuration ({armrc_path}):
-|{armrc_content}
-|--------------------------------------------------------------------------------
-
-msg.misc.event_types
-|        d DEBUG      a ADDRMAP           k DESCCHANGED   s STREAM
-|        i INFO       f AUTHDIR_NEWDESCS  g GUARD         r STREAM_BW
-|        n NOTICE     h BUILDTIMEOUT_SET  l NEWCONSENSUS  t STATUS_CLIENT
-|        w WARN       b BW                m NEWDESC       u STATUS_GENERAL
-|        e ERR        c CIRC              p NS            v STATUS_SERVER
-|                     j CLIENTS_SEEN      q ORCONN
-|          DINWE tor runlevel+            A All Events
-|          12345 arm runlevel+            X No Events
-|                                         U Unknown Events
-
-msg.setup.unknown_term
-|Unknown $TERM: ({term})
-|Either update your terminfo database or run arm using "TERM=xterm arm".
-|
-
-msg.usage.help_output
-|Usage arm [OPTION]
-|Terminal status monitor for Tor relays.
-|
-|  -i, --interface [ADDRESS:]PORT  change control interface from {address}:{port}
-|  -s, --socket SOCKET_PATH        attach using unix domain socket if present,
-|                                    SOCKET_PATH defaults to: {socket}
-|  -c, --config CONFIG_PATH        loaded configuration options, CONFIG_PATH
-|                                    defaults to: {config_path}
-|  -d, --debug LOG_PATH            writes all arm logs to the given location
-|  -l, --log EVENT_FLAGS           event types to be logged (default: {events})
-|{event_flags}
-|  -v, --version                   provides version information
-|  -h, --help                      presents this help
-|
-|Example:
-|arm -i 1643             attach to control port 1643
-|arm -l we -c /tmp/cfg   use this configuration file with 'WARN'/'ERR' events
-
-msg.usage.version_output
-|arm version {version} (released {date})
-|
-
diff --git a/arm/config/torrc.cfg b/arm/config/torrc.cfg
deleted file mode 100644
index 8e761eb..0000000
--- a/arm/config/torrc.cfg
+++ /dev/null
@@ -1,313 +0,0 @@
-################################################################################
-#
-# Information related to tor configuration options. This has two sections...
-#
-#   * torrc.alias       Aliases for configuration options tor will accept.
-#   * torrc.units       Labels accepted by tor for various units.
-#   * torrc.important   Important configuration options which are shown by default.
-#   * torrc.summary     Short summary describing the option.
-#
-################################################################################
-
-# Torrc aliases from the _option_abbrevs struct of 'src/or/config.c'. These
-# couldn't be requested via GETCONF as of 0.2.1.19, but this might have been
-# fixed. Discussion is in...
-#
-#   https://trac.torproject.org/projects/tor/ticket/1802
-#
-# TODO: Check if this workaround can be dropped later.
-
-torrc.alias l => Log
-torrc.alias AllowUnverifiedNodes => AllowInvalidNodes
-torrc.alias AutomapHostSuffixes => AutomapHostsSuffixes
-torrc.alias AutomapHostOnResolve => AutomapHostsOnResolve
-torrc.alias BandwidthRateBytes => BandwidthRate
-torrc.alias BandwidthBurstBytes => BandwidthBurst
-torrc.alias DirFetchPostPeriod => StatusFetchPeriod
-torrc.alias MaxConn => ConnLimit
-torrc.alias ORBindAddress => ORListenAddress
-torrc.alias DirBindAddress => DirListenAddress
-torrc.alias SocksBindAddress => SocksListenAddress
-torrc.alias UseHelperNodes => UseEntryGuards
-torrc.alias NumHelperNodes => NumEntryGuards
-torrc.alias UseEntryNodes => UseEntryGuards
-torrc.alias NumEntryNodes => NumEntryGuards
-torrc.alias ResolvConf => ServerDNSResolvConfFile
-torrc.alias SearchDomains => ServerDNSSearchDomains
-torrc.alias ServerDNSAllowBrokenResolvConf => ServerDNSAllowBrokenConfig
-torrc.alias PreferTunnelledDirConns => PreferTunneledDirConns
-torrc.alias BridgeAuthoritativeDirectory => BridgeAuthoritativeDir
-torrc.alias StrictEntryNodes => StrictNodes
-torrc.alias StrictExitNodes => StrictNodes
-
-# Size and time modifiers allowed by 'src/or/config.c'.
-
-torrc.units.size.b b, byte, bytes
-torrc.units.size.kb kb, kbyte, kbytes, kilobyte, kilobytes
-torrc.units.size.mb m, mb, mbyte, mbytes, megabyte, megabytes
-torrc.units.size.gb gb, gbyte, gbytes, gigabyte, gigabytes
-torrc.units.size.tb tb, terabyte, terabytes
-
-torrc.units.time.sec second, seconds
-torrc.units.time.min minute, minutes
-torrc.units.time.hour hour, hours
-torrc.units.time.day day, days
-torrc.units.time.week week, weeks
-
-# Especially important tor configuration options.
-
-torrc.important BandwidthRate
-torrc.important BandwidthBurst
-torrc.important RelayBandwidthRate
-torrc.important RelayBandwidthBurst
-torrc.important ControlPort
-torrc.important HashedControlPassword
-torrc.important CookieAuthentication
-torrc.important DataDirectory
-torrc.important Log
-torrc.important RunAsDaemon
-torrc.important User
-
-torrc.important Bridge
-torrc.important ExcludeNodes
-torrc.important MaxCircuitDirtiness
-torrc.important SocksPort
-torrc.important UseBridges
-
-torrc.important BridgeRelay
-torrc.important ContactInfo
-torrc.important ExitPolicy
-torrc.important MyFamily
-torrc.important Nickname
-torrc.important ORPort
-torrc.important PortForwarding
-torrc.important AccountingMax
-torrc.important AccountingStart
-
-torrc.important DirPortFrontPage
-torrc.important DirPort
-
-torrc.important HiddenServiceDir
-torrc.important HiddenServicePort
-
-# General Config Options
-
-torrc.summary.BandwidthRate Average bandwidth usage limit
-torrc.summary.BandwidthBurst Maximum bandwidth usage limit
-torrc.summary.MaxAdvertisedBandwidth Limit for the bandwidth we advertise as being available for relaying
-torrc.summary.RelayBandwidthRate Average bandwidth usage limit for relaying
-torrc.summary.RelayBandwidthBurst Maximum bandwidth usage limit for relaying
-torrc.summary.PerConnBWRate Average relayed bandwidth limit per connection
-torrc.summary.PerConnBWBurst Maximum relayed bandwidth limit per connection
-torrc.summary.ConnLimit Minimum number of file descriptors for Tor to start
-torrc.summary.ConstrainedSockets Shrinks sockets to ConstrainedSockSize
-torrc.summary.ConstrainedSockSize Limit for the received and transmit buffers of sockets
-torrc.summary.ControlPort Port providing access to tor controllers (arm, vidalia, etc)
-torrc.summary.ControlListenAddress Address providing controller access
-torrc.summary.ControlSocket Socket providing controller access
-torrc.summary.HashedControlPassword Hash of the password for authenticating to the control port
-torrc.summary.CookieAuthentication If set, authenticates controllers via a cookie
-torrc.summary.CookieAuthFile Location of the authentication cookie
-torrc.summary.CookieAuthFileGroupReadable Group read permissions for the authentication cookie
-torrc.summary.ControlPortWriteToFile Path for a file tor writes containing its control port
-torrc.summary.ControlPortFileGroupReadable Group read permissions for the control port file
-torrc.summary.DataDirectory Location for storing runtime data (state, keys, etc)
-torrc.summary.DirServer Alternative directory authorities
-torrc.summary.AlternateDirAuthority Alternative directory authorities (consensus only)
-torrc.summary.AlternateHSAuthority Alternative directory authorities (hidden services only)
-torrc.summary.AlternateBridgeAuthority Alternative directory authorities (bridges only)
-torrc.summary.DisableAllSwap Locks all allocated memory so they can't be paged out
-torrc.summary.FetchDirInfoEarly Keeps consensus information up to date, even if unnecessary
-torrc.summary.FetchDirInfoExtraEarly Updates consensus information when it's first available
-torrc.summary.FetchHidServDescriptors Toggles if hidden service descriptors are fetched automatically or not
-torrc.summary.FetchServerDescriptors Toggles if the consensus is fetched automatically or not
-torrc.summary.FetchUselessDescriptors Toggles if relay descriptors are fetched when they aren't strictly necessary
-torrc.summary.Group GID for the process when started
-torrc.summary.HttpProxy HTTP proxy for connecting to tor
-torrc.summary.HttpProxyAuthenticator Authentication credentials for HttpProxy
-torrc.summary.HttpsProxy SSL proxy for connecting to tor
-torrc.summary.HttpsProxyAuthenticator Authentication credentials for HttpsProxy
-torrc.summary.Socks4Proxy SOCKS 4 proxy for connecting to tor
-torrc.summary.Socks5Proxy SOCKS 5 for connecting to tor
-torrc.summary.Socks5ProxyUsername Username for connecting to the Socks5Proxy
-torrc.summary.Socks5ProxyPassword Password for connecting to the Socks5Proxy
-torrc.summary.KeepalivePeriod Rate at which to send keepalive packets
-torrc.summary.Log Runlevels and location for tor logging
-torrc.summary.LogMessageDomains Includes a domain when logging messages
-torrc.summary.OutboundBindAddress Sets the IP used for connecting to tor
-torrc.summary.PidFile Path for a file tor writes containing its process id
-torrc.summary.ProtocolWarnings Toggles if protocol errors give warnings or not
-torrc.summary.RunAsDaemon Toggles if tor runs as a daemon process
-torrc.summary.LogTimeGranularity limits granularity of log message timestamps
-torrc.summary.SafeLogging Toggles if logs are scrubbed of sensitive information
-torrc.summary.User UID for the process when started
-torrc.summary.HardwareAccel Toggles if tor attempts to use hardware acceleration
-torrc.summary.AccelName OpenSSL engine name for crypto acceleration
-torrc.summary.AccelDir Crypto acceleration library path
-torrc.summary.AvoidDiskWrites Toggles if tor avoids frequently writing to disk
-torrc.summary.TunnelDirConns Toggles if directory requests can be made over the ORPort
-torrc.summary.PreferTunneledDirConns Avoids directory requests that can't be made over the ORPort if set
-torrc.summary.CircuitPriorityHalflife Overwrite method for prioritizing traffic among relayed connections
-torrc.summary.DisableIOCP Disables use of the Windows IOCP networking API
-torrc.summary.CountPrivateBandwidth Applies rate limiting to private IP addresses
-
-# Client Config Options
-
-torrc.summary.AllowInvalidNodes Permits use of relays flagged as invalid by authorities
-torrc.summary.ExcludeSingleHopRelays Permits use of relays that allow single hop connections
-torrc.summary.Bridge Available bridges
-torrc.summary.LearnCircuitBuildTimeout Toggles adaptive timeouts for circuit creation
-torrc.summary.CircuitBuildTimeout Initial timeout for circuit creation
-torrc.summary.CircuitIdleTimeout Timeout for closing circuits that have never been used
-torrc.summary.CircuitStreamTimeout Timeout for shifting streams among circuits
-torrc.summary.ClientOnly Ensures that we aren't used as a relay or directory mirror
-torrc.summary.ExcludeNodes Relays or locales never to be used in circuits
-torrc.summary.ExcludeExitNodes Relays or locales never to be used for exits
-torrc.summary.ExitNodes Preferred final hop for circuits
-torrc.summary.EntryNodes Preferred first hops for circuits
-torrc.summary.StrictNodes Never uses notes outside of Entry/ExitNodes
-torrc.summary.FascistFirewall Only make outbound connections on FirewallPorts
-torrc.summary.FirewallPorts Ports used by FascistFirewall
-torrc.summary.HidServAuth Authentication credentials for connecting to a hidden service
-torrc.summary.ReachableAddresses Rules for bypassing the local firewall
-torrc.summary.ReachableDirAddresses Rules for bypassing the local firewall (directory fetches)
-torrc.summary.ReachableORAddresses Rules for bypassing the local firewall (OR connections)
-torrc.summary.LongLivedPorts Ports requiring highly reliable relays
-torrc.summary.MapAddress Alias mappings for address requests
-torrc.summary.NewCircuitPeriod Period for considering the creation of new circuits
-torrc.summary.MaxCircuitDirtiness Duration for reusing constructed circuits
-torrc.summary.NodeFamily Define relays as belonging to a family
-torrc.summary.EnforceDistinctSubnets Prevent use of multiple relays from the same subnet on a circuit
-torrc.summary.SocksPort Port for using tor as a Socks proxy
-torrc.summary.SocksListenAddress Address from which Socks connections can be made
-torrc.summary.SocksPolicy Access policy for the pocks port
-torrc.summary.SocksTimeout Time until idle or unestablished socks connections are closed
-torrc.summary.TrackHostExits Maintains use of the same exit whenever connecting to this destination
-torrc.summary.TrackHostExitsExpire Time until use of an exit for tracking expires
-torrc.summary.UpdateBridgesFromAuthority Toggles fetching bridge descriptors from the authorities
-torrc.summary.UseBridges Make use of configured bridges
-torrc.summary.UseEntryGuards Use guard relays for first hop
-torrc.summary.NumEntryGuards Pool size of guard relays we'll select from
-torrc.summary.SafeSocks Toggles rejecting unsafe variants of the socks protocol
-torrc.summary.TestSocks Provide notices for if socks connections are of the safe or unsafe variants
-torrc.summary.WarnUnsafeSocks Toggle warning of unsafe socks connection
-torrc.summary.VirtualAddrNetwork Address range used with MAPADDRESS
-torrc.summary.AllowNonRFC953Hostnames Toggles blocking invalid characters in hostname resolution
-torrc.summary.AllowDotExit Toggles allowing exit notation in addresses
-torrc.summary.FastFirstHopPK Toggle public key usage for the first hop
-torrc.summary.TransPort Port for transparent proxying if the OS supports it
-torrc.summary.TransListenAddress Address from which transparent proxy connections can be made
-torrc.summary.NATDPort Port for forwarding ipfw NATD connections
-torrc.summary.NATDListenAddress Address from which NATD forwarded connections can be made
-torrc.summary.AutomapHostsOnResolve Map addresses ending with special suffixes to virtual addresses
-torrc.summary.AutomapHostsSuffixes Address suffixes recognized by AutomapHostsOnResolve
-torrc.summary.DNSPort Port from which DNS responses are fetched instead of tor
-torrc.summary.DNSListenAddress Address for performing DNS resolution
-torrc.summary.ClientDNSRejectInternalAddresses Ignores DNS responses for internal addresses
-torrc.summary.ClientRejectInternalAddresses Disables use of Tor for internal connections
-torrc.summary.DownloadExtraInfo Toggles fetching of extra information about relays
-torrc.summary.FallbackNetworkstatusFile Path for a fallback cache of the consensus
-torrc.summary.WarnPlaintextPorts Toggles warnings for using risky ports
-torrc.summary.RejectPlaintextPorts Prevents connections on risky ports
-torrc.summary.AllowSingleHopCircuits Makes use of single hop exits if able
-
-# Server Config Options
-
-torrc.summary.Address Overwrites address others will use to reach this relay
-torrc.summary.AllowSingleHopExits Toggles permitting use of this relay as a single hop proxy
-torrc.summary.AssumeReachable Skips reachability test at startup
-torrc.summary.BridgeRelay Act as a bridge
-torrc.summary.ContactInfo Contact information for this relay
-torrc.summary.ExitPolicy Traffic destinations that can exit from this relay
-torrc.summary.ExitPolicyRejectPrivate Prevent exiting connection on the local network
-torrc.summary.MaxOnionsPending Decryption queue size
-torrc.summary.MyFamily Other relays this operator administers
-torrc.summary.Nickname Identifier for this relay
-torrc.summary.NumCPUs Number of processes spawned for decryption
-torrc.summary.ORPort Port used to accept relay traffic
-torrc.summary.ORListenAddress Address for relay connections
-torrc.summary.PortForwarding Use UPnP or NAT-PMP if needed to relay
-torrc.summary.PortForwardingHelper Executable for configuring port forwarding
-torrc.summary.PublishServerDescriptor Types of descriptors published
-torrc.summary.ShutdownWaitLength Delay before quitting after receiving a SIGINT signal
-torrc.summary.HeartbeatPeriod Rate at which an INFO level heartbeat message is sent
-torrc.summary.AccountingMax Amount of traffic before hibernating
-torrc.summary.AccountingStart Duration of an accounting period
-torrc.summary.RefuseUnknownExits Prevents relays not in the consensus from using us as an exit
-torrc.summary.ServerDNSResolvConfFile Overriding resolver config for DNS queries we provide
-torrc.summary.ServerDNSAllowBrokenConfig Toggles if we persist despite configuration parsing errors or not
-torrc.summary.ServerDNSSearchDomains Toggles if our DNS queries search for addresses in the local domain
-torrc.summary.ServerDNSDetectHijacking Toggles testing for DNS hijacking
-torrc.summary.ServerDNSTestAddresses Addresses to test to see if valid DNS queries are being hijacked
-torrc.summary.ServerDNSAllowNonRFC953Hostnames Toggles if we reject DNS queries with invalid characters
-torrc.summary.BridgeRecordUsageByCountry Tracks geoip information on bridge usage
-torrc.summary.ServerDNSRandomizeCase Toggles DNS query case randomization
-torrc.summary.GeoIPFile Path to file containing geoip information
-torrc.summary.CellStatistics Toggles storing circuit queue duration to disk
-torrc.summary.DirReqStatistics Toggles storing network status counts and performance to disk
-torrc.summary.EntryStatistics Toggles storing client connection counts to disk
-torrc.summary.ExitPortStatistics Toggles storing traffic and port usage data to disk
-torrc.summary.ConnDirectionStatistics Toggles storing connection use to disk
-torrc.summary.ExtraInfoStatistics Publishes statistic data in the extra-info documents
-
-# Directory Server Options
-
-torrc.summary.AuthoritativeDirectory Act as a directory authority
-torrc.summary.DirPortFrontPage Publish this html file on the DirPort
-torrc.summary.V1AuthoritativeDirectory Generates a version 1 consensus
-torrc.summary.V2AuthoritativeDirectory Generates a version 2 consensus
-torrc.summary.V3AuthoritativeDirectory Generates a version 3 consensus
-torrc.summary.VersioningAuthoritativeDirectory Provides opinions on recommended versions of tor
-torrc.summary.NamingAuthoritativeDirectory Provides opinions on fingerprint to nickname bindings
-torrc.summary.HSAuthoritativeDir Toggles accepting hidden service descriptors
-torrc.summary.HidServDirectoryV2 Toggles accepting version 2 hidden service descriptors
-torrc.summary.BridgeAuthoritativeDir Acts as a bridge authority
-torrc.summary.MinUptimeHidServDirectoryV2 Required uptime before accepting hidden service directory
-torrc.summary.DirPort Port for directory connections
-torrc.summary.DirListenAddress Address the directory service is bound to
-torrc.summary.DirPolicy Access policy for the DirPort
-torrc.summary.FetchV2Networkstatus Get the obsolete V2 consensus
-
-# Directory Authority Server Options
-
-torrc.summary.RecommendedVersions Tor versions believed to be safe
-torrc.summary.RecommendedClientVersions Tor versions believed to be safe for clients
-torrc.summary.RecommendedServerVersions Tor versions believed to be safe for relays
-torrc.summary.ConsensusParams Params entry of the networkstatus vote
-torrc.summary.DirAllowPrivateAddresses Toggles allowing arbitrary input or non-public IPs in descriptors
-torrc.summary.AuthDirBadDir Relays to be flagged as bad directory caches
-torrc.summary.AuthDirBadExit Relays to be flagged as bad exits
-torrc.summary.AuthDirInvalid Relays from which the valid flag is withheld
-torrc.summary.AuthDirReject Relays to be dropped from the consensus
-torrc.summary.AuthDirListBadDirs Toggles if we provide an opinion on bad directory caches
-torrc.summary.AuthDirListBadExits Toggles if we provide an opinion on bad exits
-torrc.summary.AuthDirRejectUnlisted Rejects further relay descriptors
-torrc.summary.AuthDirMaxServersPerAddr Limit on the number of relays accepted per ip
-torrc.summary.AuthDirMaxServersPerAuthAddr Limit on the number of relays accepted per an authority's ip
-torrc.summary.BridgePassword Password for requesting bridge information
-torrc.summary.V3AuthVotingInterval Consensus voting interval
-torrc.summary.V3AuthVoteDelay Wait time to collect votes of other authorities
-torrc.summary.V3AuthDistDelay Wait time to collect the signatures of other authorities
-torrc.summary.V3AuthNIntervalsValid Number of voting intervals a consensus is valid for
-torrc.summary.V3BandwidthsFile Path to a file containing measured relay bandwidths
-torrc.summary.V3AuthUseLegacyKey Signs consensus with both the current and legacy keys
-torrc.summary.RephistTrackTime Discards old, unchanged reliability informaition
-
-# Hidden Service Options
-
-torrc.summary.HiddenServiceDir Directory contents for the hidden service
-torrc.summary.HiddenServicePort Port the hidden service is provided on
-torrc.summary.PublishHidServDescriptors Toggles automated publishing of the hidden service to the rendezvous directory
-torrc.summary.HiddenServiceVersion Version for published hidden service descriptors
-torrc.summary.HiddenServiceAuthorizeClient Restricts access to the hidden service
-torrc.summary.RendPostPeriod Period at which the rendezvous service descriptors are refreshed
-
-# Testing Network Options
-
-torrc.summary.TestingTorNetwork Overrides other options to be a testing network
-torrc.summary.TestingV3AuthInitialVotingInterval Overrides V3AuthVotingInterval for the first consensus
-torrc.summary.TestingV3AuthInitialVoteDelay Overrides TestingV3AuthInitialVoteDelay for the first consensus
-torrc.summary.TestingV3AuthInitialDistDelay Overrides TestingV3AuthInitialDistDelay for the first consensus
-torrc.summary.TestingAuthDirTimeToLearnReachability Delay until opinions are given about which relays are running or not
-torrc.summary.TestingEstimatedDescriptorPropagationTime Delay before clients attempt to fetch descriptors from directory caches
-
diff --git a/arm/config_panel.py b/arm/config_panel.py
deleted file mode 100644
index dbae8de..0000000
--- a/arm/config_panel.py
+++ /dev/null
@@ -1,721 +0,0 @@
-"""
-Panel presenting the configuration state for tor or arm. Options can be edited
-and the resulting configuration files saved.
-"""
-
-import curses
-import threading
-
-import arm.controller
-import popups
-
-from arm.util import panel, tor_config, tor_controller, ui_tools
-
-import stem.control
-
-from stem.util import conf, enum, str_tools
-
-# TODO: The arm use cases are incomplete since they currently can't be
-# modified, have their descriptions fetched, or even get a complete listing
-# of what's available.
-
-State = enum.Enum("TOR", "ARM")  # state to be presented
-
-# mappings of option categories to the color for their entries
-
-CATEGORY_COLOR = {
-  tor_config.Category.GENERAL: "green",
-  tor_config.Category.CLIENT: "blue",
-  tor_config.Category.RELAY: "yellow",
-  tor_config.Category.DIRECTORY: "magenta",
-  tor_config.Category.AUTHORITY: "red",
-  tor_config.Category.HIDDEN_SERVICE: "cyan",
-  tor_config.Category.TESTING: "white",
-  tor_config.Category.UNKNOWN: "white",
-}
-
-# attributes of a ConfigEntry
-
-Field = enum.Enum(
-  "CATEGORY",
-  "OPTION",
-  "VALUE",
-  "TYPE",
-  "ARG_USAGE",
-  "SUMMARY",
-  "DESCRIPTION",
-  "MAN_ENTRY",
-  "IS_DEFAULT",
-)
-
-FIELD_ATTR = {
-  Field.CATEGORY: ("Category", "red"),
-  Field.OPTION: ("Option Name", "blue"),
-  Field.VALUE: ("Value", "cyan"),
-  Field.TYPE: ("Arg Type", "green"),
-  Field.ARG_USAGE: ("Arg Usage", "yellow"),
-  Field.SUMMARY: ("Summary", "green"),
-  Field.DESCRIPTION: ("Description", "white"),
-  Field.MAN_ENTRY: ("Man Page Entry", "blue"),
-  Field.IS_DEFAULT: ("Is Default", "magenta"),
-}
-
-
-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 get_field_from_label(field_label):
-  """
-  Converts field labels back to their enumeration, raising a ValueError if it
-  doesn't exist.
-  """
-
-  for entry_enum in FIELD_ATTR:
-    if field_label == FIELD_ATTR[entry_enum][0]:
-      return entry_enum
-
-
-class ConfigEntry():
-  """
-  Configuration option in the panel.
-  """
-
-  def __init__(self, option, type, is_default):
-    self.fields = {}
-    self.fields[Field.OPTION] = option
-    self.fields[Field.TYPE] = type
-    self.fields[Field.IS_DEFAULT] = is_default
-
-    # Fetches extra infromation from external sources (the arm config and tor
-    # man page). These are None if unavailable for this config option.
-
-    summary = tor_config.get_config_summary(option)
-    man_entry = tor_config.get_config_description(option)
-
-    if man_entry:
-      self.fields[Field.MAN_ENTRY] = man_entry.index
-      self.fields[Field.CATEGORY] = man_entry.category
-      self.fields[Field.ARG_USAGE] = man_entry.arg_usage
-      self.fields[Field.DESCRIPTION] = man_entry.description
-    else:
-      self.fields[Field.MAN_ENTRY] = 99999  # sorts non-man entries last
-      self.fields[Field.CATEGORY] = tor_config.Category.UNKNOWN
-      self.fields[Field.ARG_USAGE] = ""
-      self.fields[Field.DESCRIPTION] = ""
-
-    # uses the full man page description if a summary is unavailable
-
-    self.fields[Field.SUMMARY] = summary if summary is not None else self.fields[Field.DESCRIPTION]
-
-    # cache of what's displayed for this configuration option
-
-    self.label_cache = None
-    self.label_cache_args = None
-
-  def get(self, field):
-    """
-    Provides back the value in the given field.
-
-    Arguments:
-      field - enum for the field to be provided back
-    """
-
-    if field == Field.VALUE:
-      return self._get_value()
-    else:
-      return self.fields[field]
-
-  def get_all(self, fields):
-    """
-    Provides back a list with the given field values.
-
-    Arguments:
-      field - enums for the fields to be provided back
-    """
-
-    return [self.get(field) for field in fields]
-
-  def get_label(self, option_width, value_width, summary_width):
-    """
-    Provides display string of the configuration entry with the given
-    constraints on the width of the contents.
-
-    Arguments:
-      option_width  - width of the option column
-      value_width   - width of the value column
-      summary_width - width of the summary column
-    """
-
-    # Fetching the display entries is very common so this caches the values.
-    # Doing this substantially drops cpu usage when scrolling (by around 40%).
-
-    arg_set = (option_width, value_width, summary_width)
-
-    if not self.label_cache or self.label_cache_args != arg_set:
-      option_label = str_tools.crop(self.get(Field.OPTION), option_width)
-      value_label = str_tools.crop(self.get(Field.VALUE), value_width)
-      summary_label = str_tools.crop(self.get(Field.SUMMARY), summary_width, None)
-      line_text_layout = "%%-%is %%-%is %%-%is" % (option_width, value_width, summary_width)
-      self.label_cache = line_text_layout % (option_label, value_label, summary_label)
-      self.label_cache_args = arg_set
-
-    return self.label_cache
-
-  def is_unset(self):
-    """
-    True if we have no value, false otherwise.
-    """
-
-    conf_value = tor_controller().get_conf(self.get(Field.OPTION), [], True)
-
-    return not bool(conf_value)
-
-  def _get_value(self):
-    """
-    Provides the current value of the configuration entry, taking advantage of
-    the tor_tools caching to effectively query the accurate value. This uses the
-    value's type to provide a user friendly representation if able.
-    """
-
-    conf_value = ", ".join(tor_controller().get_conf(self.get(Field.OPTION), [], True))
-
-    # provides nicer values for recognized types
-
-    if not conf_value:
-      conf_value = "<none>"
-    elif self.get(Field.TYPE) == "Boolean" and conf_value in ("0", "1"):
-      conf_value = "False" if conf_value == "0" else "True"
-    elif self.get(Field.TYPE) == "DataSize" and conf_value.isdigit():
-      conf_value = str_tools.size_label(int(conf_value))
-    elif self.get(Field.TYPE) == "TimeInterval" and conf_value.isdigit():
-      conf_value = str_tools.time_label(int(conf_value), is_long = True)
-
-    return conf_value
-
-
-class ConfigPanel(panel.Panel):
-  """
-  Renders a listing of the tor or arm configuration state, allowing options to
-  be selected and edited.
-  """
-
-  def __init__(self, stdscr, config_type):
-    panel.Panel.__init__(self, stdscr, "configuration", 0)
-
-    self.config_type = config_type
-    self.conf_contents = []
-    self.conf_important_contents = []
-    self.scroller = ui_tools.Scroller(True)
-    self.vals_lock = threading.RLock()
-
-    # shows all configuration options if true, otherwise only the ones with
-    # the 'important' flag are shown
-
-    self.show_all = False
-
-    # initializes config contents if we're connected
-
-    controller = tor_controller()
-    controller.add_status_listener(self.reset_listener)
-
-    if controller.is_alive():
-      self.reset_listener(None, stem.control.State.INIT, None)
-
-  def reset_listener(self, controller, event_type, _):
-    # fetches configuration options if a new instance, otherewise keeps our
-    # current contents
-
-    if event_type == stem.control.State.INIT:
-      self._load_config_options()
-
-  def _load_config_options(self):
-    """
-    Fetches the configuration options available from tor or arm.
-    """
-
-    self.conf_contents = []
-    self.conf_important_contents = []
-
-    if self.config_type == State.TOR:
-      controller, config_option_lines = tor_controller(), []
-      custom_options = tor_config.get_custom_options()
-      config_option_query = controller.get_info("config/names", None)
-
-      if config_option_query:
-        config_option_lines = config_option_query.strip().split("\n")
-
-      for line in config_option_lines:
-        # lines are of the form "<option> <type>[ <documentation>]", like:
-        # UseEntryGuards Boolean
-        # documentation is aparently only in older versions (for instance,
-        # 0.2.1.25)
-
-        line_comp = line.strip().split(" ")
-        conf_option, conf_type = line_comp[0], line_comp[1]
-
-        # skips private and virtual entries if not configured to show them
-
-        if not CONFIG["features.config.state.showPrivateOptions"] and conf_option.startswith("__"):
-          continue
-        elif not CONFIG["features.config.state.showVirtualOptions"] and conf_type == "Virtual":
-          continue
-
-        self.conf_contents.append(ConfigEntry(conf_option, conf_type, conf_option not in custom_options))
-
-    elif self.config_type == State.ARM:
-      # loaded via the conf utility
-
-      arm_config = conf.get_config("arm")
-
-      for key in arm_config.keys():
-        pass  # TODO: implement
-
-    # mirror listing with only the important configuration options
-
-    self.conf_important_contents = []
-
-    for entry in self.conf_contents:
-      if tor_config.is_important(entry.get(Field.OPTION)):
-        self.conf_important_contents.append(entry)
-
-    # if there aren't any important options then show everything
-
-    if not self.conf_important_contents:
-      self.conf_important_contents = self.conf_contents
-
-    self.set_sort_order()  # initial sorting of the contents
-
-  def get_selection(self):
-    """
-    Provides the currently selected entry.
-    """
-
-    return self.scroller.get_cursor_selection(self._get_config_options())
-
-  def set_filtering(self, is_filtered):
-    """
-    Sets if configuration options are filtered or not.
-
-    Arguments:
-      is_filtered - if true then only relatively important options will be
-                   shown, otherwise everything is shown
-    """
-
-    self.show_all = not is_filtered
-
-  def set_sort_order(self, ordering = None):
-    """
-    Sets the configuration attributes we're sorting by and resorts the
-    contents.
-
-    Arguments:
-      ordering - new ordering, if undefined then this resorts with the last
-                 set ordering
-    """
-
-    self.vals_lock.acquire()
-
-    if ordering:
-      CONFIG["features.config.order"] = ordering
-
-    self.conf_contents.sort(key=lambda i: (i.get_all(CONFIG["features.config.order"])))
-    self.conf_important_contents.sort(key=lambda i: (i.get_all(CONFIG["features.config.order"])))
-    self.vals_lock.release()
-
-  def show_sort_dialog(self):
-    """
-    Provides the sort dialog for our configuration options.
-    """
-
-    # set ordering for config options
-
-    title_label = "Config Option Ordering:"
-    options = [FIELD_ATTR[field][0] for field in Field]
-    old_selection = [FIELD_ATTR[field][0] for field in CONFIG["features.config.order"]]
-    option_colors = dict([FIELD_ATTR[field] for field in Field])
-    results = popups.show_sort_dialog(title_label, options, old_selection, option_colors)
-
-    if results:
-      # converts labels back to enums
-      result_enums = [get_field_from_label(label) for label in results]
-      self.set_sort_order(result_enums)
-
-  def handle_key(self, key):
-    with self.vals_lock:
-      if key.is_scroll():
-        page_height = self.get_preferred_size()[0] - 1
-        detail_panel_height = CONFIG["features.config.selectionDetails.height"]
-
-        if detail_panel_height > 0 and detail_panel_height + 2 <= page_height:
-          page_height -= (detail_panel_height + 1)
-
-        is_changed = self.scroller.handle_key(key, self._get_config_options(), page_height)
-
-        if is_changed:
-          self.redraw(True)
-      elif key.is_selection() and self._get_config_options():
-        # Prompts the user to edit the selected configuration value. The
-        # interface is locked to prevent updates between setting the value
-        # and showing any errors.
-
-        with panel.CURSES_LOCK:
-          selection = self.get_selection()
-          config_option = selection.get(Field.OPTION)
-
-          if selection.is_unset():
-            initial_value = ""
-          else:
-            initial_value = selection.get(Field.VALUE)
-
-          prompt_msg = "%s Value (esc to cancel): " % config_option
-          is_prepopulated = CONFIG["features.config.prepopulateEditValues"]
-          new_value = popups.input_prompt(prompt_msg, initial_value if is_prepopulated else "")
-
-          if new_value is not None and new_value != initial_value:
-            try:
-              if selection.get(Field.TYPE) == "Boolean":
-                # if the value's a boolean then allow for 'true' and 'false' inputs
-
-                if new_value.lower() == "true":
-                  new_value = "1"
-                elif new_value.lower() == "false":
-                  new_value = "0"
-              elif selection.get(Field.TYPE) == "LineList":
-                # set_option accepts list inputs when there's multiple values
-                new_value = new_value.split(",")
-
-              tor_controller().set_conf(config_option, new_value)
-
-              # forces the label to be remade with the new value
-
-              selection.label_cache = None
-
-              # resets the is_default flag
-
-              custom_options = tor_config.get_custom_options()
-              selection.fields[Field.IS_DEFAULT] = config_option not in custom_options
-
-              self.redraw(True)
-            except Exception as exc:
-              popups.show_msg("%s (press any key)" % exc)
-      elif key.match('a'):
-        self.show_all = not self.show_all
-        self.redraw(True)
-      elif key.match('s'):
-        self.show_sort_dialog()
-      elif key.match('v'):
-        self.show_write_dialog()
-      else:
-        return False
-
-      return True
-
-  def show_write_dialog(self):
-    """
-    Provies an interface to confirm if the configuration is saved and, if so,
-    where.
-    """
-
-    # display a popup for saving the current configuration
-
-    config_lines = tor_config.get_custom_options(True)
-    popup, width, height = popups.init(len(config_lines) + 2)
-
-    if not popup:
-      return
-
-    try:
-      # displayed options (truncating the labels if there's limited room)
-
-      if width >= 30:
-        selection_options = ("Save", "Save As...", "Cancel")
-      else:
-        selection_options = ("Save", "Save As", "X")
-
-      # checks if we can show options beside the last line of visible content
-
-      is_option_line_separate = False
-      last_index = min(height - 2, len(config_lines) - 1)
-
-      # if we don't have room to display the selection options and room to
-      # grow then display the selection options on its own line
-
-      if width < (30 + len(config_lines[last_index])):
-        popup.set_height(height + 1)
-        popup.redraw(True)  # recreates the window instance
-        new_height, _ = popup.get_preferred_size()
-
-        if new_height > height:
-          height = new_height
-          is_option_line_separate = True
-
-      selection = 2
-
-      while True:
-        # if the popup has been resized then recreate it (needed for the
-        # proper border height)
-
-        new_height, new_width = popup.get_preferred_size()
-
-        if (height, width) != (new_height, new_width):
-          height, width = new_height, new_width
-          popup.redraw(True)
-
-        # if there isn't room to display the popup then cancel it
-
-        if height <= 2:
-          selection = 2
-          break
-
-        popup.win.erase()
-        popup.win.box()
-        popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
-
-        visible_config_lines = height - 3 if is_option_line_separate else height - 2
-
-        for i in range(visible_config_lines):
-          line = str_tools.crop(config_lines[i], width - 2)
-
-          if " " in line:
-            option, arg = line.split(" ", 1)
-            popup.addstr(i + 1, 1, option, curses.A_BOLD, 'green')
-            popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD, 'cyan')
-          else:
-            popup.addstr(i + 1, 1, line, curses.A_BOLD, 'green')
-
-        # draws selection options (drawn right to left)
-
-        draw_x = width - 1
-
-        for i in range(len(selection_options) - 1, -1, -1):
-          option_label = selection_options[i]
-          draw_x -= (len(option_label) + 2)
-
-          # if we've run out of room then drop the option (this will only
-          # occure on tiny displays)
-
-          if draw_x < 1:
-            break
-
-          selection_format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
-          popup.addstr(height - 2, draw_x, "[")
-          popup.addstr(height - 2, draw_x + 1, option_label, selection_format, curses.A_BOLD)
-          popup.addstr(height - 2, draw_x + len(option_label) + 1, "]")
-
-          draw_x -= 1  # space gap between the options
-
-        popup.win.refresh()
-
-        key = arm.controller.get_controller().key_input()
-
-        if key.match('left'):
-          selection = max(0, selection - 1)
-        elif key.match('right'):
-          selection = min(len(selection_options) - 1, selection + 1)
-        elif key.is_selection():
-          break
-
-      if selection in (0, 1):
-        loaded_torrc, prompt_canceled = tor_config.get_torrc(), False
-
-        try:
-          config_location = loaded_torrc.get_config_location()
-        except IOError:
-          config_location = ""
-
-        if selection == 1:
-          # prompts user for a configuration location
-          config_location = popups.input_prompt("Save to (esc to cancel): ", config_location)
-
-          if not config_location:
-            prompt_canceled = True
-
-        if not prompt_canceled:
-          try:
-            tor_config.save_conf(config_location, config_lines)
-            msg = "Saved configuration to %s" % config_location
-          except IOError as exc:
-            msg = "Unable to save configuration (%s)" % exc.strerror
-
-          popups.show_msg(msg, 2)
-    finally:
-      popups.finalize()
-
-  def get_help(self):
-    return [
-      ('up arrow', 'scroll up a line', None),
-      ('down arrow', 'scroll down a line', None),
-      ('page up', 'scroll up a page', None),
-      ('page down', 'scroll down a page', None),
-      ('enter', 'edit configuration option', None),
-      ('v', 'save configuration', None),
-      ('a', 'toggle option filtering', None),
-      ('s', 'sort ordering', None),
-    ]
-
-  def draw(self, width, height):
-    self.vals_lock.acquire()
-
-    # panel with details for the current selection
-
-    detail_panel_height = CONFIG["features.config.selectionDetails.height"]
-    is_scrollbar_visible = False
-
-    if detail_panel_height == 0 or detail_panel_height + 2 >= height:
-      # no detail panel
-
-      detail_panel_height = 0
-      scroll_location = self.scroller.get_scroll_location(self._get_config_options(), height - 1)
-      cursor_selection = self.get_selection()
-      is_scrollbar_visible = len(self._get_config_options()) > height - 1
-    else:
-      # Shrink detail panel if there isn't sufficient room for the whole
-      # thing. The extra line is for the bottom border.
-
-      detail_panel_height = min(height - 1, detail_panel_height + 1)
-      scroll_location = self.scroller.get_scroll_location(self._get_config_options(), height - 1 - detail_panel_height)
-      cursor_selection = self.get_selection()
-      is_scrollbar_visible = len(self._get_config_options()) > height - detail_panel_height - 1
-
-      if cursor_selection is not None:
-        self._draw_selection_panel(cursor_selection, width, detail_panel_height, is_scrollbar_visible)
-
-    # draws the top label
-
-    if self.is_title_visible():
-      config_type = "Tor" if self.config_type == State.TOR else "Arm"
-      hidden_msg = "press 'a' to hide most options" if self.show_all else "press 'a' to show all options"
-      title_label = "%s Configuration (%s):" % (config_type, hidden_msg)
-      self.addstr(0, 0, title_label, curses.A_STANDOUT)
-
-    # draws left-hand scroll bar if content's longer than the height
-
-    scroll_offset = 1
-
-    if is_scrollbar_visible:
-      scroll_offset = 3
-      self.add_scroll_bar(scroll_location, scroll_location + height - detail_panel_height - 1, len(self._get_config_options()), 1 + detail_panel_height)
-
-    option_width = CONFIG["features.config.state.colWidth.option"]
-    value_width = CONFIG["features.config.state.colWidth.value"]
-    description_width = max(0, width - scroll_offset - option_width - value_width - 2)
-
-    # if the description column is overly long then use its space for the
-    # value instead
-
-    if description_width > 80:
-      value_width += description_width - 80
-      description_width = 80
-
-    for line_number in range(scroll_location, len(self._get_config_options())):
-      entry = self._get_config_options()[line_number]
-      draw_line = line_number + detail_panel_height + 1 - scroll_location
-
-      line_format = [curses.A_NORMAL if entry.get(Field.IS_DEFAULT) else curses.A_BOLD]
-
-      if entry.get(Field.CATEGORY):
-        line_format += [CATEGORY_COLOR[entry.get(Field.CATEGORY)]]
-
-      if entry == cursor_selection:
-        line_format += [curses.A_STANDOUT]
-
-      line_text = entry.get_label(option_width, value_width, description_width)
-      self.addstr(draw_line, scroll_offset, line_text, *line_format)
-
-      if draw_line >= height:
-        break
-
-    self.vals_lock.release()
-
-  def _get_config_options(self):
-    return self.conf_contents if self.show_all else self.conf_important_contents
-
-  def _draw_selection_panel(self, selection, width, detail_panel_height, is_scrollbar_visible):
-    """
-    Renders a panel for the selected configuration option.
-    """
-
-    # This is a solid border unless the scrollbar is visible, in which case a
-    # 'T' pipe connects the border to the bar.
-
-    ui_tools.draw_box(self, 0, 0, width, detail_panel_height + 1)
-
-    if is_scrollbar_visible:
-      self.addch(detail_panel_height, 1, curses.ACS_TTEE)
-
-    selection_format = (curses.A_BOLD, CATEGORY_COLOR[selection.get(Field.CATEGORY)])
-
-    # first entry:
-    # <option> (<category> Option)
-
-    option_label = " (%s Option)" % selection.get(Field.CATEGORY)
-    self.addstr(1, 2, selection.get(Field.OPTION) + option_label, *selection_format)
-
-    # second entry:
-    # Value: <value> ([default|custom], <type>, usage: <argument usage>)
-
-    if detail_panel_height >= 3:
-      value_attr = []
-      value_attr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
-      value_attr.append(selection.get(Field.TYPE))
-      value_attr.append("usage: %s" % (selection.get(Field.ARG_USAGE)))
-      value_attr_label = ", ".join(value_attr)
-
-      value_label_width = width - 12 - len(value_attr_label)
-      value_label = str_tools.crop(selection.get(Field.VALUE), value_label_width)
-
-      self.addstr(2, 2, "Value: %s (%s)" % (value_label, value_attr_label), *selection_format)
-
-    # remainder is filled with the man page description
-
-    description_height = max(0, detail_panel_height - 3)
-    description_content = "Description: " + selection.get(Field.DESCRIPTION)
-
-    for i in range(description_height):
-      # checks if we're done writing the description
-
-      if not description_content:
-        break
-
-      # there's a leading indent after the first line
-
-      if i > 0:
-        description_content = "  " + description_content
-
-      # we only want to work with content up until the next newline
-
-      if "\n" in description_content:
-        line_content, description_content = description_content.split("\n", 1)
-      else:
-        line_content, description_content = description_content, ""
-
-      if i != description_height - 1:
-        # there's more lines to display
-
-        msg, remainder = str_tools.crop(line_content, width - 3, 4, 4, str_tools.Ending.HYPHEN, True)
-        description_content = remainder.strip() + description_content
-      else:
-        # this is the last line, end it with an ellipse
-
-        msg = str_tools.crop(line_content, width - 3, 4, 4)
-
-      self.addstr(3 + i, 2, msg, *selection_format)
diff --git a/arm/connections/__init__.py b/arm/connections/__init__.py
deleted file mode 100644
index 447adf6..0000000
--- a/arm/connections/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""
-Resources related to our connection panel.
-"""
-
-__all__ = [
-  'circ_entry',
-  'conn_entry',
-  'conn_panel',
-  'count_popup',
-  'descriptor_popup',
-  'entries',
-]
diff --git a/arm/connections/circ_entry.py b/arm/connections/circ_entry.py
deleted file mode 100644
index c6db152..0000000
--- a/arm/connections/circ_entry.py
+++ /dev/null
@@ -1,253 +0,0 @@
-"""
-Connection panel entries for client circuits. This includes a header entry
-followed by an entry for each hop in the circuit. For instance:
-
-89.188.20.246:42667    -->  217.172.182.26 (de)       General / Built     8.6m (CIRCUIT)
-|  85.8.28.4 (se)               98FBC3B2B93897A78CDD797EF549E6B62C9A8523    1 / Guard
-|  91.121.204.76 (fr)           546387D93F8D40CFF8842BB9D3A8EC477CEDA984    2 / Middle
-+- 217.172.182.26 (de)          5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86    3 / Exit
-"""
-
-import curses
-
-from arm.connections import entries, conn_entry
-from arm.util import tor_controller
-
-from stem.util import str_tools
-
-ADDRESS_LOOKUP_CACHE = {}
-
-
-class CircEntry(conn_entry.ConnectionEntry):
-  def __init__(self, circuit_id, status, purpose, path):
-    conn_entry.ConnectionEntry.__init__(self, "127.0.0.1", "0", "127.0.0.1", "0")
-
-    self.circuit_id = circuit_id
-    self.status = status
-
-    # drops to lowercase except the first letter
-
-    if len(purpose) >= 2:
-      purpose = purpose[0].upper() + purpose[1:].lower()
-
-    self.lines = [CircHeaderLine(self.circuit_id, purpose)]
-
-    # Overwrites attributes of the initial line to make it more fitting as the
-    # header for our listing.
-
-    self.lines[0].base_type = conn_entry.Category.CIRCUIT
-
-    self.update(status, path)
-
-  def update(self, status, path):
-    """
-    Our status and path can change over time if the circuit is still in the
-    process of being built. Updates these attributes of our relay.
-
-    Arguments:
-      status - new status of the circuit
-      path   - list of fingerprints for the series of relays involved in the
-               circuit
-    """
-
-    self.status = status
-    self.lines = [self.lines[0]]
-    controller = tor_controller()
-
-    if status == "BUILT" and not self.lines[0].is_built:
-      exit_ip, exit_port = get_relay_address(controller, path[-1], ("192.168.0.1", "0"))
-      self.lines[0].set_exit(exit_ip, exit_port, path[-1])
-
-    for i in range(len(path)):
-      relay_fingerprint = path[i]
-      relay_ip, relay_port = get_relay_address(controller, relay_fingerprint, ("192.168.0.1", "0"))
-
-      if i == len(path) - 1:
-        if status == "BUILT":
-          placement_type = "Exit"
-        else:
-          placement_type = "Extending"
-      elif i == 0:
-        placement_type = "Guard"
-      else:
-        placement_type = "Middle"
-
-      placement_label = "%i / %s" % (i + 1, placement_type)
-
-      self.lines.append(CircLine(relay_ip, relay_port, relay_fingerprint, placement_label))
-
-    self.lines[-1].is_last = True
-
-
-class CircHeaderLine(conn_entry.ConnectionLine):
-  """
-  Initial line of a client entry. This has the same basic format as connection
-  lines except that its etc field has circuit attributes.
-  """
-
-  def __init__(self, circuit_id, purpose):
-    conn_entry.ConnectionLine.__init__(self, "127.0.0.1", "0", "0.0.0.0", "0", False, False)
-    self.circuit_id = circuit_id
-    self.purpose = purpose
-    self.is_built = False
-
-  def set_exit(self, exit_address, exit_port, exit_fingerprint):
-    conn_entry.ConnectionLine.__init__(self, "127.0.0.1", "0", exit_address, exit_port, False, False)
-    self.is_built = True
-    self.foreign.fingerprint_overwrite = exit_fingerprint
-
-  def get_type(self):
-    return conn_entry.Category.CIRCUIT
-
-  def get_destination_label(self, max_length, include_locale=False, include_hostname=False):
-    if not self.is_built:
-      return "Building..."
-
-    return conn_entry.ConnectionLine.get_destination_label(self, max_length, include_locale, include_hostname)
-
-  def get_etc_content(self, width, listing_type):
-    """
-    Attempts to provide all circuit related stats. Anything that can't be
-    shown completely (not enough room) is dropped.
-    """
-
-    etc_attr = ["Purpose: %s" % self.purpose, "Circuit ID: %s" % self.circuit_id]
-
-    for i in range(len(etc_attr), -1, -1):
-      etc_label = ", ".join(etc_attr[:i])
-
-      if len(etc_label) <= width:
-        return ("%%-%is" % width) % etc_label
-
-    return ""
-
-  def get_details(self, width):
-    if not self.is_built:
-      detail_format = (curses.A_BOLD, conn_entry.CATEGORY_COLOR[self.get_type()])
-      return [("Building Circuit...", detail_format)]
-    else:
-      return conn_entry.ConnectionLine.get_details(self, width)
-
-
-class CircLine(conn_entry.ConnectionLine):
-  """
-  An individual hop in a circuit. This overwrites the displayed listing, but
-  otherwise makes use of the ConnectionLine attributes (for the detail display,
-  caching, etc).
-  """
-
-  def __init__(self, remote_address, remote_port, remote_fingerprint, placement_label):
-    conn_entry.ConnectionLine.__init__(self, "127.0.0.1", "0", remote_address, remote_port)
-    self.foreign.fingerprint_overwrite = remote_fingerprint
-    self.placement_label = placement_label
-    self.include_port = False
-
-    # determines the sort of left hand bracketing we use
-
-    self.is_last = False
-
-  def get_type(self):
-    return conn_entry.Category.CIRCUIT
-
-  def get_listing_prefix(self):
-    if self.is_last:
-      return (ord(' '), curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
-    else:
-      return (ord(' '), curses.ACS_VLINE, ord(' '), ord(' '))
-
-  def get_listing_entry(self, width, current_time, listing_type):
-    """
-    Provides the [(msg, attr)...] listing for this relay in the circuilt
-    listing. Lines are composed of the following components:
-      <bracket> <dst> <etc> <placement label>
-
-    The dst and etc entries largely match their ConnectionEntry counterparts.
-
-    Arguments:
-      width       - maximum length of the line
-      current_time - the current unix time (ignored)
-      listing_type - primary attribute we're listing connections by
-    """
-
-    return entries.ConnectionPanelLine.get_listing_entry(self, width, current_time, listing_type)
-
-  def _get_listing_entry(self, width, current_time, listing_type):
-    line_format = conn_entry.CATEGORY_COLOR[self.get_type()]
-
-    # The required widths are the sum of the following:
-    # initial space (1 character)
-    # bracketing (3 characters)
-    # placement_label (14 characters)
-    # gap between etc and placement label (5 characters)
-
-    baseline_space = 14 + 5
-
-    dst, etc = "", ""
-
-    if listing_type == entries.ListingType.IP_ADDRESS:
-      # TODO: include hostname when that's available
-      # dst width is derived as:
-      # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char
-
-      dst = "%-53s" % self.get_destination_label(53, include_locale = True)
-
-      # fills the nickname into the empty space here
-
-      dst = "%s%-25s   " % (dst[:25], str_tools.crop(self.foreign.get_nickname(), 25, 0))
-
-      etc = self.get_etc_content(width - baseline_space - len(dst), listing_type)
-    elif listing_type == entries.ListingType.HOSTNAME:
-      # min space for the hostname is 40 characters
-
-      etc = self.get_etc_content(width - baseline_space - 40, listing_type)
-      dst_layout = "%%-%is" % (width - baseline_space - len(etc))
-      dst = dst_layout % self.foreign.get_hostname(self.foreign.get_address())
-    elif listing_type == entries.ListingType.FINGERPRINT:
-      # dst width is derived as:
-      # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
-
-      dst = "%-55s" % self.foreign.get_fingerprint()
-      etc = self.get_etc_content(width - baseline_space - len(dst), listing_type)
-    else:
-      # min space for the nickname is 56 characters
-
-      etc = self.get_etc_content(width - baseline_space - 56, listing_type)
-      dst_layout = "%%-%is" % (width - baseline_space - len(etc))
-      dst = dst_layout % self.foreign.get_nickname()
-
-    return ((dst + etc, line_format),
-            (" " * (width - baseline_space - len(dst) - len(etc) + 5), line_format),
-            ("%-14s" % self.placement_label, line_format))
-
-
-def get_relay_address(controller, relay_fingerprint, default = None):
-  """
-  Provides the (IP Address, ORPort) tuple for a given relay. If the lookup
-  fails then this returns the default.
-
-  Arguments:
-    relay_fingerprint - fingerprint of the relay
-  """
-
-  result = default
-
-  if controller.is_alive():
-    # query the address if it isn't yet cached
-    if relay_fingerprint not in ADDRESS_LOOKUP_CACHE:
-      if relay_fingerprint == controller.get_info("fingerprint", None):
-        # this is us, simply check the config
-        my_address = controller.get_info("address", None)
-        my_or_port = controller.get_conf("ORPort", None)
-
-        if my_address and my_or_port:
-          ADDRESS_LOOKUP_CACHE[relay_fingerprint] = (my_address, my_or_port)
-      else:
-        # check the consensus for the relay
-        relay = controller.get_network_status(relay_fingerprint, None)
-
-        if relay:
-          ADDRESS_LOOKUP_CACHE[relay_fingerprint] = (relay.address, relay.or_port)
-
-    result = ADDRESS_LOOKUP_CACHE.get(relay_fingerprint, default)
-
-  return result
diff --git a/arm/connections/conn_entry.py b/arm/connections/conn_entry.py
deleted file mode 100644
index 98b3dca..0000000
--- a/arm/connections/conn_entry.py
+++ /dev/null
@@ -1,1258 +0,0 @@
-"""
-Connection panel entries related to actual connections to or from the system
-(ie, results seen by netstat, lsof, etc).
-"""
-
-import time
-import curses
-
-from arm.util import tor_controller
-from arm.connections import entries
-
-import stem.control
-
-from stem.util import conf, connection, enum, str_tools
-
-# Connection Categories:
-#   Inbound      Relay connection, coming to us.
-#   Outbound     Relay connection, leaving us.
-#   Exit         Outbound relay connection leaving the Tor network.
-#   Hidden       Connections to a hidden service we're providing.
-#   Socks        Socks connections for applications using Tor.
-#   Circuit      Circuits our tor client has created.
-#   Directory    Fetching tor consensus information.
-#   Control      Tor controller (arm, vidalia, etc).
-
-Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL")
-
-CATEGORY_COLOR = {
-  Category.INBOUND: "green",
-  Category.OUTBOUND: "blue",
-  Category.EXIT: "red",
-  Category.HIDDEN: "magenta",
-  Category.SOCKS: "yellow",
-  Category.CIRCUIT: "cyan",
-  Category.DIRECTORY: "magenta",
-  Category.CONTROL: "red",
-}
-
-# static data for listing format
-# <src>  -->  <dst>  <etc><padding>
-
-LABEL_FORMAT = "%s  -->  %s  %s%s"
-LABEL_MIN_PADDING = 2  # min space between listing label and following data
-
-# sort value for scrubbed ip addresses
-
-SCRUBBED_IP_VAL = 255 ** 4
-
-CONFIG = 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,
-})
-
-FINGERPRINT_TRACKER = None
-
-
-def get_fingerprint_tracker():
-  global FINGERPRINT_TRACKER
-
-  if FINGERPRINT_TRACKER is None:
-    FINGERPRINT_TRACKER = FingerprintTracker()
-
-  return FINGERPRINT_TRACKER
-
-
-class Endpoint:
-  """
-  Collection of attributes associated with a connection endpoint. This is a
-  thin wrapper for torUtil functions, making use of its caching for
-  performance.
-  """
-
-  def __init__(self, address, port):
-    self.address = address
-    self.port = port
-
-    # if true, we treat the port as an definitely not being an ORPort when
-    # searching for matching fingerprints (otherwise we use it to possably
-    # narrow results when unknown)
-
-    self.is_not_or_port = True
-
-    # if set then this overwrites fingerprint lookups
-
-    self.fingerprint_overwrite = None
-
-  def get_address(self):
-    """
-    Provides the IP address of the endpoint.
-    """
-
-    return self.address
-
-  def get_port(self):
-    """
-    Provides the port of the endpoint.
-    """
-
-    return self.port
-
-  def get_hostname(self, default = None):
-    """
-    Provides the hostname associated with the relay's address. This is a
-    non-blocking call and returns None if the address either can't be resolved
-    or hasn't been resolved yet.
-
-    Arguments:
-      default - return value if no hostname is available
-    """
-
-    # TODO: skipping all hostname resolution to be safe for now
-    # try:
-    #   myHostname = hostnames.resolve(self.address)
-    # except:
-    #   # either a ValueError or IOError depending on the source of the lookup failure
-    #   myHostname = None
-    #
-    # if not myHostname: return default
-    # else: return myHostname
-
-    return default
-
-  def get_locale(self, default=None):
-    """
-    Provides the two letter country code for the IP address' locale.
-
-    Arguments:
-      default - return value if no locale information is available
-    """
-
-    controller = tor_controller()
-    return controller.get_info("ip-to-country/%s" % self.address, default)
-
-  def get_fingerprint(self):
-    """
-    Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be
-    determined.
-    """
-
-    if self.fingerprint_overwrite:
-      return self.fingerprint_overwrite
-
-    my_fingerprint = get_fingerprint_tracker().get_relay_fingerprint(self.address)
-
-    # If there were multiple matches and our port is likely the ORPort then
-    # try again with that to narrow the results.
-
-    if not my_fingerprint and not self.is_not_or_port:
-      my_fingerprint = get_fingerprint_tracker().get_relay_fingerprint(self.address, self.port)
-
-    if my_fingerprint:
-      return my_fingerprint
-    else:
-      return "UNKNOWN"
-
-  def get_nickname(self):
-    """
-    Provides the nickname of the relay, retuning "UNKNOWN" if it can't be
-    determined.
-    """
-
-    my_fingerprint = self.get_fingerprint()
-
-    if my_fingerprint != "UNKNOWN":
-      my_nickname = get_fingerprint_tracker().get_relay_nickname(my_fingerprint)
-
-      if my_nickname:
-        return my_nickname
-      else:
-        return "UNKNOWN"
-    else:
-      return "UNKNOWN"
-
-
-class ConnectionEntry(entries.ConnectionPanelEntry):
-  """
-  Represents a connection being made to or from this system. These only
-  concern real connections so it includes the inbound, outbound, directory,
-  application, and controller categories.
-  """
-
-  def __init__(self, local_address, local_port, remote_address, remote_port):
-    entries.ConnectionPanelEntry.__init__(self)
-    self.lines = [ConnectionLine(local_address, local_port, remote_address, remote_port)]
-
-  def get_sort_value(self, attr, listing_type):
-    """
-    Provides the value of a single attribute used for sorting purposes.
-    """
-
-    connection_line = self.lines[0]
-
-    if attr == entries.SortAttr.IP_ADDRESS:
-      if connection_line.is_private():
-        return SCRUBBED_IP_VAL  # orders at the end
-
-      return connection_line.sort_address
-    elif attr == entries.SortAttr.PORT:
-      return connection_line.sort_port
-    elif attr == entries.SortAttr.HOSTNAME:
-      if connection_line.is_private():
-        return ""
-
-      return connection_line.foreign.get_hostname("")
-    elif attr == entries.SortAttr.FINGERPRINT:
-      return connection_line.foreign.get_fingerprint()
-    elif attr == entries.SortAttr.NICKNAME:
-      my_nickname = connection_line.foreign.get_nickname()
-
-      if my_nickname == "UNKNOWN":
-        return "z" * 20  # orders at the end
-      else:
-        return my_nickname.lower()
-    elif attr == entries.SortAttr.CATEGORY:
-      return Category.index_of(connection_line.get_type())
-    elif attr == entries.SortAttr.UPTIME:
-      return connection_line.start_time
-    elif attr == entries.SortAttr.COUNTRY:
-      if connection.is_private_address(self.lines[0].foreign.get_address()):
-        return ""
-      else:
-        return connection_line.foreign.get_locale("")
-    else:
-      return entries.ConnectionPanelEntry.get_sort_value(self, attr, listing_type)
-
-
-class ConnectionLine(entries.ConnectionPanelLine):
-  """
-  Display component of the ConnectionEntry.
-  """
-
-  def __init__(self, local_address, local_port, remote_address, remote_port, include_port=True, include_expanded_addresses=True):
-    entries.ConnectionPanelLine.__init__(self)
-
-    self.local = Endpoint(local_address, local_port)
-    self.foreign = Endpoint(remote_address, remote_port)
-    self.start_time = time.time()
-    self.is_initial_connection = False
-
-    # overwrite the local fingerprint with ours
-
-    controller = tor_controller()
-    self.local.fingerprint_overwrite = controller.get_info("fingerprint", None)
-
-    # True if the connection has matched the properties of a client/directory
-    # connection every time we've checked. The criteria we check is...
-    #   client    - first hop in an established circuit
-    #   directory - matches an established single-hop circuit (probably a
-    #               directory mirror)
-
-    self._possible_client = True
-    self._possible_directory = True
-
-    # attributes for SOCKS, HIDDEN, and CONTROL connections
-
-    self.application_name = None
-    self.application_pid = None
-    self.is_application_resolving = False
-
-    my_or_port = controller.get_conf("ORPort", None)
-    my_dir_port = controller.get_conf("DirPort", None)
-    my_socks_port = controller.get_conf("SocksPort", "9050")
-    my_ctl_port = controller.get_conf("ControlPort", None)
-    my_hidden_service_ports = get_hidden_service_ports(controller)
-
-    # the ORListenAddress can overwrite the ORPort
-
-    listen_addr = controller.get_conf("ORListenAddress", None)
-
-    if listen_addr and ":" in listen_addr:
-      my_or_port = listen_addr[listen_addr.find(":") + 1:]
-
-    if local_port in (my_or_port, my_dir_port):
-      self.base_type = Category.INBOUND
-      self.local.is_not_or_port = False
-    elif local_port == my_socks_port:
-      self.base_type = Category.SOCKS
-    elif remote_port in my_hidden_service_ports:
-      self.base_type = Category.HIDDEN
-    elif local_port == my_ctl_port:
-      self.base_type = Category.CONTROL
-    else:
-      self.base_type = Category.OUTBOUND
-      self.foreign.is_not_or_port = False
-
-    self.cached_type = None
-
-    # includes the port or expanded ip address field when displaying listing
-    # information if true
-
-    self.include_port = include_port
-    self.include_expanded_addresses = include_expanded_addresses
-
-    # cached immutable values used for sorting
-
-    ip_value = 0
-
-    for comp in self.foreign.get_address().split("."):
-      ip_value *= 255
-      ip_value += int(comp)
-
-    self.sort_address = ip_value
-    self.sort_port = int(self.foreign.get_port())
-
-  def get_listing_entry(self, width, current_time, listing_type):
-    """
-    Provides the tuple list for this connection's listing. Lines are composed
-    of the following components:
-      <src>  -->  <dst>     <etc>     <uptime> (<type>)
-
-    ListingType.IP_ADDRESS:
-      src - <internal addr:port> --> <external addr:port>
-      dst - <destination addr:port>
-      etc - <fingerprint> <nickname>
-
-    ListingType.HOSTNAME:
-      src - localhost:<port>
-      dst - <destination hostname:port>
-      etc - <destination addr:port> <fingerprint> <nickname>
-
-    ListingType.FINGERPRINT:
-      src - localhost
-      dst - <destination fingerprint>
-      etc - <nickname> <destination addr:port>
-
-    ListingType.NICKNAME:
-      src - <source nickname>
-      dst - <destination nickname>
-      etc - <fingerprint> <destination addr:port>
-
-    Arguments:
-      width       - maximum length of the line
-      current_time - unix timestamp for what the results should consider to be
-                    the current time
-      listing_type - primary attribute we're listing connections by
-    """
-
-    # fetch our (most likely cached) display entry for the listing
-
-    my_listing = entries.ConnectionPanelLine.get_listing_entry(self, width, current_time, listing_type)
-
-    # fill in the current uptime and return the results
-
-    if CONFIG["features.connection.markInitialConnections"]:
-      time_prefix = "+" if self.is_initial_connection else " "
-    else:
-      time_prefix = ""
-
-    time_label = time_prefix + "%5s" % str_tools.time_label(current_time - self.start_time, 1)
-    my_listing[2] = (time_label, my_listing[2][1])
-
-    return my_listing
-
-  def is_unresolved_application(self):
-    """
-    True if our display uses application information that hasn't yet been resolved.
-    """
-
-    return self.application_name is None and self.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
-
-  def _get_listing_entry(self, width, current_time, listing_type):
-    entry_type = self.get_type()
-
-    # Lines are split into the following components in reverse:
-    # init gap - " "
-    # content  - "<src>  -->  <dst>     <etc>     "
-    # time     - "<uptime>"
-    # preType  - " ("
-    # category - "<type>"
-    # postType - ")   "
-
-    line_format = CATEGORY_COLOR[entry_type]
-    time_width = 6 if CONFIG["features.connection.markInitialConnections"] else 5
-
-    draw_entry = [(" ", line_format),
-                  (self._get_listing_content(width - (12 + time_width) - 1, listing_type), line_format),
-                  (" " * time_width, line_format),
-                  (" (", line_format),
-                  (entry_type.upper(), line_format, curses.A_BOLD),
-                  (")" + " " * (9 - len(entry_type)), line_format)]
-
-    return draw_entry
-
-  def _get_details(self, width):
-    """
-    Provides details on the connection, correlated against available consensus
-    data.
-
-    Arguments:
-      width - available space to display in
-    """
-
-    detail_format = (curses.A_BOLD, CATEGORY_COLOR[self.get_type()])
-    return [(line, detail_format) for line in self._get_detail_content(width)]
-
-  def reset_display(self):
-    entries.ConnectionPanelLine.reset_display(self)
-    self.cached_type = None
-
-  def is_private(self):
-    """
-    Returns true if the endpoint is private, possibly belonging to a client
-    connection or exit traffic.
-    """
-
-    if not CONFIG["features.connection.showIps"]:
-      return True
-
-    # This is used to scrub private information from the interface. Relaying
-    # etiquette (and wiretapping laws) say these are bad things to look at so
-    # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON!
-
-    my_type = self.get_type()
-
-    if my_type == Category.INBOUND:
-      # if we're a guard or bridge and the connection doesn't belong to a
-      # known relay then it might be client traffic
-
-      controller = tor_controller()
-
-      my_flags = []
-      my_fingerprint = self.get_info("fingerprint", None)
-
-      if my_fingerprint:
-        my_status_entry = self.controller.get_network_status(my_fingerprint)
-
-        if my_status_entry:
-          my_flags = my_status_entry.flags
-
-      if "Guard" in my_flags or controller.get_conf("BridgeRelay", None) == "1":
-        all_matches = get_fingerprint_tracker().get_relay_fingerprint(self.foreign.get_address(), get_all_matches = True)
-
-        return all_matches == []
-    elif my_type == Category.EXIT:
-      # DNS connections exiting us aren't private (since they're hitting our
-      # resolvers). Everything else, however, is.
-
-      # TODO: Ideally this would also double check that it's a UDP connection
-      # (since DNS is the only UDP connections Tor will relay), however this
-      # will take a bit more work to propagate the information up from the
-      # connection resolver.
-
-      return self.foreign.get_port() != "53"
-
-    # for everything else this isn't a concern
-
-    return False
-
-  def get_type(self):
-    """
-    Provides our best guess at the current type of the connection. This
-    depends on consensus results, our current client circuits, etc. Results
-    are cached until this entry's display is reset.
-    """
-
-    # caches both to simplify the calls and to keep the type consistent until
-    # we want to reflect changes
-
-    if not self.cached_type:
-      if self.base_type == Category.OUTBOUND:
-        # Currently the only non-static categories are OUTBOUND vs...
-        # - EXIT since this depends on the current consensus
-        # - CIRCUIT if this is likely to belong to our guard usage
-        # - DIRECTORY if this is a single-hop circuit (directory mirror?)
-        #
-        # The exitability, circuits, and fingerprints are all cached by the
-        # tor_tools util keeping this a quick lookup.
-
-        controller = tor_controller()
-        destination_fingerprint = self.foreign.get_fingerprint()
-
-        if destination_fingerprint == "UNKNOWN":
-          # Not a known relay. This might be an exit connection.
-
-          if is_exiting_allowed(controller, self.foreign.get_address(), self.foreign.get_port()):
-            self.cached_type = Category.EXIT
-        elif self._possible_client or self._possible_directory:
-          # This belongs to a known relay. If we haven't eliminated ourselves as
-          # a possible client or directory connection then check if it still
-          # holds true.
-
-          my_circuits = controller.get_circuits([])
-
-          if self._possible_client:
-            # Checks that this belongs to the first hop in a circuit that's
-            # either unestablished or longer than a single hop (ie, anything but
-            # a built 1-hop connection since those are most likely a directory
-            # mirror).
-
-            for circ in my_circuits:
-              if circ.path and circ.path[0][0] == destination_fingerprint and (circ.status != "BUILT" or len(circ.path) > 1):
-                self.cached_type = Category.CIRCUIT  # matched a probable guard connection
-
-            # if we fell through, we can eliminate ourselves as a guard in the future
-            if not self.cached_type:
-              self._possible_client = False
-
-          if self._possible_directory:
-            # Checks if we match a built, single hop circuit.
-
-            for circ in my_circuits:
-              if circ.path and circ.path[0][0] == destination_fingerprint and circ.status == "BUILT" and len(circ.path) == 1:
-                self.cached_type = Category.DIRECTORY
-
-            # if we fell through, eliminate ourselves as a directory connection
-            if not self.cached_type:
-              self._possible_directory = False
-
-      if not self.cached_type:
-        self.cached_type = self.base_type
-
-    return self.cached_type
-
-  def get_etc_content(self, width, listing_type):
-    """
-    Provides the optional content for the connection.
-
-    Arguments:
-      width       - maximum length of the line
-      listing_type - primary attribute we're listing connections by
-    """
-
-    # for applications show the command/pid
-
-    if self.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
-      display_label = ""
-
-      if self.application_name:
-        if self.application_pid:
-          display_label = "%s (%s)" % (self.application_name, self.application_pid)
-        else:
-          display_label = self.application_name
-      elif self.is_application_resolving:
-        display_label = "resolving..."
-      else:
-        display_label = "UNKNOWN"
-
-      if len(display_label) < width:
-        return ("%%-%is" % width) % display_label
-      else:
-        return ""
-
-    # for everything else display connection/consensus information
-
-    destination_address = self.get_destination_label(26, include_locale = True)
-    etc, used_space = "", 0
-
-    if listing_type == entries.ListingType.IP_ADDRESS:
-      if width > used_space + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-
-        etc += "%-40s  " % self.foreign.get_fingerprint()
-        used_space += 42
-
-      if width > used_space + 10 and CONFIG["features.connection.showColumn.nickname"]:
-        # show nickname (column width: remainder)
-
-        nickname_space = width - used_space
-        nickname_label = str_tools.crop(self.foreign.get_nickname(), nickname_space, 0)
-        etc += ("%%-%is  " % nickname_space) % nickname_label
-        used_space += nickname_space + 2
-    elif listing_type == entries.ListingType.HOSTNAME:
-      if width > used_space + 28 and CONFIG["features.connection.showColumn.destination"]:
-        # show destination ip/port/locale (column width: 28 characters)
-        etc += "%-26s  " % destination_address
-        used_space += 28
-
-      if width > used_space + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.get_fingerprint()
-        used_space += 42
-
-      if width > used_space + 17 and CONFIG["features.connection.showColumn.nickname"]:
-        # show nickname (column width: min 17 characters, uses half of the remainder)
-        nickname_space = 15 + (width - (used_space + 17)) / 2
-        nickname_label = str_tools.crop(self.foreign.get_nickname(), nickname_space, 0)
-        etc += ("%%-%is  " % nickname_space) % nickname_label
-        used_space += (nickname_space + 2)
-    elif listing_type == entries.ListingType.FINGERPRINT:
-      if width > used_space + 17:
-        # show nickname (column width: min 17 characters, consumes any remaining space)
-
-        nickname_space = width - used_space - 2
-
-        # if there's room then also show a column with the destination
-        # ip/port/locale (column width: 28 characters)
-
-        is_locale_included = width > used_space + 45
-        is_locale_included &= CONFIG["features.connection.showColumn.destination"]
-
-        if is_locale_included:
-          nickname_space -= 28
-
-        if CONFIG["features.connection.showColumn.nickname"]:
-          nickname_label = str_tools.crop(self.foreign.get_nickname(), nickname_space, 0)
-          etc += ("%%-%is  " % nickname_space) % nickname_label
-          used_space += nickname_space + 2
-
-        if is_locale_included:
-          etc += "%-26s  " % destination_address
-          used_space += 28
-    else:
-      if width > used_space + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
-        # show fingerprint (column width: 42 characters)
-        etc += "%-40s  " % self.foreign.get_fingerprint()
-        used_space += 42
-
-      if width > used_space + 28 and CONFIG["features.connection.showColumn.destination"]:
-        # show destination ip/port/locale (column width: 28 characters)
-        etc += "%-26s  " % destination_address
-        used_space += 28
-
-    return ("%%-%is" % width) % etc
-
-  def _get_listing_content(self, width, listing_type):
-    """
-    Provides the source, destination, and extra info for our listing.
-
-    Arguments:
-      width       - maximum length of the line
-      listing_type - primary attribute we're listing connections by
-    """
-
-    controller = tor_controller()
-    my_type = self.get_type()
-    destination_address = self.get_destination_label(26, include_locale = True)
-
-    # The required widths are the sum of the following:
-    # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters)
-    # - base data for the listing
-    # - that extra field plus any previous
-
-    used_space = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING
-    local_port = ":%s" % self.local.get_port() if self.include_port else ""
-
-    src, dst, etc = "", "", ""
-
-    if listing_type == entries.ListingType.IP_ADDRESS:
-      my_external_address = controller.get_info("address", self.local.get_address())
-      address_differ = my_external_address != self.local.get_address()
-
-      # Expanding doesn't make sense, if the connection isn't actually
-      # going through Tor's external IP address. As there isn't a known
-      # method for checking if it is, we're checking the type instead.
-      #
-      # This isn't entirely correct. It might be a better idea to check if
-      # the source and destination addresses are both private, but that might
-      # not be perfectly reliable either.
-
-      is_expansion_type = my_type not in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
-
-      if is_expansion_type:
-        src_address = my_external_address + local_port
-      else:
-        src_address = self.local.get_address() + local_port
-
-      if my_type in (Category.SOCKS, Category.CONTROL):
-        # Like inbound connections these need their source and destination to
-        # be swapped. However, this only applies when listing by IP or hostname
-        # (their fingerprint and nickname are both for us). Reversing the
-        # fields here to keep the same column alignments.
-
-        src = "%-21s" % destination_address
-        dst = "%-26s" % src_address
-      else:
-        src = "%-21s" % src_address  # ip:port = max of 21 characters
-        dst = "%-26s" % destination_address  # ip:port (xx) = max of 26 characters
-
-      used_space += len(src) + len(dst)  # base data requires 47 characters
-
-      # Showing the fingerprint (which has the width of 42) has priority over
-      # an expanded address field. Hence check if we either have space for
-      # both or wouldn't be showing the fingerprint regardless.
-
-      is_expanded_address_visible = width > used_space + 28
-
-      if is_expanded_address_visible and CONFIG["features.connection.showColumn.fingerprint"]:
-        is_expanded_address_visible = width < used_space + 42 or width > used_space + 70
-
-      if address_differ and is_expansion_type and is_expanded_address_visible and self.include_expanded_addresses and CONFIG["features.connection.showColumn.expandedIp"]:
-        # include the internal address in the src (extra 28 characters)
-
-        internal_address = self.local.get_address() + local_port
-
-        # If this is an inbound connection then reverse ordering so it's:
-        # <foreign> --> <external> --> <internal>
-        # when the src and dst are swapped later
-
-        if my_type == Category.INBOUND:
-          src = "%-21s  -->  %s" % (src, internal_address)
-        else:
-          src = "%-21s  -->  %s" % (internal_address, src)
-
-        used_space += 28
-
-      etc = self.get_etc_content(width - used_space, listing_type)
-      used_space += len(etc)
-    elif listing_type == entries.ListingType.HOSTNAME:
-      # 15 characters for source, and a min of 40 reserved for the destination
-      # TODO: when actually functional the src and dst need to be swapped for
-      # SOCKS and CONTROL connections
-
-      src = "localhost%-6s" % local_port
-      used_space += len(src)
-      min_hostname_space = 40
-
-      etc = self.get_etc_content(width - used_space - min_hostname_space, listing_type)
-      used_space += len(etc)
-
-      hostname_space = width - used_space
-      used_space = width  # prevents padding at the end
-
-      if self.is_private():
-        dst = ("%%-%is" % hostname_space) % "<scrubbed>"
-      else:
-        hostname = self.foreign.get_hostname(self.foreign.get_address())
-        port_label = ":%-5s" % self.foreign.get_port() if self.include_port else ""
-
-        # truncates long hostnames and sets dst to <hostname>:<port>
-
-        hostname = str_tools.crop(hostname, hostname_space, 0)
-        dst = ("%%-%is" % hostname_space) % (hostname + port_label)
-    elif listing_type == entries.ListingType.FINGERPRINT:
-      src = "localhost"
-
-      if my_type == Category.CONTROL:
-        dst = "localhost"
-      else:
-        dst = self.foreign.get_fingerprint()
-
-      dst = "%-40s" % dst
-
-      used_space += len(src) + len(dst)  # base data requires 49 characters
-
-      etc = self.get_etc_content(width - used_space, listing_type)
-      used_space += len(etc)
-    else:
-      # base data requires 50 min characters
-      src = self.local.get_nickname()
-
-      if my_type == Category.CONTROL:
-        dst = self.local.get_nickname()
-      else:
-        dst = self.foreign.get_nickname()
-
-      min_base_space = 50
-
-      etc = self.get_etc_content(width - used_space - min_base_space, listing_type)
-      used_space += len(etc)
-
-      base_space = width - used_space
-      used_space = width  # prevents padding at the end
-
-      if len(src) + len(dst) > base_space:
-        src = str_tools.crop(src, base_space / 3)
-        dst = str_tools.crop(dst, base_space - len(src))
-
-      # pads dst entry to its max space
-
-      dst = ("%%-%is" % (base_space - len(src))) % dst
-
-    if my_type == Category.INBOUND:
-      src, dst = dst, src
-
-    padding = " " * (width - used_space + LABEL_MIN_PADDING)
-
-    return LABEL_FORMAT % (src, dst, etc, padding)
-
-  def _get_detail_content(self, width):
-    """
-    Provides a list with detailed information for this connection.
-
-    Arguments:
-      width - max length of lines
-    """
-
-    lines = [""] * 7
-    lines[0] = "address: %s" % self.get_destination_label(width - 11)
-    lines[1] = "locale: %s" % ("??" if self.is_private() else self.foreign.get_locale("??"))
-
-    # Remaining data concerns the consensus results, with three possible cases:
-    # - if there's a single match then display its details
-    # - if there's multiple potential relays then list all of the combinations
-    #   of ORPorts / Fingerprints
-    # - if no consensus data is available then say so (probably a client or
-    #   exit connection)
-
-    fingerprint = self.foreign.get_fingerprint()
-    controller = tor_controller()
-
-    if fingerprint != "UNKNOWN":
-      # single match - display information available about it
-
-      ns_entry = controller.get_info("ns/id/%s" % fingerprint, None)
-      desc_entry = controller.get_info("desc/id/%s" % fingerprint, None)
-
-      # append the fingerprint to the second line
-
-      lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint)
-
-      if ns_entry:
-        # example consensus entry:
-        # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0
-        # s Exit Fast Guard Named Running Stable Valid
-        # w Bandwidth=2540
-        # p accept 20-23,43,53,79-81,88,110,143,194,443
-
-        ns_lines = ns_entry.split("\n")
-
-        first_line_comp = ns_lines[0].split(" ")
-
-        if len(first_line_comp) >= 9:
-          _, nickname, _, _, published_date, published_time, _, or_port, dir_port = first_line_comp[:9]
-        else:
-          nickname, published_date, published_time, or_port, dir_port = "", "", "", "", ""
-
-        flags = "unknown"
-
-        if len(ns_lines) >= 2 and ns_lines[1].startswith("s "):
-          flags = ns_lines[1][2:]
-
-        exit_policy = None
-        descriptor = controller.get_server_descriptor(fingerprint, None)
-
-        if descriptor:
-          exit_policy = descriptor.exit_policy
-
-        if exit_policy:
-          policy_label = exit_policy.summary()
-        else:
-          policy_label = "unknown"
-
-        dir_port_label = "" if dir_port == "0" else "dirport: %s" % dir_port
-        lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, or_port, dir_port_label)
-        lines[3] = "published: %s %s" % (published_time, published_date)
-        lines[4] = "flags: %s" % flags.replace(" ", ", ")
-        lines[5] = "exit policy: %s" % policy_label
-
-      if desc_entry:
-        tor_version, platform, contact = "", "", ""
-
-        for desc_line in desc_entry.split("\n"):
-          if desc_line.startswith("platform"):
-            # has the tor version and platform, ex:
-            # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64
-
-            tor_version = desc_line[13:desc_line.find(" ", 13)]
-            platform = desc_line[desc_line.rfind(" on ") + 4:]
-          elif desc_line.startswith("contact"):
-            contact = desc_line[8:]
-
-            # clears up some highly common obscuring
-
-            for alias in (" at ", " AT "):
-              contact = contact.replace(alias, "@")
-
-            for alias in (" dot ", " DOT "):
-              contact = contact.replace(alias, ".")
-
-            break  # contact lines come after the platform
-
-        lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, tor_version)
-
-        # contact information is an optional field
-
-        if contact:
-          lines[6] = "contact: %s" % contact
-    else:
-      all_matches = get_fingerprint_tracker().get_relay_fingerprint(self.foreign.get_address(), get_all_matches = True)
-
-      if all_matches:
-        # multiple matches
-        lines[2] = "Multiple matches, possible fingerprints are:"
-
-        for i in range(len(all_matches)):
-          is_last_line = i == 3
-
-          relay_port, relay_fingerprint = all_matches[i]
-          line_text = "%i. or port: %-5s fingerprint: %s" % (i, relay_port, relay_fingerprint)
-
-          # if there's multiple lines remaining at the end then give a count
-
-          remaining_relays = len(all_matches) - i
-
-          if is_last_line and remaining_relays > 1:
-            line_text = "... %i more" % remaining_relays
-
-          lines[3 + i] = line_text
-
-          if is_last_line:
-            break
-      else:
-        # no consensus entry for this ip address
-        lines[2] = "No consensus data found"
-
-    # crops any lines that are too long
-
-    for i in range(len(lines)):
-      lines[i] = str_tools.crop(lines[i], width - 2)
-
-    return lines
-
-  def get_destination_label(self, max_length, include_locale = False, include_hostname = False):
-    """
-    Provides a short description of the destination. This is made up of two
-    components, the base <ip addr>:<port> and an extra piece of information in
-    parentheses. The IP address is scrubbed from private connections.
-
-    Extra information is...
-    - the port's purpose for exit connections
-    - the locale and/or hostname if set to do so, the address isn't private,
-      and isn't on the local network
-    - nothing otherwise
-
-    Arguments:
-      max_length       - maximum length of the string returned
-      include_locale   - possibly includes the locale
-      include_hostname - possibly includes the hostname
-    """
-
-    # the port and port derived data can be hidden by config or without include_port
-
-    include_port = self.include_port and (CONFIG["features.connection.showExitPort"] or self.get_type() != Category.EXIT)
-
-    # destination of the connection
-
-    address_label = "<scrubbed>" if self.is_private() else self.foreign.get_address()
-    port_label = ":%s" % self.foreign.get_port() if include_port else ""
-    destination_address = address_label + port_label
-
-    # Only append the extra info if there's at least a couple characters of
-    # space (this is what's needed for the country codes).
-
-    if len(destination_address) + 5 <= max_length:
-      space_available = max_length - len(destination_address) - 3
-
-      if self.get_type() == Category.EXIT and include_port:
-        purpose = connection.port_usage(self.foreign.get_port())
-
-        if purpose:
-          # BitTorrent is a common protocol to truncate, so just use "Torrent"
-          # if there's not enough room.
-
-          if len(purpose) > space_available and purpose == "BitTorrent":
-            purpose = "Torrent"
-
-          # crops with a hyphen if too long
-
-          purpose = str_tools.crop(purpose, space_available, ending = str_tools.Ending.HYPHEN)
-
-          destination_address += " (%s)" % purpose
-      elif not connection.is_private_address(self.foreign.get_address()):
-        extra_info = []
-        controller = tor_controller()
-
-        if include_locale and not controller.is_geoip_unavailable():
-          foreign_locale = self.foreign.get_locale("??")
-          extra_info.append(foreign_locale)
-          space_available -= len(foreign_locale) + 2
-
-        if include_hostname:
-          destination_hostname = self.foreign.get_hostname()
-
-          if destination_hostname:
-            # determines the full space available, taking into account the ", "
-            # dividers if there's multiple pieces of extra data
-
-            max_hostname_space = space_available - 2 * len(extra_info)
-            destination_hostname = str_tools.crop(destination_hostname, max_hostname_space)
-            extra_info.append(destination_hostname)
-            space_available -= len(destination_hostname)
-
-        if extra_info:
-          destination_address += " (%s)" % ", ".join(extra_info)
-
-    return destination_address[:max_length]
-
-
-def get_hidden_service_ports(controller, default = []):
-  """
-  Provides the target ports hidden services are configured to use.
-
-  Arguments:
-    default - value provided back if unable to query the hidden service ports
-  """
-
-  result = []
-  hs_options = controller.get_conf_map("HiddenServiceOptions", {})
-
-  for entry in hs_options.get("HiddenServicePort", []):
-    # HiddenServicePort entries are of the form...
-    #
-    #   VIRTPORT [TARGET]
-    #
-    # ... with the TARGET being an address, port, or address:port. If the
-    # target port isn't defined then uses the VIRTPORT.
-
-    hs_port = None
-
-    if ' ' in entry:
-      virtport, target = entry.split(' ', 1)
-
-      if ':' in target:
-        hs_port = target.split(':', 1)[1]  # target is an address:port
-      elif target.isdigit():
-        hs_port = target  # target is a port
-      else:
-        hs_port = virtport  # target is an address
-    else:
-      hs_port = entry  # just has the virtual port
-
-    if hs_port.isdigit():
-      result.append(hs_port)
-
-  if result:
-    return result
-  else:
-    return default
-
-
-def is_exiting_allowed(controller, ip_address, port):
-  """
-  Checks if the given destination can be exited to by this relay, returning
-  True if so and False otherwise.
-  """
-
-  result = False
-
-  if controller.is_alive():
-    # If we allow any exiting then this could be relayed DNS queries,
-    # otherwise the policy is checked. Tor still makes DNS connections to
-    # test when exiting isn't allowed, but nothing is relayed over them.
-    # I'm registering these as non-exiting to avoid likely user confusion:
-    # https://trac.torproject.org/projects/tor/ticket/965
-
-    our_policy = controller.get_exit_policy(None)
-
-    if our_policy and our_policy.is_exiting_allowed() and port == "53":
-      result = True
-    else:
-      result = our_policy and our_policy.can_exit_to(ip_address, port)
-
-  return result
-
-
-class FingerprintTracker:
-  def __init__(self):
-    # mappings of ip -> [(port, fingerprint), ...]
-
-    self._fingerprint_mappings = None
-
-    # lookup cache with (ip, port) -> fingerprint mappings
-
-    self._fingerprint_lookup_cache = {}
-
-    # lookup cache with fingerprint -> nickname mappings
-
-    self._nickname_lookup_cache = {}
-
-    controller = tor_controller()
-
-    controller.add_event_listener(self.new_consensus_event, stem.control.EventType.NEWCONSENSUS)
-    controller.add_event_listener(self.new_desc_event, stem.control.EventType.NEWDESC)
-
-  def new_consensus_event(self, event):
-    self._fingerprint_lookup_cache = {}
-    self._nickname_lookup_cache = {}
-
-    if self._fingerprint_mappings is not None:
-      self._fingerprint_mappings = self._get_fingerprint_mappings(event.desc)
-
-  def new_desc_event(self, event):
-    # If we're tracking ip address -> fingerprint mappings then update with
-    # the new relays.
-
-    self._fingerprint_lookup_cache = {}
-
-    if self._fingerprint_mappings is not None:
-      desc_fingerprints = [fingerprint for (fingerprint, nickname) in event.relays]
-
-      for fingerprint in desc_fingerprints:
-        # gets consensus data for the new descriptor
-
-        try:
-          desc = tor_controller().get_network_status(fingerprint)
-        except stem.ControllerError:
-          continue
-
-        # updates fingerprintMappings with new data
-
-        if desc.address in self._fingerprint_mappings:
-          # if entry already exists with the same orport, remove it
-
-          orport_match = None
-
-          for entry_port, entry_fingerprint in self._fingerprint_mappings[desc.address]:
-            if entry_port == desc.or_port:
-              orport_match = (entry_port, entry_fingerprint)
-              break
-
-          if orport_match:
-            self._fingerprint_mappings[desc.address].remove(orport_match)
-
-          # add the new entry
-
-          self._fingerprint_mappings[desc.address].append((desc.or_port, desc.fingerprint))
-        else:
-          self._fingerprint_mappings[desc.address] = [(desc.or_port, desc.fingerprint)]
-
-  def get_relay_fingerprint(self, relay_address, relay_port = None, get_all_matches = False):
-    """
-    Provides the fingerprint associated with the given address. If there's
-    multiple potential matches or the mapping is unknown then this returns
-    None. This disambiguates the fingerprint if there's multiple relays on
-    the same ip address by several methods, one of them being to pick relays
-    we have a connection with.
-
-    Arguments:
-      relay_address  - address of relay to be returned
-      relay_port     - orport of relay (to further narrow the results)
-      get_all_matches - ignores the relay_port and provides all of the
-                      (port, fingerprint) tuples matching the given
-                      address
-    """
-
-    result = None
-    controller = tor_controller()
-
-    if controller.is_alive():
-      if get_all_matches:
-        # populates the ip -> fingerprint mappings if not yet available
-        if self._fingerprint_mappings is None:
-          self._fingerprint_mappings = self._get_fingerprint_mappings()
-
-        if relay_address in self._fingerprint_mappings:
-          result = self._fingerprint_mappings[relay_address]
-        else:
-          result = []
-      else:
-        # query the fingerprint if it isn't yet cached
-        if (relay_address, relay_port) not in self._fingerprint_lookup_cache:
-          relay_fingerprint = self._get_relay_fingerprint(controller, relay_address, relay_port)
-          self._fingerprint_lookup_cache[(relay_address, relay_port)] = relay_fingerprint
-
-        result = self._fingerprint_lookup_cache[(relay_address, relay_port)]
-
-    return result
-
-  def get_relay_nickname(self, relay_fingerprint):
-    """
-    Provides the nickname associated with the given relay. This provides None
-    if no such relay exists, and "Unnamed" if the name hasn't been set.
-
-    Arguments:
-      relay_fingerprint - fingerprint of the relay
-    """
-
-    result = None
-    controller = tor_controller()
-
-    if controller.is_alive():
-      # query the nickname if it isn't yet cached
-      if relay_fingerprint not in self._nickname_lookup_cache:
-        if relay_fingerprint == controller.get_info("fingerprint", None):
-          # this is us, simply check the config
-          my_nickname = controller.get_conf("Nickname", "Unnamed")
-          self._nickname_lookup_cache[relay_fingerprint] = my_nickname
-        else:
-          ns_entry = controller.get_network_status(relay_fingerprint, None)
-
-          if ns_entry:
-            self._nickname_lookup_cache[relay_fingerprint] = ns_entry.nickname
-
-      result = self._nickname_lookup_cache[relay_fingerprint]
-
-    return result
-
-  def _get_relay_fingerprint(self, controller, relay_address, relay_port):
-    """
-    Provides the fingerprint associated with the address/port combination.
-
-    Arguments:
-      relay_address - address of relay to be returned
-      relay_port    - orport of relay (to further narrow the results)
-    """
-
-    # If we were provided with a string port then convert to an int (so
-    # lookups won't mismatch based on type).
-
-    if isinstance(relay_port, str):
-      relay_port = int(relay_port)
-
-    # checks if this matches us
-
-    if relay_address == controller.get_info("address", None):
-      if not relay_port or str(relay_port) == controller.get_conf("ORPort", None):
-        return controller.get_info("fingerprint", None)
-
-    # if we haven't yet populated the ip -> fingerprint mappings then do so
-
-    if self._fingerprint_mappings is None:
-      self._fingerprint_mappings = self._get_fingerprint_mappings()
-
-    potential_matches = self._fingerprint_mappings.get(relay_address)
-
-    if not potential_matches:
-      return None  # no relay matches this ip address
-
-    if len(potential_matches) == 1:
-      # There's only one relay belonging to this ip address. If the port
-      # matches then we're done.
-
-      match = potential_matches[0]
-
-      if relay_port and match[0] != relay_port:
-        return None
-      else:
-        return match[1]
-    elif relay_port:
-      # Multiple potential matches, so trying to match based on the port.
-      for entry_port, entry_fingerprint in potential_matches:
-        if entry_port == relay_port:
-          return entry_fingerprint
-
-    return None
-
-  def _get_fingerprint_mappings(self, descriptors = None):
-    """
-    Provides IP address to (port, fingerprint) tuple mappings for all of the
-    currently cached relays.
-
-    Arguments:
-      descriptors - router status entries (fetched if not provided)
-    """
-
-    results = {}
-    controller = tor_controller()
-
-    if controller.is_alive():
-      # fetch the current network status if not provided
-
-      if not descriptors:
-        try:
-          descriptors = controller.get_network_statuses()
-        except stem.ControllerError:
-          descriptors = []
-
-      # construct mappings of ips to relay data
-
-      for desc in descriptors:
-        results.setdefault(desc.address, []).append((desc.or_port, desc.fingerprint))
-
-    return results
diff --git a/arm/connections/conn_panel.py b/arm/connections/conn_panel.py
deleted file mode 100644
index 76b2807..0000000
--- a/arm/connections/conn_panel.py
+++ /dev/null
@@ -1,674 +0,0 @@
-"""
-Listing of the currently established connections tor has made.
-"""
-
-import re
-import time
-import curses
-import threading
-
-import arm.popups
-import arm.util.tracker
-
-from arm.connections import count_popup, descriptor_popup, entries, conn_entry, circ_entry
-from arm.util import panel, tor_controller, tracker, ui_tools
-
-from stem.control import State
-from stem.util import conf, connection, enum
-
-# height of the detail panel content, not counting top and bottom border
-
-DETAILS_HEIGHT = 7
-
-# listing types
-
-Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-
-
-def conf_handler(key, value):
-  if key == "features.connection.listing_type":
-    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.listing_type": 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):
-  """
-  Listing of connections tor is making, with information correlated against
-  the current consensus and other data sources.
-  """
-
-  def __init__(self, stdscr):
-    panel.Panel.__init__(self, stdscr, "connections", 0)
-    threading.Thread.__init__(self)
-    self.setDaemon(True)
-
-    # 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 not CONFIG["features.connection.showIps"] and CONFIG["features.connection.listing_type"] == 0:
-      arm_config = conf.get_config("arm")
-      arm_config.set("features.connection.listing_type", Listing.keys()[Listing.index_of(Listing.FINGERPRINT)])
-
-    self._scroller = ui_tools.Scroller(True)
-    self._title = "Connections:"  # title line of the panel
-    self._entries = []            # last fetched display entries
-    self._entry_lines = []        # individual lines rendered from the entries listing
-    self._show_details = False    # presents the details panel if true
-
-    self._last_update = -1        # time the content was last revised
-    self._is_tor_running = True   # indicates if tor is currently running or not
-    self._halt_time = None        # time when tor was stopped
-    self._halt = False            # terminates thread if true
-    self._cond = threading.Condition()  # used for pausing the thread
-    self.vals_lock = threading.RLock()
-
-    # Tracks exiting port and client country statistics
-
-    self._client_locale_usage = {}
-    self._exit_port_usage = {}
-
-    # If we're a bridge and been running over a day then prepopulates with the
-    # last day's clients.
-
-    controller = tor_controller()
-    bridge_clients = controller.get_info("status/clients-seen", None)
-
-    if bridge_clients:
-      # Response has a couple arguments...
-      # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8
-
-      country_summary = None
-
-      for arg in bridge_clients.split():
-        if arg.startswith("CountrySummary="):
-          country_summary = arg[15:]
-          break
-
-      if country_summary:
-        for entry in country_summary.split(","):
-          if re.match("^..=[0-9]+$", entry):
-            locale, count = entry.split("=", 1)
-            self._client_locale_usage[locale] = int(count)
-
-    # Last sampling received from the ConnectionResolver, used to detect when
-    # it changes.
-
-    self._last_resource_fetch = -1
-
-    # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections
-
-    self._app_resolver = tracker.get_port_usage_tracker()
-
-    # rate limits appResolver queries to once per update
-
-    self.app_resolve_since_update = False
-
-    # mark the initially exitsing connection uptimes as being estimates
-
-    for entry in self._entries:
-      if isinstance(entry, conn_entry.ConnectionEntry):
-        entry.getLines()[0].is_initial_connection = True
-
-    # listens for when tor stops so we know to stop reflecting changes
-
-    controller.add_status_listener(self.tor_state_listener)
-
-  def tor_state_listener(self, controller, event_type, _):
-    """
-    Freezes the connection contents when Tor stops.
-    """
-
-    self._is_tor_running = event_type in (State.INIT, State.RESET)
-
-    if self._is_tor_running:
-      self._halt_time = None
-    else:
-      self._halt_time = time.time()
-
-    self.redraw(True)
-
-  def get_pause_time(self):
-    """
-    Provides the time Tor stopped if it isn't running. Otherwise this is the
-    time we were last paused.
-    """
-
-    if self._halt_time:
-      return self._halt_time
-    else:
-      return panel.Panel.get_pause_time(self)
-
-  def set_sort_order(self, ordering = None):
-    """
-    Sets the connection attributes we're sorting by and resorts the contents.
-
-    Arguments:
-      ordering - new ordering, if undefined then this resorts with the last
-                 set ordering
-    """
-
-    self.vals_lock.acquire()
-
-    if ordering:
-      arm_config = conf.get_config("arm")
-
-      ordering_keys = [entries.SortAttr.keys()[entries.SortAttr.index_of(v)] for v in ordering]
-      arm_config.set("features.connection.order", ", ".join(ordering_keys))
-
-    self._entries.sort(key = lambda i: (i.get_sort_values(CONFIG["features.connection.order"], self.get_listing_type())))
-
-    self._entry_lines = []
-
-    for entry in self._entries:
-      self._entry_lines += entry.getLines()
-
-    self.vals_lock.release()
-
-  def get_listing_type(self):
-    """
-    Provides the priority content we list connections by.
-    """
-
-    return CONFIG["features.connection.listing_type"]
-
-  def set_listing_type(self, listing_type):
-    """
-    Sets the priority information presented by the panel.
-
-    Arguments:
-      listing_type - Listing instance for the primary information to be shown
-    """
-
-    if self.get_listing_type() == listing_type:
-      return
-
-    self.vals_lock.acquire()
-
-    arm_config = conf.get_config("arm")
-    arm_config.set("features.connection.listing_type", Listing.keys()[Listing.index_of(listing_type)])
-
-    # if we're sorting by the listing then we need to resort
-
-    if entries.SortAttr.LISTING in CONFIG["features.connection.order"]:
-      self.set_sort_order()
-
-    self.vals_lock.release()
-
-  def is_clients_allowed(self):
-    """
-    True if client connections are permissable, false otherwise.
-    """
-
-    controller = tor_controller()
-
-    my_flags = []
-    my_fingerprint = self.get_info("fingerprint", None)
-
-    if my_fingerprint:
-      my_status_entry = self.controller.get_network_status(my_fingerprint)
-
-      if my_status_entry:
-        my_flags = my_status_entry.flags
-
-    return "Guard" in my_flags or controller.get_conf("BridgeRelay", None) == "1"
-
-  def is_exits_allowed(self):
-    """
-    True if exit connections are permissable, false otherwise.
-    """
-
-    controller = tor_controller()
-
-    if not controller.get_conf("ORPort", None):
-      return False  # no ORPort
-
-    policy = controller.get_exit_policy(None)
-
-    return policy and policy.is_exiting_allowed()
-
-  def show_sort_dialog(self):
-    """
-    Provides the sort dialog for our connections.
-    """
-
-    # set ordering for connection options
-
-    title_label = "Connection Ordering:"
-    options = list(entries.SortAttr)
-    old_selection = CONFIG["features.connection.order"]
-    option_colors = dict([(attr, entries.SORT_COLORS[attr]) for attr in options])
-    results = arm.popups.show_sort_dialog(title_label, options, old_selection, option_colors)
-
-    if results:
-      self.set_sort_order(results)
-
-  def handle_key(self, key):
-    with self.vals_lock:
-      if key.is_scroll():
-        page_height = self.get_preferred_size()[0] - 1
-
-        if self._show_details:
-          page_height -= (DETAILS_HEIGHT + 1)
-
-        is_changed = self._scroller.handle_key(key, self._entry_lines, page_height)
-
-        if is_changed:
-          self.redraw(True)
-      elif key.is_selection():
-        self._show_details = not self._show_details
-        self.redraw(True)
-      elif key.match('s'):
-        self.show_sort_dialog()
-      elif key.match('u'):
-        # provides a menu to pick the connection resolver
-
-        title = "Resolver Util:"
-        options = ["auto"] + list(connection.Resolver)
-        conn_resolver = arm.util.tracker.get_connection_tracker()
-
-        current_overwrite = conn_resolver.get_custom_resolver()
-
-        if current_overwrite is None:
-          old_selection = 0
-        else:
-          old_selection = options.index(current_overwrite)
-
-        selection = arm.popups.show_menu(title, options, old_selection)
-
-        # applies new setting
-
-        if selection != -1:
-          selected_option = options[selection] if selection != 0 else None
-          conn_resolver.set_custom_resolver(selected_option)
-      elif key.match('l'):
-        # provides a menu to pick the primary information we list connections by
-
-        title = "List By:"
-        options = list(entries.ListingType)
-
-        # dropping the HOSTNAME listing type until we support displaying that content
-
-        options.remove(arm.connections.entries.ListingType.HOSTNAME)
-
-        old_selection = options.index(self.get_listing_type())
-        selection = arm.popups.show_menu(title, options, old_selection)
-
-        # applies new setting
-
-        if selection != -1:
-          self.set_listing_type(options[selection])
-      elif key.match('d'):
-        # presents popup for raw consensus data
-        descriptor_popup.show_descriptor_popup(self)
-      elif key.match('c') and self.is_clients_allowed():
-        count_popup.showCountDialog(count_popup.CountType.CLIENT_LOCALE, self._client_locale_usage)
-      elif key.match('e') and self.is_exits_allowed():
-        count_popup.showCountDialog(count_popup.CountType.EXIT_PORT, self._exit_port_usage)
-      else:
-        return False
-
-      return True
-
-  def run(self):
-    """
-    Keeps connections listing updated, checking for new entries at a set rate.
-    """
-
-    last_draw = time.time() - 1
-
-    # Fetches out initial connection results. The wait is so this doesn't
-    # run during arm's interface initialization (otherwise there's a
-    # noticeable pause before the first redraw).
-
-    self._cond.acquire()
-    self._cond.wait(0.2)
-    self._cond.release()
-    self._update()             # populates initial entries
-    self._resolve_apps(False)  # resolves initial applications
-
-    while not self._halt:
-      current_time = time.time()
-
-      if self.is_paused() or not self._is_tor_running or current_time - last_draw < CONFIG["features.connection.refreshRate"]:
-        self._cond.acquire()
-
-        if not self._halt:
-          self._cond.wait(0.2)
-
-        self._cond.release()
-      else:
-        # updates content if their's new results, otherwise just redraws
-
-        self._update()
-        self.redraw(True)
-
-        # we may have missed multiple updates due to being paused, showing
-        # another panel, etc so last_draw might need to jump multiple ticks
-
-        draw_ticks = (time.time() - last_draw) / CONFIG["features.connection.refreshRate"]
-        last_draw += CONFIG["features.connection.refreshRate"] * draw_ticks
-
-  def get_help(self):
-    resolver_util = arm.util.tracker.get_connection_tracker().get_custom_resolver()
-
-    options = [
-      ('up arrow', 'scroll up a line', None),
-      ('down arrow', 'scroll down a line', None),
-      ('page up', 'scroll up a page', None),
-      ('page down', 'scroll down a page', None),
-      ('enter', 'show connection details', None),
-      ('d', 'raw consensus descriptor', None),
-    ]
-
-    if self.is_clients_allowed():
-      options.append(('c', 'client locale usage summary', None))
-
-    if self.is_exits_allowed():
-      options.append(('e', 'exit port usage summary', None))
-
-    options.append(('l', 'listed identity', self.get_listing_type().lower()))
-    options.append(('s', 'sort ordering', None))
-    options.append(('u', 'resolving utility', 'auto' if resolver_util is None else resolver_util))
-    return options
-
-  def get_selection(self):
-    """
-    Provides the currently selected connection entry.
-    """
-
-    return self._scroller.get_cursor_selection(self._entry_lines)
-
-  def draw(self, width, height):
-    self.vals_lock.acquire()
-
-    # if we don't have any contents then refuse to show details
-
-    if not self._entries:
-      self._show_details = False
-
-    # extra line when showing the detail panel is for the bottom border
-
-    detail_panel_offset = DETAILS_HEIGHT + 1 if self._show_details else 0
-    is_scrollbar_visible = len(self._entry_lines) > height - detail_panel_offset - 1
-
-    scroll_location = self._scroller.get_scroll_location(self._entry_lines, height - detail_panel_offset - 1)
-    cursor_selection = self.get_selection()
-
-    # draws the detail panel if currently displaying it
-
-    if self._show_details and cursor_selection:
-      # This is a solid border unless the scrollbar is visible, in which case a
-      # 'T' pipe connects the border to the bar.
-
-      ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
-
-      if is_scrollbar_visible:
-        self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
-
-      draw_entries = cursor_selection.get_details(width)
-
-      for i in range(min(len(draw_entries), DETAILS_HEIGHT)):
-        self.addstr(1 + i, 2, draw_entries[i][0], *draw_entries[i][1])
-
-    # title label with connection counts
-
-    if self.is_title_visible():
-      title = "Connection Details:" if self._show_details else self._title
-      self.addstr(0, 0, title, curses.A_STANDOUT)
-
-    scroll_offset = 0
-
-    if is_scrollbar_visible:
-      scroll_offset = 2
-      self.add_scroll_bar(scroll_location, scroll_location + height - detail_panel_offset - 1, len(self._entry_lines), 1 + detail_panel_offset)
-
-    if self.is_paused() or not self._is_tor_running:
-      current_time = self.get_pause_time()
-    else:
-      current_time = time.time()
-
-    for line_number in range(scroll_location, len(self._entry_lines)):
-      entry_line = self._entry_lines[line_number]
-
-      # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up
-      # resolution for the applicaitions they belong to
-
-      if isinstance(entry_line, conn_entry.ConnectionLine) and entry_line.is_unresolved_application():
-        self._resolve_apps()
-
-      # hilighting if this is the selected line
-
-      extra_format = curses.A_STANDOUT if entry_line == cursor_selection else curses.A_NORMAL
-
-      draw_line = line_number + detail_panel_offset + 1 - scroll_location
-
-      prefix = entry_line.get_listing_prefix()
-
-      for i in range(len(prefix)):
-        self.addch(draw_line, scroll_offset + i, prefix[i])
-
-      x_offset = scroll_offset + len(prefix)
-      draw_entry = entry_line.get_listing_entry(width - scroll_offset - len(prefix), current_time, self.get_listing_type())
-
-      for msg, attr in draw_entry:
-        attr |= extra_format
-        self.addstr(draw_line, x_offset, msg, *attr)
-        x_offset += len(msg)
-
-      if draw_line >= height:
-        break
-
-    self.vals_lock.release()
-
-  def stop(self):
-    """
-    Halts further resolutions and terminates the thread.
-    """
-
-    self._cond.acquire()
-    self._halt = True
-    self._cond.notifyAll()
-    self._cond.release()
-
-  def _update(self):
-    """
-    Fetches the newest resolved connections.
-    """
-
-    self.app_resolve_since_update = False
-
-    # if we don't have an initialized resolver then this is a no-op
-
-    if not arm.util.tracker.get_connection_tracker().is_alive():
-      return
-
-    conn_resolver = arm.util.tracker.get_connection_tracker()
-    current_resolution_count = conn_resolver.run_counter()
-
-    self.vals_lock.acquire()
-
-    new_entries = []  # the new results we'll display
-
-    # Fetches new connections and client circuits...
-    # new_connections  [(local ip, local port, foreign ip, foreign port)...]
-    # new_circuits     {circuit_id => (status, purpose, path)...}
-
-    new_connections = [(conn.local_address, conn.local_port, conn.remote_address, conn.remote_port) for conn in conn_resolver.get_value()]
-    new_circuits = {}
-
-    for circ in tor_controller().get_circuits():
-      # Skips established single-hop circuits (these are for directory
-      # fetches, not client circuits)
-
-      if not (circ.status == "BUILT" and len(circ.path) == 1):
-        new_circuits[circ.id] = (circ.status, circ.purpose, [entry[0] for entry in circ.path])
-
-    # Populates new_entries with any of our old entries that still exist.
-    # This is both for performance and to keep from resetting the uptime
-    # attributes. Note that CircEntries are a ConnectionEntry subclass so
-    # we need to check for them first.
-
-    for old_entry in self._entries:
-      if isinstance(old_entry, circ_entry.CircEntry):
-        new_entry = new_circuits.get(old_entry.circuit_id)
-
-        if new_entry:
-          old_entry.update(new_entry[0], new_entry[2])
-          new_entries.append(old_entry)
-          del new_circuits[old_entry.circuit_id]
-      elif isinstance(old_entry, conn_entry.ConnectionEntry):
-        connection_line = old_entry.getLines()[0]
-        conn_attr = (connection_line.local.get_address(), connection_line.local.get_port(),
-                     connection_line.foreign.get_address(), connection_line.foreign.get_port())
-
-        if conn_attr in new_connections:
-          new_entries.append(old_entry)
-          new_connections.remove(conn_attr)
-
-    # Reset any display attributes for the entries we're keeping
-
-    for entry in new_entries:
-      entry.reset_display()
-
-    # Adds any new connection and circuit entries.
-
-    for local_address, local_port, remote_address, remote_port in new_connections:
-      new_conn_entry = conn_entry.ConnectionEntry(local_address, local_port, remote_address, remote_port)
-      new_conn_line = new_conn_entry.getLines()[0]
-
-      if new_conn_line.get_type() != conn_entry.Category.CIRCUIT:
-        new_entries.append(new_conn_entry)
-
-        # updates exit port and client locale usage information
-        if new_conn_line.is_private():
-          if new_conn_line.get_type() == conn_entry.Category.INBOUND:
-            # client connection, update locale information
-
-            client_locale = new_conn_line.foreign.get_locale()
-
-            if client_locale:
-              self._client_locale_usage[client_locale] = self._client_locale_usage.get(client_locale, 0) + 1
-          elif new_conn_line.get_type() == conn_entry.Category.EXIT:
-            exit_port = new_conn_line.foreign.get_port()
-            self._exit_port_usage[exit_port] = self._exit_port_usage.get(exit_port, 0) + 1
-
-    for circuit_id in new_circuits:
-      status, purpose, path = new_circuits[circuit_id]
-      new_entries.append(circ_entry.CircEntry(circuit_id, status, purpose, path))
-
-    # Counts the relays in each of the categories. This also flushes the
-    # type cache for all of the connections (in case its changed since last
-    # fetched).
-
-    category_types = list(conn_entry.Category)
-    type_counts = dict((type, 0) for type in category_types)
-
-    for entry in new_entries:
-      if isinstance(entry, conn_entry.ConnectionEntry):
-        type_counts[entry.getLines()[0].get_type()] += 1
-      elif isinstance(entry, circ_entry.CircEntry):
-        type_counts[conn_entry.Category.CIRCUIT] += 1
-
-    # makes labels for all the categories with connections (ie,
-    # "21 outbound", "1 control", etc)
-
-    count_labels = []
-
-    for category in category_types:
-      if type_counts[category] > 0:
-        count_labels.append("%i %s" % (type_counts[category], category.lower()))
-
-    if count_labels:
-      self._title = "Connections (%s):" % ", ".join(count_labels)
-    else:
-      self._title = "Connections:"
-
-    self._entries = new_entries
-
-    self._entry_lines = []
-
-    for entry in self._entries:
-      self._entry_lines += entry.getLines()
-
-    self.set_sort_order()
-    self._last_resource_fetch = current_resolution_count
-    self.vals_lock.release()
-
-  def _resolve_apps(self, flag_query = True):
-    """
-    Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and
-    CONTROL entries.
-
-    Arguments:
-      flag_query - sets a flag to prevent further call from being respected
-                  until the next update if true
-    """
-
-    if self.app_resolve_since_update or not CONFIG["features.connection.resolveApps"]:
-      return
-
-    unresolved_lines = [l for l in self._entry_lines if isinstance(l, conn_entry.ConnectionLine) and l.is_unresolved_application()]
-
-    # get the ports used for unresolved applications
-
-    app_ports = []
-
-    for line in unresolved_lines:
-      app_conn = line.local if line.get_type() == conn_entry.Category.HIDDEN else line.foreign
-      app_ports.append(app_conn.get_port())
-
-    # Queue up resolution for the unresolved ports (skips if it's still working
-    # on the last query).
-
-    if app_ports and not self._app_resolver.is_alive():
-      self._app_resolver.get_processes_using_ports(app_ports)
-
-    # Fetches results. If the query finishes quickly then this is what we just
-    # asked for, otherwise these belong to an earlier resolution.
-    #
-    # The application resolver might have given up querying (for instance, if
-    # the lsof lookups aren't working on this platform or lacks permissions).
-    # The is_application_resolving flag lets the unresolved entries indicate if there's
-    # a lookup in progress for them or not.
-
-    time.sleep(0.2)  # TODO: previous resolver only blocked while awaiting a lookup
-    app_results = self._app_resolver.get_processes_using_ports(app_ports)
-
-    for line in unresolved_lines:
-      is_local = line.get_type() == conn_entry.Category.HIDDEN
-      line_port = line.local.get_port() if is_local else line.foreign.get_port()
-
-      if line_port in app_results:
-        # sets application attributes if there's a result with this as the
-        # inbound port
-
-        for inbound_port, outbound_port, cmd, pid in app_results[line_port]:
-          app_port = outbound_port if is_local else inbound_port
-
-          if line_port == app_port:
-            line.application_name = cmd
-            line.application_pid = pid
-            line.is_application_resolving = False
-      else:
-        line.is_application_resolving = self._app_resolver.is_alive
-
-    if flag_query:
-      self.app_resolve_since_update = True
diff --git a/arm/connections/count_popup.py b/arm/connections/count_popup.py
deleted file mode 100644
index 1e22870..0000000
--- a/arm/connections/count_popup.py
+++ /dev/null
@@ -1,111 +0,0 @@
-"""
-Provides a dialog with client locale or exiting port counts.
-"""
-
-import curses
-import operator
-
-import arm.controller
-import arm.popups
-
-from stem.util import connection, enum, log
-
-CountType = enum.Enum("CLIENT_LOCALE", "EXIT_PORT")
-EXIT_USAGE_WIDTH = 15
-
-
-def showCountDialog(count_type, counts):
-  """
-  Provides a dialog with bar graphs and percentages for the given set of
-  counts. Pressing any key closes the dialog.
-
-  Arguments:
-    count_type - type of counts being presented
-    counts    - mapping of labels to counts
-  """
-
-  is_no_stats = not counts
-  no_stats_msg = "Usage stats aren't available yet, press any key..."
-
-  if is_no_stats:
-    popup, width, height = arm.popups.init(3, len(no_stats_msg) + 4)
-  else:
-    popup, width, height = arm.popups.init(4 + max(1, len(counts)), 80)
-
-  if not popup:
-    return
-
-  try:
-    control = arm.controller.get_controller()
-
-    popup.win.box()
-
-    # dialog title
-
-    if count_type == CountType.CLIENT_LOCALE:
-      title = "Client Locales"
-    elif count_type == CountType.EXIT_PORT:
-      title = "Exiting Port Usage"
-    else:
-      title = ""
-      log.warn("Unrecognized count type: %s" % count_type)
-
-    popup.addstr(0, 0, title, curses.A_STANDOUT)
-
-    if is_no_stats:
-      popup.addstr(1, 2, no_stats_msg, curses.A_BOLD, 'cyan')
-    else:
-      sorted_counts = sorted(counts.iteritems(), key=operator.itemgetter(1))
-      sorted_counts.reverse()
-
-      # constructs string formatting for the max key and value display width
-
-      key_width, val_width, value_total = 3, 1, 0
-
-      for k, v in sorted_counts:
-        key_width = max(key_width, len(k))
-        val_width = max(val_width, len(str(v)))
-        value_total += v
-
-      # extra space since we're adding usage informaion
-
-      if count_type == CountType.EXIT_PORT:
-        key_width += EXIT_USAGE_WIDTH
-
-      label_format = "%%-%is %%%ii (%%%%%%-2i)" % (key_width, val_width)
-
-      for i in range(height - 4):
-        k, v = sorted_counts[i]
-
-        # includes a port usage column
-
-        if count_type == CountType.EXIT_PORT:
-          usage = connection.port_usage(k)
-
-          if usage:
-            key_format = "%%-%is   %%s" % (key_width - EXIT_USAGE_WIDTH)
-            k = key_format % (k, usage[:EXIT_USAGE_WIDTH - 3])
-
-        label = label_format % (k, v, v * 100 / value_total)
-        popup.addstr(i + 1, 2, label, curses.A_BOLD, 'green')
-
-        # All labels have the same size since they're based on the max widths.
-        # If this changes then this'll need to be the max label width.
-
-        label_width = len(label)
-
-        # draws simple bar graph for percentages
-
-        fill_width = v * (width - 4 - label_width) / value_total
-
-        for j in range(fill_width):
-          popup.addstr(i + 1, 3 + label_width + j, " ", curses.A_STANDOUT, 'red')
-
-      popup.addstr(height - 2, 2, "Press any key...")
-
-    popup.win.refresh()
-
-    curses.cbreak()
-    control.key_input()
-  finally:
-    arm.popups.finalize()
diff --git a/arm/connections/descriptor_popup.py b/arm/connections/descriptor_popup.py
deleted file mode 100644
index d169309..0000000
--- a/arm/connections/descriptor_popup.py
+++ /dev/null
@@ -1,273 +0,0 @@
-"""
-Popup providing the raw descriptor and consensus information for a relay.
-"""
-
-import math
-import curses
-
-import arm.popups
-import arm.connections.conn_entry
-
-from arm.util import panel, tor_controller, ui_tools
-
-from stem.util import str_tools
-
-# field keywords used to identify areas for coloring
-
-LINE_NUM_COLOR = "yellow"
-HEADER_COLOR = "cyan"
-HEADER_PREFIX = ["ns/id/", "desc/id/"]
-
-SIG_COLOR = "red"
-SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
-SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
-
-UNRESOLVED_MSG = "No consensus data available"
-ERROR_MSG = "Unable to retrieve data"
-
-
-def show_descriptor_popup(conn_panel):
-  """
-  Presents consensus descriptor in popup window with the following controls:
-  Up, Down, Page Up, Page Down - scroll descriptor
-  Right, Left - next / previous connection
-  Enter, Space, d, D - close popup
-
-  Arguments:
-    conn_panel - connection panel providing the dialog
-  """
-
-  # hides the title of the connection panel
-
-  conn_panel.set_title_visible(False)
-  conn_panel.redraw(True)
-
-  control = arm.controller.get_controller()
-  panel.CURSES_LOCK.acquire()
-  is_done = False
-
-  try:
-    while not is_done:
-      selection = conn_panel.get_selection()
-
-      if not selection:
-        break
-
-      fingerprint = selection.foreign.get_fingerprint()
-
-      if fingerprint == "UNKNOWN":
-        fingerprint = None
-
-      display_text = get_display_text(fingerprint)
-      display_color = arm.connections.conn_entry.CATEGORY_COLOR[selection.get_type()]
-      show_line_number = fingerprint is not None
-
-      # determines the maximum popup size the display_text can fill
-
-      popup_height, popup_width = get_preferred_size(display_text, conn_panel.max_x, show_line_number)
-
-      popup, _, height = arm.popups.init(popup_height, popup_width)
-
-      if not popup:
-        break
-
-      scroll, is_changed = 0, True
-
-      try:
-        while not is_done:
-          if is_changed:
-            draw(popup, fingerprint, display_text, display_color, scroll, show_line_number)
-            is_changed = False
-
-          key = control.key_input()
-
-          if key.is_scroll():
-            # TODO: This is a bit buggy in that scrolling is by display_text
-            # lines rather than the displayed lines, causing issues when
-            # content wraps. The result is that we can't have a scrollbar and
-            # can't scroll to the bottom if there's a multi-line being
-            # displayed. However, trying to correct this introduces a big can
-            # of worms and after hours decided that this isn't worth the
-            # effort...
-
-            new_scroll = ui_tools.get_scroll_position(key, scroll, height - 2, len(display_text))
-
-            if scroll != new_scroll:
-              scroll, is_changed = new_scroll, True
-          elif key.is_selection() or key.match('d'):
-            is_done = True  # closes popup
-          elif key.match('left', 'right'):
-            # navigation - pass on to conn_panel and recreate popup
-
-            conn_panel.handle_key(panel.KeyInput(curses.KEY_UP) if key.match('left') else panel.KeyInput(curses.KEY_DOWN))
-            break
-      finally:
-        arm.popups.finalize()
-  finally:
-    conn_panel.set_title_visible(True)
-    conn_panel.redraw(True)
-    panel.CURSES_LOCK.release()
-
-
-def get_display_text(fingerprint):
-  """
-  Provides the descriptor and consensus entry for a relay. This is a list of
-  lines to be displayed by the dialog.
-  """
-
-  if not fingerprint:
-    return [UNRESOLVED_MSG]
-
-  controller, description = tor_controller(), []
-
-  description.append("ns/id/%s" % fingerprint)
-  consensus_entry = controller.get_info("ns/id/%s" % fingerprint, None)
-
-  if consensus_entry:
-    description += consensus_entry.split("\n")
-  else:
-    description += [ERROR_MSG, ""]
-
-  description.append("desc/id/%s" % fingerprint)
-  descriptor_entry = controller.get_info("desc/id/%s" % fingerprint, None)
-
-  if descriptor_entry:
-    description += descriptor_entry.split("\n")
-  else:
-    description += [ERROR_MSG]
-
-  return description
-
-
-def get_preferred_size(text, max_width, show_line_number):
-  """
-  Provides the (height, width) tuple for the preferred size of the given text.
-  """
-
-  width, height = 0, len(text) + 2
-  line_number_width = int(math.log10(len(text))) + 1
-
-  for line in text:
-    # width includes content, line number field, and border
-
-    line_width = len(line) + 5
-
-    if show_line_number:
-      line_width += line_number_width
-
-    width = max(width, line_width)
-
-    # tracks number of extra lines that will be taken due to text wrap
-    height += (line_width - 2) / max_width
-
-  return (height, width)
-
-
-def draw(popup, fingerprint, display_text, display_color, scroll, show_line_number):
-  popup.win.erase()
-  popup.win.box()
-  x_offset = 2
-
-  if fingerprint:
-    title = "Consensus Descriptor (%s):" % fingerprint
-  else:
-    title = "Consensus Descriptor:"
-
-  popup.addstr(0, 0, title, curses.A_STANDOUT)
-
-  line_number_width = int(math.log10(len(display_text))) + 1
-  is_encryption_block = False   # flag indicating if we're currently displaying a key
-
-  # checks if first line is in an encryption block
-
-  for i in range(0, scroll):
-    line_text = display_text[i].strip()
-
-    if line_text in SIG_START_KEYS:
-      is_encryption_block = True
-    elif line_text in SIG_END_KEYS:
-      is_encryption_block = False
-
-  draw_line, page_height = 1, popup.max_y - 2
-
-  for i in range(scroll, scroll + page_height):
-    line_text = display_text[i].strip()
-    x_offset = 2
-
-    if show_line_number:
-      line_number_label = ("%%%ii" % line_number_width) % (i + 1)
-
-      popup.addstr(draw_line, x_offset, line_number_label, curses.A_BOLD, LINE_NUM_COLOR)
-      x_offset += line_number_width + 1
-
-    # Most consensus and descriptor lines are keyword/value pairs. Both are
-    # shown with the same color, but the keyword is bolded.
-
-    keyword, value = line_text, ""
-    draw_format = display_color
-
-    if line_text.startswith(HEADER_PREFIX[0]) or line_text.startswith(HEADER_PREFIX[1]):
-      keyword, value = line_text, ""
-      draw_format = HEADER_COLOR
-    elif line_text == UNRESOLVED_MSG or line_text == ERROR_MSG:
-      keyword, value = line_text, ""
-    elif line_text in SIG_START_KEYS:
-      keyword, value = line_text, ""
-      is_encryption_block = True
-      draw_format = SIG_COLOR
-    elif line_text in SIG_END_KEYS:
-      keyword, value = line_text, ""
-      is_encryption_block = False
-      draw_format = SIG_COLOR
-    elif is_encryption_block:
-      keyword, value = "", line_text
-      draw_format = SIG_COLOR
-    elif " " in line_text:
-      div_index = line_text.find(" ")
-      keyword, value = line_text[:div_index], line_text[div_index:]
-
-    display_queue = [(keyword, (draw_format, curses.A_BOLD)), (value, (draw_format,))]
-    cursor_location = x_offset
-
-    while display_queue:
-      msg, msg_format = display_queue.pop(0)
-
-      if not msg:
-        continue
-
-      max_msg_size = popup.max_x - 1 - cursor_location
-
-      if len(msg) >= max_msg_size:
-        # needs to split up the line
-
-        msg, remainder = str_tools.crop(msg, max_msg_size, None, ending = None, get_remainder = True)
-
-        if x_offset == cursor_location and msg == "":
-          # first word is longer than the line
-
-          msg = str_tools.crop(remainder, max_msg_size)
-
-          if " " in remainder:
-            remainder = remainder.split(" ", 1)[1]
-          else:
-            remainder = ""
-
-        popup.addstr(draw_line, cursor_location, msg, *msg_format)
-        cursor_location = x_offset
-
-        if remainder:
-          display_queue.insert(0, (remainder.strip(), msg_format))
-          draw_line += 1
-      else:
-        popup.addstr(draw_line, cursor_location, msg, *msg_format)
-        cursor_location += len(msg)
-
-      if draw_line > page_height:
-        break
-
-    draw_line += 1
-
-    if draw_line > page_height:
-      break
-
-  popup.win.refresh()
diff --git a/arm/connections/entries.py b/arm/connections/entries.py
deleted file mode 100644
index 85bce32..0000000
--- a/arm/connections/entries.py
+++ /dev/null
@@ -1,179 +0,0 @@
-"""
-Interface for entries in the connection panel. These consist of two parts: the
-entry itself (ie, Tor connection, client circuit, etc) and the lines it
-consists of in the listing.
-"""
-
-from stem.util import enum
-
-# attributes we can list entries by
-
-ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
-
-SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT", "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY")
-
-SORT_COLORS = {
-  SortAttr.CATEGORY: "red",
-  SortAttr.UPTIME: "yellow",
-  SortAttr.LISTING: "green",
-  SortAttr.IP_ADDRESS: "blue",
-  SortAttr.PORT: "blue",
-  SortAttr.HOSTNAME: "magenta",
-  SortAttr.FINGERPRINT: "cyan",
-  SortAttr.NICKNAME: "cyan",
-  SortAttr.COUNTRY: "blue",
-}
-
-# maximum number of ports a system can have
-
-PORT_COUNT = 65536
-
-
-class ConnectionPanelEntry:
-  """
-  Common parent for connection panel entries. This consists of a list of lines
-  in the panel listing. This caches results until the display indicates that
-  they should be flushed.
-  """
-
-  def __init__(self):
-    self.lines = []
-    self.flush_cache = True
-
-  def getLines(self):
-    """
-    Provides the individual lines in the connection listing.
-    """
-
-    if self.flush_cache:
-      self.lines = self._get_lines(self.lines)
-      self.flush_cache = False
-
-    return self.lines
-
-  def _get_lines(self, old_results):
-    # implementation of getLines
-
-    for line in old_results:
-      line.reset_display()
-
-    return old_results
-
-  def get_sort_values(self, sort_attrs, listing_type):
-    """
-    Provides the value used in comparisons to sort based on the given
-    attribute.
-
-    Arguments:
-      sort_attrs   - list of SortAttr values for the field being sorted on
-      listing_type - ListingType enumeration for the attribute we're listing
-                    entries by
-    """
-
-    return [self.get_sort_value(attr, listing_type) for attr in sort_attrs]
-
-  def get_sort_value(self, attr, listing_type):
-    """
-    Provides the value of a single attribute used for sorting purposes.
-
-    Arguments:
-      attr        - list of SortAttr values for the field being sorted on
-      listing_type - ListingType enumeration for the attribute we're listing
-                    entries by
-    """
-
-    if attr == SortAttr.LISTING:
-      if listing_type == ListingType.IP_ADDRESS:
-        # uses the IP address as the primary value, and port as secondary
-        sort_value = self.get_sort_value(SortAttr.IP_ADDRESS, listing_type) * PORT_COUNT
-        sort_value += self.get_sort_value(SortAttr.PORT, listing_type)
-        return sort_value
-      elif listing_type == ListingType.HOSTNAME:
-        return self.get_sort_value(SortAttr.HOSTNAME, listing_type)
-      elif listing_type == ListingType.FINGERPRINT:
-        return self.get_sort_value(SortAttr.FINGERPRINT, listing_type)
-      elif listing_type == ListingType.NICKNAME:
-        return self.get_sort_value(SortAttr.NICKNAME, listing_type)
-
-    return ""
-
-  def reset_display(self):
-    """
-    Flushes cached display results.
-    """
-
-    self.flush_cache = True
-
-
-class ConnectionPanelLine:
-  """
-  Individual line in the connection panel listing.
-  """
-
-  def __init__(self):
-    # cache for displayed information
-    self._listing_cache = None
-    self._listing_cache_args = (None, None)
-
-    self._details_cache = None
-    self._details_cache_args = None
-
-    self._descriptor_cache = None
-    self._descriptor_cache_args = None
-
-  def get_listing_prefix(self):
-    """
-    Provides a list of characters to be appended before the listing entry.
-    """
-
-    return ()
-
-  def get_listing_entry(self, width, current_time, listing_type):
-    """
-    Provides a [(msg, attr)...] tuple list for contents to be displayed in the
-    connection panel listing.
-
-    Arguments:
-      width       - available space to display in
-      current_time - unix timestamp for what the results should consider to be
-                    the current time (this may be ignored due to caching)
-      listing_type - ListingType enumeration for the highest priority content
-                    to be displayed
-    """
-
-    if self._listing_cache_args != (width, listing_type):
-      self._listing_cache = self._get_listing_entry(width, current_time, listing_type)
-      self._listing_cache_args = (width, listing_type)
-
-    return self._listing_cache
-
-  def _get_listing_entry(self, width, current_time, listing_type):
-    # implementation of get_listing_entry
-    return None
-
-  def get_details(self, width):
-    """
-    Provides a list of [(msg, attr)...] tuple listings with detailed
-    information for this connection.
-
-    Arguments:
-      width - available space to display in
-    """
-
-    if self._details_cache_args != width:
-      self._details_cache = self._get_details(width)
-      self._details_cache_args = width
-
-    return self._details_cache
-
-  def _get_details(self, width):
-    # implementation of get_details
-    return []
-
-  def reset_display(self):
-    """
-    Flushes cached display results.
-    """
-
-    self._listing_cache_args = (None, None)
-    self._details_cache_args = None
diff --git a/arm/controller.py b/arm/controller.py
deleted file mode 100644
index 1525412..0000000
--- a/arm/controller.py
+++ /dev/null
@@ -1,657 +0,0 @@
-"""
-Main interface loop for arm, periodically redrawing the screen and issuing
-user input to the proper panels.
-"""
-
-import os
-import time
-import curses
-import threading
-
-import arm.arguments
-import arm.menu.menu
-import arm.popups
-import arm.header_panel
-import arm.log_panel
-import arm.config_panel
-import arm.torrc_panel
-import arm.graph_panel
-import arm.connections.conn_panel
-import arm.util.tracker
-
-import stem
-
-from stem.control import State
-
-from arm.util import panel, tor_config, tor_controller, ui_tools
-
-from stem.util import conf, log, system
-
-ARM_CONTROLLER = None
-
-
-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.data_directory": "~/.arm",
-  "features.acsSupport": True,
-  "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,
-  "start_time": 0,
-}, conf_handler)
-
-
-def get_controller():
-  """
-  Provides the arm controller instance.
-  """
-
-  return ARM_CONTROLLER
-
-
-def stop_controller():
-  """
-  Halts our Controller, providing back the thread doing so.
-  """
-
-  def halt_controller():
-    control = get_controller()
-
-    if control:
-      for panel_impl in control.get_daemon_panels():
-        panel_impl.stop()
-
-      for panel_impl in control.get_daemon_panels():
-        panel_impl.join()
-
-  halt_thread = threading.Thread(target = halt_controller)
-  halt_thread.start()
-  return halt_thread
-
-
-def init_controller(stdscr, start_time):
-  """
-  Spawns the controller, and related panels for it.
-
-  Arguments:
-    stdscr - curses window
-  """
-
-  global ARM_CONTROLLER
-
-  # initializes the panels
-
-  sticky_panels = [
-    arm.header_panel.HeaderPanel(stdscr, start_time),
-    LabelPanel(stdscr),
-  ]
-
-  page_panels, first_page_panels = [], []
-
-  # first page: graph and log
-  if CONFIG["features.panels.show.graph"]:
-    first_page_panels.append(arm.graph_panel.GraphPanel(stdscr))
-
-  if CONFIG["features.panels.show.log"]:
-    expanded_events = arm.arguments.expand_events(CONFIG["startup.events"])
-    first_page_panels.append(arm.log_panel.LogPanel(stdscr, expanded_events))
-
-  if first_page_panels:
-    page_panels.append(first_page_panels)
-
-  # second page: connections
-  if CONFIG["features.panels.show.connection"]:
-    page_panels.append([arm.connections.conn_panel.ConnectionPanel(stdscr)])
-
-    # The DisableDebuggerAttachment will prevent our connection panel from really
-    # functioning. It'll have circuits, but little else. If this is the case then
-    # notify the user and tell them what they can do to fix it.
-
-    controller = tor_controller()
-
-    if controller.get_conf("DisableDebuggerAttachment", None) == "1":
-      log.notice("Tor is preventing system utilities like netstat and lsof from working. This means that arm can't provide you with connection information. You can change this by adding 'DisableDebuggerAttachment 0' to your torrc and restarting tor. For more information see...\nhttps://trac.torproject.org/3313")
-      arm.util.tracker.get_connection_tracker().set_paused(True)
-    else:
-      # Configures connection resoultions. This is paused/unpaused according to
-      # if Tor's connected or not.
-
-      controller.add_status_listener(conn_reset_listener)
-
-      tor_pid = controller.get_pid(None)
-
-      if tor_pid:
-        # use the tor pid to help narrow connection results
-        tor_cmd = system.name_by_pid(tor_pid)
-
-        if tor_cmd is None:
-          tor_cmd = "tor"
-
-        resolver = arm.util.tracker.get_connection_tracker()
-        log.info("Operating System: %s, Connection Resolvers: %s" % (os.uname()[0], ", ".join(resolver._resolvers)))
-        resolver.start()
-      else:
-        # constructs singleton resolver and, if tor isn't connected, initizes
-        # it to be paused
-
-        arm.util.tracker.get_connection_tracker().set_paused(not controller.is_alive())
-
-  # third page: config
-
-  if CONFIG["features.panels.show.config"]:
-    page_panels.append([arm.config_panel.ConfigPanel(stdscr, arm.config_panel.State.TOR)])
-
-  # fourth page: torrc
-
-  if CONFIG["features.panels.show.torrc"]:
-    page_panels.append([arm.torrc_panel.TorrcPanel(stdscr, arm.torrc_panel.Config.TORRC)])
-
-  # initializes the controller
-
-  ARM_CONTROLLER = Controller(stdscr, sticky_panels, page_panels)
-
-
-class LabelPanel(panel.Panel):
-  """
-  Panel that just displays a single line of text.
-  """
-
-  def __init__(self, stdscr):
-    panel.Panel.__init__(self, stdscr, "msg", 0, height=1)
-    self.msg_text = ""
-    self.msg_attr = curses.A_NORMAL
-
-  def set_message(self, msg, attr = None):
-    """
-    Sets the message being displayed by the panel.
-
-    Arguments:
-      msg  - string to be displayed
-      attr - attribute for the label, normal text if undefined
-    """
-
-    if attr is None:
-      attr = curses.A_NORMAL
-
-    self.msg_text = msg
-    self.msg_attr = attr
-
-  def draw(self, width, height):
-    self.addstr(0, 0, self.msg_text, self.msg_attr)
-
-
-class Controller:
-  """
-  Tracks the global state of the interface
-  """
-
-  def __init__(self, stdscr, sticky_panels, page_panels):
-    """
-    Creates a new controller instance. Panel lists are ordered as they appear,
-    top to bottom on the page.
-
-    Arguments:
-      stdscr       - curses window
-      sticky_panels - panels shown at the top of each page
-      page_panels   - list of pages, each being a list of the panels on it
-    """
-
-    self._screen = stdscr
-    self._sticky_panels = sticky_panels
-    self._page_panels = page_panels
-    self._page = 0
-    self._is_paused = False
-    self._force_redraw = False
-    self._is_done = False
-    self._last_drawn = 0
-    self.set_msg()  # initializes our control message
-
-  def get_screen(self):
-    """
-    Provides our curses window.
-    """
-
-    return self._screen
-
-  def key_input(self):
-    """
-    Gets keystroke from the user.
-    """
-
-    return panel.KeyInput(self.get_screen().getch())
-
-  def get_page_count(self):
-    """
-    Provides the number of pages the interface has. This may be zero if all
-    page panels have been disabled.
-    """
-
-    return len(self._page_panels)
-
-  def get_page(self):
-    """
-    Provides the number belonging to this page. Page numbers start at zero.
-    """
-
-    return self._page
-
-  def set_page(self, page_number):
-    """
-    Sets the selected page, raising a ValueError if the page number is invalid.
-
-    Arguments:
-      page_number - page number to be selected
-    """
-
-    if page_number < 0 or page_number >= self.get_page_count():
-      raise ValueError("Invalid page number: %i" % page_number)
-
-    if page_number != self._page:
-      self._page = page_number
-      self._force_redraw = True
-      self.set_msg()
-
-  def next_page(self):
-    """
-    Increments the page number.
-    """
-
-    self.set_page((self._page + 1) % len(self._page_panels))
-
-  def prev_page(self):
-    """
-    Decrements the page number.
-    """
-
-    self.set_page((self._page - 1) % len(self._page_panels))
-
-  def is_paused(self):
-    """
-    True if the interface is paused, false otherwise.
-    """
-
-    return self._is_paused
-
-  def set_paused(self, is_pause):
-    """
-    Sets the interface to be paused or unpaused.
-    """
-
-    if is_pause != self._is_paused:
-      self._is_paused = is_pause
-      self._force_redraw = True
-      self.set_msg()
-
-      for panel_impl in self.get_all_panels():
-        panel_impl.set_paused(is_pause)
-
-  def get_panel(self, name):
-    """
-    Provides the panel with the given identifier. This returns None if no such
-    panel exists.
-
-    Arguments:
-      name - name of the panel to be fetched
-    """
-
-    for panel_impl in self.get_all_panels():
-      if panel_impl.get_name() == name:
-        return panel_impl
-
-    return None
-
-  def get_sticky_panels(self):
-    """
-    Provides the panels visibile at the top of every page.
-    """
-
-    return list(self._sticky_panels)
-
-  def get_display_panels(self, page_number = None, include_sticky = True):
-    """
-    Provides all panels belonging to a page and sticky content above it. This
-    is ordered they way they are presented (top to bottom) on the page.
-
-    Arguments:
-      page_number    - page number of the panels to be returned, the current
-                      page if None
-      include_sticky - includes sticky panels in the results if true
-    """
-
-    return_page = self._page if page_number is None else page_number
-
-    if self._page_panels:
-      if include_sticky:
-        return self._sticky_panels + self._page_panels[return_page]
-      else:
-        return list(self._page_panels[return_page])
-    else:
-      return self._sticky_panels if include_sticky else []
-
-  def get_daemon_panels(self):
-    """
-    Provides thread panels.
-    """
-
-    thread_panels = []
-
-    for panel_impl in self.get_all_panels():
-      if isinstance(panel_impl, threading.Thread):
-        thread_panels.append(panel_impl)
-
-    return thread_panels
-
-  def get_all_panels(self):
-    """
-    Provides all panels in the interface.
-    """
-
-    all_panels = list(self._sticky_panels)
-
-    for page in self._page_panels:
-      all_panels += list(page)
-
-    return all_panels
-
-  def redraw(self, force = True):
-    """
-    Redraws the displayed panel content.
-
-    Arguments:
-      force - redraws reguardless of if it's needed if true, otherwise ignores
-              the request when there arne't changes to be displayed
-    """
-
-    force |= self._force_redraw
-    self._force_redraw = False
-
-    current_time = time.time()
-
-    if CONFIG["features.refreshRate"] != 0:
-      if self._last_drawn + CONFIG["features.refreshRate"] <= current_time:
-        force = True
-
-    display_panels = self.get_display_panels()
-
-    occupied_content = 0
-
-    for panel_impl in display_panels:
-      panel_impl.set_top(occupied_content)
-      occupied_content += panel_impl.get_height()
-
-    # apparently curses may cache display contents unless we explicitely
-    # request a redraw here...
-    # https://trac.torproject.org/projects/tor/ticket/2830#comment:9
-
-    if force:
-      self._screen.clear()
-
-    for panel_impl in display_panels:
-      panel_impl.redraw(force)
-
-    if force:
-      self._last_drawn = current_time
-
-  def request_redraw(self):
-    """
-    Requests that all content is redrawn when the interface is next rendered.
-    """
-
-    self._force_redraw = True
-
-  def get_last_redraw_time(self):
-    """
-    Provides the time when the content was last redrawn, zero if the content
-    has never been drawn.
-    """
-
-    return self._last_drawn
-
-  def set_msg(self, msg = None, attr = None, redraw = False):
-    """
-    Sets the message displayed in the interfaces control panel. This uses our
-    default prompt if no arguments are provided.
-
-    Arguments:
-      msg    - string to be displayed
-      attr   - attribute for the label, normal text if undefined
-      redraw - redraws right away if true, otherwise redraws when display
-               content is next normally drawn
-    """
-
-    if msg is None:
-      msg = ""
-
-      if attr is None:
-        if not self._is_paused:
-          msg = "page %i / %i - m: menu, p: pause, h: page help, q: quit" % (self._page + 1, len(self._page_panels))
-          attr = curses.A_NORMAL
-        else:
-          msg = "Paused"
-          attr = curses.A_STANDOUT
-
-    control_panel = self.get_panel("msg")
-    control_panel.set_message(msg, attr)
-
-    if redraw:
-      control_panel.redraw(True)
-    else:
-      self._force_redraw = True
-
-  def get_data_directory(self):
-    """
-    Provides the path where arm's resources are being placed. The path ends
-    with a slash and is created if it doesn't already exist.
-    """
-
-    data_dir = os.path.expanduser(CONFIG["startup.data_directory"])
-
-    if not data_dir.endswith("/"):
-      data_dir += "/"
-
-    if not os.path.exists(data_dir):
-      os.makedirs(data_dir)
-
-    return data_dir
-
-  def is_done(self):
-    """
-    True if arm should be terminated, false otherwise.
-    """
-
-    return self._is_done
-
-  def quit(self):
-    """
-    Terminates arm after the input is processed. Optionally if we're connected
-    to a arm generated tor instance then this may check if that should be shut
-    down too.
-    """
-
-    self._is_done = True
-
-    # check if the torrc has a "ARM_SHUTDOWN" comment flag, if so then shut
-    # down the instance
-
-    is_shutdown_flag_present = False
-    torrc_contents = tor_config.get_torrc().get_contents()
-
-    if torrc_contents:
-      for line in torrc_contents:
-        if "# ARM_SHUTDOWN" in line:
-          is_shutdown_flag_present = True
-          break
-
-    if is_shutdown_flag_present:
-      try:
-        tor_controller().close()
-      except IOError as exc:
-        arm.popups.show_msg(str(exc), 3, curses.A_BOLD)
-
-
-def heartbeat_check(is_unresponsive):
-  """
-  Logs if its been ten seconds since the last BW event.
-
-  Arguments:
-    is_unresponsive - flag for if we've indicated to be responsive or not
-  """
-
-  controller = tor_controller()
-  last_heartbeat = controller.get_latest_heartbeat()
-
-  if controller.is_alive():
-    if not is_unresponsive and (time.time() - last_heartbeat) >= 10:
-      is_unresponsive = True
-      log.notice("Relay unresponsive (last heartbeat: %s)" % time.ctime(last_heartbeat))
-    elif is_unresponsive and (time.time() - last_heartbeat) < 10:
-      # really shouldn't happen (meant Tor froze for a bit)
-      is_unresponsive = False
-      log.notice("Relay resumed")
-
-  return is_unresponsive
-
-
-def conn_reset_listener(controller, event_type, _):
-  """
-  Pauses connection resolution when tor's shut down, and resumes with the new
-  pid if started again.
-  """
-
-  resolver = arm.util.tracker.get_connection_tracker()
-
-  if resolver.is_alive():
-    resolver.set_paused(event_type == State.CLOSED)
-
-    if event_type in (State.INIT, State.RESET):
-      # Reload the torrc contents. If the torrc panel is present then it will
-      # do this instead since it wants to do validation and redraw _after_ the
-      # new contents are loaded.
-
-      if get_controller().get_panel("torrc") is None:
-        tor_config.get_torrc().load(True)
-
-
-def start_arm(stdscr):
-  """
-  Main draw loop context.
-
-  Arguments:
-    stdscr    - curses window
-  """
-
-  init_controller(stdscr, CONFIG['start_time'])
-  control = get_controller()
-
-  if not CONFIG["features.acsSupport"]:
-    ui_tools.disable_acs()
-
-  # provides notice about any unused config keys
-
-  for key in conf.get_config("arm").unused_keys():
-    log.notice("Unused configuration entry: %s" % key)
-
-  # tells daemon panels to start
-
-  for panel_impl in control.get_daemon_panels():
-    panel_impl.start()
-
-  # allows for background transparency
-
-  try:
-    curses.use_default_colors()
-  except curses.error:
-    pass
-
-  # makes the cursor invisible
-
-  try:
-    curses.curs_set(0)
-  except curses.error:
-    pass
-
-  # logs the initialization time
-
-  log.info("arm started (initialization took %0.3f seconds)" % (time.time() - CONFIG['start_time']))
-
-  # main draw loop
-
-  override_key = None      # uses this rather than waiting on user input
-  is_unresponsive = False  # flag for heartbeat responsiveness check
-
-  while not control.is_done():
-    display_panels = control.get_display_panels()
-    is_unresponsive = heartbeat_check(is_unresponsive)
-
-    # sets panel visability
-
-    for panel_impl in control.get_all_panels():
-      panel_impl.set_visible(panel_impl in display_panels)
-
-    # redraws the interface if it's needed
-
-    control.redraw(False)
-    stdscr.refresh()
-
-    # wait for user keyboard input until timeout, unless an override was set
-
-    if override_key:
-      key, override_key = override_key, None
-    else:
-      curses.halfdelay(CONFIG["features.redrawRate"] * 10)
-      key = panel.KeyInput(stdscr.getch())
-
-    if key.match('right'):
-      control.next_page()
-    elif key.match('left'):
-      control.prev_page()
-    elif key.match('p'):
-      control.set_paused(not control.is_paused())
-    elif key.match('m'):
-      arm.menu.menu.show_menu()
-    elif key.match('q'):
-      # provides prompt to confirm that arm should exit
-
-      if CONFIG["features.confirmQuit"]:
-        msg = "Are you sure (q again to confirm)?"
-        confirmation_key = arm.popups.show_msg(msg, attr = curses.A_BOLD)
-        quit_confirmed = confirmation_key.match('q')
-      else:
-        quit_confirmed = True
-
-      if quit_confirmed:
-        control.quit()
-    elif key.match('x'):
-      # provides prompt to confirm that arm should issue a sighup
-
-      msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?"
-      confirmation_key = arm.popups.show_msg(msg, attr = curses.A_BOLD)
-
-      if confirmation_key in (ord('x'), ord('X')):
-        try:
-          tor_controller().signal(stem.Signal.RELOAD)
-        except IOError as exc:
-          log.error("Error detected when reloading tor: %s" % exc.strerror)
-    elif key.match('h'):
-      override_key = arm.popups.show_help_popup()
-    elif key == ord('l') - 96:
-      # force redraw when ctrl+l is pressed
-      control.redraw(True)
-    else:
-      for panel_impl in display_panels:
-        is_keystroke_consumed = panel_impl.handle_key(key)
-
-        if is_keystroke_consumed:
-          break
diff --git a/arm/demo_glyphs.py b/arm/demo_glyphs.py
deleted file mode 100755
index 7781139..0000000
--- a/arm/demo_glyphs.py
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/env python
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-"""
-Displays all ACS options with their corresponding representation. These are
-undocumented in the pydocs. For more information see the following man page:
-
-http://www.mkssoftware.com/docs/man5/terminfo.5.asp
-"""
-
-import curses
-
-
-def main():
-  try:
-    curses.wrapper(_show_glyphs)
-  except KeyboardInterrupt:
-    pass  # quit
-
-
-def _show_glyphs(stdscr):
-  """
-  Renders a chart with the ACS glyphs.
-  """
-
-  try:
-    curses.use_default_colors()  # allow semi-transparent backgrounds
-  except curses.error:
-    pass
-
-  try:
-    curses.curs_set(0)  # attempt to make the cursor invisible
-  except curses.error:
-    pass
-
-  height, width = stdscr.getmaxyx()
-  columns = width / 30
-
-  if columns == 0:
-    return  # not wide enough to show anything
-
-  # mapping of keycodes to their ACS option names (for instance, ACS_LTEE)
-
-  acs_options = dict((v, k) for (k, v) in curses.__dict__.items() if k.startswith('ACS_'))
-
-  stdscr.addstr(0, 0, 'Curses Glyphs:', curses.A_STANDOUT)
-  x, y = 0, 2
-
-  for keycode in sorted(acs_options.keys()):
-    stdscr.addstr(y, x * 30, '%s (%i)' % (acs_options[keycode], keycode))
-    stdscr.addch(y, (x * 30) + 25, keycode)
-
-    x += 1
-
-    if x >= columns:
-      x, y = 0, y + 1
-
-      if y >= height:
-        break
-
-  stdscr.getch()  # quit on keyboard input
-
-
-if __name__ == '__main__':
-  main()
diff --git a/arm/graph_panel.py b/arm/graph_panel.py
deleted file mode 100644
index 7779945..0000000
--- a/arm/graph_panel.py
+++ /dev/null
@@ -1,734 +0,0 @@
-"""
-Graphs of tor related statistics. For example...
-
-Downloaded (0.0 B/sec):           Uploaded (0.0 B/sec):
-  34                                30
-                            *                                 *
-                    **  *   *                          *      **
-      *   *  *      ** **   **          ***  **       ** **   **
-     *********      ******  ******     *********      ******  ******
-   0 ************ ****************   0 ************ ****************
-         25s  50   1m   1.6  2.0           25s  50   1m   1.6  2.0
-"""
-
-import copy
-import curses
-import time
-
-import arm.controller
-import arm.popups
-import arm.util.tracker
-
-from arm.util import bandwidth_from_state, join, msg, panel, tor_controller
-
-from stem.control import EventType, Listener
-from stem.util import conf, enum, log, str_tools, system
-
-GraphStat = enum.Enum(('BANDWIDTH', 'bandwidth'), ('CONNECTIONS', 'connections'), ('SYSTEM_RESOURCES', 'resources'))
-Interval = enum.Enum(('EACH_SECOND', 'each second'), ('FIVE_SECONDS', '5 seconds'), ('THIRTY_SECONDS', '30 seconds'), ('MINUTELY', 'minutely'), ('FIFTEEN_MINUTE', '15 minute'), ('THIRTY_MINUTE', '30 minute'), ('HOURLY', 'hourly'), ('DAILY', 'daily'))
-Bounds = enum.Enum(('GLOBAL_MAX', 'global_max'), ('LOCAL_MAX', 'local_max'), ('TIGHT', 'tight'))
-
-INTERVAL_SECONDS = {
-  Interval.EACH_SECOND: 1,
-  Interval.FIVE_SECONDS: 5,
-  Interval.THIRTY_SECONDS: 30,
-  Interval.MINUTELY: 60,
-  Interval.FIFTEEN_MINUTE: 900,
-  Interval.THIRTY_MINUTE: 1800,
-  Interval.HOURLY: 3600,
-  Interval.DAILY: 86400,
-}
-
-PRIMARY_COLOR, SECONDARY_COLOR = 'green', 'cyan'
-
-ACCOUNTING_RATE = 5
-DEFAULT_CONTENT_HEIGHT = 4  # space needed for labeling above and below the graph
-WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
-COLLAPSE_WIDTH = 135  # width at which to move optional stats from the title to x-axis label
-
-
-def conf_handler(key, value):
-  if key == 'features.graph.height':
-    return max(1, value)
-  elif key == 'features.graph.max_width':
-    return max(1, value)
-  elif key == 'features.graph.type':
-    if value != 'none' and value not in GraphStat:
-      log.warn("'%s' isn't a valid graph type, options are: none, %s" % (CONFIG['features.graph.type'], ', '.join(GraphStat)))
-      return CONFIG['features.graph.type']  # keep the default
-  elif key == 'features.graph.interval':
-    if value not in Interval:
-      log.warn("'%s' isn't a valid graphing interval, options are: %s" % (value, ', '.join(Interval)))
-      return CONFIG['features.graph.interval']  # keep the default
-  elif key == 'features.graph.bound':
-    if value not in Bounds:
-      log.warn("'%s' isn't a valid graph bounds, options are: %s" % (value, ', '.join(Bounds)))
-      return CONFIG['features.graph.bound']  # keep the default
-
-
-CONFIG = conf.config_dict('arm', {
-  'attr.hibernate_color': {},
-  'attr.graph.title': {},
-  'attr.graph.header.primary': {},
-  'attr.graph.header.secondary': {},
-  'features.graph.height': 7,
-  'features.graph.type': GraphStat.BANDWIDTH,
-  'features.graph.interval': Interval.EACH_SECOND,
-  'features.graph.bound': Bounds.LOCAL_MAX,
-  'features.graph.max_width': 150,
-  'features.panels.show.connection': True,
-  'features.graph.bw.prepopulate': True,
-  'features.graph.bw.transferInBytes': False,
-  'features.graph.bw.accounting.show': True,
-  'tor.chroot': '',
-}, conf_handler)
-
-
-class Stat(object):
-  """
-  Graphable statistical information.
-
-  :var int latest_value: last value we recorded
-  :var int total: sum of all values we've recorded
-  :var int tick: number of events we've processed
-  :var float start_time: unix timestamp for when we started
-  :var dict values: mapping of intervals to an array of samplings from newest to oldest
-  :var dict max_value: mapping of intervals to the maximum value it has had
-  """
-
-  def __init__(self, clone = None):
-    if clone:
-      self.latest_value = clone.latest_value
-      self.total = clone.total
-      self.tick = clone.tick
-      self.start_time = clone.start_time
-      self.values = copy.deepcopy(clone.values)
-      self.max_value = dict(clone.max_value)
-      self._in_process_value = dict(clone._in_process_value)
-    else:
-      self.latest_value = 0
-      self.total = 0
-      self.tick = 0
-      self.start_time = time.time()
-      self.values = dict([(i, CONFIG['features.graph.max_width'] * [0]) for i in Interval])
-      self.max_value = dict([(i, 0) for i in Interval])
-      self._in_process_value = dict([(i, 0) for i in Interval])
-
-  def average(self, by_time = False):
-    return self.total / (time.time() - self.start_time) if by_time else self.total / max(1, self.tick)
-
-  def update(self, new_value):
-    self.latest_value = new_value
-    self.total += new_value
-    self.tick += 1
-
-    for interval in Interval:
-      interval_seconds = INTERVAL_SECONDS[interval]
-      self._in_process_value[interval] += new_value
-
-      if self.tick % interval_seconds == 0:
-        new_entry = self._in_process_value[interval] / interval_seconds
-        self.values[interval] = [new_entry] + self.values[interval][:-1]
-        self.max_value[interval] = max(self.max_value[interval], new_entry)
-        self._in_process_value[interval] = 0
-
-
-class GraphCategory(object):
-  """
-  Category for the graph. This maintains two subgraphs, updating them each
-  second with updated stats.
-
-  :var Stat primary: first subgraph
-  :var Stat secondary: second subgraph
-  :var list title_stats: additional information to include in the graph title
-  :var list primary_header_stats: additional information for the primary header
-  :var list secondary_header_stats: additional information for the secondary header
-  """
-
-  def __init__(self, clone = None):
-    if clone:
-      self.primary = Stat(clone.primary)
-      self.secondary = Stat(clone.secondary)
-      self.title_stats = list(clone.title_stats)
-      self.primary_header_stats = list(clone.primary_header_stats)
-      self.secondary_header_stats = list(clone.secondary_header_stats)
-    else:
-      self.primary = Stat()
-      self.secondary = Stat()
-      self.title_stats = []
-      self.primary_header_stats = []
-      self.secondary_header_stats = []
-
-  def y_axis_label(self, value, is_primary):
-    """
-    Provides the label we should display on our y-axis.
-
-    :param int value: value being shown on the y-axis
-    :param bool is_primary: True if this is the primary attribute, False if
-      it's the secondary
-
-    :returns: **str** with our y-axis label
-    """
-
-    return str(value)
-
-  def bandwidth_event(self, event):
-    """
-    Called when it's time to process another event. All graphs use tor BW
-    events to keep in sync with each other (this happens once per second).
-    """
-
-    pass
-
-
-class BandwidthStats(GraphCategory):
-  """
-  Tracks tor's bandwidth usage.
-  """
-
-  def __init__(self, clone = None):
-    GraphCategory.__init__(self, clone)
-
-    if not clone:
-      # We both show our 'total' attributes and use it to determine our average.
-      #
-      # If we can get *both* our start time and the totals from tor (via 'GETINFO
-      # traffic/*') then that's ideal, but if not then just track the total for
-      # the time arm is run.
-
-      controller = tor_controller()
-
-      read_total = controller.get_info('traffic/read', None)
-      write_total = controller.get_info('traffic/written', None)
-      start_time = system.start_time(controller.get_pid(None))
-
-      if read_total and write_total and start_time:
-        self.primary.total = int(read_total)
-        self.secondary.total = int(write_total)
-        self.primary.start_time = self.secondary.start_time = start_time
-
-  def y_axis_label(self, value, is_primary):
-    return self._size_label(value, 0)
-
-  def bandwidth_event(self, event):
-    self.primary.update(event.read)
-    self.secondary.update(event.written)
-
-    self.primary_header_stats = [
-      '%-14s' % ('%s/sec' % self._size_label(self.primary.latest_value)),
-      '- avg: %s/sec' % self._size_label(self.primary.average(by_time = True)),
-      ', total: %s' % self._size_label(self.primary.total),
-    ]
-
-    self.secondary_header_stats = [
-      '%-14s' % ('%s/sec' % self._size_label(self.secondary.latest_value)),
-      '- avg: %s/sec' % self._size_label(self.secondary.average(by_time = True)),
-      ', total: %s' % self._size_label(self.secondary.total),
-    ]
-
-    controller = tor_controller()
-
-    stats = []
-    bw_rate = controller.get_effective_rate(None)
-    bw_burst = controller.get_effective_rate(None, burst = True)
-
-    if bw_rate and bw_burst:
-      bw_rate_label = self._size_label(bw_rate)
-      bw_burst_label = self._size_label(bw_burst)
-
-      # if both are using rounded values then strip off the '.0' decimal
-
-      if '.0' in bw_rate_label and '.0' in bw_burst_label:
-        bw_rate_label = bw_rate_label.split('.', 1)[0]
-        bw_burst_label = bw_burst_label.split('.', 1)[0]
-
-      stats.append('limit: %s/s' % bw_rate_label)
-      stats.append('burst: %s/s' % bw_burst_label)
-
-    my_router_status_entry = controller.get_network_status(default = None)
-    measured_bw = getattr(my_router_status_entry, 'bandwidth', None)
-
-    if measured_bw:
-      stats.append('measured: %s/s' % self._size_label(measured_bw))
-    else:
-      my_server_descriptor = controller.get_server_descriptor(default = None)
-      observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None)
-
-      if observed_bw:
-        stats.append('observed: %s/s' % self._size_label(observed_bw))
-
-    self.title_stats = stats
-
-  def prepopulate_from_state(self):
-    """
-    Attempts to use tor's state file to prepopulate values for the 15 minute
-    interval via the BWHistoryReadValues/BWHistoryWriteValues values.
-
-    :returns: **float** for the number of seconds of data missing
-
-    :raises: **ValueError** if unable to get the bandwidth information from our
-      state file
-    """
-
-    def update_values(stat, entries, latest_time):
-      # fill missing entries with the last value
-
-      missing_entries = int((time.time() - latest_time) / 900)
-      entries = entries + [entries[-1]] * missing_entries
-
-      # pad if too short and truncate if too long
-
-      entry_count = CONFIG['features.graph.max_width']
-      entries = [0] * (entry_count - len(entries)) + entries[-entry_count:]
-
-      stat.values[Interval.FIFTEEN_MINUTE] = entries
-      stat.max_value[Interval.FIFTEEN_MINUTE] = max(entries)
-      stat.latest_value = entries[-1] * 900
-
-    stats = bandwidth_from_state()
-
-    update_values(self.primary, stats.read_entries, stats.last_read_time)
-    update_values(self.secondary, stats.write_entries, stats.last_write_time)
-
-    return time.time() - min(stats.last_read_time, stats.last_write_time)
-
-  def _size_label(self, byte_count, decimal = 1):
-    """
-    Alias for str_tools.size_label() that accounts for if the user prefers bits
-    or bytes.
-    """
-
-    return str_tools.size_label(byte_count, decimal, is_bytes = CONFIG['features.graph.bw.transferInBytes'])
-
-
-class ConnectionStats(GraphCategory):
-  """
-  Tracks number of inbound and outbound connections.
-  """
-
-  def bandwidth_event(self, event):
-    inbound_count, outbound_count = 0, 0
-
-    controller = tor_controller()
-    or_ports = controller.get_ports(Listener.OR, [])
-    dir_ports = controller.get_ports(Listener.DIR, [])
-    control_ports = controller.get_ports(Listener.CONTROL, [])
-
-    for entry in arm.util.tracker.get_connection_tracker().get_value():
-      if entry.local_port in or_ports or entry.local_port in dir_ports:
-        inbound_count += 1
-      elif entry.local_port in control_ports:
-        pass  # control connection
-      else:
-        outbound_count += 1
-
-    self.primary.update(inbound_count)
-    self.secondary.update(outbound_count)
-
-    self.primary_header_stats = [str(self.primary.latest_value), ', avg: %s' % self.primary.average()]
-    self.secondary_header_stats = [str(self.secondary.latest_value), ', avg: %s' % self.secondary.average()]
-
-
-class ResourceStats(GraphCategory):
-  """
-  Tracks cpu and memory usage of the tor process.
-  """
-
-  def y_axis_label(self, value, is_primary):
-    return '%i%%' % value if is_primary else str_tools.size_label(value)
-
-  def bandwidth_event(self, event):
-    resources = arm.util.tracker.get_resource_tracker().get_value()
-    self.primary.update(resources.cpu_sample * 100)  # decimal percentage to whole numbers
-    self.secondary.update(resources.memory_bytes)
-
-    self.primary_header_stats = ['%0.1f%%' % self.primary.latest_value, ', avg: %0.1f%%' % self.primary.average()]
-    self.secondary_header_stats = [str_tools.size_label(self.secondary.latest_value, 1), ', avg: %s' % str_tools.size_label(self.secondary.average(), 1)]
-
-
-class GraphPanel(panel.Panel):
-  """
-  Panel displaying graphical information of GraphCategory instances.
-  """
-
-  def __init__(self, stdscr):
-    panel.Panel.__init__(self, stdscr, 'graph', 0)
-
-    self._displayed_stat = None if CONFIG['features.graph.type'] == 'none' else CONFIG['features.graph.type']
-    self._update_interval = CONFIG['features.graph.interval']
-    self._bounds = CONFIG['features.graph.bound']
-    self._graph_height = CONFIG['features.graph.height']
-
-    self._accounting_stats = None
-
-    self._stats = {
-      GraphStat.BANDWIDTH: BandwidthStats(),
-      GraphStat.SYSTEM_RESOURCES: ResourceStats(),
-    }
-
-    if CONFIG['features.panels.show.connection']:
-      self._stats[GraphStat.CONNECTIONS] = ConnectionStats()
-    elif self._displayed_stat == GraphStat.CONNECTIONS:
-      log.warn("The connection graph is unavailble when you set 'features.panels.show.connection false'.")
-      self._displayed_stat = GraphStat.BANDWIDTH
-
-    self.set_pause_attr('_stats')
-    self.set_pause_attr('_accounting_stats')
-
-    # prepopulates bandwidth values from state file
-
-    controller = tor_controller()
-
-    if controller.is_alive() and CONFIG['features.graph.bw.prepopulate']:
-      try:
-        missing_seconds = self._stats[GraphStat.BANDWIDTH].prepopulate_from_state()
-
-        if missing_seconds:
-          log.notice(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(missing_seconds, 0, True)))
-        else:
-          log.notice(msg('panel.graphing.prepopulation_all_successful'))
-
-        self.update_interval = Interval.FIFTEEN_MINUTE
-      except ValueError as exc:
-        log.info(msg('panel.graphing.prepopulation_failure', error = exc))
-
-    controller.add_event_listener(self._update_accounting, EventType.BW)
-    controller.add_event_listener(self._update_stats, EventType.BW)
-    controller.add_status_listener(lambda *args: self.redraw(True))
-
-  @property
-  def displayed_stat(self):
-    return self._displayed_stat
-
-  @displayed_stat.setter
-  def displayed_stat(self, value):
-    if value is not None and value not in self._stats.keys():
-      raise ValueError("%s isn't a graphed statistic" % value)
-
-    self._displayed_stat = value
-
-  def stat_options(self):
-    return self._stats.keys()
-
-  @property
-  def update_interval(self):
-    return self._update_interval
-
-  @update_interval.setter
-  def update_interval(self, value):
-    if value not in Interval:
-      raise ValueError("%s isn't a valid graphing update interval" % value)
-
-    self._update_interval = value
-
-  @property
-  def bounds_type(self):
-    return self._bounds
-
-  @bounds_type.setter
-  def bounds_type(self, value):
-    if value not in Bounds:
-      raise ValueError("%s isn't a valid type of bounds" % value)
-
-    self._bounds = value
-
-  def get_height(self):
-    """
-    Provides the height of the content.
-    """
-
-    if not self.displayed_stat:
-      return 0
-
-    height = DEFAULT_CONTENT_HEIGHT + self._graph_height
-
-    if self.displayed_stat == GraphStat.BANDWIDTH and self._accounting_stats:
-      height += 3
-
-    return height
-
-  def set_graph_height(self, new_graph_height):
-    self._graph_height = max(1, new_graph_height)
-
-  def resize_graph(self):
-    """
-    Prompts for user input to resize the graph panel. Options include...
-      down arrow - grow graph
-      up arrow - shrink graph
-      enter / space - set size
-    """
-
-    control = arm.controller.get_controller()
-
-    with panel.CURSES_LOCK:
-      try:
-        while True:
-          msg = 'press the down/up to resize the graph, and enter when done'
-          control.set_msg(msg, curses.A_BOLD, True)
-          curses.cbreak()
-          key = control.key_input()
-
-          if key.match('down'):
-            # don't grow the graph if it's already consuming the whole display
-            # (plus an extra line for the graph/log gap)
-
-            max_height = self.parent.getmaxyx()[0] - self.top
-            current_height = self.get_height()
-
-            if current_height < max_height + 1:
-              self.set_graph_height(self._graph_height + 1)
-          elif key.match('up'):
-            self.set_graph_height(self._graph_height - 1)
-          elif key.is_selection():
-            break
-
-          control.redraw()
-      finally:
-        control.set_msg()
-
-  def handle_key(self, key):
-    if key.match('r'):
-      self.resize_graph()
-    elif key.match('b'):
-      # uses the next boundary type
-      self.bounds_type = Bounds.next(self.bounds_type)
-      self.redraw(True)
-    elif key.match('s'):
-      # provides a menu to pick the graphed stats
-
-      available_stats = self._stats.keys()
-      available_stats.sort()
-
-      # uses sorted, camel cased labels for the options
-
-      options = ['None']
-
-      for label in available_stats:
-        words = label.split()
-        options.append(' '.join(word[0].upper() + word[1:] for word in words))
-
-      if self.displayed_stat:
-        initial_selection = available_stats.index(self.displayed_stat) + 1
-      else:
-        initial_selection = 0
-
-      selection = arm.popups.show_menu('Graphed Stats:', options, initial_selection)
-
-      # applies new setting
-
-      if selection == 0:
-        self.displayed_stat = None
-      elif selection != -1:
-        self.displayed_stat = available_stats[selection - 1]
-    elif key.match('i'):
-      # provides menu to pick graph panel update interval
-
-      selection = arm.popups.show_menu('Update Interval:', list(Interval), list(Interval).index(self.update_interval))
-
-      if selection != -1:
-        self.update_interval = list(Interval)[selection]
-    else:
-      return False
-
-    return True
-
-  def get_help(self):
-    return [
-      ('r', 'resize graph', None),
-      ('s', 'graphed stats', self.displayed_stat if self.displayed_stat else 'none'),
-      ('b', 'graph bounds', self.bounds_type.replace('_', ' ')),
-      ('i', 'graph update interval', self.update_interval),
-    ]
-
-  def draw(self, width, height):
-    if not self.displayed_stat:
-      return
-
-    param = self.get_attr('_stats')[self.displayed_stat]
-    graph_column = min((width - 10) / 2, CONFIG['features.graph.max_width'])
-
-    if self.is_title_visible():
-      title = CONFIG['attr.graph.title'].get(self.displayed_stat, '')
-      title_stats = join(param.title_stats, ', ', width - len(title) - 4)
-      title = '%s (%s):' % (title, title_stats) if title_stats else '%s:' % title
-      self.addstr(0, 0, title, curses.A_STANDOUT)
-
-    # top labels
-
-    primary_header = CONFIG['attr.graph.header.primary'].get(self.displayed_stat, '')
-    primary_header_stats = join(param.primary_header_stats, '', (width / 2) - len(primary_header) - 4)
-    left = '%s (%s):' % (primary_header, primary_header_stats) if primary_header_stats else '%s:' % primary_header
-    self.addstr(1, 0, left, curses.A_BOLD, PRIMARY_COLOR)
-
-    secondary_header = CONFIG['attr.graph.header.secondary'].get(self.displayed_stat, '')
-    secondary_header_stats = join(param.secondary_header_stats, '', (width / 2) - len(secondary_header) - 4)
-    right = '%s (%s):' % (secondary_header, secondary_header_stats) if secondary_header_stats else '%s:' % secondary_header
-    self.addstr(1, graph_column + 5, right, curses.A_BOLD, SECONDARY_COLOR)
-
-    # determines max/min value on the graph
-
-    if self.bounds_type == Bounds.GLOBAL_MAX:
-      primary_max_bound = param.primary.max_value[self.update_interval]
-      secondary_max_bound = param.secondary.max_value[self.update_interval]
-    else:
-      # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima
-      if graph_column < 2:
-        # nothing being displayed
-        primary_max_bound, secondary_max_bound = 0, 0
-      else:
-        primary_max_bound = max(param.primary.values[self.update_interval][:graph_column])
-        secondary_max_bound = max(param.secondary.values[self.update_interval][:graph_column])
-
-    primary_min_bound = secondary_min_bound = 0
-
-    if self.bounds_type == Bounds.TIGHT:
-      primary_min_bound = min(param.primary.values[self.update_interval][:graph_column])
-      secondary_min_bound = min(param.secondary.values[self.update_interval][:graph_column])
-
-      # if the max = min (ie, all values are the same) then use zero lower
-      # bound so a graph is still displayed
-
-      if primary_min_bound == primary_max_bound:
-        primary_min_bound = 0
-
-      if secondary_min_bound == secondary_max_bound:
-        secondary_min_bound = 0
-
-    # displays upper and lower bounds
-
-    # TODO: we need to get the longest y_axis_label() result so we can offset
-    # following content by that
-
-    self.addstr(2, 0, param.y_axis_label(primary_max_bound, True), PRIMARY_COLOR)
-    self.addstr(self._graph_height + 1, 0, param.y_axis_label(primary_min_bound, True), PRIMARY_COLOR)
-
-    self.addstr(2, graph_column + 5, param.y_axis_label(secondary_max_bound, False), SECONDARY_COLOR)
-    self.addstr(self._graph_height + 1, graph_column + 5, param.y_axis_label(secondary_min_bound, False), SECONDARY_COLOR)
-
-    # displays intermediate bounds on every other row
-
-    ticks = (self._graph_height - 3) / 2
-
-    for i in range(ticks):
-      row = self._graph_height - (2 * i) - 3
-
-      if self._graph_height % 2 == 0 and i >= (ticks / 2):
-        row -= 1
-
-      if primary_min_bound != primary_max_bound:
-        primary_val = (primary_max_bound - primary_min_bound) * (self._graph_height - row - 1) / (self._graph_height - 1)
-
-        if primary_val not in (primary_min_bound, primary_max_bound):
-          self.addstr(row + 2, 0, param.y_axis_label(primary_val, True), PRIMARY_COLOR)
-
-      if secondary_min_bound != secondary_max_bound:
-        secondary_val = (secondary_max_bound - secondary_min_bound) * (self._graph_height - row - 1) / (self._graph_height - 1)
-
-        if secondary_val not in (secondary_min_bound, secondary_max_bound):
-          self.addstr(row + 2, graph_column + 5, param.y_axis_label(secondary_val, False), SECONDARY_COLOR)
-
-    # creates bar graph (both primary and secondary)
-
-    for col in range(graph_column):
-      column_count = int(param.primary.values[self.update_interval][col]) - primary_min_bound
-      column_height = int(min(self._graph_height, self._graph_height * column_count / (max(1, primary_max_bound) - primary_min_bound)))
-
-      for row in range(column_height):
-        self.addstr(self._graph_height + 1 - row, col + 5, ' ', curses.A_STANDOUT, PRIMARY_COLOR)
-
-      column_count = int(param.secondary.values[self.update_interval][col]) - secondary_min_bound
-      column_height = int(min(self._graph_height, self._graph_height * column_count / (max(1, secondary_max_bound) - secondary_min_bound)))
-
-      for row in range(column_height):
-        self.addstr(self._graph_height + 1 - row, col + graph_column + 10, ' ', curses.A_STANDOUT, SECONDARY_COLOR)
-
-    # bottom labeling of x-axis
-
-    interval_sec = INTERVAL_SECONDS[self.update_interval]
-
-    interval_spacing = 10 if graph_column >= WIDE_LABELING_GRAPH_COL else 5
-    units_label, decimal_precision = None, 0
-
-    for i in range((graph_column - 4) / interval_spacing):
-      loc = (i + 1) * interval_spacing
-      time_label = str_tools.time_label(loc * interval_sec, decimal_precision)
-
-      if not units_label:
-        units_label = time_label[-1]
-      elif units_label != time_label[-1]:
-        # upped scale so also up precision of future measurements
-        units_label = time_label[-1]
-        decimal_precision += 1
-      else:
-        # if constrained on space then strips labeling since already provided
-        time_label = time_label[:-1]
-
-      self.addstr(self._graph_height + 2, 4 + loc, time_label, PRIMARY_COLOR)
-      self.addstr(self._graph_height + 2, graph_column + 10 + loc, time_label, SECONDARY_COLOR)
-
-    # if display is narrow, overwrites x-axis labels with avg / total stats
-
-    labeling_line = DEFAULT_CONTENT_HEIGHT + self._graph_height - 2
-
-    if self.displayed_stat == GraphStat.BANDWIDTH and width <= COLLAPSE_WIDTH:
-      # clears line
-
-      self.addstr(labeling_line, 0, ' ' * width)
-      graph_column = min((width - 10) / 2, CONFIG['features.graph.max_width'])
-
-      runtime = time.time() - param.start_time
-      primary_footer = 'total: %s, avg: %s/sec' % (param._size_label(param.primary.total), param._size_label(param.primary.total / runtime))
-      secondary_footer = 'total: %s, avg: %s/sec' % (param._size_label(param.secondary.total), param._size_label(param.secondary.total / runtime))
-
-      self.addstr(labeling_line, 1, primary_footer, PRIMARY_COLOR)
-      self.addstr(labeling_line, graph_column + 6, secondary_footer, SECONDARY_COLOR)
-
-    # provides accounting stats if enabled
-
-    accounting_stats = self.get_attr('_accounting_stats')
-
-    if self.displayed_stat == GraphStat.BANDWIDTH and accounting_stats:
-      if tor_controller().is_alive():
-        hibernate_color = CONFIG['attr.hibernate_color'].get(accounting_stats.status, 'red')
-
-        x, y = 0, labeling_line + 2
-        x = self.addstr(y, x, 'Accounting (', curses.A_BOLD)
-        x = self.addstr(y, x, accounting_stats.status, curses.A_BOLD, hibernate_color)
-        x = self.addstr(y, x, ')', curses.A_BOLD)
-
-        self.addstr(y, 35, 'Time to reset: %s' % str_tools.short_time_label(accounting_stats.time_until_reset))
-
-        self.addstr(y + 1, 2, '%s / %s' % (accounting_stats.read_bytes, accounting_stats.read_limit), PRIMARY_COLOR)
-        self.addstr(y + 1, 37, '%s / %s' % (accounting_stats.written_bytes, accounting_stats.write_limit), SECONDARY_COLOR)
-      else:
-        self.addstr(labeling_line + 2, 0, 'Accounting:', curses.A_BOLD)
-        self.addstr(labeling_line + 2, 12, 'Connection Closed...')
-
-  def copy_attr(self, attr):
-    if attr == '_stats':
-      return dict([(key, type(self._stats[key])(self._stats[key])) for key in self._stats])
-    else:
-      return panel.Panel.copy_attr(self, attr)
-
-  def _update_accounting(self, event):
-    if not CONFIG['features.graph.bw.accounting.show']:
-      self._accounting_stats = None
-    elif not self._accounting_stats or time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE:
-      old_accounting_stats = self._accounting_stats
-      self._accounting_stats = tor_controller().get_accounting_stats(None)
-
-      # if we either added or removed accounting info then redraw the whole
-      # screen to account for resizing
-
-      if bool(old_accounting_stats) != bool(self._accounting_stats):
-        arm.controller.get_controller().redraw()
-
-  def _update_stats(self, event):
-    for stat in self._stats.values():
-      stat.bandwidth_event(event)
-
-    param = self.get_attr('_stats')[self.displayed_stat]
-    update_rate = INTERVAL_SECONDS[self.update_interval]
-
-    if param.primary.tick % update_rate == 0:
-      self.redraw(True)
diff --git a/arm/header_panel.py b/arm/header_panel.py
deleted file mode 100644
index 7206870..0000000
--- a/arm/header_panel.py
+++ /dev/null
@@ -1,480 +0,0 @@
-"""
-Top panel for every page, containing basic system and tor related information.
-This expands the information it presents to two columns if there's room
-available.
-"""
-
-import collections
-import os
-import time
-import curses
-import threading
-
-import arm.controller
-import arm.popups
-
-import stem
-
-from stem.control import Listener
-from stem.util import conf, log, proc, str_tools, system
-
-from arm.util import msg, tor_controller, panel, tracker
-
-MIN_DUAL_COL_WIDTH = 141  # minimum width where we'll show two columns
-SHOW_FD_THRESHOLD = 60  # show file descriptor usage if usage is over this percentage
-UPDATE_RATE = 5  # rate in seconds at which we refresh
-
-CONFIG = conf.config_dict('arm', {
-  'attr.flag_colors': {},
-  'attr.version_status_colors': {},
-})
-
-
-class HeaderPanel(panel.Panel, threading.Thread):
-  """
-  Top area containing tor settings and system information.
-  """
-
-  def __init__(self, stdscr, start_time):
-    panel.Panel.__init__(self, stdscr, 'header', 0)
-    threading.Thread.__init__(self)
-    self.setDaemon(True)
-
-    self._vals = get_sampling()
-
-    self._pause_condition = threading.Condition()
-    self._halt = False  # terminates thread if true
-
-    tor_controller().add_status_listener(self.reset_listener)
-
-  def is_wide(self, width = None):
-    """
-    True if we should show two columns of information, False otherwise.
-    """
-
-    if width is None:
-      width = self.get_parent().getmaxyx()[1]
-
-    return width >= MIN_DUAL_COL_WIDTH
-
-  def get_height(self):
-    """
-    Provides the height of the content, which is dynamically determined by the
-    panel's maximum width.
-    """
-
-    if self._vals.is_relay:
-      return 4 if self.is_wide() else 6
-    else:
-      return 3 if self.is_wide() else 4
-
-  def send_newnym(self):
-    """
-    Requests a new identity and provides a visual queue.
-    """
-
-    controller = tor_controller()
-
-    if not controller.is_newnym_available():
-      return
-
-    controller.signal(stem.Signal.NEWNYM)
-
-    # If we're wide then the newnym label in this panel will give an
-    # indication that the signal was sent. Otherwise use a msg.
-
-    if not self.is_wide():
-      arm.popups.show_msg('Requesting a new identity', 1)
-
-  def handle_key(self, key):
-    if key.match('n'):
-      self.send_newnym()
-    elif key.match('r') and not self._vals.is_connected:
-      # TODO: This is borked. Not quite sure why but our attempt to call
-      # PROTOCOLINFO fails with a socket error, followed by completely freezing
-      # arm. This is exposing two bugs...
-      #
-      # * This should be working. That's a stem issue.
-      # * Our interface shouldn't be locking up. That's an arm issue.
-
-      return True
-
-      controller = tor_controller()
-
-      try:
-        controller.connect()
-
-        try:
-          controller.authenticate()  # TODO: should account for our chroot
-        except stem.connection.MissingPassword:
-          password = arm.popups.input_prompt('Controller Password: ')
-
-          if password:
-            controller.authenticate(password)
-
-        log.notice("Reconnected to Tor's control port")
-        arm.popups.show_msg('Tor reconnected', 1)
-      except Exception as exc:
-        arm.popups.show_msg('Unable to reconnect (%s)' % exc, 3)
-        controller.close()
-    else:
-      return False
-
-    return True
-
-  def draw(self, width, height):
-    vals = self._vals  # local reference to avoid concurrency concerns
-    is_wide = self.is_wide(width)
-
-    # space available for content
-
-    left_width = max(width / 2, 77) if is_wide else width
-    right_width = width - left_width
-
-    self._draw_platform_section(0, 0, left_width, vals)
-
-    if vals.is_connected:
-      self._draw_ports_section(0, 1, left_width, vals)
-    else:
-      self._draw_disconnected(0, 1, left_width, vals)
-
-    if is_wide:
-      self._draw_resource_usage(left_width, 0, right_width, vals)
-
-      if vals.is_relay:
-        self._draw_fingerprint_and_fd_usage(left_width, 1, right_width, vals)
-        self._draw_flags(0, 2, left_width, vals)
-        self._draw_exit_policy(left_width, 2, right_width, vals)
-      elif vals.is_connected:
-        self._draw_newnym_option(left_width, 1, right_width, vals)
-    else:
-      self._draw_resource_usage(0, 2, left_width, vals)
-
-      if vals.is_relay:
-        self._draw_fingerprint_and_fd_usage(0, 3, left_width, vals)
-        self._draw_flags(0, 4, left_width, vals)
-
-  def _draw_platform_section(self, x, y, width, vals):
-    """
-    Section providing the user's hostname, platform, and version information...
-
-      arm - odin (Linux 3.5.0-52-generic)        Tor 0.2.5.1-alpha-dev (unrecommended)
-      |------ platform (40 characters) ------|   |----------- tor version -----------|
-    """
-
-    initial_x, space_left = x, min(width, 40)
-
-    x = self.addstr(y, x, vals.format('arm - {hostname}', space_left))
-    space_left -= x - initial_x
-
-    if space_left >= 10:
-      self.addstr(y, x, ' (%s)' % vals.format('{platform}', space_left - 3))
-
-    x, space_left = initial_x + 43, width - 43
-
-    if vals.version != 'Unknown' and space_left >= 10:
-      x = self.addstr(y, x, vals.format('Tor {version}', space_left))
-      space_left -= x - 43 - initial_x
-
-      if space_left >= 7 + len(vals.version_status):
-        version_color = CONFIG['attr.version_status_colors'].get(vals.version_status, 'white')
-
-        x = self.addstr(y, x, ' (')
-        x = self.addstr(y, x, vals.version_status, version_color)
-        self.addstr(y, x, ')')
-
-  def _draw_ports_section(self, x, y, width, vals):
-    """
-    Section providing our nickname, address, and port information...
-
-      Unnamed - 0.0.0.0:7000, Control Port (cookie): 9051
-    """
-
-    if not vals.is_relay:
-      x = self.addstr(y, x, 'Relaying Disabled', 'cyan')
-    else:
-      x = self.addstr(y, x, vals.format('{nickname} - {address}:{or_port}'))
-
-      if vals.dir_port != '0':
-        x = self.addstr(y, x, vals.format(', Dir Port: {dir_port}'))
-
-    if vals.control_port:
-      if width >= x + 19 + len(vals.control_port) + len(vals.auth_type):
-        auth_color = 'red' if vals.auth_type == 'open' else 'green'
-
-        x = self.addstr(y, x, ', Control Port (')
-        x = self.addstr(y, x, vals.auth_type, auth_color)
-        self.addstr(y, x, vals.format('): {control_port}'))
-      else:
-        self.addstr(y, x, vals.format(', Control Port: {control_port}'))
-    elif vals.socket_path:
-      self.addstr(y, x, vals.format(', Control Socket: {socket_path}'))
-
-  def _draw_disconnected(self, x, y, width, vals):
-    """
-    Message indicating that tor is disconnected...
-
-      Tor Disconnected (15:21 07/13/2014, press r to reconnect)
-    """
-
-    x = self.addstr(y, x, 'Tor Disconnected', curses.A_BOLD, 'red')
-    self.addstr(y, x, vals.format(' ({last_heartbeat}, press r to reconnect)'))
-
-  def _draw_resource_usage(self, x, y, width, vals):
-    """
-    System resource usage of the tor process...
-
-      cpu: 0.0% tor, 1.0% arm    mem: 0 (0.0%)       pid: 16329  uptime: 12-20:42:07
-    """
-
-    if vals.start_time:
-      if not vals.is_connected:
-        now = vals.connection_time
-      elif self.is_paused():
-        now = self.get_pause_time()
-      else:
-        now = time.time()
-
-      uptime = str_tools.short_time_label(now - vals.start_time)
-    else:
-      uptime = ''
-
-    sys_fields = (
-      (0, vals.format('cpu: {tor_cpu}% tor, {arm_cpu}% arm')),
-      (27, vals.format('mem: {memory} ({memory_percent}%)')),
-      (47, vals.format('pid: {pid}')),
-      (59, 'uptime: %s' % uptime),
-    )
-
-    for (start, label) in sys_fields:
-      if width >= start + len(label):
-        self.addstr(y, x + start, label)
-      else:
-        break
-
-  def _draw_fingerprint_and_fd_usage(self, x, y, width, vals):
-    """
-    Presents our fingerprint, and our file descriptor usage if we're running
-    out...
-
-      fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B, file desc: 900 / 1000 (90%)
-    """
-
-    initial_x, space_left = x, width
-
-    x = self.addstr(y, x, vals.format('fingerprint: {fingerprint}', width))
-    space_left -= x - initial_x
-
-    if space_left >= 30 and vals.fd_used and vals.fd_limit != -1:
-      fd_percent = 100 * vals.fd_used / vals.fd_limit
-
-      if fd_percent >= SHOW_FD_THRESHOLD:
-        if fd_percent >= 95:
-          percentage_format = (curses.A_BOLD, 'red')
-        elif fd_percent >= 90:
-          percentage_format = ('red',)
-        elif fd_percent >= 60:
-          percentage_format = ('yellow',)
-        else:
-          percentage_format = ()
-
-        x = self.addstr(y, x, ', file descriptors' if space_left >= 37 else ', file desc')
-        x = self.addstr(y, x, vals.format(': {fd_used} / {fd_limit} ('))
-        x = self.addstr(y, x, '%i%%' % fd_percent, *percentage_format)
-        self.addstr(y, x, ')')
-
-  def _draw_flags(self, x, y, width, vals):
-    """
-    Presents flags held by our relay...
-
-      flags: Running, Valid
-    """
-
-    x = self.addstr(y, x, 'flags: ')
-
-    if vals.flags:
-      for i, flag in enumerate(vals.flags):
-        flag_color = CONFIG['attr.flag_colors'].get(flag, 'white')
-        x = self.addstr(y, x, flag, curses.A_BOLD, flag_color)
-
-        if i < len(vals.flags) - 1:
-          x = self.addstr(y, x, ', ')
-    else:
-      self.addstr(y, x, 'none', curses.A_BOLD, 'cyan')
-
-  def _draw_exit_policy(self, x, y, width, vals):
-    """
-    Presents our exit policy...
-
-      exit policy: reject *:*
-    """
-
-    x = self.addstr(y, x, 'exit policy: ')
-
-    if not vals.exit_policy:
-      return
-
-    rules = list(vals.exit_policy.strip_private().strip_default())
-
-    for i, rule in enumerate(rules):
-      policy_color = 'green' if rule.is_accept else 'red'
-      x = self.addstr(y, x, str(rule), curses.A_BOLD, policy_color)
-
-      if i < len(rules) - 1:
-        x = self.addstr(y, x, ', ')
-
-    if vals.exit_policy.has_default():
-      if rules:
-        x = self.addstr(y, x, ', ')
-
-      self.addstr(y, x, '<default>', curses.A_BOLD, 'cyan')
-
-  def _draw_newnym_option(self, x, y, width, vals):
-    """
-    Provide a notice for requiesting a new identity, and time until it's next
-    available if in the process of building circuits.
-    """
-
-    if vals.newnym_wait == 0:
-      self.addstr(y, x, "press 'n' for a new identity")
-    else:
-      plural = 's' if vals.newnym_wait > 1 else ''
-      self.addstr(y, x, 'building circuits, available again in %i second%s' % (vals.newnym_wait, plural))
-
-  def run(self):
-    """
-    Keeps stats updated, checking for new information at a set rate.
-    """
-
-    last_ran = -1
-
-    while not self._halt:
-      if self.is_paused() or not self._vals.is_connected or (time.time() - last_ran) < UPDATE_RATE:
-        with self._pause_condition:
-          if not self._halt:
-            self._pause_condition.wait(0.2)
-
-        continue  # done waiting, try again
-
-      self._update()
-      last_ran = time.time()
-
-  def stop(self):
-    """
-    Halts further resolutions and terminates the thread.
-    """
-
-    with self._pause_condition:
-      self._halt = True
-      self._pause_condition.notifyAll()
-
-  def reset_listener(self, controller, event_type, _):
-    self._update()
-
-  def _update(self):
-    previous_height = self.get_height()
-    self._vals = get_sampling(self._vals)
-
-    if self._vals.fd_used and self._vals.fd_limit != -1:
-      fd_percent = 100 * self._vals.fd_used / self._vals.fd_limit
-
-      if fd_percent >= 90:
-        log_msg = msg('panel.header.fd_used_at_ninety_percent', percentage = fd_percent)
-        log.log_once('fd_used_at_ninety_percent', log.WARN, log_msg)
-        log.DEDUPLICATION_MESSAGE_IDS.add('fd_used_at_sixty_percent')
-      elif fd_percent >= 60:
-        log_msg = msg('panel.header.fd_used_at_sixty_percent', percentage = fd_percent)
-        log.log_once('fd_used_at_sixty_percent', log.NOTICE, log_msg)
-
-    if previous_height != self.get_height():
-      # We're toggling between being a relay and client, causing the height
-      # of this panel to change. Redraw all content so we don't get
-      # overlapping content.
-
-      arm.controller.get_controller().redraw()
-    else:
-      self.redraw(True)  # just need to redraw ourselves
-
-
-def get_sampling(last_sampling = None):
-  controller = tor_controller()
-  retrieved = time.time()
-
-  pid = controller.get_pid('')
-  tor_resources = tracker.get_resource_tracker().get_value()
-  arm_total_cpu_time = sum(os.times()[:3])
-
-  or_listeners = controller.get_listeners(Listener.OR, [])
-  control_listeners = controller.get_listeners(Listener.CONTROL, [])
-
-  if controller.get_conf('HashedControlPassword', None):
-    auth_type = 'password'
-  elif controller.get_conf('CookieAuthentication', None) == '1':
-    auth_type = 'cookie'
-  else:
-    auth_type = 'open'
-
-  try:
-    fd_used = proc.file_descriptors_used(pid)
-  except IOError:
-    fd_used = None
-
-  if last_sampling:
-    arm_cpu_delta = arm_total_cpu_time - last_sampling.arm_total_cpu_time
-    arm_time_delta = retrieved - last_sampling.retrieved
-
-    python_cpu_time = arm_cpu_delta / arm_time_delta
-    sys_call_cpu_time = 0.0  # TODO: add a wrapper around call() to get this
-
-    arm_cpu = python_cpu_time + sys_call_cpu_time
-  else:
-    arm_cpu = 0.0
-
-  attr = {
-    'retrieved': retrieved,
-    'is_connected': controller.is_alive(),
-    'connection_time': controller.connection_time(),
-    'last_heartbeat': time.strftime('%H:%M %m/%d/%Y', time.localtime(controller.get_latest_heartbeat())),
-
-    'fingerprint': controller.get_info('fingerprint', 'Unknown'),
-    'nickname': controller.get_conf('Nickname', ''),
-    'newnym_wait': controller.get_newnym_wait(),
-    'exit_policy': controller.get_exit_policy(None),
-    'flags': getattr(controller.get_network_status(default = None), 'flags', []),
-
-    'version': str(controller.get_version('Unknown')).split()[0],
-    'version_status': controller.get_info('status/version/current', 'Unknown'),
-
-    'address': or_listeners[0][0] if (or_listeners and or_listeners[0][0] != '0.0.0.0') else controller.get_info('address', 'Unknown'),
-    'or_port': or_listeners[0][1] if or_listeners else '',
-    'dir_port': controller.get_conf('DirPort', '0'),
-    'control_port': str(control_listeners[0][1]) if control_listeners else None,
-    'socket_path': controller.get_conf('ControlSocket', None),
-    'is_relay': bool(or_listeners),
-
-    'auth_type': auth_type,
-    'pid': pid,
-    'start_time': system.start_time(pid),
-    'fd_limit': int(controller.get_info('process/descriptor-limit', '-1')),
-    'fd_used': fd_used,
-
-    'arm_total_cpu_time': arm_total_cpu_time,
-    'tor_cpu': '%0.1f' % (100 * tor_resources.cpu_sample),
-    'arm_cpu': arm_cpu,
-    'memory': str_tools.size_label(tor_resources.memory_bytes) if tor_resources.memory_bytes > 0 else 0,
-    'memory_percent': '%0.1f' % (100 * tor_resources.memory_percent),
-
-    'hostname': os.uname()[1],
-    'platform': '%s %s' % (os.uname()[0], os.uname()[2]),  # [platform name] [version]
-  }
-
-  class Sampling(collections.namedtuple('Sampling', attr.keys())):
-    def format(self, message, crop_width = None):
-      formatted_msg = message.format(**super(Sampling, self).__dict__)
-
-      if crop_width:
-        formatted_msg = str_tools.crop(formatted_msg, crop_width)
-
-      return formatted_msg
-
-  return Sampling(**attr)
diff --git a/arm/log_panel.py b/arm/log_panel.py
deleted file mode 100644
index f0e2a23..0000000
--- a/arm/log_panel.py
+++ /dev/null
@@ -1,1369 +0,0 @@
-"""
-Panel providing a chronological log of events its been configured to listen
-for. This provides prepopulation from the log file and supports filtering by
-regular expressions.
-"""
-
-import re
-import os
-import time
-import curses
-import logging
-import threading
-
-import stem
-from stem.control import State
-from stem.response import events
-from stem.util import conf, log, str_tools, system
-
-import arm.arguments
-import arm.popups
-from arm import __version__
-from arm.util import panel, tor_controller, ui_tools
-
-RUNLEVEL_EVENT_COLOR = {
-  log.DEBUG: "magenta",
-  log.INFO: "blue",
-  log.NOTICE: "green",
-  log.WARN: "yellow",
-  log.ERR: "red",
-}
-
-DAYBREAK_EVENT = "DAYBREAK"  # special event for marking when the date changes
-TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
-
-ENTRY_INDENT = 2  # spaces an entry's message is indented after the first line
-
-
-def conf_handler(key, value):
-  if key == "features.log.max_lines_per_entry":
-    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.log_panel.size":
-    return max(1000, value)
-
-
-CONFIG = conf.config_dict("arm", {
-  "features.log_file": "",
-  "features.log.showDateDividers": True,
-  "features.log.showDuplicateEntries": False,
-  "features.log.entryDuration": 7,
-  "features.log.max_lines_per_entry": 6,
-  "features.log.prepopulate": True,
-  "features.log.prepopulateReadLimit": 5000,
-  "features.log.maxRefreshRate": 300,
-  "features.log.regex": [],
-  "cache.log_panel.size": 1000,
-  "msg.misc.event_types": '',
-  "tor.chroot": '',
-}, conf_handler)
-
-DUPLICATE_MSG = " [%i duplicate%s hidden]"
-
-# The height of the drawn content is estimated based on the last time we redrew
-# the panel. It's chiefly used for scrolling and the bar indicating its
-# position. Letting the estimate be too inaccurate results in a display bug, so
-# redraws the display if it's off by this threshold.
-
-CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
-
-# static starting portion of common log entries, fetched from the config when
-# needed if None
-
-COMMON_LOG_MESSAGES = None
-
-# cached values and the arguments that generated it for the get_daybreaks and
-# get_duplicates functions
-
-CACHED_DAYBREAKS_ARGUMENTS = (None, None)  # events, current day
-CACHED_DAYBREAKS_RESULT = None
-CACHED_DUPLICATES_ARGUMENTS = None  # events
-CACHED_DUPLICATES_RESULT = None
-
-# duration we'll wait for the deduplication function before giving up (in ms)
-
-DEDUPLICATION_TIMEOUT = 100
-
-# maximum number of regex filters we'll remember
-
-MAX_REGEX_FILTERS = 5
-
-
-def days_since(timestamp = None):
-  """
-  Provides the number of days since the epoch converted to local time (rounded
-  down).
-
-  Arguments:
-    timestamp - unix timestamp to convert, current time if undefined
-  """
-
-  if timestamp is None:
-    timestamp = time.time()
-
-  return int((timestamp - TIMEZONE_OFFSET) / 86400)
-
-
-def load_log_messages():
-  """
-  Fetches a mapping of common log messages to their runlevels from the config.
-  """
-
-  global COMMON_LOG_MESSAGES
-  arm_config = conf.get_config("arm")
-
-  COMMON_LOG_MESSAGES = {}
-
-  for conf_key in arm_config.keys():
-    if conf_key.startswith("dedup."):
-      event_type = conf_key[4:].upper()
-      messages = arm_config.get(conf_key, [])
-      COMMON_LOG_MESSAGES[event_type] = messages
-
-
-def get_log_file_entries(runlevels, read_limit = None, add_limit = None):
-  """
-  Parses tor's log file for past events matching the given runlevels, providing
-  a list of log entries (ordered newest to oldest). Limiting the number of read
-  entries is suggested to avoid parsing everything from logs in the GB and TB
-  range.
-
-  Arguments:
-    runlevels - event types (DEBUG - ERR) to be returned
-    read_limit - max lines of the log file that'll be read (unlimited if None)
-    add_limit  - maximum entries to provide back (unlimited if None)
-  """
-
-  start_time = time.time()
-
-  if not runlevels:
-    return []
-
-  # checks tor's configuration for the log file's location (if any exists)
-
-  logging_types, logging_location = None, None
-
-  for logging_entry in tor_controller().get_conf("Log", [], True):
-    # looks for an entry like: notice file /var/log/tor/notices.log
-
-    entry_comp = logging_entry.split()
-
-    if entry_comp[1] == "file":
-      logging_types, logging_location = entry_comp[0], entry_comp[2]
-      break
-
-  if not logging_location:
-    return []
-
-  # includes the prefix for tor paths
-
-  logging_location = CONFIG['tor.chroot'] + logging_location
-
-  # if the runlevels argument is a superset of the log file then we can
-  # limit the read contents to the add_limit
-
-  runlevels = list(log.Runlevel)
-  logging_types = logging_types.upper()
-
-  if add_limit and (not read_limit or read_limit > add_limit):
-    if "-" in logging_types:
-      div_index = logging_types.find("-")
-      start_index = runlevels.index(logging_types[:div_index])
-      end_index = runlevels.index(logging_types[div_index + 1:])
-      log_file_run_levels = runlevels[start_index:end_index + 1]
-    else:
-      start_index = runlevels.index(logging_types)
-      log_file_run_levels = runlevels[start_index:]
-
-    # checks if runlevels we're reporting are a superset of the file's contents
-
-    is_file_subset = True
-
-    for runlevel_type in log_file_run_levels:
-      if runlevel_type not in runlevels:
-        is_file_subset = False
-        break
-
-    if is_file_subset:
-      read_limit = add_limit
-
-  # tries opening the log file, cropping results to avoid choking on huge logs
-
-  lines = []
-
-  try:
-    if read_limit:
-      lines = system.call("tail -n %i %s" % (read_limit, logging_location))
-
-      if not lines:
-        raise IOError()
-    else:
-      log_file = open(logging_location, "r")
-      lines = log_file.readlines()
-      log_file.close()
-  except IOError:
-    log.warn("Unable to read tor's log file: %s" % logging_location)
-
-  if not lines:
-    return []
-
-  logged_events = []
-  current_unix_time, current_local_time = time.time(), time.localtime()
-
-  for i in range(len(lines) - 1, -1, -1):
-    line = lines[i]
-
-    # entries look like:
-    # Jul 15 18:29:48.806 [notice] Parsing GEOIP file.
-
-    line_comp = line.split()
-
-    # Checks that we have all the components we expect. This could happen if
-    # we're either not parsing a tor log or in weird edge cases (like being
-    # out of disk space)
-
-    if len(line_comp) < 4:
-      continue
-
-    event_type = line_comp[3][1:-1].upper()
-
-    if event_type in runlevels:
-      # converts timestamp to unix time
-
-      timestamp = " ".join(line_comp[:3])
-
-      # strips the decimal seconds
-
-      if "." in timestamp:
-        timestamp = timestamp[:timestamp.find(".")]
-
-      # Ignoring wday and yday since they aren't used.
-      #
-      # Pretend the year is 2012, because 2012 is a leap year, and parsing a
-      # date with strptime fails if Feb 29th is passed without a year that's
-      # actually a leap year. We can't just use the current year, because we
-      # might be parsing old logs which didn't get rotated.
-      #
-      # https://trac.torproject.org/projects/tor/ticket/5265
-
-      timestamp = "2012 " + timestamp
-      event_time_comp = list(time.strptime(timestamp, "%Y %b %d %H:%M:%S"))
-      event_time_comp[8] = current_local_time.tm_isdst
-      event_time = time.mktime(event_time_comp)  # converts local to unix time
-
-      # The above is gonna be wrong if the logs are for the previous year. If
-      # the event's in the future then correct for this.
-
-      if event_time > current_unix_time + 60:
-        event_time_comp[0] -= 1
-        event_time = time.mktime(event_time_comp)
-
-      event_msg = " ".join(line_comp[4:])
-      logged_events.append(LogEntry(event_time, event_type, event_msg, RUNLEVEL_EVENT_COLOR[event_type]))
-
-    if "opening log file" in line:
-      break  # this entry marks the start of this tor instance
-
-  if add_limit:
-    logged_events = logged_events[:add_limit]
-
-  log.info("Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(logged_events), logging_location, read_limit, time.time() - start_time))
-
-  return logged_events
-
-
-def get_daybreaks(events, ignore_time_for_cache = False):
-  """
-  Provides the input events back with special 'DAYBREAK_EVENT' markers inserted
-  whenever the date changed between log entries (or since the most recent
-  event). The timestamp matches the beginning of the day for the following
-  entry.
-
-  Arguments:
-    events             - chronologically ordered listing of events
-    ignore_time_for_cache - skips taking the day into consideration for providing
-                         cached results if true
-  """
-
-  global CACHED_DAYBREAKS_ARGUMENTS, CACHED_DAYBREAKS_RESULT
-
-  if not events:
-    return []
-
-  new_listing = []
-  current_day = days_since()
-  last_day = current_day
-
-  if CACHED_DAYBREAKS_ARGUMENTS[0] == events and \
-    (ignore_time_for_cache or CACHED_DAYBREAKS_ARGUMENTS[1] == current_day):
-    return list(CACHED_DAYBREAKS_RESULT)
-
-  for entry in events:
-    event_day = days_since(entry.timestamp)
-
-    if event_day != last_day:
-      marker_timestamp = (event_day * 86400) + TIMEZONE_OFFSET
-      new_listing.append(LogEntry(marker_timestamp, DAYBREAK_EVENT, "", "white"))
-
-    new_listing.append(entry)
-    last_day = event_day
-
-  CACHED_DAYBREAKS_ARGUMENTS = (list(events), current_day)
-  CACHED_DAYBREAKS_RESULT = list(new_listing)
-
-  return new_listing
-
-
-def get_duplicates(events):
-  """
-  Deduplicates a list of log entries, providing back a tuple listing with the
-  log entry and count of duplicates following it. Entries in different days are
-  not considered to be duplicates. This times out, returning None if it takes
-  longer than DEDUPLICATION_TIMEOUT.
-
-  Arguments:
-    events - chronologically ordered listing of events
-  """
-
-  global CACHED_DUPLICATES_ARGUMENTS, CACHED_DUPLICATES_RESULT
-
-  if CACHED_DUPLICATES_ARGUMENTS == events:
-    return list(CACHED_DUPLICATES_RESULT)
-
-  # loads common log entries from the config if they haven't been
-
-  if COMMON_LOG_MESSAGES is None:
-    load_log_messages()
-
-  start_time = time.time()
-  events_remaining = list(events)
-  return_events = []
-
-  while events_remaining:
-    entry = events_remaining.pop(0)
-    duplicate_indices = is_duplicate(entry, events_remaining, True)
-
-    # checks if the call timeout has been reached
-
-    if (time.time() - start_time) > DEDUPLICATION_TIMEOUT / 1000.0:
-      return None
-
-    # drops duplicate entries
-
-    duplicate_indices.reverse()
-
-    for i in duplicate_indices:
-      del events_remaining[i]
-
-    return_events.append((entry, len(duplicate_indices)))
-
-  CACHED_DUPLICATES_ARGUMENTS = list(events)
-  CACHED_DUPLICATES_RESULT = list(return_events)
-
-  return return_events
-
-
-def is_duplicate(event, event_set, get_duplicates = False):
-  """
-  True if the event is a duplicate for something in the event_set, false
-  otherwise. If the get_duplicates flag is set this provides the indices of
-  the duplicates instead.
-
-  Arguments:
-    event         - event to search for duplicates of
-    event_set      - set to look for the event in
-    get_duplicates - instead of providing back a boolean this gives a list of
-                    the duplicate indices in the event_set
-  """
-
-  duplicate_indices = []
-
-  for i in range(len(event_set)):
-    forward_entry = event_set[i]
-
-    # if showing dates then do duplicate detection for each day, rather
-    # than globally
-
-    if forward_entry.type == DAYBREAK_EVENT:
-      break
-
-    if event.type == forward_entry.type:
-      is_duplicate = False
-
-      if event.msg == forward_entry.msg:
-        is_duplicate = True
-      elif event.type in COMMON_LOG_MESSAGES:
-        for common_msg in COMMON_LOG_MESSAGES[event.type]:
-          # if it starts with an asterisk then check the whole message rather
-          # than just the start
-
-          if common_msg[0] == "*":
-            is_duplicate = common_msg[1:] in event.msg and common_msg[1:] in forward_entry.msg
-          else:
-            is_duplicate = event.msg.startswith(common_msg) and forward_entry.msg.startswith(common_msg)
-
-          if is_duplicate:
-            break
-
-      if is_duplicate:
-        if get_duplicates:
-          duplicate_indices.append(i)
-        else:
-          return True
-
-  if get_duplicates:
-    return duplicate_indices
-  else:
-    return False
-
-
-class LogEntry():
-  """
-  Individual log file entry, having the following attributes:
-    timestamp - unix timestamp for when the event occurred
-    event_type - event type that occurred ("INFO", "BW", "ARM_WARN", etc)
-    msg       - message that was logged
-    color     - color of the log entry
-  """
-
-  def __init__(self, timestamp, event_type, msg, color):
-    self.timestamp = timestamp
-    self.type = event_type
-    self.msg = msg
-    self.color = color
-    self._display_message = None
-
-  def get_display_message(self, include_date = False):
-    """
-    Provides the entry's message for the log.
-
-    Arguments:
-      include_date - appends the event's date to the start of the message
-    """
-
-    if include_date:
-      # not the common case so skip caching
-      entry_time = time.localtime(self.timestamp)
-      time_label = "%i/%i/%i %02i:%02i:%02i" % (entry_time[1], entry_time[2], entry_time[0], entry_time[3], entry_time[4], entry_time[5])
-      return "%s [%s] %s" % (time_label, self.type, self.msg)
-
-    if not self._display_message:
-      entry_time = time.localtime(self.timestamp)
-      self._display_message = "%02i:%02i:%02i [%s] %s" % (entry_time[3], entry_time[4], entry_time[5], self.type, self.msg)
-
-    return self._display_message
-
-
-class LogPanel(panel.Panel, threading.Thread, logging.Handler):
-  """
-  Listens for and displays tor, arm, and stem events. This can prepopulate
-  from tor's log file if it exists.
-  """
-
-  def __init__(self, stdscr, logged_events):
-    panel.Panel.__init__(self, stdscr, "log", 0)
-    logging.Handler.__init__(self, level = log.logging_level(log.DEBUG))
-
-    self.setFormatter(logging.Formatter(
-      fmt = '%(asctime)s [%(levelname)s] %(message)s',
-      datefmt = '%m/%d/%Y %H:%M:%S'),
-    )
-
-    threading.Thread.__init__(self)
-    self.setDaemon(True)
-
-    # Make sure that the msg.* messages are loaded. Lazy loading it later is
-    # fine, but this way we're sure it happens before warning about unused
-    # config options.
-
-    load_log_messages()
-
-    # regex filters the user has defined
-
-    self.filter_options = []
-
-    for filter in CONFIG["features.log.regex"]:
-      # checks if we can't have more filters
-
-      if len(self.filter_options) >= MAX_REGEX_FILTERS:
-        break
-
-      try:
-        re.compile(filter)
-        self.filter_options.append(filter)
-      except re.error as exc:
-        log.notice("Invalid regular expression pattern (%s): %s" % (exc, filter))
-
-    self.logged_events = []  # needs to be set before we receive any events
-
-    # restricts the input to the set of events we can listen to, and
-    # configures the controller to liten to them
-
-    self.logged_events = self.set_event_listening(logged_events)
-
-    self.set_pause_attr("msg_log")       # tracks the message log when we're paused
-    self.msg_log = []                    # log entries, sorted by the timestamp
-    self.regex_filter = None             # filter for presented log events (no filtering if None)
-    self.last_content_height = 0         # height of the rendered content when last drawn
-    self.log_file = None                 # file log messages are saved to (skipped if None)
-    self.scroll = 0
-
-    self._last_update = -1               # time the content was last revised
-    self._halt = False                   # terminates thread if true
-    self._cond = threading.Condition()   # used for pausing/resuming the thread
-
-    # restricts concurrent write access to attributes used to draw the display
-    # and pausing:
-    # msg_log, logged_events, regex_filter, scroll
-
-    self.vals_lock = threading.RLock()
-
-    # cached parameters (invalidated if arguments for them change)
-    # last set of events we've drawn with
-
-    self._last_logged_events = []
-
-    # _get_title (args: logged_events, regex_filter pattern, width)
-
-    self._title_cache = None
-    self._title_args = (None, None, None)
-
-    self.reprepopulate_events()
-
-    # leaving last_content_height as being too low causes initialization problems
-
-    self.last_content_height = len(self.msg_log)
-
-    # adds listeners for tor and stem events
-
-    controller = tor_controller()
-    controller.add_status_listener(self._reset_listener)
-
-    # opens log file if we'll be saving entries
-
-    if CONFIG["features.log_file"]:
-      log_path = CONFIG["features.log_file"]
-
-      try:
-        # make dir if the path doesn't already exist
-
-        base_dir = os.path.dirname(log_path)
-
-        if not os.path.exists(base_dir):
-          os.makedirs(base_dir)
-
-        self.log_file = open(log_path, "a")
-        log.notice("arm %s opening log file (%s)" % (__version__, log_path))
-      except IOError as exc:
-        log.error("Unable to write to log file: %s" % exc.strerror)
-        self.log_file = None
-      except OSError as exc:
-        log.error("Unable to write to log file: %s" % exc)
-        self.log_file = None
-
-    stem_logger = log.get_logger()
-    stem_logger.addHandler(self)
-
-  def emit(self, record):
-    if record.levelname == "WARNING":
-      record.levelname = "WARN"
-
-    event_color = RUNLEVEL_EVENT_COLOR[record.levelname]
-    self.register_event(LogEntry(int(record.created), "ARM_%s" % record.levelname, record.msg, event_color))
-
-  def reprepopulate_events(self):
-    """
-    Clears the event log and repopulates it from the arm and tor backlogs.
-    """
-
-    self.vals_lock.acquire()
-
-    # clears the event log
-
-    self.msg_log = []
-
-    # fetches past tor events from log file, if available
-
-    if CONFIG["features.log.prepopulate"]:
-      set_runlevels = list(set.intersection(set(self.logged_events), set(list(log.Runlevel))))
-      read_limit = CONFIG["features.log.prepopulateReadLimit"]
-      add_limit = CONFIG["cache.log_panel.size"]
-
-      for entry in get_log_file_entries(set_runlevels, read_limit, add_limit):
-        self.msg_log.append(entry)
-
-    # crops events that are either too old, or more numerous than the caching size
-
-    self._trim_events(self.msg_log)
-
-    self.vals_lock.release()
-
-  def set_duplicate_visability(self, is_visible):
-    """
-    Sets if duplicate log entries are collaped or expanded.
-
-    Arguments:
-      is_visible - if true all log entries are shown, otherwise they're
-                   deduplicated
-    """
-
-    arm_config = conf.get_config("arm")
-    arm_config.set("features.log.showDuplicateEntries", str(is_visible))
-
-  def register_tor_event(self, event):
-    """
-    Translates a stem.response.event.Event instance into a LogEvent, and calls
-    register_event().
-    """
-
-    msg, color = ' '.join(str(event).split(' ')[1:]), "white"
-
-    if isinstance(event, events.CircuitEvent):
-      color = "yellow"
-    elif isinstance(event, events.BandwidthEvent):
-      color = "cyan"
-      msg = "READ: %i, WRITTEN: %i" % (event.read, event.written)
-    elif isinstance(event, events.LogEvent):
-      color = RUNLEVEL_EVENT_COLOR[event.runlevel]
-      msg = event.message
-    elif isinstance(event, events.NetworkStatusEvent):
-      color = "blue"
-    elif isinstance(event, events.NewConsensusEvent):
-      color = "magenta"
-    elif isinstance(event, events.GuardEvent):
-      color = "yellow"
-    elif event.type not in arm.arguments.TOR_EVENT_TYPES.values():
-      color = "red"  # unknown event type
-
-    self.register_event(LogEntry(event.arrived_at, event.type, msg, color))
-
-  def register_event(self, event):
-    """
-    Notes event and redraws log. If paused it's held in a temporary buffer.
-
-    Arguments:
-      event - LogEntry for the event that occurred
-    """
-
-    if event.type not in self.logged_events:
-      return
-
-    # strips control characters to avoid screwing up the terminal
-
-    event.msg = ui_tools.get_printable(event.msg)
-
-    # note event in the log file if we're saving them
-
-    if self.log_file:
-      try:
-        self.log_file.write(event.get_display_message(True) + "\n")
-        self.log_file.flush()
-      except IOError as exc:
-        log.error("Unable to write to log file: %s" % exc.strerror)
-        self.log_file = None
-
-    self.vals_lock.acquire()
-    self.msg_log.insert(0, event)
-    self._trim_events(self.msg_log)
-
-    # notifies the display that it has new content
-
-    if not self.regex_filter or self.regex_filter.search(event.get_display_message()):
-      self._cond.acquire()
-      self._cond.notifyAll()
-      self._cond.release()
-
-    self.vals_lock.release()
-
-  def set_logged_events(self, event_types):
-    """
-    Sets the event types recognized by the panel.
-
-    Arguments:
-      event_types - event types to be logged
-    """
-
-    if event_types == self.logged_events:
-      return
-
-    self.vals_lock.acquire()
-
-    # configures the controller to listen for these tor events, and provides
-    # back a subset without anything we're failing to listen to
-
-    set_types = self.set_event_listening(event_types)
-    self.logged_events = set_types
-    self.redraw(True)
-    self.vals_lock.release()
-
-  def get_filter(self):
-    """
-    Provides our currently selected regex filter.
-    """
-
-    return self.filter_options[0] if self.regex_filter else None
-
-  def set_filter(self, log_filter):
-    """
-    Filters log entries according to the given regular expression.
-
-    Arguments:
-      log_filter - regular expression used to determine which messages are
-                  shown, None if no filter should be applied
-    """
-
-    if log_filter == self.regex_filter:
-      return
-
-    self.vals_lock.acquire()
-    self.regex_filter = log_filter
-    self.redraw(True)
-    self.vals_lock.release()
-
-  def make_filter_selection(self, selected_option):
-    """
-    Makes the given filter selection, applying it to the log and reorganizing
-    our filter selection.
-
-    Arguments:
-      selected_option - regex filter we've already added, None if no filter
-                       should be applied
-    """
-
-    if selected_option:
-      try:
-        self.set_filter(re.compile(selected_option))
-
-        # move selection to top
-
-        self.filter_options.remove(selected_option)
-        self.filter_options.insert(0, selected_option)
-      except re.error as exc:
-        # shouldn't happen since we've already checked validity
-
-        log.warn("Invalid regular expression ('%s': %s) - removing from listing" % (selected_option, exc))
-        self.filter_options.remove(selected_option)
-    else:
-      self.set_filter(None)
-
-  def show_filter_prompt(self):
-    """
-    Prompts the user to add a new regex filter.
-    """
-
-    regex_input = arm.popups.input_prompt("Regular expression: ")
-
-    if regex_input:
-      try:
-        self.set_filter(re.compile(regex_input))
-
-        if regex_input in self.filter_options:
-          self.filter_options.remove(regex_input)
-
-        self.filter_options.insert(0, regex_input)
-      except re.error as exc:
-        arm.popups.show_msg("Unable to compile expression: %s" % exc, 2)
-
-  def show_event_selection_prompt(self):
-    """
-    Prompts the user to select the events being listened for.
-    """
-
-    # allow user to enter new types of events to log - unchanged if left blank
-
-    popup, width, height = arm.popups.init(11, 80)
-
-    if popup:
-      try:
-        # displays the available flags
-
-        popup.win.box()
-        popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT)
-        event_lines = CONFIG['msg.misc.event_types'].split("\n")
-
-        for i in range(len(event_lines)):
-          popup.addstr(i + 1, 1, event_lines[i][6:])
-
-        popup.win.refresh()
-
-        user_input = arm.popups.input_prompt("Events to log: ")
-
-        if user_input:
-          user_input = user_input.replace(' ', '')  # strips spaces
-
-          try:
-            self.set_logged_events(arm.arguments.expand_events(user_input))
-          except ValueError as exc:
-            arm.popups.show_msg("Invalid flags: %s" % str(exc), 2)
-      finally:
-        arm.popups.finalize()
-
-  def show_snapshot_prompt(self):
-    """
-    Lets user enter a path to take a snapshot, canceling if left blank.
-    """
-
-    path_input = arm.popups.input_prompt("Path to save log snapshot: ")
-
-    if path_input:
-      try:
-        self.save_snapshot(path_input)
-        arm.popups.show_msg("Saved: %s" % path_input, 2)
-      except IOError as exc:
-        arm.popups.show_msg("Unable to save snapshot: %s" % exc.strerror, 2)
-
-  def clear(self):
-    """
-    Clears the contents of the event log.
-    """
-
-    self.vals_lock.acquire()
-    self.msg_log = []
-    self.redraw(True)
-    self.vals_lock.release()
-
-  def save_snapshot(self, path):
-    """
-    Saves the log events currently being displayed to the given path. This
-    takes filers into account. This overwrites the file if it already exists,
-    and raises an IOError if there's a problem.
-
-    Arguments:
-      path - path where to save the log snapshot
-    """
-
-    path = os.path.abspath(os.path.expanduser(path))
-
-    # make dir if the path doesn't already exist
-
-    base_dir = os.path.dirname(path)
-
-    try:
-      if not os.path.exists(base_dir):
-        os.makedirs(base_dir)
-    except OSError as exc:
-      raise IOError("unable to make directory '%s'" % base_dir)
-
-    snapshot_file = open(path, "w")
-    self.vals_lock.acquire()
-
-    try:
-      for entry in self.msg_log:
-        is_visible = not self.regex_filter or self.regex_filter.search(entry.get_display_message())
-
-        if is_visible:
-          snapshot_file.write(entry.get_display_message(True) + "\n")
-
-      self.vals_lock.release()
-    except Exception as exc:
-      self.vals_lock.release()
-      raise exc
-
-  def handle_key(self, key):
-    if key.is_scroll():
-      page_height = self.get_preferred_size()[0] - 1
-      new_scroll = ui_tools.get_scroll_position(key, self.scroll, page_height, self.last_content_height)
-
-      if self.scroll != new_scroll:
-        self.vals_lock.acquire()
-        self.scroll = new_scroll
-        self.redraw(True)
-        self.vals_lock.release()
-    elif key.match('u'):
-      self.vals_lock.acquire()
-      self.set_duplicate_visability(not CONFIG["features.log.showDuplicateEntries"])
-      self.redraw(True)
-      self.vals_lock.release()
-    elif key.match('c'):
-      msg = "This will clear the log. Are you sure (c again to confirm)?"
-      key_press = arm.popups.show_msg(msg, attr = curses.A_BOLD)
-
-      if key_press.match('c'):
-        self.clear()
-    elif key.match('f'):
-      # Provides menu to pick regular expression filters or adding new ones:
-      # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
-
-      options = ["None"] + self.filter_options + ["New..."]
-      old_selection = 0 if not self.regex_filter else 1
-
-      # does all activity under a curses lock to prevent redraws when adding
-      # new filters
-
-      panel.CURSES_LOCK.acquire()
-
-      try:
-        selection = arm.popups.show_menu("Log Filter:", options, old_selection)
-
-        # applies new setting
-
-        if selection == 0:
-          self.set_filter(None)
-        elif selection == len(options) - 1:
-          # selected 'New...' option - prompt user to input regular expression
-          self.show_filter_prompt()
-        elif selection != -1:
-          self.make_filter_selection(self.filter_options[selection - 1])
-      finally:
-        panel.CURSES_LOCK.release()
-
-      if len(self.filter_options) > MAX_REGEX_FILTERS:
-        del self.filter_options[MAX_REGEX_FILTERS:]
-    elif key.match('e'):
-      self.show_event_selection_prompt()
-    elif key.match('a'):
-      self.show_snapshot_prompt()
-    else:
-      return False
-
-    return True
-
-  def get_help(self):
-    return [
-      ('up arrow', 'scroll log up a line', None),
-      ('down arrow', 'scroll log down a line', None),
-      ('a', 'save snapshot of the log', None),
-      ('e', 'change logged events', None),
-      ('f', 'log regex filter', 'enabled' if self.regex_filter else 'disabled'),
-      ('u', 'duplicate log entries', 'visible' if CONFIG['features.log.showDuplicateEntries'] else 'hidden'),
-      ('c', 'clear event log', None),
-    ]
-
-  def draw(self, width, height):
-    """
-    Redraws message log. Entries stretch to use available space and may
-    contain up to two lines. Starts with newest entries.
-    """
-
-    current_log = self.get_attr("msg_log")
-
-    self.vals_lock.acquire()
-    self._last_logged_events, self._last_update = list(current_log), time.time()
-
-    # draws the top label
-
-    if self.is_title_visible():
-      self.addstr(0, 0, self._get_title(width), curses.A_STANDOUT)
-
-    # restricts scroll location to valid bounds
-
-    self.scroll = max(0, min(self.scroll, self.last_content_height - height + 1))
-
-    # draws left-hand scroll bar if content's longer than the height
-
-    msg_indent, divider_indent = 1, 0  # offsets for scroll bar
-    is_scroll_bar_visible = self.last_content_height > height - 1
-
-    if is_scroll_bar_visible:
-      msg_indent, divider_indent = 3, 2
-      self.add_scroll_bar(self.scroll, self.scroll + height - 1, self.last_content_height, 1)
-
-    # draws log entries
-
-    line_count = 1 - self.scroll
-    seen_first_date_divider = False
-    divider_attr, duplicate_attr = (curses.A_BOLD, 'yellow'), (curses.A_BOLD, 'green')
-
-    is_dates_shown = self.regex_filter is None and CONFIG["features.log.showDateDividers"]
-    event_log = get_daybreaks(current_log, self.is_paused()) if is_dates_shown else list(current_log)
-
-    if not CONFIG["features.log.showDuplicateEntries"]:
-      deduplicated_log = get_duplicates(event_log)
-
-      if deduplicated_log is None:
-        log.warn("Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive.")
-        self.set_duplicate_visability(True)
-        deduplicated_log = [(entry, 0) for entry in event_log]
-    else:
-      deduplicated_log = [(entry, 0) for entry in event_log]
-
-    # determines if we have the minimum width to show date dividers
-
-    show_daybreaks = width - divider_indent >= 3
-
-    while deduplicated_log:
-      entry, duplicate_count = deduplicated_log.pop(0)
-
-      if self.regex_filter and not self.regex_filter.search(entry.get_display_message()):
-        continue  # filter doesn't match log message - skip
-
-      # checks if we should be showing a divider with the date
-
-      if entry.type == DAYBREAK_EVENT:
-        # bottom of the divider
-
-        if seen_first_date_divider:
-          if line_count >= 1 and line_count < height and show_daybreaks:
-            self.addch(line_count, divider_indent, curses.ACS_LLCORNER, *divider_attr)
-            self.hline(line_count, divider_indent + 1, width - divider_indent - 2, *divider_attr)
-            self.addch(line_count, width - 1, curses.ACS_LRCORNER, *divider_attr)
-
-          line_count += 1
-
-        # top of the divider
-
-        if line_count >= 1 and line_count < height and show_daybreaks:
-          time_label = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp))
-          self.addch(line_count, divider_indent, curses.ACS_ULCORNER, *divider_attr)
-          self.addch(line_count, divider_indent + 1, curses.ACS_HLINE, *divider_attr)
-          self.addstr(line_count, divider_indent + 2, time_label, curses.A_BOLD, *divider_attr)
-
-          line_length = width - divider_indent - len(time_label) - 3
-          self.hline(line_count, divider_indent + len(time_label) + 2, line_length, *divider_attr)
-          self.addch(line_count, divider_indent + len(time_label) + 2 + line_length, curses.ACS_URCORNER, *divider_attr)
-
-        seen_first_date_divider = True
-        line_count += 1
-      else:
-        # entry contents to be displayed, tuples of the form:
-        # (msg, formatting, includeLinebreak)
-
-        display_queue = []
-
-        msg_comp = entry.get_display_message().split("\n")
-
-        for i in range(len(msg_comp)):
-          font = curses.A_BOLD if "ERR" in entry.type else curses.A_NORMAL  # emphasizes ERR messages
-          display_queue.append((msg_comp[i].strip(), (font, entry.color), i != len(msg_comp) - 1))
-
-        if duplicate_count:
-          plural_label = "s" if duplicate_count > 1 else ""
-          duplicate_msg = DUPLICATE_MSG % (duplicate_count, plural_label)
-          display_queue.append((duplicate_msg, duplicate_attr, False))
-
-        cursor_location, line_offset = msg_indent, 0
-        max_entries_per_line = CONFIG["features.log.max_lines_per_entry"]
-
-        while display_queue:
-          msg, format, include_break = display_queue.pop(0)
-          draw_line = line_count + line_offset
-
-          if line_offset == max_entries_per_line:
-            break
-
-          max_msg_size = width - cursor_location - 1
-
-          if len(msg) > max_msg_size:
-            # message is too long - break it up
-            if line_offset == max_entries_per_line - 1:
-              msg = str_tools.crop(msg, max_msg_size)
-            else:
-              msg, remainder = str_tools.crop(msg, max_msg_size, 4, 4, str_tools.Ending.HYPHEN, True)
-              display_queue.insert(0, (remainder.strip(), format, include_break))
-
-            include_break = True
-
-          if draw_line < height and draw_line >= 1:
-            if seen_first_date_divider and width - divider_indent >= 3 and show_daybreaks:
-              self.addch(draw_line, divider_indent, curses.ACS_VLINE, *divider_attr)
-              self.addch(draw_line, width - 1, curses.ACS_VLINE, *divider_attr)
-
-            self.addstr(draw_line, cursor_location, msg, *format)
-
-          cursor_location += len(msg)
-
-          if include_break or not display_queue:
-            line_offset += 1
-            cursor_location = msg_indent + ENTRY_INDENT
-
-        line_count += line_offset
-
-      # if this is the last line and there's room, then draw the bottom of the divider
-
-      if not deduplicated_log and seen_first_date_divider:
-        if line_count < height and show_daybreaks:
-          self.addch(line_count, divider_indent, curses.ACS_LLCORNER, *divider_attr)
-          self.hline(line_count, divider_indent + 1, width - divider_indent - 2, *divider_attr)
-          self.addch(line_count, width - 1, curses.ACS_LRCORNER, *divider_attr)
-
-        line_count += 1
-
-    # redraw the display if...
-    # - last_content_height was off by too much
-    # - we're off the bottom of the page
-
-    new_content_height = line_count + self.scroll - 1
-    content_height_delta = abs(self.last_content_height - new_content_height)
-    force_redraw, force_redraw_reason = True, ""
-
-    if content_height_delta >= CONTENT_HEIGHT_REDRAW_THRESHOLD:
-      force_redraw_reason = "estimate was off by %i" % content_height_delta
-    elif new_content_height > height and self.scroll + height - 1 > new_content_height:
-      force_redraw_reason = "scrolled off the bottom of the page"
-    elif not is_scroll_bar_visible and new_content_height > height - 1:
-      force_redraw_reason = "scroll bar wasn't previously visible"
-    elif is_scroll_bar_visible and new_content_height <= height - 1:
-      force_redraw_reason = "scroll bar shouldn't be visible"
-    else:
-      force_redraw = False
-
-    self.last_content_height = new_content_height
-
-    if force_redraw:
-      log.debug("redrawing the log panel with the corrected content height (%s)" % force_redraw_reason)
-      self.redraw(True)
-
-    self.vals_lock.release()
-
-  def redraw(self, force_redraw=False, block=False):
-    # determines if the content needs to be redrawn or not
-    panel.Panel.redraw(self, force_redraw, block)
-
-  def run(self):
-    """
-    Redraws the display, coalescing updates if events are rapidly logged (for
-    instance running at the DEBUG runlevel) while also being immediately
-    responsive if additions are less frequent.
-    """
-
-    last_day = days_since()  # used to determine if the date has changed
-
-    while not self._halt:
-      current_day = days_since()
-      time_since_reset = time.time() - self._last_update
-      max_log_update_rate = CONFIG["features.log.maxRefreshRate"] / 1000.0
-
-      sleep_time = 0
-
-      if (self.msg_log == self._last_logged_events and last_day == current_day) or self.is_paused():
-        sleep_time = 5
-      elif time_since_reset < max_log_update_rate:
-        sleep_time = max(0.05, max_log_update_rate - time_since_reset)
-
-      if sleep_time:
-        self._cond.acquire()
-
-        if not self._halt:
-          self._cond.wait(sleep_time)
-
-        self._cond.release()
-      else:
-        last_day = current_day
-        self.redraw(True)
-
-        # makes sure that we register this as an update, otherwise lacking the
-        # curses lock can cause a busy wait here
-
-        self._last_update = time.time()
-
-  def stop(self):
-    """
-    Halts further resolutions and terminates the thread.
-    """
-
-    self._cond.acquire()
-    self._halt = True
-    self._cond.notifyAll()
-    self._cond.release()
-
-  def set_event_listening(self, events):
-    """
-    Configures the events Tor listens for, filtering non-tor events from what we
-    request from the controller. This returns a sorted list of the events we
-    successfully set.
-
-    Arguments:
-      events - event types to attempt to set
-    """
-
-    events = set(events)  # drops duplicates
-
-    # accounts for runlevel naming difference
-
-    if "ERROR" in events:
-      events.add("ERR")
-      events.remove("ERROR")
-
-    if "WARNING" in events:
-      events.add("WARN")
-      events.remove("WARNING")
-
-    tor_events = events.intersection(set(arm.arguments.TOR_EVENT_TYPES.values()))
-    arm_events = events.intersection(set(["ARM_%s" % runlevel for runlevel in log.Runlevel.keys()]))
-
-    # adds events unrecognized by arm if we're listening to the 'UNKNOWN' type
-
-    if "UNKNOWN" in events:
-      tor_events.update(set(arm.arguments.missing_event_types()))
-
-    controller = tor_controller()
-    controller.remove_event_listener(self.register_tor_event)
-
-    for event_type in list(tor_events):
-      try:
-        controller.add_event_listener(self.register_tor_event, event_type)
-      except stem.ProtocolError:
-        tor_events.remove(event_type)
-
-    # provides back the input set minus events we failed to set
-
-    return sorted(tor_events.union(arm_events))
-
-  def _reset_listener(self, controller, event_type, _):
-    # if we're attaching to a new tor instance then clears the log and
-    # prepopulates it with the content belonging to this instance
-
-    if event_type == State.INIT:
-      self.reprepopulate_events()
-      self.redraw(True)
-    elif event_type == State.CLOSED:
-      log.notice("Tor control port closed")
-
-  def _get_title(self, width):
-    """
-    Provides the label used for the panel, looking like:
-      Events (ARM NOTICE - ERR, BW - filter: prepopulate):
-
-    This truncates the attributes (with an ellipse) if too long, and condenses
-    runlevel ranges if there's three or more in a row (for instance ARM_INFO,
-    ARM_NOTICE, and ARM_WARN becomes "ARM_INFO - WARN").
-
-    Arguments:
-      width - width constraint the label needs to fix in
-    """
-
-    # usually the attributes used to make the label are decently static, so
-    # provide cached results if they're unchanged
-
-    self.vals_lock.acquire()
-    current_pattern = self.regex_filter.pattern if self.regex_filter else None
-    is_unchanged = self._title_args[0] == self.logged_events
-    is_unchanged &= self._title_args[1] == current_pattern
-    is_unchanged &= self._title_args[2] == width
-
-    if is_unchanged:
-      self.vals_lock.release()
-      return self._title_cache
-
-    events_list = list(self.logged_events)
-
-    if not events_list:
-      if not current_pattern:
-        panel_label = "Events:"
-      else:
-        label_pattern = str_tools.crop(current_pattern, width - 18)
-        panel_label = "Events (filter: %s):" % label_pattern
-    else:
-      # does the following with all runlevel types (tor, arm, and stem):
-      # - pulls to the start of the list
-      # - condenses range if there's three or more in a row (ex. "ARM_INFO - WARN")
-      # - condense further if there's identical runlevel ranges for multiple
-      #   types (ex. "NOTICE - ERR, ARM_NOTICE - ERR" becomes "TOR/ARM NOTICE - ERR")
-
-      tmp_runlevels = []  # runlevels pulled from the list (just the runlevel part)
-      runlevel_ranges = []  # tuple of type, start_level, end_level for ranges to be consensed
-
-      # reverses runlevels and types so they're appended in the right order
-
-      reversed_runlevels = list(log.Runlevel)
-      reversed_runlevels.reverse()
-
-      for prefix in ("ARM_", ""):
-        # blank ending runlevel forces the break condition to be reached at the end
-        for runlevel in reversed_runlevels + [""]:
-          event_type = prefix + runlevel
-          if runlevel and event_type in events_list:
-            # runlevel event found, move to the tmp list
-            events_list.remove(event_type)
-            tmp_runlevels.append(runlevel)
-          elif tmp_runlevels:
-            # adds all tmp list entries to the start of events_list
-            if len(tmp_runlevels) >= 3:
-              # save condense sequential runlevels to be added later
-              runlevel_ranges.append((prefix, tmp_runlevels[-1], tmp_runlevels[0]))
-            else:
-              # adds runlevels individaully
-              for tmp_runlevel in tmp_runlevels:
-                events_list.insert(0, prefix + tmp_runlevel)
-
-            tmp_runlevels = []
-
-      # adds runlevel ranges, condensing if there's identical ranges
-
-      for i in range(len(runlevel_ranges)):
-        if runlevel_ranges[i]:
-          prefix, start_level, end_level = runlevel_ranges[i]
-
-          # check for matching ranges
-
-          matches = []
-
-          for j in range(i + 1, len(runlevel_ranges)):
-            if runlevel_ranges[j] and runlevel_ranges[j][1] == start_level and runlevel_ranges[j][2] == end_level:
-              matches.append(runlevel_ranges[j])
-              runlevel_ranges[j] = None
-
-          if matches:
-            # strips underscores and replaces empty entries with "TOR"
-
-            prefixes = [entry[0] for entry in matches] + [prefix]
-
-            for k in range(len(prefixes)):
-              if prefixes[k] == "":
-                prefixes[k] = "TOR"
-              else:
-                prefixes[k] = prefixes[k].replace("_", "")
-
-            events_list.insert(0, "%s %s - %s" % ("/".join(prefixes), start_level, end_level))
-          else:
-            events_list.insert(0, "%s%s - %s" % (prefix, start_level, end_level))
-
-      # truncates to use an ellipsis if too long, for instance:
-
-      attr_label = ", ".join(events_list)
-
-      if current_pattern:
-        attr_label += " - filter: %s" % current_pattern
-
-      attr_label = str_tools.crop(attr_label, width - 10, 1)
-
-      if attr_label:
-        attr_label = " (%s)" % attr_label
-
-      panel_label = "Events%s:" % attr_label
-
-    # cache results and return
-
-    self._title_cache = panel_label
-    self._title_args = (list(self.logged_events), current_pattern, width)
-    self.vals_lock.release()
-
-    return panel_label
-
-  def _trim_events(self, event_listing):
-    """
-    Crops events that have either:
-    - grown beyond the cache limit
-    - outlived the configured log duration
-
-    Argument:
-      event_listing - listing of log entries
-    """
-
-    cache_size = CONFIG["cache.log_panel.size"]
-
-    if len(event_listing) > cache_size:
-      del event_listing[cache_size:]
-
-    log_ttl = CONFIG["features.log.entryDuration"]
-
-    if log_ttl > 0:
-      current_day = days_since()
-
-      breakpoint = None  # index at which to crop from
-
-      for i in range(len(event_listing) - 1, -1, -1):
-        days_since_event = current_day - days_since(event_listing[i].timestamp)
-
-        if days_since_event > log_ttl:
-          breakpoint = i  # older than the ttl
-        else:
-          break
-
-      # removes entries older than the ttl
-
-      if breakpoint is not None:
-        del event_listing[breakpoint:]
diff --git a/arm/menu/__init__.py b/arm/menu/__init__.py
deleted file mode 100644
index 54644fe..0000000
--- a/arm/menu/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""
-Resources for displaying the menu.
-"""
-
-__all__ = [
-  'actions',
-  'item',
-  'menu',
-]
diff --git a/arm/menu/actions.py b/arm/menu/actions.py
deleted file mode 100644
index 8b1e4f2..0000000
--- a/arm/menu/actions.py
+++ /dev/null
@@ -1,327 +0,0 @@
-"""
-Generates the menu for arm, binding options with their related actions.
-"""
-
-import functools
-
-import arm.popups
-import arm.controller
-import arm.menu.item
-import arm.graph_panel
-import arm.util.tracker
-
-from arm.util import tor_controller, ui_tools
-
-import stem
-import stem.util.connection
-
-from stem.util import conf, str_tools
-
-CONFIG = conf.config_dict('arm', {
-  'features.log.showDuplicateEntries': False,
-})
-
-
-def make_menu():
-  """
-  Constructs the base menu and all of its contents.
-  """
-
-  base_menu = arm.menu.item.Submenu("")
-  base_menu.add(make_actions_menu())
-  base_menu.add(make_view_menu())
-
-  control = arm.controller.get_controller()
-
-  for page_panel in control.get_display_panels(include_sticky = False):
-    if page_panel.get_name() == "graph":
-      base_menu.add(make_graph_menu(page_panel))
-    elif page_panel.get_name() == "log":
-      base_menu.add(make_log_menu(page_panel))
-    elif page_panel.get_name() == "connections":
-      base_menu.add(make_connections_menu(page_panel))
-    elif page_panel.get_name() == "configuration":
-      base_menu.add(make_configuration_menu(page_panel))
-    elif page_panel.get_name() == "torrc":
-      base_menu.add(make_torrc_menu(page_panel))
-
-  base_menu.add(make_help_menu())
-
-  return base_menu
-
-
-def make_actions_menu():
-  """
-  Submenu consisting of...
-    Close Menu
-    New Identity
-    Pause / Unpause
-    Reset Tor
-    Exit
-  """
-
-  control = arm.controller.get_controller()
-  controller = tor_controller()
-  header_panel = control.get_panel("header")
-  actions_menu = arm.menu.item.Submenu("Actions")
-  actions_menu.add(arm.menu.item.MenuItem("Close Menu", None))
-  actions_menu.add(arm.menu.item.MenuItem("New Identity", header_panel.send_newnym))
-
-  if controller.is_alive():
-    actions_menu.add(arm.menu.item.MenuItem("Stop Tor", controller.close))
-
-  actions_menu.add(arm.menu.item.MenuItem("Reset Tor", functools.partial(controller.signal, stem.Signal.RELOAD)))
-
-  if control.is_paused():
-    label, arg = "Unpause", False
-  else:
-    label, arg = "Pause", True
-
-  actions_menu.add(arm.menu.item.MenuItem(label, functools.partial(control.set_paused, arg)))
-  actions_menu.add(arm.menu.item.MenuItem("Exit", control.quit))
-
-  return actions_menu
-
-
-def make_view_menu():
-  """
-  Submenu consisting of...
-    [X] <Page 1>
-    [ ] <Page 2>
-    [ ] etc...
-        Color (Submenu)
-  """
-
-  view_menu = arm.menu.item.Submenu("View")
-  control = arm.controller.get_controller()
-
-  if control.get_page_count() > 0:
-    page_group = arm.menu.item.SelectionGroup(control.set_page, control.get_page())
-
-    for i in range(control.get_page_count()):
-      page_panels = control.get_display_panels(page_number = i, include_sticky = False)
-      label = " / ".join([str_tools._to_camel_case(panel.get_name()) for panel in page_panels])
-
-      view_menu.add(arm.menu.item.SelectionMenuItem(label, page_group, i))
-
-  if ui_tools.is_color_supported():
-    color_menu = arm.menu.item.Submenu("Color")
-    color_group = arm.menu.item.SelectionGroup(ui_tools.set_color_override, ui_tools.get_color_override())
-
-    color_menu.add(arm.menu.item.SelectionMenuItem("All", color_group, None))
-
-    for color in ui_tools.COLOR_LIST:
-      color_menu.add(arm.menu.item.SelectionMenuItem(str_tools._to_camel_case(color), color_group, color))
-
-    view_menu.add(color_menu)
-
-  return view_menu
-
-
-def make_help_menu():
-  """
-  Submenu consisting of...
-    Hotkeys
-    About
-  """
-
-  help_menu = arm.menu.item.Submenu("Help")
-  help_menu.add(arm.menu.item.MenuItem("Hotkeys", arm.popups.show_help_popup))
-  help_menu.add(arm.menu.item.MenuItem("About", arm.popups.show_about_popup))
-  return help_menu
-
-
-def make_graph_menu(graph_panel):
-  """
-  Submenu for the graph panel, consisting of...
-    [X] <Stat 1>
-    [ ] <Stat 2>
-    [ ] <Stat 2>
-        Resize...
-        Interval (Submenu)
-        Bounds (Submenu)
-
-  Arguments:
-    graph_panel - instance of the graph panel
-  """
-
-  graph_menu = arm.menu.item.Submenu("Graph")
-
-  # stats options
-
-  stat_group = arm.menu.item.SelectionGroup(functools.partial(setattr, graph_panel, 'displayed_stat'), graph_panel.displayed_stat)
-  available_stats = graph_panel.stat_options()
-  available_stats.sort()
-
-  for stat_key in ["None"] + available_stats:
-    label = str_tools._to_camel_case(stat_key, divider = " ")
-    stat_key = None if stat_key == "None" else stat_key
-    graph_menu.add(arm.menu.item.SelectionMenuItem(label, stat_group, stat_key))
-
-  # resizing option
-
-  graph_menu.add(arm.menu.item.MenuItem("Resize...", graph_panel.resize_graph))
-
-  # interval submenu
-
-  interval_menu = arm.menu.item.Submenu("Interval")
-  interval_group = arm.menu.item.SelectionGroup(functools.partial(setattr, graph_panel, 'update_interval'), graph_panel.update_interval)
-
-  for interval in arm.graph_panel.Interval:
-    interval_menu.add(arm.menu.item.SelectionMenuItem(interval, interval_group, interval))
-
-  graph_menu.add(interval_menu)
-
-  # bounds submenu
-
-  bounds_menu = arm.menu.item.Submenu("Bounds")
-  bounds_group = arm.menu.item.SelectionGroup(functools.partial(setattr, graph_panel, 'bounds_type'), graph_panel.bounds_type)
-
-  for bounds_type in arm.graph_panel.Bounds:
-    bounds_menu.add(arm.menu.item.SelectionMenuItem(bounds_type, bounds_group, bounds_type))
-
-  graph_menu.add(bounds_menu)
-
-  return graph_menu
-
-
-def make_log_menu(log_panel):
-  """
-  Submenu for the log panel, consisting of...
-    Events...
-    Snapshot...
-    Clear
-    Show / Hide Duplicates
-    Filter (Submenu)
-
-  Arguments:
-    log_panel - instance of the log panel
-  """
-
-  log_menu = arm.menu.item.Submenu("Log")
-
-  log_menu.add(arm.menu.item.MenuItem("Events...", log_panel.show_event_selection_prompt))
-  log_menu.add(arm.menu.item.MenuItem("Snapshot...", log_panel.show_snapshot_prompt))
-  log_menu.add(arm.menu.item.MenuItem("Clear", log_panel.clear))
-
-  if CONFIG["features.log.showDuplicateEntries"]:
-    label, arg = "Hide", False
-  else:
-    label, arg = "Show", True
-
-  log_menu.add(arm.menu.item.MenuItem("%s Duplicates" % label, functools.partial(log_panel.set_duplicate_visability, arg)))
-
-  # filter submenu
-
-  filter_menu = arm.menu.item.Submenu("Filter")
-  filter_group = arm.menu.item.SelectionGroup(log_panel.make_filter_selection, log_panel.get_filter())
-
-  filter_menu.add(arm.menu.item.SelectionMenuItem("None", filter_group, None))
-
-  for option in log_panel.filter_options:
-    filter_menu.add(arm.menu.item.SelectionMenuItem(option, filter_group, option))
-
-  filter_menu.add(arm.menu.item.MenuItem("New...", log_panel.show_filter_prompt))
-  log_menu.add(filter_menu)
-
-  return log_menu
-
-
-def make_connections_menu(conn_panel):
-  """
-  Submenu for the connections panel, consisting of...
-    [X] IP Address
-    [ ] Fingerprint
-    [ ] Nickname
-        Sorting...
-        Resolver (Submenu)
-
-  Arguments:
-    conn_panel - instance of the connections panel
-  """
-
-  connections_menu = arm.menu.item.Submenu("Connections")
-
-  # listing options
-
-  listing_group = arm.menu.item.SelectionGroup(conn_panel.set_listing_type, conn_panel.get_listing_type())
-
-  listing_options = list(arm.connections.entries.ListingType)
-  listing_options.remove(arm.connections.entries.ListingType.HOSTNAME)
-
-  for option in listing_options:
-    connections_menu.add(arm.menu.item.SelectionMenuItem(option, listing_group, option))
-
-  # sorting option
-
-  connections_menu.add(arm.menu.item.MenuItem("Sorting...", conn_panel.show_sort_dialog))
-
-  # resolver submenu
-
-  conn_resolver = arm.util.tracker.get_connection_tracker()
-  resolver_menu = arm.menu.item.Submenu("Resolver")
-  resolver_group = arm.menu.item.SelectionGroup(conn_resolver.set_custom_resolver, conn_resolver.get_custom_resolver())
-
-  resolver_menu.add(arm.menu.item.SelectionMenuItem("auto", resolver_group, None))
-
-  for option in stem.util.connection.Resolver:
-    resolver_menu.add(arm.menu.item.SelectionMenuItem(option, resolver_group, option))
-
-  connections_menu.add(resolver_menu)
-
-  return connections_menu
-
-
-def make_configuration_menu(config_panel):
-  """
-  Submenu for the configuration panel, consisting of...
-    Save Config...
-    Sorting...
-    Filter / Unfilter Options
-
-  Arguments:
-    config_panel - instance of the configuration panel
-  """
-
-  config_menu = arm.menu.item.Submenu("Configuration")
-  config_menu.add(arm.menu.item.MenuItem("Save Config...", config_panel.show_write_dialog))
-  config_menu.add(arm.menu.item.MenuItem("Sorting...", config_panel.show_sort_dialog))
-
-  if config_panel.show_all:
-    label, arg = "Filter", True
-  else:
-    label, arg = "Unfilter", False
-
-  config_menu.add(arm.menu.item.MenuItem("%s Options" % label, functools.partial(config_panel.set_filtering, arg)))
-
-  return config_menu
-
-
-def make_torrc_menu(torrc_panel):
-  """
-  Submenu for the torrc panel, consisting of...
-    Reload
-    Show / Hide Comments
-    Show / Hide Line Numbers
-
-  Arguments:
-    torrc_panel - instance of the torrc panel
-  """
-
-  torrc_menu = arm.menu.item.Submenu("Torrc")
-  torrc_menu.add(arm.menu.item.MenuItem("Reload", torrc_panel.reload_torrc))
-
-  if torrc_panel.strip_comments:
-    label, arg = "Show", True
-  else:
-    label, arg = "Hide", False
-
-  torrc_menu.add(arm.menu.item.MenuItem("%s Comments" % label, functools.partial(torrc_panel.set_comments_visible, arg)))
-
-  if torrc_panel.show_line_num:
-    label, arg = "Hide", False
-  else:
-    label, arg = "Show", True
-  torrc_menu.add(arm.menu.item.MenuItem("%s Line Numbers" % label, functools.partial(torrc_panel.set_line_number_visible, arg)))
-
-  return torrc_menu
diff --git a/arm/menu/item.py b/arm/menu/item.py
deleted file mode 100644
index 8dd14bc..0000000
--- a/arm/menu/item.py
+++ /dev/null
@@ -1,207 +0,0 @@
-"""
-Menu item, representing an option in the drop-down menu.
-"""
-
-import arm.controller
-
-
-class MenuItem():
-  """
-  Option in a drop-down menu.
-  """
-
-  def __init__(self, label, callback):
-    self._label = label
-    self._callback = callback
-    self._parent = None
-
-  def get_label(self):
-    """
-    Provides a tuple of three strings representing the prefix, label, and
-    suffix for this item.
-    """
-
-    return ("", self._label, "")
-
-  def get_parent(self):
-    """
-    Provides the Submenu we're contained within.
-    """
-
-    return self._parent
-
-  def get_hierarchy(self):
-    """
-    Provides a list with all of our parents, up to the root.
-    """
-
-    my_hierarchy = [self]
-    while my_hierarchy[-1].get_parent():
-      my_hierarchy.append(my_hierarchy[-1].get_parent())
-
-    my_hierarchy.reverse()
-    return my_hierarchy
-
-  def get_root(self):
-    """
-    Provides the base submenu we belong to.
-    """
-
-    if self._parent:
-      return self._parent.get_root()
-    else:
-      return self
-
-  def select(self):
-    """
-    Performs the callback for the menu item, returning true if we should close
-    the menu and false otherwise.
-    """
-
-    if self._callback:
-      control = arm.controller.get_controller()
-      control.set_msg()
-      control.redraw()
-      self._callback()
-    return True
-
-  def next(self):
-    """
-    Provides the next option for the submenu we're in, raising a ValueError
-    if we don't have a parent.
-    """
-
-    return self._get_sibling(1)
-
-  def prev(self):
-    """
-    Provides the previous option for the submenu we're in, raising a ValueError
-    if we don't have a parent.
-    """
-
-    return self._get_sibling(-1)
-
-  def _get_sibling(self, offset):
-    """
-    Provides our sibling with a given index offset from us, raising a
-    ValueError if we don't have a parent.
-
-    Arguments:
-      offset - index offset for the sibling to be returned
-    """
-
-    if self._parent:
-      my_siblings = self._parent.get_children()
-
-      try:
-        my_index = my_siblings.index(self)
-        return my_siblings[(my_index + offset) % len(my_siblings)]
-      except ValueError:
-        # We expect a bidirectional references between submenus and their
-        # children. If we don't have this then our menu's screwed up.
-
-        msg = "The '%s' submenu doesn't contain '%s' (children: '%s')" % (self, self._parent, "', '".join(my_siblings))
-        raise ValueError(msg)
-    else:
-      raise ValueError("Menu option '%s' doesn't have a parent" % self)
-
-  def __str__(self):
-    return self._label
-
-
-class Submenu(MenuItem):
-  """
-  Menu item that lists other menu options.
-  """
-
-  def __init__(self, label):
-    MenuItem.__init__(self, label, None)
-    self._children = []
-
-  def get_label(self):
-    """
-    Provides our label with a ">" suffix to indicate that we have suboptions.
-    """
-
-    my_label = MenuItem.get_label(self)[1]
-    return ("", my_label, " >")
-
-  def add(self, menu_item):
-    """
-    Adds the given menu item to our listing. This raises a ValueError if the
-    item already has a parent.
-
-    Arguments:
-      menu_item - menu option to be added
-    """
-
-    if menu_item.get_parent():
-      raise ValueError("Menu option '%s' already has a parent" % menu_item)
-    else:
-      menu_item._parent = self
-      self._children.append(menu_item)
-
-  def get_children(self):
-    """
-    Provides the menu and submenus we contain.
-    """
-
-    return list(self._children)
-
-  def is_empty(self):
-    """
-    True if we have no children, false otherwise.
-    """
-
-    return not bool(self._children)
-
-  def select(self):
-    return False
-
-
-class SelectionGroup():
-  """
-  Radio button groups that SelectionMenuItems can belong to.
-  """
-
-  def __init__(self, action, selected_arg):
-    self.action = action
-    self.selected_arg = selected_arg
-
-
-class SelectionMenuItem(MenuItem):
-  """
-  Menu item with an associated group which determines the selection. This is
-  for the common single argument getter/setter pattern.
-  """
-
-  def __init__(self, label, group, arg):
-    MenuItem.__init__(self, label, None)
-    self._group = group
-    self._arg = arg
-
-  def is_selected(self):
-    """
-    True if we're the selected item, false otherwise.
-    """
-
-    return self._arg == self._group.selected_arg
-
-  def get_label(self):
-    """
-    Provides our label with a "[X]" prefix if selected and "[ ]" if not.
-    """
-
-    my_label = MenuItem.get_label(self)[1]
-    my_prefix = "[X] " if self.is_selected() else "[ ] "
-    return (my_prefix, my_label, "")
-
-  def select(self):
-    """
-    Performs the group's setter action with our argument.
-    """
-
-    if not self.is_selected():
-      self._group.action(self._arg)
-
-    return True
diff --git a/arm/menu/menu.py b/arm/menu/menu.py
deleted file mode 100644
index e8fe297..0000000
--- a/arm/menu/menu.py
+++ /dev/null
@@ -1,192 +0,0 @@
-"""
-Display logic for presenting the menu.
-"""
-
-import curses
-
-import arm.popups
-import arm.controller
-import arm.menu.item
-import arm.menu.actions
-
-from arm.util import ui_tools
-
-
-class MenuCursor:
-  """
-  Tracks selection and key handling in the menu.
-  """
-
-  def __init__(self, initial_selection):
-    self._selection = initial_selection
-    self._is_done = False
-
-  def is_done(self):
-    """
-    Provides true if a selection has indicated that we should close the menu.
-    False otherwise.
-    """
-
-    return self._is_done
-
-  def get_selection(self):
-    """
-    Provides the currently selected menu item.
-    """
-
-    return self._selection
-
-  def handle_key(self, key):
-    is_selection_submenu = isinstance(self._selection, arm.menu.item.Submenu)
-    selection_hierarchy = self._selection.get_hierarchy()
-
-    if key.is_selection():
-      if is_selection_submenu:
-        if not self._selection.is_empty():
-          self._selection = self._selection.get_children()[0]
-      else:
-        self._is_done = self._selection.select()
-    elif key.match('up'):
-      self._selection = self._selection.prev()
-    elif key.match('down'):
-      self._selection = self._selection.next()
-    elif key.match('left'):
-      if len(selection_hierarchy) <= 3:
-        # shift to the previous main submenu
-
-        prev_submenu = selection_hierarchy[1].prev()
-        self._selection = prev_submenu.get_children()[0]
-      else:
-        # go up a submenu level
-
-        self._selection = self._selection.get_parent()
-    elif key.match('right'):
-      if is_selection_submenu:
-        # open submenu (same as making a selection)
-
-        if not self._selection.is_empty():
-          self._selection = self._selection.get_children()[0]
-      else:
-        # shift to the next main submenu
-
-        next_submenu = selection_hierarchy[1].next()
-        self._selection = next_submenu.get_children()[0]
-    elif key.match('esc', 'm'):
-      self._is_done = True
-
-
-def show_menu():
-  popup, _, _ = arm.popups.init(1, below_static = False)
-
-  if not popup:
-    return
-
-  control = arm.controller.get_controller()
-
-  try:
-    # generates the menu and uses the initial selection of the first item in
-    # the file menu
-
-    menu = arm.menu.actions.make_menu()
-    cursor = MenuCursor(menu.get_children()[0].get_children()[0])
-
-    while not cursor.is_done():
-      # sets the background color
-
-      popup.win.clear()
-      popup.win.bkgd(' ', curses.A_STANDOUT | ui_tools.get_color("red"))
-      selection_hierarchy = cursor.get_selection().get_hierarchy()
-
-      # provide a message saying how to close the menu
-
-      control.set_msg("Press m or esc to close the menu.", curses.A_BOLD, True)
-
-      # renders the menu bar, noting where the open submenu is positioned
-
-      draw_left, selection_left = 0, 0
-
-      for top_level_item in menu.get_children():
-        draw_format = curses.A_BOLD
-
-        if top_level_item == selection_hierarchy[1]:
-          draw_format |= curses.A_UNDERLINE
-          selection_left = draw_left
-
-        draw_label = " %s " % top_level_item.get_label()[1]
-        popup.addstr(0, draw_left, draw_label, draw_format)
-        popup.addch(0, draw_left + len(draw_label), curses.ACS_VLINE)
-
-        draw_left += len(draw_label) + 1
-
-      # recursively shows opened submenus
-
-      _draw_submenu(cursor, 1, 1, selection_left)
-
-      popup.win.refresh()
-
-      curses.cbreak()
-      cursor.handle_key(control.key_input())
-
-      # redraws the rest of the interface if we're rendering on it again
-
-      if not cursor.is_done():
-        control.redraw()
-  finally:
-    control.set_msg()
-    arm.popups.finalize()
-
-
-def _draw_submenu(cursor, level, top, left):
-  selection_hierarchy = cursor.get_selection().get_hierarchy()
-
-  # checks if there's nothing to display
-
-  if len(selection_hierarchy) < level + 2:
-    return
-
-  # fetches the submenu and selection we're displaying
-
-  submenu = selection_hierarchy[level]
-  selection = selection_hierarchy[level + 1]
-
-  # gets the size of the prefix, middle, and suffix columns
-
-  all_label_sets = [entry.get_label() for entry in submenu.get_children()]
-  prefix_col_size = max([len(entry[0]) for entry in all_label_sets])
-  middle_col_size = max([len(entry[1]) for entry in all_label_sets])
-  suffix_col_size = max([len(entry[2]) for entry in all_label_sets])
-
-  # formatted string so we can display aligned menu entries
-
-  label_format = " %%-%is%%-%is%%-%is " % (prefix_col_size, middle_col_size, suffix_col_size)
-  menu_width = len(label_format % ("", "", ""))
-
-  popup, _, _ = arm.popups.init(len(submenu.get_children()), menu_width, top, left, below_static = False)
-
-  if not popup:
-    return
-
-  try:
-    # sets the background color
-
-    popup.win.bkgd(' ', curses.A_STANDOUT | ui_tools.get_color("red"))
-
-    draw_top, selection_top = 0, 0
-
-    for menu_item in submenu.get_children():
-      if menu_item == selection:
-        draw_format = (curses.A_BOLD, 'white')
-        selection_top = draw_top
-      else:
-        draw_format = (curses.A_NORMAL,)
-
-      popup.addstr(draw_top, 0, label_format % menu_item.get_label(), *draw_format)
-      draw_top += 1
-
-    popup.win.refresh()
-
-    # shows the next submenu
-
-    _draw_submenu(cursor, level + 1, top + selection_top, left + menu_width)
-  finally:
-    arm.popups.finalize()
diff --git a/arm/popups.py b/arm/popups.py
deleted file mode 100644
index e852b95..0000000
--- a/arm/popups.py
+++ /dev/null
@@ -1,392 +0,0 @@
-"""
-Functions for displaying popups in the interface.
-"""
-
-import curses
-
-import arm.controller
-
-from arm import __version__, __release_date__
-from arm.util import panel, ui_tools
-
-
-def init(height = -1, width = -1, top = 0, left = 0, below_static = True):
-  """
-  Preparation for displaying a popup. This creates a popup with a valid
-  subwindow instance. If that's successful then the curses lock is acquired
-  and this returns a tuple of the...
-  (popup, draw width, draw height)
-  Otherwise this leaves curses unlocked and returns None.
-
-  Arguments:
-    height      - maximum height of the popup
-    width       - maximum width of the popup
-    top         - top position, relative to the sticky content
-    left        - left position from the screen
-    below_static - positions popup below static content if true
-  """
-
-  control = arm.controller.get_controller()
-
-  if below_static:
-    sticky_height = sum([sticky_panel.get_height() for sticky_panel in control.get_sticky_panels()])
-  else:
-    sticky_height = 0
-
-  popup = panel.Panel(control.get_screen(), "popup", top + sticky_height, left, height, width)
-  popup.set_visible(True)
-
-  # Redraws the popup to prepare a subwindow instance. If none is spawned then
-  # the panel can't be drawn (for instance, due to not being visible).
-
-  popup.redraw(True)
-
-  if popup.win is not None:
-    panel.CURSES_LOCK.acquire()
-    return (popup, popup.max_x - 1, popup.max_y)
-  else:
-    return (None, 0, 0)
-
-
-def finalize():
-  """
-  Cleans up after displaying a popup, releasing the cureses lock and redrawing
-  the rest of the display.
-  """
-
-  arm.controller.get_controller().request_redraw()
-  panel.CURSES_LOCK.release()
-
-
-def input_prompt(msg, initial_value = ""):
-  """
-  Prompts the user to enter a string on the control line (which usually
-  displays the page number and basic controls).
-
-  Arguments:
-    msg          - message to prompt the user for input with
-    initial_value - initial value of the field
-  """
-
-  panel.CURSES_LOCK.acquire()
-  control = arm.controller.get_controller()
-  msg_panel = control.get_panel("msg")
-  msg_panel.set_message(msg)
-  msg_panel.redraw(True)
-  user_input = msg_panel.getstr(0, len(msg), initial_value)
-  control.set_msg()
-  panel.CURSES_LOCK.release()
-
-  return user_input
-
-
-def show_msg(msg, max_wait = -1, attr = curses.A_STANDOUT):
-  """
-  Displays a single line message on the control line for a set time. Pressing
-  any key will end the message. This returns the key pressed.
-
-  Arguments:
-    msg     - message to be displayed to the user
-    max_wait - time to show the message, indefinite if -1
-    attr    - attributes with which to draw the message
-  """
-
-  with panel.CURSES_LOCK:
-    control = arm.controller.get_controller()
-    control.set_msg(msg, attr, True)
-
-    if max_wait == -1:
-      curses.cbreak()
-    else:
-      curses.halfdelay(max_wait * 10)
-
-    key_press = control.key_input()
-    control.set_msg()
-    return key_press
-
-
-def show_help_popup():
-  """
-  Presents a popup with instructions for the current page's hotkeys. This
-  returns the user input used to close the popup. If the popup didn't close
-  properly, this is an arrow, enter, or scroll key then this returns None.
-  """
-
-  popup, _, height = init(9, 80)
-
-  if not popup:
-    return
-
-  exit_key = None
-
-  try:
-    control = arm.controller.get_controller()
-    page_panels = control.get_display_panels()
-
-    # the first page is the only one with multiple panels, and it looks better
-    # with the log entries first, so reversing the order
-
-    page_panels.reverse()
-
-    help_options = []
-
-    for entry in page_panels:
-      help_options += entry.get_help()
-
-    # test doing afterward in case of overwriting
-
-    popup.win.box()
-    popup.addstr(0, 0, "Page %i Commands:" % (control.get_page() + 1), curses.A_STANDOUT)
-
-    for i in range(len(help_options)):
-      if i / 2 >= height - 2:
-        break
-
-      # draws entries in the form '<key>: <description>[ (<selection>)]', for
-      # instance...
-      # u: duplicate log entries (hidden)
-
-      key, description, selection = help_options[i]
-
-      if key:
-        description = ": " + description
-
-      row = (i / 2) + 1
-      col = 2 if i % 2 == 0 else 41
-
-      popup.addstr(row, col, key, curses.A_BOLD)
-      col += len(key)
-      popup.addstr(row, col, description)
-      col += len(description)
-
-      if selection:
-        popup.addstr(row, col, " (")
-        popup.addstr(row, col + 2, selection, curses.A_BOLD)
-        popup.addstr(row, col + 2 + len(selection), ")")
-
-    # tells user to press a key if the lower left is unoccupied
-
-    if len(help_options) < 13 and height == 9:
-      popup.addstr(7, 2, "Press any key...")
-
-    popup.win.refresh()
-    curses.cbreak()
-    exit_key = control.key_input()
-  finally:
-    finalize()
-
-  if not exit_key.is_selection() and not exit_key.is_scroll() and \
-    not exit_key.match('left', 'right'):
-    return exit_key
-  else:
-    return None
-
-
-def show_about_popup():
-  """
-  Presents a popup with author and version information.
-  """
-
-  popup, _, height = init(9, 80)
-
-  if not popup:
-    return
-
-  try:
-    control = arm.controller.get_controller()
-
-    popup.win.box()
-    popup.addstr(0, 0, "About:", curses.A_STANDOUT)
-    popup.addstr(1, 2, "arm, version %s (released %s)" % (__version__, __release_date__), curses.A_BOLD)
-    popup.addstr(2, 4, "Written by Damian Johnson (atagar at torproject.org)")
-    popup.addstr(3, 4, "Project page: www.atagar.com/arm")
-    popup.addstr(5, 2, "Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)")
-    popup.addstr(7, 2, "Press any key...")
-    popup.win.refresh()
-
-    curses.cbreak()
-    control.key_input()
-  finally:
-    finalize()
-
-
-def show_sort_dialog(title, options, old_selection, option_colors):
-  """
-  Displays a sorting dialog of the form:
-
-    Current Order: <previous selection>
-    New Order: <selections made>
-
-    <option 1>    <option 2>    <option 3>   Cancel
-
-  Options are colored when among the "Current Order" or "New Order", but not
-  when an option below them. If cancel is selected or the user presses escape
-  then this returns None. Otherwise, the new ordering is provided.
-
-  Arguments:
-    title   - title displayed for the popup window
-    options      - ordered listing of option labels
-    old_selection - current ordering
-    option_colors - mappings of options to their color
-  """
-
-  popup, _, _ = init(9, 80)
-
-  if not popup:
-    return
-
-  new_selections = []  # new ordering
-
-  try:
-    cursor_location = 0     # index of highlighted option
-    curses.cbreak()         # wait indefinitely for key presses (no timeout)
-
-    selection_options = list(options)
-    selection_options.append("Cancel")
-
-    while len(new_selections) < len(old_selection):
-      popup.win.erase()
-      popup.win.box()
-      popup.addstr(0, 0, title, curses.A_STANDOUT)
-
-      _draw_sort_selection(popup, 1, 2, "Current Order: ", old_selection, option_colors)
-      _draw_sort_selection(popup, 2, 2, "New Order: ", new_selections, option_colors)
-
-      # presents remaining options, each row having up to four options with
-      # spacing of nineteen cells
-
-      row, col = 4, 0
-
-      for i in range(len(selection_options)):
-        option_format = curses.A_STANDOUT if cursor_location == i else curses.A_NORMAL
-        popup.addstr(row, col * 19 + 2, selection_options[i], option_format)
-        col += 1
-
-        if col == 4:
-          row, col = row + 1, 0
-
-      popup.win.refresh()
-
-      key = arm.controller.get_controller().key_input()
-
-      if key.match('left'):
-        cursor_location = max(0, cursor_location - 1)
-      elif key.match('right'):
-        cursor_location = min(len(selection_options) - 1, cursor_location + 1)
-      elif key.match('up'):
-        cursor_location = max(0, cursor_location - 4)
-      elif key.match('down'):
-        cursor_location = min(len(selection_options) - 1, cursor_location + 4)
-      elif key.is_selection():
-        selection = selection_options[cursor_location]
-
-        if selection == "Cancel":
-          break
-        else:
-          new_selections.append(selection)
-          selection_options.remove(selection)
-          cursor_location = min(cursor_location, len(selection_options) - 1)
-      elif key == 27:
-        break  # esc - cancel
-  finally:
-    finalize()
-
-  if len(new_selections) == len(old_selection):
-    return new_selections
-  else:
-    return None
-
-
-def _draw_sort_selection(popup, y, x, prefix, options, option_colors):
-  """
-  Draws a series of comma separated sort selections. The whole line is bold
-  and sort options also have their specified color. Example:
-
-    Current Order: Man Page Entry, Option Name, Is Default
-
-  Arguments:
-    popup        - panel in which to draw sort selection
-    y            - vertical location
-    x            - horizontal location
-    prefix       - initial string description
-    options      - sort options to be shown
-    option_colors - mappings of options to their color
-  """
-
-  popup.addstr(y, x, prefix, curses.A_BOLD)
-  x += len(prefix)
-
-  for i in range(len(options)):
-    sort_type = options[i]
-    sort_color = ui_tools.get_color(option_colors.get(sort_type, "white"))
-    popup.addstr(y, x, sort_type, sort_color | curses.A_BOLD)
-    x += len(sort_type)
-
-    # comma divider between options, if this isn't the last
-
-    if i < len(options) - 1:
-      popup.addstr(y, x, ", ", curses.A_BOLD)
-      x += 2
-
-
-def show_menu(title, options, old_selection):
-  """
-  Provides menu with options laid out in a single column. User can cancel
-  selection with the escape key, in which case this proives -1. Otherwise this
-  returns the index of the selection.
-
-  Arguments:
-    title        - title displayed for the popup window
-    options      - ordered listing of options to display
-    old_selection - index of the initially selected option (uses the first
-                   selection without a carrot if -1)
-  """
-
-  max_width = max(map(len, options)) + 9
-  popup, _, _ = init(len(options) + 2, max_width)
-
-  if not popup:
-    return
-
-  selection = old_selection if old_selection != -1 else 0
-
-  try:
-    # hides the title of the first panel on the page
-
-    control = arm.controller.get_controller()
-    top_panel = control.get_display_panels(include_sticky = False)[0]
-    top_panel.set_title_visible(False)
-    top_panel.redraw(True)
-
-    curses.cbreak()   # wait indefinitely for key presses (no timeout)
-
-    while True:
-      popup.win.erase()
-      popup.win.box()
-      popup.addstr(0, 0, title, curses.A_STANDOUT)
-
-      for i in range(len(options)):
-        label = options[i]
-        format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
-        tab = "> " if i == old_selection else "  "
-        popup.addstr(i + 1, 2, tab)
-        popup.addstr(i + 1, 4, " %s " % label, format)
-
-      popup.win.refresh()
-
-      key = control.key_input()
-
-      if key.match('up'):
-        selection = max(0, selection - 1)
-      elif key.match('down'):
-        selection = min(len(options) - 1, selection + 1)
-      elif key.is_selection():
-        break
-      elif key.match('esc'):
-        selection = -1
-        break
-  finally:
-    top_panel.set_title_visible(True)
-    finalize()
-
-  return selection
diff --git a/arm/resources/arm.1 b/arm/resources/arm.1
deleted file mode 100644
index 68159cc..0000000
--- a/arm/resources/arm.1
+++ /dev/null
@@ -1,69 +0,0 @@
-.TH arm 1 "27 August 2010"
-.SH NAME
-arm - Terminal Tor status monitor
-
-.SH SYNOPSIS
-arm [\fIOPTION\fR]
-
-.SH DESCRIPTION
-The anonymizing relay monitor (arm) is a terminal status monitor for Tor
-relays, intended for command-line aficionados, ssh connections, and anyone
-stuck with a tty terminal. This works much like top does for system usage,
-providing real time statistics for:
-  * bandwidth, cpu, and memory usage
-  * relay's current configuration
-  * logged events
-  * connection details (ip, hostname, fingerprint, and consensus data)
-  * etc
-
-Defaults and interface properties are configurable via a user provided
-configuration file (for an example see the provided \fBarmrc.sample\fR).
-Releases and information are available at \fIhttp://www.atagar.com/arm\fR.
-
-.SH OPTIONS
-.TP
-\fB\-i\fR, \fB\-\-interface [ADDRESS:]PORT\fR
-tor control port arm should attach to (default is \fB127.0.0.1:9051\fR)
-
-.TP
-\fB\-c\fR, \fB\-\-config CONFIG_PATH\fR
-user provided configuration file (default is \fB~/.arm/armrc\fR)
-
-.TP
-\fB\-d\fR, \fB\-\-debug\fR
-writes all arm logs to ~/.arm/log
-
-.TP
-\fB\-e\fR, \fB\-\-event EVENT_FLAGS\fR
-flags for tor, arm, and torctl events to be logged (default is \fBN3\fR)
-
-  d DEBUG      a ADDRMAP           k DESCCHANGED   s STREAM
-  i INFO       f AUTHDIR_NEWDESCS  g GUARD         r STREAM_BW
-  n NOTICE     h BUILDTIMEOUT_SET  l NEWCONSENSUS  t STATUS_CLIENT
-  w WARN       b BW                m NEWDESC       u STATUS_GENERAL
-  e ERR        c CIRC              p NS            v STATUS_SERVER
-               j CLIENTS_SEEN      q ORCONN
-    DINWE tor runlevel+            A All Events
-    12345 arm runlevel+            X No Events
-    67890 torctl runlevel+         U Unknown Events
-
-.TP
-\fB\-v\fR, \fB\-\-version\fR
-provides version information
-
-.TP
-\fB\-h\fR, \fB\-\-help\fR
-provides usage information
-
-.SH FILES
-.TP
-\fB~/.arm/armrc\fR
-Your personal arm configuration file
-
-.TP
-\fB/usr/share/doc/arm/armrc.sample\fR
-Sample armrc configuration file that documents all options
-
-.SH AUTHOR
-Written by Damian Johnson (atagar at torproject.org)
-
diff --git a/arm/resources/tor-arm.desktop b/arm/resources/tor-arm.desktop
deleted file mode 100644
index da94017..0000000
--- a/arm/resources/tor-arm.desktop
+++ /dev/null
@@ -1,12 +0,0 @@
-[Desktop Entry]
-Name=Tor monitor
-Name[es]=Monitor de Tor
-Comment=Status monitor for Tor routers
-Comment[es]=Monitor de estado para routers Tor
-GenericName=Monitor
-GenericName[es]=Monitor
-Exec=arm -g
-Icon=tor-arm
-Terminal=false
-Type=Application
-Categories=System;Monitor;GTK;
diff --git a/arm/resources/tor-arm.svg b/arm/resources/tor-arm.svg
deleted file mode 100644
index 8e710ab..0000000
--- a/arm/resources/tor-arm.svg
+++ /dev/null
@@ -1,1074 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:xlink="http://www.w3.org/1999/xlink"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   version="1.0"
-   width="128"
-   height="128"
-   id="svg2"
-   inkscape:version="0.48.1 r9760"
-   sodipodi:docname="utilities-system-monitor.svg">
-  <metadata
-     id="metadata261">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="1024"
-     inkscape:window-height="550"
-     id="namedview259"
-     showgrid="false"
-     inkscape:zoom="2.3828125"
-     inkscape:cx="64"
-     inkscape:cy="63.692344"
-     inkscape:window-x="0"
-     inkscape:window-y="25"
-     inkscape:window-maximized="1"
-     inkscape:current-layer="layer1" />
-  <defs
-     id="defs4">
-    <linearGradient
-       id="linearGradient4199">
-      <stop
-         style="stop-color:white;stop-opacity:1"
-         offset="0"
-         id="stop4201" />
-      <stop
-         style="stop-color:white;stop-opacity:0"
-         offset="1"
-         id="stop4203" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient4167">
-      <stop
-         style="stop-color:#171717;stop-opacity:1"
-         offset="0"
-         id="stop4169" />
-      <stop
-         style="stop-color:#777;stop-opacity:1"
-         offset="1"
-         id="stop4171" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient4159">
-      <stop
-         style="stop-color:white;stop-opacity:1"
-         offset="0"
-         id="stop4161" />
-      <stop
-         style="stop-color:white;stop-opacity:0"
-         offset="1"
-         id="stop4163" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient4142">
-      <stop
-         style="stop-color:#e5ff00;stop-opacity:1"
-         offset="0"
-         id="stop4144" />
-      <stop
-         style="stop-color:#e5ff00;stop-opacity:0"
-         offset="1"
-         id="stop4146" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3399">
-      <stop
-         style="stop-color:yellow;stop-opacity:1"
-         offset="0"
-         id="stop3401" />
-      <stop
-         style="stop-color:yellow;stop-opacity:0"
-         offset="1"
-         id="stop3403" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3391">
-      <stop
-         style="stop-color:#ffff1d;stop-opacity:1"
-         offset="0"
-         id="stop3393" />
-      <stop
-         style="stop-color:#ffff6f;stop-opacity:0"
-         offset="1"
-         id="stop3395" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3383">
-      <stop
-         style="stop-color:yellow;stop-opacity:1"
-         offset="0"
-         id="stop3385" />
-      <stop
-         style="stop-color:yellow;stop-opacity:0"
-         offset="1"
-         id="stop3387" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient4111">
-      <stop
-         style="stop-color:black;stop-opacity:1"
-         offset="0"
-         id="stop4113" />
-      <stop
-         style="stop-color:black;stop-opacity:0"
-         offset="1"
-         id="stop4115" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient4031">
-      <stop
-         style="stop-color:#292929;stop-opacity:1"
-         offset="0"
-         id="stop4033" />
-      <stop
-         style="stop-color:#e9e9e9;stop-opacity:1"
-         offset="1"
-         id="stop4035" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient4002">
-      <stop
-         style="stop-color:lime;stop-opacity:1"
-         offset="0"
-         id="stop4004" />
-      <stop
-         style="stop-color:#f0ff80;stop-opacity:0"
-         offset="1"
-         id="stop4006" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3785">
-      <stop
-         style="stop-color:black;stop-opacity:1"
-         offset="0"
-         id="stop3787" />
-      <stop
-         style="stop-color:black;stop-opacity:0"
-         offset="1"
-         id="stop3789" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3761">
-      <stop
-         style="stop-color:#f6f6f6;stop-opacity:1"
-         offset="0"
-         id="stop3763" />
-      <stop
-         style="stop-color:#5a5a5a;stop-opacity:1"
-         offset="1"
-         id="stop3765" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3749">
-      <stop
-         style="stop-color:#181818;stop-opacity:1"
-         offset="0"
-         id="stop3751" />
-      <stop
-         style="stop-color:#ababab;stop-opacity:1"
-         offset="1"
-         id="stop3753" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3737">
-      <stop
-         style="stop-color:gray;stop-opacity:1"
-         offset="0"
-         id="stop3739" />
-      <stop
-         style="stop-color:#232323;stop-opacity:1"
-         offset="1"
-         id="stop3741" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3729">
-      <stop
-         style="stop-color:#ededed;stop-opacity:1"
-         offset="0"
-         id="stop3731" />
-      <stop
-         style="stop-color:#bcbcbc;stop-opacity:1"
-         offset="1"
-         id="stop3733" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3570">
-      <stop
-         style="stop-color:black;stop-opacity:1"
-         offset="0"
-         id="stop3572" />
-      <stop
-         style="stop-color:black;stop-opacity:0"
-         offset="1"
-         id="stop3574" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3470">
-      <stop
-         style="stop-color:#ddd;stop-opacity:1"
-         offset="0"
-         id="stop3472" />
-      <stop
-         style="stop-color:#fbfbfb;stop-opacity:1"
-         offset="1"
-         id="stop3474" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3452">
-      <stop
-         style="stop-color:#979797;stop-opacity:1"
-         offset="0"
-         id="stop3454" />
-      <stop
-         style="stop-color:#454545;stop-opacity:1"
-         offset="1"
-         id="stop3456" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3440">
-      <stop
-         style="stop-color:black;stop-opacity:1"
-         offset="0"
-         id="stop3442" />
-      <stop
-         style="stop-color:black;stop-opacity:0"
-         offset="1"
-         id="stop3444" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3384">
-      <stop
-         style="stop-color:black;stop-opacity:1"
-         offset="0"
-         id="stop3386" />
-      <stop
-         style="stop-color:black;stop-opacity:0"
-         offset="1"
-         id="stop3388" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3292">
-      <stop
-         style="stop-color:#5e5e5e;stop-opacity:1"
-         offset="0"
-         id="stop3294" />
-      <stop
-         style="stop-color:#292929;stop-opacity:1"
-         offset="1"
-         id="stop3296" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3275">
-      <stop
-         style="stop-color:#323232;stop-opacity:1"
-         offset="0"
-         id="stop3277" />
-      <stop
-         style="stop-color:#1a1a1a;stop-opacity:1"
-         offset="1"
-         id="stop3279" />
-    </linearGradient>
-    <linearGradient
-       id="linearGradient3265">
-      <stop
-         style="stop-color:white;stop-opacity:1"
-         offset="0"
-         id="stop3267" />
-      <stop
-         style="stop-color:white;stop-opacity:0"
-         offset="1"
-         id="stop3269" />
-    </linearGradient>
-    <filter
-       id="filter3162">
-      <feGaussianBlur
-         id="feGaussianBlur3164"
-         stdDeviation="0.14753906"
-         inkscape:collect="always" />
-    </filter>
-    <filter
-       id="filter3193">
-      <feGaussianBlur
-         id="feGaussianBlur3195"
-         stdDeviation="0.12753906"
-         inkscape:collect="always" />
-    </filter>
-    <filter
-       id="filter3247"
-       height="1.60944"
-       y="-0.30472"
-       width="1.03826"
-       x="-0.019130022">
-      <feGaussianBlur
-         id="feGaussianBlur3249"
-         stdDeviation="0.89273437"
-         inkscape:collect="always" />
-    </filter>
-    <radialGradient
-       cx="64"
-       cy="7.1979251"
-       r="56"
-       fx="64"
-       fy="7.1979251"
-       id="radialGradient3271"
-       xlink:href="#linearGradient3265"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.236503,0,0,0.798045,-15.13621,10.25573)" />
-    <radialGradient
-       cx="56"
-       cy="65.961678"
-       r="44"
-       fx="56"
-       fy="64.752823"
-       id="radialGradient3281"
-       xlink:href="#linearGradient3292"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(2.206761,0,0,2.057714,-67.57862,-106.9325)" />
-    <radialGradient
-       cx="56"
-       cy="60"
-       r="44"
-       fx="56"
-       fy="99.821198"
-       id="radialGradient3287"
-       xlink:href="#linearGradient3275"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.497439,3.473066e-8,-3.238492e-8,1.3963,-27.85656,-45.05228)" />
-    <radialGradient
-       cx="56"
-       cy="60"
-       r="44"
-       fx="56"
-       fy="99.821198"
-       id="radialGradient3289"
-       xlink:href="#linearGradient3275"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.497439,3.473066e-8,-3.238492e-8,1.3963,-27.85656,-44.05228)" />
-    <clipPath
-       id="clipPath3361">
-      <rect
-         width="88"
-         height="72"
-         rx="5.0167508"
-         ry="5.0167508"
-         x="12"
-         y="24"
-         style="opacity:1;fill:url(#radialGradient3365);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-         id="rect3363" />
-    </clipPath>
-    <radialGradient
-       cx="56"
-       cy="65.961678"
-       r="44"
-       fx="56"
-       fy="64.752823"
-       id="radialGradient3365"
-       xlink:href="#linearGradient3292"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(2.206761,0,0,2.057714,-67.57862,-106.9325)" />
-    <linearGradient
-       x1="52.513512"
-       y1="97"
-       x2="52.513512"
-       y2="74.244766"
-       id="linearGradient3390"
-       xlink:href="#linearGradient3384"
-       gradientUnits="userSpaceOnUse" />
-    <clipPath
-       id="clipPath3402">
-      <rect
-         width="88"
-         height="72"
-         rx="5.0167508"
-         ry="5.0167508"
-         x="12"
-         y="24"
-         style="opacity:1;fill:url(#radialGradient3406);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-         id="rect3404" />
-    </clipPath>
-    <radialGradient
-       cx="56"
-       cy="65.961678"
-       r="44"
-       fx="56"
-       fy="64.752823"
-       id="radialGradient3406"
-       xlink:href="#linearGradient3292"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(2.206761,0,0,2.057714,-67.57862,-106.9325)" />
-    <filter
-       id="filter3424">
-      <feGaussianBlur
-         id="feGaussianBlur3426"
-         stdDeviation="0.23507812"
-         inkscape:collect="always" />
-    </filter>
-    <filter
-       id="filter3430">
-      <feGaussianBlur
-         id="feGaussianBlur3432"
-         stdDeviation="0.23507812"
-         inkscape:collect="always" />
-    </filter>
-    <linearGradient
-       x1="100"
-       y1="92.763115"
-       x2="100"
-       y2="60"
-       id="linearGradient3446"
-       xlink:href="#linearGradient3440"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="100"
-       y1="92.763115"
-       x2="100"
-       y2="60"
-       id="linearGradient3450"
-       xlink:href="#linearGradient3440"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="translate(0,-120)" />
-    <radialGradient
-       cx="108.33566"
-       cy="25.487402"
-       r="4.171701"
-       fx="108.33566"
-       fy="25.487402"
-       id="radialGradient3458"
-       xlink:href="#linearGradient3452"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.432375,0,0,1.432375,-46.84166,-11.02012)" />
-    <linearGradient
-       x1="110.75722"
-       y1="32.559616"
-       x2="106.72433"
-       y2="24.216215"
-       id="linearGradient3476"
-       xlink:href="#linearGradient3470"
-       gradientUnits="userSpaceOnUse" />
-    <filter
-       id="filter3549"
-       height="1.348368"
-       y="-0.17418399"
-       width="1.1806649"
-       x="-0.090332433">
-      <feGaussianBlur
-         id="feGaussianBlur3551"
-         stdDeviation="0.099971814"
-         inkscape:collect="always" />
-    </filter>
-    <filter
-       id="filter3553"
-       height="1.2047423"
-       y="-0.10237114"
-       width="1.2103517"
-       x="-0.10517583">
-      <feGaussianBlur
-         id="feGaussianBlur3555"
-         stdDeviation="0.099971814"
-         inkscape:collect="always" />
-    </filter>
-    <filter
-       id="filter3557"
-       height="1.348368"
-       y="-0.17418399"
-       width="1.1806649"
-       x="-0.090332433">
-      <feGaussianBlur
-         id="feGaussianBlur3559"
-         stdDeviation="0.099971814"
-         inkscape:collect="always" />
-    </filter>
-    <filter
-       id="filter3561"
-       height="1.2047423"
-       y="-0.10237114"
-       width="1.2103517"
-       x="-0.10517583">
-      <feGaussianBlur
-         id="feGaussianBlur3563"
-         stdDeviation="0.099971814"
-         inkscape:collect="always" />
-    </filter>
-    <linearGradient
-       x1="111.58585"
-       y1="31.213261"
-       x2="116.79939"
-       y2="35.079716"
-       id="linearGradient3576"
-       xlink:href="#linearGradient3570"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="translate(-0.559618,-0.203498)" />
-    <filter
-       id="filter3590">
-      <feGaussianBlur
-         id="feGaussianBlur3592"
-         stdDeviation="0.29695312"
-         inkscape:collect="always" />
-    </filter>
-    <linearGradient
-       x1="111.58585"
-       y1="31.213261"
-       x2="116.79939"
-       y2="35.079716"
-       id="linearGradient3671"
-       xlink:href="#linearGradient3570"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="translate(-0.559618,-0.203498)" />
-    <radialGradient
-       cx="108.33566"
-       cy="25.487402"
-       r="4.171701"
-       fx="108.33566"
-       fy="25.487402"
-       id="radialGradient3673"
-       xlink:href="#linearGradient3452"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.432375,0,0,1.432375,-46.84166,-11.02012)" />
-    <linearGradient
-       x1="110.75722"
-       y1="32.559616"
-       x2="106.72433"
-       y2="24.216215"
-       id="linearGradient3675"
-       xlink:href="#linearGradient3470"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="111.58585"
-       y1="31.213261"
-       x2="116.79939"
-       y2="35.079716"
-       id="linearGradient3711"
-       xlink:href="#linearGradient3570"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="translate(-0.559618,-0.203498)" />
-    <radialGradient
-       cx="108.33566"
-       cy="25.487402"
-       r="4.171701"
-       fx="108.33566"
-       fy="25.487402"
-       id="radialGradient3713"
-       xlink:href="#linearGradient3452"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.432375,0,0,1.432375,-46.84166,-11.02012)" />
-    <linearGradient
-       x1="110.75722"
-       y1="32.559616"
-       x2="106.72433"
-       y2="24.216215"
-       id="linearGradient3715"
-       xlink:href="#linearGradient3470"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="110"
-       y1="84"
-       x2="110"
-       y2="72.081078"
-       id="linearGradient3735"
-       xlink:href="#linearGradient3729"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="110"
-       y1="84"
-       x2="110"
-       y2="88"
-       id="linearGradient3743"
-       xlink:href="#linearGradient3737"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="110"
-       y1="84"
-       x2="110"
-       y2="72.081078"
-       id="linearGradient3747"
-       xlink:href="#linearGradient3729"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1,0,0,0.2,0,-90.8)" />
-    <radialGradient
-       cx="110"
-       cy="87.735802"
-       r="4"
-       fx="110"
-       fy="87.735802"
-       id="radialGradient3755"
-       xlink:href="#linearGradient3749"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(5.343975,0,0,6.161922,-477.8373,-454.2492)" />
-    <linearGradient
-       x1="113.34818"
-       y1="79.669319"
-       x2="118.02862"
-       y2="79.669319"
-       id="linearGradient3791"
-       xlink:href="#linearGradient3785"
-       gradientUnits="userSpaceOnUse" />
-    <filter
-       id="filter3853"
-       height="1.1794737"
-       y="-0.089736843"
-       width="1.6153383"
-       x="-0.30766916">
-      <feGaussianBlur
-         id="feGaussianBlur3855"
-         stdDeviation="0.54783699"
-         inkscape:collect="always" />
-    </filter>
-    <linearGradient
-       x1="98.899841"
-       y1="40.170177"
-       x2="98.899841"
-       y2="104.503"
-       id="linearGradient4008"
-       xlink:href="#linearGradient4002"
-       gradientUnits="userSpaceOnUse" />
-    <clipPath
-       id="clipPath4019">
-      <rect
-         width="88"
-         height="72"
-         rx="5.0167508"
-         ry="5.0167508"
-         x="12"
-         y="24"
-         style="opacity:0.65263157;fill:url(#linearGradient4023);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-         id="rect4021" />
-    </clipPath>
-    <linearGradient
-       x1="100"
-       y1="92.763115"
-       x2="100"
-       y2="60"
-       id="linearGradient4023"
-       xlink:href="#linearGradient3440"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="100"
-       y1="92.763115"
-       x2="100"
-       y2="72.820351"
-       id="linearGradient4027"
-       xlink:href="#linearGradient3440"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="100"
-       y1="65.697929"
-       x2="95.716316"
-       y2="65.697929"
-       id="linearGradient4099"
-       xlink:href="#linearGradient3440"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="100"
-       y1="65.697929"
-       x2="95.909744"
-       y2="65.697929"
-       id="linearGradient4103"
-       xlink:href="#linearGradient3440"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="translate(-112,0)" />
-    <linearGradient
-       x1="48.9221"
-       y1="24"
-       x2="48.9221"
-       y2="30.250481"
-       id="linearGradient4107"
-       xlink:href="#linearGradient3440"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="translate(-112,0)" />
-    <radialGradient
-       cx="64"
-       cy="73.977821"
-       r="52"
-       fx="64"
-       fy="73.977821"
-       id="radialGradient4119"
-       xlink:href="#linearGradient4111"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1,0,0,0.285229,0,74.89936)"
-       spreadMethod="reflect" />
-    <filter
-       id="filter4137"
-       height="1.5494737"
-       y="-0.27473684"
-       width="1.0634008"
-       x="-0.031700405">
-      <feGaussianBlur
-         id="feGaussianBlur4139"
-         stdDeviation="1.3736842"
-         inkscape:collect="always" />
-    </filter>
-    <clipPath
-       id="clipPath3379">
-      <rect
-         width="88"
-         height="72"
-         rx="5.0167508"
-         ry="5.0167508"
-         x="-100"
-         y="23"
-         transform="scale(-1,1)"
-         style="opacity:0.32105264;fill:black;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-         id="rect3381" />
-    </clipPath>
-    <linearGradient
-       x1="100.11033"
-       y1="69.474098"
-       x2="-17.198158"
-       y2="69.474098"
-       id="linearGradient3389"
-       xlink:href="#linearGradient3383"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="101.41602"
-       y1="64.334373"
-       x2="-35.975773"
-       y2="64.334373"
-       id="linearGradient3397"
-       xlink:href="#linearGradient3391"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="99.727539"
-       y1="63.027271"
-       x2="-3.3565123"
-       y2="63.027271"
-       id="linearGradient3405"
-       xlink:href="#linearGradient3399"
-       gradientUnits="userSpaceOnUse" />
-    <filter
-       id="filter3411"
-       height="1.3350769"
-       y="-0.16753846"
-       width="1.0821887"
-       x="-0.04109434">
-      <feGaussianBlur
-         id="feGaussianBlur3413"
-         stdDeviation="1.815"
-         inkscape:collect="always" />
-    </filter>
-    <filter
-       id="filter4138"
-       height="1.252"
-       y="-0.126"
-       width="1.252"
-       x="-0.126">
-      <feGaussianBlur
-         id="feGaussianBlur4140"
-         stdDeviation="0.21"
-         inkscape:collect="always" />
-    </filter>
-    <radialGradient
-       cx="18"
-       cy="102"
-       r="2"
-       fx="18"
-       fy="102"
-       id="radialGradient4148"
-       xlink:href="#linearGradient4142"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(3.25543,0,0,3.25543,-40.59774,-230.0538)" />
-    <linearGradient
-       x1="20.930662"
-       y1="96.872108"
-       x2="23.156008"
-       y2="105.17721"
-       id="linearGradient4165"
-       xlink:href="#linearGradient4159"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       x1="34.736519"
-       y1="106.93066"
-       x2="21.263483"
-       y2="100"
-       id="linearGradient4173"
-       xlink:href="#linearGradient4167"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(0.666667,0,0,1,5.333334,0)" />
-    <filter
-       id="filter4190">
-      <feGaussianBlur
-         id="feGaussianBlur4192"
-         stdDeviation="2.6020349"
-         inkscape:collect="always" />
-    </filter>
-    <linearGradient
-       x1="29.355932"
-       y1="27.119223"
-       x2="35.527592"
-       y2="50.152176"
-       id="linearGradient4205"
-       xlink:href="#linearGradient4199"
-       gradientUnits="userSpaceOnUse" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3440"
-       id="linearGradient3238"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.4101341,0,0,1.4101341,-142.94128,-20.830999)"
-       x1="100"
-       y1="65.697929"
-       x2="95.909744"
-       y2="65.697929" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3440"
-       id="linearGradient3240"
-       gradientUnits="userSpaceOnUse"
-       x1="100"
-       y1="65.697929"
-       x2="95.716316"
-       y2="65.697929"
-       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3440"
-       id="linearGradient3242"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.4101341,0,0,1.4101341,-142.94128,-20.830999)"
-       x1="48.9221"
-       y1="24"
-       x2="48.9221"
-       y2="30.250481" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient4199"
-       id="linearGradient3244"
-       gradientUnits="userSpaceOnUse"
-       x1="29.355932"
-       y1="27.119223"
-       x2="35.527592"
-       y2="50.152176"
-       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3440"
-       id="linearGradient3246"
-       gradientUnits="userSpaceOnUse"
-       x1="100"
-       y1="92.763115"
-       x2="100"
-       y2="72.820351"
-       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3383"
-       id="linearGradient3248"
-       gradientUnits="userSpaceOnUse"
-       x1="100.11033"
-       y1="69.474098"
-       x2="-17.198158"
-       y2="69.474098" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3391"
-       id="linearGradient3250"
-       gradientUnits="userSpaceOnUse"
-       x1="101.41602"
-       y1="64.334373"
-       x2="-35.975773"
-       y2="64.334373" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3399"
-       id="linearGradient3252"
-       gradientUnits="userSpaceOnUse"
-       x1="99.727539"
-       y1="63.027271"
-       x2="-3.3565123"
-       y2="63.027271" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3440"
-       id="linearGradient3254"
-       gradientUnits="userSpaceOnUse"
-       x1="100"
-       y1="92.763115"
-       x2="100"
-       y2="60"
-       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)" />
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3440"
-       id="linearGradient3256"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-148.3851)"
-       x1="100"
-       y1="92.763115"
-       x2="100"
-       y2="60" />
-    <radialGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3292"
-       id="radialGradient3258"
-       gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(3.111829,0,0,2.9016527,-110.28866,-171.62017)"
-       cx="56"
-       cy="65.961678"
-       fx="56"
-       fy="64.752823"
-       r="44" />
-  </defs>
-  <g
-     id="layer1">
-    <rect
-       width="124.0918"
-       height="101.52966"
-       rx="7.0742917"
-       ry="7.0742917"
-       x="1.9278687"
-       y="13.01222"
-       style="fill:url(#radialGradient3258);fill-opacity:1;stroke:none"
-       id="rect3273" />
-    <g
-       style="opacity:0.25789478;fill:#ff7e00;stroke:#d3d7cf"
-       clip-path="url(#clipPath3361)"
-       id="g3349"
-       transform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)">
-      <path
-         d="m 24.5,19.5 0,80"
-         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-         id="path3300"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 40.5,19.5 0,80"
-         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-         id="path3307"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 56.5,19.5 0,80"
-         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-         id="path3309"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 72.5,19.5 0,80"
-         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-         id="path3311"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 88.5,19.5 0,80"
-         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-         id="path3317"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 0.5,60.5 110.61729,0"
-         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-         id="path3325"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 0.5,79.5 110.61729,0"
-         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-         id="path3327"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 0.5,40.5 110.61729,0"
-         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-         id="path3329"
-         inkscape:connector-curvature="0" />
-    </g>
-    <rect
-       width="124.0918"
-       height="101.52966"
-       rx="7.0742917"
-       ry="7.0742917"
-       x="1.9278687"
-       y="-114.54188"
-       transform="scale(1,-1)"
-       style="opacity:0.32105264;fill:url(#linearGradient3256);fill-opacity:1;stroke:none"
-       id="rect3448" />
-    <rect
-       width="124.0918"
-       height="101.52966"
-       rx="7.0742917"
-       ry="7.0742917"
-       x="1.9278687"
-       y="13.01222"
-       style="opacity:0.43684214;fill:url(#linearGradient3254);fill-opacity:1;stroke:none"
-       id="rect4025" />
-    <g
-       transform="matrix(1.4101341,0,0,1.4101341,-14.993741,-19.420865)"
-       clip-path="url(#clipPath3379)"
-       id="g4010">
-      <path
-         d="M 16.246914,126.84803 -2.6446783,98.771282 12,79.49 l 12,0 12,-24 16,0 12,16 12,0 8,-12 15.306836,0 5.779584,0 -0.0494,65.38272"
-         style="opacity:0.28494622;fill:url(#linearGradient3248);fill-opacity:1;fill-rule:evenodd;stroke:none"
-         id="path3431"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 4,59.49 8,20 12,0 12,-24 16,0 12,16 12,0 8,-12 15.306836,0 8.693164,0"
-         style="fill:none;stroke:url(#linearGradient3250);stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-         id="path3413"
-         inkscape:connector-curvature="0" />
-      <path
-         d="m 4,59.49 8,20 12,0 12,-24 16,0 12,16 12,0 8,-12 15.306836,0 8.693164,0"
-         style="fill:none;stroke:url(#linearGradient3252);stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
-         id="path3857"
-         inkscape:connector-curvature="0" />
-    </g>
-    <rect
-       width="124.0918"
-       height="101.52966"
-       rx="7.0742917"
-       ry="7.0742917"
-       x="1.9278687"
-       y="13.01222"
-       style="opacity:0.32105264;fill:url(#linearGradient3246);fill-opacity:1;stroke:none"
-       id="rect3438" />
-    <path
-       d="m 9.0226062,13.012221 c -3.9191562,0 -7.0947373,3.175579 -7.0947373,7.094737 l 0,30.62635 C 25.678508,39.547637 58.966862,32.577831 95.833988,32.577831 c 10.395432,0 20.489952,0.541015 30.185682,1.586401 l 0,-14.057274 c 0,-3.919156 -3.17558,-7.094737 -7.09474,-7.094737 l -109.9023238,0 z"
-       style="opacity:0.225;fill:url(#linearGradient3244);fill-opacity:1;stroke:none"
-       id="rect4194"
-       inkscape:connector-curvature="0" />
-    <rect
-       width="124.0918"
-       height="101.52966"
-       rx="7.0742917"
-       ry="7.0742917"
-       x="-126.01967"
-       y="13.01222"
-       transform="scale(-1,1)"
-       style="opacity:0.32105264;fill:url(#linearGradient3242);fill-opacity:1;stroke:none"
-       id="rect4105" />
-    <rect
-       width="124.0918"
-       height="101.52966"
-       rx="7.0742917"
-       ry="7.0742917"
-       x="1.9278687"
-       y="13.01222"
-       style="opacity:0.32105264;fill:url(#linearGradient3240);fill-opacity:1;stroke:none"
-       id="rect4097" />
-    <rect
-       width="124.0918"
-       height="101.52966"
-       rx="7.0742917"
-       ry="7.0742917"
-       x="-126.01967"
-       y="13.01222"
-       transform="scale(-1,1)"
-       style="opacity:0.32105264;fill:url(#linearGradient3238);fill-opacity:1;stroke:none"
-       id="rect4101" />
-  </g>
-</svg>
diff --git a/arm/resources/torConfigDesc.txt b/arm/resources/torConfigDesc.txt
deleted file mode 100644
index 9fa83e0..0000000
--- a/arm/resources/torConfigDesc.txt
+++ /dev/null
@@ -1,1123 +0,0 @@
-Tor Version 0.2.2.13-alpha
-General
-index: 46
-acceldir
-DIR
-Specify this option if using dynamic hardware acceleration and the engine implementation library resides somewhere other than the OpenSSL default.
---------------------------------------------------------------------------------
-General
-index: 45
-accelname
-NAME
-When using OpenSSL hardware crypto acceleration attempt to load the dynamic engine of this name. This must be used for any dynamic hardware engine. Names can be verified with the openssl engine command.
---------------------------------------------------------------------------------
-Relay
-index: 119
-accountingmax
-N bytes|KB|MB|GB|TB
-Never send more than the specified number of bytes in a given accounting period, or receive more than that number in the period. For example, with AccountingMax set to 1 GB, a server could send 900 MB and receive 800 MB and continue running. It will only hibernate once one of the two reaches 1 GB. When the number of bytes is exhausted, Tor will hibernate until some time in the next accounting period. To prevent all servers from waking at the same time, Tor will also wait until a random point in each period before waking up. If you have bandwidth cost issues, enabling hibernation is preferable to setting a low bandwidth, since it provides users with a collection of fast servers that are up some of the time, which is more useful than a set of slow servers that are always "available".
---------------------------------------------------------------------------------
-Relay
-index: 120
-accountingstart
-day|week|month [day] HH:MM
-Specify how long accounting periods last. If month is given, each accounting period runs from the time HH:MM on the dayth day of one month to the same day and time of the next. (The day must be between 1 and 28.) If week is given, each accounting period runs from the time HH:MM of the dayth day of one week to the same day and time of the next week, with Monday as day 1 and Sunday as day 7. If day is given, each accounting period runs from the time HH:MM each day to the same time on the next day. All times are local, and given in 24-hour time. (Defaults to "month 1 0:00".)
---------------------------------------------------------------------------------
-Relay
-index: 104
-address
-address
-The IP address or fully qualified domain name of this server (e.g. moria.mit.edu). You can leave this unset, and Tor will guess your IP address.
---------------------------------------------------------------------------------
-Client
-index: 89
-allowdotexit
-0|1
-If enabled, we convert "www.google.com.foo.exit" addresses on the SocksPort/TransPort/NatdPort into "www.google.com" addresses that exit from the node "foo". Disabled by default since attacking websites and exit relays can use it to manipulate your path selection. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 51
-allowinvalidnodes
-entry|exit|middle|introduction|rendezvous|...
-If some Tor servers are obviously not working right, the directory authorities can manually mark them as invalid, meaning that it's not recommended you use them for entry or exit positions in your circuits. You can opt to use them in some circuit positions, though. The default is "middle,rendezvous", and other choices are not advised.
---------------------------------------------------------------------------------
-Client
-index: 88
-allownonrfc953hostnames
-0|1
-When this option is disabled, Tor blocks hostnames containing illegal characters (like @ and :) rather than sending them to an exit node to be resolved. This helps trap accidental attempts to resolve URLs and so on. (Default: 0)
---------------------------------------------------------------------------------
-Relay
-index: 105
-allowsinglehopexits
-0|1
-This option controls whether clients can use this server as a single hop proxy. If set to 1, clients can use this server as an exit even if it is the only hop in the circuit. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 20
-alternatebridgeauthority
-[nickname] [flags] address:port  fingerprint
-As DirServer, but replaces less of the default directory authorities. Using AlternateDirAuthority replaces the default Tor directory authorities, but leaves the hidden service authorities and bridge authorities in place. Similarly, Using AlternateHSAuthority replaces the default hidden service authorities, but not the directory or bridge authorities.
---------------------------------------------------------------------------------
-General
-index: 18
-alternatedirauthority
-[nickname] [flags] address:port fingerprint
-
---------------------------------------------------------------------------------
-General
-index: 19
-alternatehsauthority
-[nickname] [flags] address:port fingerprint
-
---------------------------------------------------------------------------------
-Relay
-index: 106
-assumereachable
-0|1
-This option is used when bootstrapping a new Tor network. If set to 1, don't do self-reachability testing; just upload your server descriptor immediately. If AuthoritativeDirectory is also set, this option instructs the dirserver to bypass remote reachability testing too and list all connected servers as running.
---------------------------------------------------------------------------------
-Authority
-index: 154
-authdirbaddir
-AddressPattern...
-Authoritative directories only. A set of address patterns for servers that will be listed as bad directories in any network status document this authority publishes, if AuthDirListBadDirs is set.
---------------------------------------------------------------------------------
-Authority
-index: 155
-authdirbadexit
-AddressPattern...
-Authoritative directories only. A set of address patterns for servers that will be listed as bad exits in any network status document this authority publishes, if AuthDirListBadExits is set.
---------------------------------------------------------------------------------
-Authority
-index: 156
-authdirinvalid
-AddressPattern...
-Authoritative directories only. A set of address patterns for servers that will never be listed as "valid" in any network status document that this authority publishes.
---------------------------------------------------------------------------------
-Authority
-index: 158
-authdirlistbaddirs
-0|1
-Authoritative directories only. If set to 1, this directory has some opinion about which nodes are unsuitable as directory caches. (Do not set this to 1 unless you plan to list non-functioning directories as bad; otherwise, you are effectively voting in favor of every declared directory.)
---------------------------------------------------------------------------------
-Authority
-index: 159
-authdirlistbadexits
-0|1
-Authoritative directories only. If set to 1, this directory has some opinion about which nodes are unsuitable as exit nodes. (Do not set this to 1 unless you plan to list non-functioning exits as bad; otherwise, you are effectively voting in favor of every declared exit as an exit.)
---------------------------------------------------------------------------------
-Authority
-index: 161
-authdirmaxserversperaddr
-NUM
-Authoritative directories only. The maximum number of servers that we will list as acceptable on a single IP address. Set this to "0" for "no limit". (Default: 2)
---------------------------------------------------------------------------------
-Authority
-index: 162
-authdirmaxserversperauthaddr
-NUM
-Authoritative directories only. Like AuthDirMaxServersPerAddr, but applies to addresses shared with directory authorities. (Default: 5)
---------------------------------------------------------------------------------
-Authority
-index: 157
-authdirreject
-AddressPattern...
-Authoritative directories only. A set of address patterns for servers that will never be listed at all in any network status document that this authority publishes, or accepted as an OR address in any descriptor submitted for publication by this authority.
---------------------------------------------------------------------------------
-Authority
-index: 160
-authdirrejectunlisted
-0|1
-Authoritative directories only. If set to 1, the directory server rejects all uploaded server descriptors that aren't explicitly listed in the fingerprints file. This acts as a "panic button" if we get hit with a Sybil attack. (Default: 0)
---------------------------------------------------------------------------------
-Directory
-index: 135
-authoritativedirectory
-0|1
-When this option is set to 1, Tor operates as an authoritative directory server. Instead of caching the directory, it generates its own list of good servers, signs it, and sends that to the clients. Unless the clients already have you listed as a trusted directory, you probably do not want to set this option. Please coordinate with the other admins at tor-ops at torproject.org if you think you should be a directory.
---------------------------------------------------------------------------------
-Client
-index: 95
-automaphostsonresolve
-0|1
-When this option is enabled, and we get a request to resolve an address that ends with one of the suffixes in AutomapHostsSuffixes, we map an unused virtual address to that address, and return the new virtual address. This is handy for making ".onion" addresses work with applications that resolve an address and then connect to it. (Default: 0).
---------------------------------------------------------------------------------
-Client
-index: 96
-automaphostssuffixes
-SUFFIX,SUFFIX,...
-A comma-separated list of suffixes to use with AutomapHostsOnResolve. The "." suffix is equivalent to "all addresses." (Default: .exit,.onion).
---------------------------------------------------------------------------------
-General
-index: 47
-avoiddiskwrites
-0|1
-If non-zero, try to write to disk less frequently than we would otherwise. This is useful when running on flash memory or other media that support only a limited number of writes. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 1
-bandwidthburst
-N bytes|KB|MB|GB
-Limit the maximum token bucket size (also known as the burst) to the given number of bytes in each direction. (Default: 10 MB)
---------------------------------------------------------------------------------
-General
-index: 0
-bandwidthrate
-N bytes|KB|MB|GB
-A token bucket limits the average incoming bandwidth usage on this node to the specified number of bytes per second, and the average outgoing bandwidth usage to that same value. (Default: 5 MB)
---------------------------------------------------------------------------------
-Client
-index: 53
-bridge
-IP:ORPort [fingerprint]
-When set along with UseBridges, instructs Tor to use the relay at "IP:ORPort" as a "bridge" relaying into the Tor network. If "fingerprint" is provided (using the same format as for DirServer), we will verify that the relay running at that location has the right fingerprint. We also use fingerprint to look up the bridge descriptor at the bridge authority, if it's provided and if UpdateBridgesFromAuthority is set too.
---------------------------------------------------------------------------------
-Directory
-index: 144
-bridgeauthoritativedir
-0|1
-When this option is set in addition to AuthoritativeDirectory, Tor accepts and serves router descriptors, but it caches and serves the main networkstatus documents rather than generating its own. (Default: 0)
---------------------------------------------------------------------------------
-Relay
-index: 127
-bridgerecordusagebycountry
-0|1
-When this option is enabled and BridgeRelay is also enabled, and we have GeoIP data, Tor keeps a keep a per-country count of how many client addresses have contacted it so that it can help the bridge authority guess which countries have blocked access to it. (Default: 1)
---------------------------------------------------------------------------------
-Relay
-index: 107
-bridgerelay
-0|1
-Sets the relay to act as a "bridge" with respect to relaying connections from bridge users to the Tor network. Mainly it influences how the relay will cache and serve directory information. Usually used in combination with PublishServerDescriptor.
---------------------------------------------------------------------------------
-Relay
-index: 130
-cellstatistics
-0|1
-When this option is enabled, Tor writes statistics on the mean time that cells spend in circuit queues to disk every 24 hours. Cannot be changed while Tor is running. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 54
-circuitbuildtimeout
-NUM
-Try for at most NUM seconds when building circuits. If the circuit isn't open in that time, give up on it. (Default: 1 minute.)
---------------------------------------------------------------------------------
-Client
-index: 55
-circuitidletimeout
-NUM
-If we have kept a clean (never used) circuit around for NUM seconds, then close it. This way when the Tor client is entirely idle, it can expire all of its circuits, and then expire its TLS connections. Also, if we end up making a circuit that is not useful for exiting any of the requests we're receiving, it won't forever take up a slot in the circuit list. (Default: 1 hour.)
---------------------------------------------------------------------------------
-General
-index: 50
-circuitpriorityhalflife
-NUM1
-If this value is set, we override the default algorithm for choosing which circuit's cell to deliver or relay next. When the value is 0, we round-robin between the active circuits on a connection, delivering one cell from each in turn. When the value is positive, we prefer delivering cells from whichever connection has the lowest weighted cell count, where cells are weighted exponentially according to the supplied CircuitPriorityHalflife value (in seconds). If this option is not set at all, we use the behavior recommended in the current consensus networkstatus. This is an advanced option; you generally shouldn't have to mess with it. (Default: not set.)
---------------------------------------------------------------------------------
-Client
-index: 56
-circuitstreamtimeout
-NUM
-If non-zero, this option overrides our internal timeout schedule for how many seconds until we detach a stream from a circuit and try a new circuit. If your network is particularly slow, you might want to set this to a number like 60. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 99
-clientdnsrejectinternaladdresses
-0|1
-If true, Tor does not believe any anonymously retrieved DNS answer that tells it that an address resolves to an internal address (like 127.0.0.1 or 192.168.0.1). This option prevents certain browser-based attacks; don't turn it off unless you know what you're doing. (Default: 1).
---------------------------------------------------------------------------------
-Client
-index: 57
-clientonly
-0|1
-If set to 1, Tor will under no circumstances run as a server or serve directory requests. The default is to run as a client unless ORPort is configured. (Usually, you don't need to set this; Tor is pretty smart at figuring out whether you are reliable and high-bandwidth enough to be a useful server.) (Default: 0)
---------------------------------------------------------------------------------
-Authority
-index: 152
-consensusparams
-STRING
-STRING is a space-separated list of key=value pairs that Tor will include in the "params" line of its networkstatus vote.
---------------------------------------------------------------------------------
-General
-index: 7
-constrainedsockets
-0|1
-If set, Tor will tell the kernel to attempt to shrink the buffers for all sockets to the size specified in ConstrainedSockSize. This is useful for virtual servers and other environments where system level TCP buffers may be limited. If you're on a virtual server, and you encounter the "Error creating network socket: No buffer space available" message, you are likely experiencing this problem. 
-
-The preferred solution is to have the admin increase the buffer pool for the host itself via /proc/sys/net/ipv4/tcp_mem or equivalent facility; this configuration option is a second-resort. 
-
-The DirPort option should also not be used if TCP buffers are scarce. The cached directory requests consume additional sockets which exacerbates the problem. 
-
-You should not enable this feature unless you encounter the "no buffer space available" issue. Reducing the TCP buffers affects window size for the TCP stream and will reduce throughput in proportion to round trip time on long paths. (Default: 0.)
---------------------------------------------------------------------------------
-General
-index: 8
-constrainedsocksize
-N bytes|KB
-When ConstrainedSockets is enabled the receive and transmit buffers for all sockets will be set to this limit. Must be a value between 2048 and 262144, in 1024 byte increments. Default of 8192 is recommended.
---------------------------------------------------------------------------------
-Relay
-index: 108
-contactinfo
-email_address
-Administrative contact information for server. This line might get picked up by spam harvesters, so you may want to obscure the fact that it's an email address.
---------------------------------------------------------------------------------
-General
-index: 10
-controllistenaddress
-IP[:PORT]
-Bind the controller listener to this address. If you specify a port, bind to this port rather than the one specified in ControlPort. We strongly recommend that you leave this alone unless you know what you're doing, since giving attackers access to your control listener is really dangerous. (Default: 127.0.0.1) This directive can be specified multiple times to bind to multiple addresses/ports.
---------------------------------------------------------------------------------
-General
-index: 9
-controlport
-Port
-If set, Tor will accept connections on this port and allow those connections to control the Tor process using the Tor Control Protocol (described in control-spec.txt). Note: unless you also specify one of HashedControlPassword or CookieAuthentication, setting this option will cause Tor to allow any process on the local host to control it. This option is required for many Tor controllers; most use the value of 9051.
---------------------------------------------------------------------------------
-General
-index: 11
-controlsocket
-Path
-Like ControlPort, but listens on a Unix domain socket, rather than a TCP socket. (Unix and Unix-like systems only.)
---------------------------------------------------------------------------------
-General
-index: 13
-cookieauthentication
-0|1
-If this option is set to 1, don't allow any connections on the control port except when the connecting process knows the contents of a file named "control_auth_cookie", which Tor will create in its data directory. This authentication method should only be used on systems with good filesystem security. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 14
-cookieauthfile
-Path
-If set, this option overrides the default location and file name for Tor's cookie file. (See CookieAuthentication above.)
---------------------------------------------------------------------------------
-General
-index: 15
-cookieauthfilegroupreadable
-0|1|Groupname
-If this option is set to 0, don't allow the filesystem group to read the cookie file. If the option is set to 1, make the cookie file readable by the default GID. [Making the file readable by other groups is not yet implemented; let us know if you need this for some reason.] (Default: 0).
---------------------------------------------------------------------------------
-General
-index: 16
-datadirectory
-DIR
-Store working data in DIR (Default: /usr/local/var/lib/tor)
---------------------------------------------------------------------------------
-Authority
-index: 153
-dirallowprivateaddresses
-0|1
-If set to 1, Tor will accept router descriptors with arbitrary "Address" elements. Otherwise, if the address is not an IP address or is a private IP address, it will reject the router descriptor. Defaults to 0.
---------------------------------------------------------------------------------
-Directory
-index: 147
-dirlistenaddress
-IP[:PORT]
-Bind the directory service to this address. If you specify a port, bind to this port rather than the one specified in DirPort. (Default: 0.0.0.0) This directive can be specified multiple times to bind to multiple addresses/ports.
---------------------------------------------------------------------------------
-Directory
-index: 148
-dirpolicy
-policy,policy,...
-Set an entrance policy for this server, to limit who can connect to the directory ports. The policies have the same form as exit policies above.
---------------------------------------------------------------------------------
-Directory
-index: 146
-dirport
-PORT
-Advertise the directory service on this port.
---------------------------------------------------------------------------------
-Directory
-index: 136
-dirportfrontpage
-FILENAME
-When this option is set, it takes an HTML file and publishes it as "/" on the DirPort. Now relay operators can provide a disclaimer without needing to set up a separate webserver. There's a sample disclaimer in contrib/tor-exit-notice.html.
---------------------------------------------------------------------------------
-Relay
-index: 131
-dirreqstatistics
-0|1
-When this option is enabled, Tor writes statistics on the number and response time of network status requests to disk every 24 hours. Cannot be changed while Tor is running. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 17
-dirserver
-[nickname] [flags] address:port fingerprint
-Use a nonstandard authoritative directory server at the provided address and port, with the specified key fingerprint. This option can be repeated many times, for multiple authoritative directory servers. Flags are separated by spaces, and determine what kind of an authority this directory is. By default, every authority is authoritative for current ("v2")-style directories, unless the "no-v2" flag is given. If the "v1" flags is provided, Tor will use this server as an authority for old-style (v1) directories as well. (Only directory mirrors care about this.) Tor will use this server as an authority for hidden service information if the "hs" flag is set, or if the "v1" flag is set and the "no-hs" flag is not set. Tor will use this authority as a bridge authoritative directory if the "bridge" flag is set. If a flag "orport=port" is given, Tor will use the given port when opening encrypted tunnels to the dirserver. Lastly, if a flag "v3ident=fp" is given, the dirserver is a v3 directo
 ry authority whose v3 long-term signing key has the fingerprint fp. 
-
-If no dirserver line is given, Tor will use the default directory servers. NOTE: this option is intended for setting up a private Tor network with its own directory authorities. If you use it, you will be distinguishable from other users, because you won't believe the same authorities they do.
---------------------------------------------------------------------------------
-General
-index: 21
-disableallswap
-0|1
-If set to 1, Tor will attempt to lock all current and future memory pages, so that memory cannot be paged out. Windows, OS X and Solaris are currently not supported. We believe that this feature works on modern Gnu/Linux distributions, and that it should work on *BSD systems (untested). This option requires that you start your Tor as root, and you should use the User option to properly reduce Tor's privileges. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 98
-dnslistenaddress
-IP[:PORT]
-Bind to this address to listen for DNS connections. (Default: 127.0.0.1).
---------------------------------------------------------------------------------
-Client
-index: 97
-dnsport
-PORT
-If non-zero, Tor listens for UDP DNS requests on this port and resolves them anonymously. (Default: 0).
---------------------------------------------------------------------------------
-Client
-index: 100
-downloadextrainfo
-0|1
-If true, Tor downloads and caches "extra-info" documents. These documents contain information about servers other than the information in their regular router descriptors. Tor does not use this information for anything itself; to save bandwidth, leave this option turned off. (Default: 0).
---------------------------------------------------------------------------------
-Client
-index: 74
-enforcedistinctsubnets
-0|1
-If 1, Tor will not put two servers whose IP addresses are "too close" on the same circuit. Currently, two addresses are "too close" if they lie in the same /16 range. (Default: 1)
---------------------------------------------------------------------------------
-Client
-index: 60
-entrynodes
-node,node,...
-A list of identity fingerprints, nicknames, country codes and address patterns of nodes to use for the first hop in normal circuits. These are treated only as preferences unless StrictNodes (see below) is also set.
---------------------------------------------------------------------------------
-Relay
-index: 132
-entrystatistics
-0|1
-When this option is enabled, Tor writes statistics on the number of directly connecting clients to disk every 24 hours. Cannot be changed while Tor is running. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 59
-excludeexitnodes
-node,node,...
-A list of identity fingerprints, nicknames, country codes and address patterns of nodes to never use when picking an exit node. Note that any node listed in ExcludeNodes is automatically considered to be part of this list.
---------------------------------------------------------------------------------
-Client
-index: 58
-excludenodes
-node,node,...
-A list of identity fingerprints, nicknames, country codes and address patterns of nodes to never use when building a circuit. (Example: ExcludeNodes SlowServer, $ EFFFFFFFFFFFFFFF, {cc}, 255.254.0.0/8)
---------------------------------------------------------------------------------
-Client
-index: 52
-excludesinglehoprelays
-0|1
-This option controls whether circuits built by Tor will include relays with the AllowSingleHopExits flag set to true. If ExcludeSingleHopRelays is set to 0, these relays will be included. Note that these relays might be at higher risk of being seized or observed, so they are not normally included. (Default: 1)
---------------------------------------------------------------------------------
-Client
-index: 61
-exitnodes
-node,node,...
-A list of identity fingerprints, nicknames, country codes and address patterns of nodes to use for the last hop in normal exit circuits. These are treated only as preferences unless StrictNodes (see below) is also set.
---------------------------------------------------------------------------------
-Relay
-index: 109
-exitpolicy
-policy,policy,...
-Set an exit policy for this server. Each policy is of the form "accept|reject ADDR[/MASK][:PORT]". If /MASK is omitted then this policy just applies to the host given. Instead of giving a host or network you can also use "*" to denote the universe (0.0.0.0/0).  PORT can be a single port number, an interval of ports "FROM_PORT-TO_PORT", or "*". If PORT is omitted, that means "*". 
-
-For example, "accept 18.7.22.69:*,reject 18.0.0.0/8:*,accept *:*" would reject any traffic destined for MIT except for web.mit.edu, and accept anything else. 
-
-To specify all internal and link-local networks (including 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8, 192.168.0.0/16, 10.0.0.0/8, and 172.16.0.0/12), you can use the "private" alias instead of an address. These addresses are rejected by default (at the beginning of your exit policy), along with your public IP address, unless you set the ExitPolicyRejectPrivate config option to 0. For example, once you've done that, you could allow HTTP to 127.0.0.1 and block all other connections to internal networks with "accept 127.0.0.1:80,reject private:*", though that may also allow connections to your own computer that are addressed to its public (external) IP address. See RFC 1918 and RFC 3330 for more details about internal and reserved IP address space. 
-
-This directive can be specified multiple times so you don't have to put it all on one line. 
-
-Policies are considered first to last, and the first match wins. If you want to _replace_ the default exit policy, end your exit policy with either a reject *:* or an accept *:*. Otherwise, you're _augmenting_ (prepending to) the default exit policy. The default exit policy is: 
-
-    reject *:25
-    reject *:119
-    reject *:135-139
-    reject *:445
-    reject *:563
-    reject *:1214
-    reject *:4661-4666
-    reject *:6346-6429
-    reject *:6699
-    reject *:6881-6999
-    accept *:*
---------------------------------------------------------------------------------
-Relay
-index: 110
-exitpolicyrejectprivate
-0|1
-Reject all private (local) networks, along with your own public IP address, at the beginning of your exit policy. See above entry on ExitPolicy. (Default: 1)
---------------------------------------------------------------------------------
-Relay
-index: 133
-exitportstatistics
-0|1
-When this option is enabled, Tor writes statistics on the number of relayed bytes and opened stream per exit port to disk every 24 hours. Cannot be changed while Tor is running. (Default: 0)
---------------------------------------------------------------------------------
-Relay
-index: 134
-extrainfostatistics
-0|1
-When this option is enabled, Tor includes previously gathered statistics in its extra-info documents that it uploads to the directory authorities. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 101
-fallbacknetworkstatusfile
-FILENAME
-If Tor doesn't have a cached networkstatus file, it starts out using this one instead. Even if this file is out of date, Tor can still use it to learn about directory mirrors, so it doesn't need to put load on the authorities. (Default: None).
---------------------------------------------------------------------------------
-Client
-index: 63
-fascistfirewall
-0|1
-If 1, Tor will only create outgoing connections to ORs running on ports that your firewall allows (defaults to 80 and 443; see FirewallPorts). This will allow you to run Tor as a client behind a firewall with restrictive policies, but will not allow you to run as a server behind such a firewall. If you prefer more fine-grained control, use ReachableAddresses instead.
---------------------------------------------------------------------------------
-Client
-index: 90
-fastfirsthoppk
-0|1
-When this option is disabled, Tor uses the public key step for the first hop of creating circuits. Skipping it is generally safe since we have already used TLS to authenticate the relay and to establish forward-secure keys. Turning this option off makes circuit building slower. 
-
-Note that Tor will always use the public key step for the first hop if it's operating as a relay, and it will never use the public key step if it doesn't yet know the onion key of the first hop. (Default: 1)
---------------------------------------------------------------------------------
-General
-index: 22
-fetchdirinfoearly
-0|1
-If set to 1, Tor will always fetch directory information like other directory caches, even if you don't meet the normal criteria for fetching early. Normal users should leave it off. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 23
-fetchdirinfoextraearly
-0|1
-If set to 1, Tor will fetch directory information before other directory caches. It will attempt to download directory information closer to the start of the consensus period. Normal users should leave it off. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 24
-fetchhidservdescriptors
-0|1
-If set to 0, Tor will never fetch any hidden service descriptors from the rendezvous directories. This option is only useful if you're using a Tor controller that handles hidden service fetches for you. (Default: 1)
---------------------------------------------------------------------------------
-General
-index: 25
-fetchserverdescriptors
-0|1
-If set to 0, Tor will never fetch any network status summaries or server descriptors from the directory servers. This option is only useful if you're using a Tor controller that handles directory fetches for you. (Default: 1)
---------------------------------------------------------------------------------
-General
-index: 26
-fetchuselessdescriptors
-0|1
-If set to 1, Tor will fetch every non-obsolete descriptor from the authorities that it hears about. Otherwise, it will avoid fetching useless descriptors, for example for routers that are not running. This option is useful if you're using the contributed "exitlist" script to enumerate Tor nodes that exit to certain addresses. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 64
-firewallports
-PORTS
-A list of ports that your firewall allows you to connect to. Only used when FascistFirewall is set. This option is deprecated; use ReachableAddresses instead. (Default: 80, 443)
---------------------------------------------------------------------------------
-Relay
-index: 129
-geoipfile
-filename
-A filename containing GeoIP data, for use with BridgeRecordUsageByCountry.
---------------------------------------------------------------------------------
-General
-index: 44
-hardwareaccel
-0|1
-If non-zero, try to use built-in (static) crypto hardware acceleration when available. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 12
-hashedcontrolpassword
-hashed_password
-Don't allow any connections on the control port except when the other process knows the password whose one-way hash is hashed_password. You can compute the hash of a password by running "tor --hash-password password". You can provide several acceptable passwords by using more than one HashedControlPassword line.
---------------------------------------------------------------------------------
-Hidden Service
-index: 171
-hiddenserviceauthorizeclient
-auth-type client-name,client-name,...
-If configured, the hidden service is accessible for authorized clients only. The auth-type can either be 'basic' for a general-purpose authorization protocol or 'stealth' for a less scalable protocol that also hides service activity from unauthorized clients. Only clients that are listed here are authorized to access the hidden service. Valid client names are 1 to 19 characters long and only use characters in A-Za-z0-9+-_ (no spaces). If this option is set, the hidden service is not accessible for clients without authorization any more. Generated authorization data can be found in the hostname file. Clients need to put this authorization data in their configuration file using HidServAuth.
---------------------------------------------------------------------------------
-Hidden Service
-index: 167
-hiddenservicedir
-DIRECTORY
-Store data files for a hidden service in DIRECTORY. Every hidden service must have a separate directory. You may use this option multiple times to specify multiple services.
---------------------------------------------------------------------------------
-Hidden Service
-index: 168
-hiddenserviceport
-VIRTPORT [TARGET]
-Configure a virtual port VIRTPORT for a hidden service. You may use this option multiple times; each time applies to the service using the most recent hiddenservicedir. By default, this option maps the virtual port to the same port on 127.0.0.1. You may override the target port, address, or both by specifying a target of addr, port, or addr:port. You may also have multiple lines with the same VIRTPORT: when a user connects to that VIRTPORT, one of the TARGETs from those lines will be chosen at random.
---------------------------------------------------------------------------------
-Hidden Service
-index: 170
-hiddenserviceversion
-version,version,...
-A list of rendezvous service descriptor versions to publish for the hidden service. Currently, only version 2 is supported. (Default: 2)
---------------------------------------------------------------------------------
-Client
-index: 65
-hidservauth
-onion-address auth-cookie [service-name]
-Client authorization for a hidden service. Valid onion addresses contain 16 characters in a-z2-7 plus ".onion", and valid auth cookies contain 22 characters in A-Za-z0-9+/. The service name is only used for internal purposes, e.g., for Tor controllers. This option may be used multiple times for different hidden services. If a hidden service uses authorization and this option is not set, the hidden service is not accessible. Hidden services can be configured to require authorization using the HiddenServiceAuthorizeClient option.
---------------------------------------------------------------------------------
-Directory
-index: 143
-hidservdirectoryv2
-0|1
-When this option is set, Tor accepts and serves v2 hidden service descriptors. Setting DirPort is not required for this, because clients connect via the ORPort by default. (Default: 1)
---------------------------------------------------------------------------------
-Directory
-index: 142
-hsauthoritativedir
-0|1
-When this option is set in addition to AuthoritativeDirectory, Tor also accepts and serves hidden service descriptors. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 27
-httpproxy
-host[:port]
-Tor will make all its directory requests through this host:port (or host:80 if port is not specified), rather than connecting directly to any directory servers.
---------------------------------------------------------------------------------
-General
-index: 28
-httpproxyauthenticator
-username:password
-If defined, Tor will use this username:password for Basic HTTP proxy authentication, as in RFC 2617. This is currently the only form of HTTP proxy authentication that Tor supports; feel free to submit a patch if you want it to support others.
---------------------------------------------------------------------------------
-General
-index: 29
-httpsproxy
-host[:port]
-Tor will make all its OR (SSL) connections through this host:port (or host:443 if port is not specified), via HTTP CONNECT rather than connecting directly to servers. You may want to set FascistFirewall to restrict the set of ports you might try to connect to, if your HTTPS proxy only allows connecting to certain ports.
---------------------------------------------------------------------------------
-General
-index: 30
-httpsproxyauthenticator
-username:password
-If defined, Tor will use this username:password for Basic HTTPS proxy authentication, as in RFC 2617. This is currently the only form of HTTPS proxy authentication that Tor supports; feel free to submit a patch if you want it to support others.
---------------------------------------------------------------------------------
-General
-index: 35
-keepaliveperiod
-NUM
-To keep firewalls from expiring connections, send a padding keepalive cell every NUM seconds on open connections that are in use. If the connection has no open circuits, it will instead be closed after NUM seconds of idleness. (Default: 5 minutes)
---------------------------------------------------------------------------------
-General
-index: 37
-log
-minSeverity[-maxSeverity] file FILENAME
-As above, but send log messages to the listed filename. The "Log" option may appear more than once in a configuration file. Messages are sent to all the logs that match their severity level.
---------------------------------------------------------------------------------
-Client
-index: 69
-longlivedports
-PORTS
-A list of ports for services that tend to have long-running connections (e.g. chat and interactive shells). Circuits for streams that use these ports will contain only high-uptime nodes, to reduce the chance that a node will go down before the stream is finished. (Default: 21, 22, 706, 1863, 5050, 5190, 5222, 5223, 6667, 6697, 8300)
---------------------------------------------------------------------------------
-Client
-index: 70
-mapaddress
-address newaddress
-When a request for address arrives to Tor, it will rewrite it to newaddress before processing it. For example, if you always want connections to www.indymedia.org to exit via torserver (where torserver is the nickname of the server), use "MapAddress www.indymedia.org www.indymedia.org.torserver.exit".
---------------------------------------------------------------------------------
-General
-index: 2
-maxadvertisedbandwidth
-N bytes|KB|MB|GB
-If set, we will not advertise more than this amount of bandwidth for our BandwidthRate. Server operators who want to reduce the number of clients who ask to build circuits through them (since this is proportional to advertised bandwidth rate) can thus reduce the CPU demands on their server without impacting network performance.
---------------------------------------------------------------------------------
-Client
-index: 72
-maxcircuitdirtiness
-NUM
-Feel free to reuse a circuit that was first used at most NUM seconds ago, but never attach a new stream to a circuit that is too old. (Default: 10 minutes)
---------------------------------------------------------------------------------
-Relay
-index: 111
-maxonionspending
-NUM
-If you have more than this number of onionskins queued for decrypt, reject new ones. (Default: 100)
---------------------------------------------------------------------------------
-Directory
-index: 145
-minuptimehidservdirectoryv2
-N seconds|minutes|hours|days|weeks
-Minimum uptime of a v2 hidden service directory to be accepted as such by authoritative directories. (Default: 24 hours)
---------------------------------------------------------------------------------
-Relay
-index: 112
-myfamily
-node,node,...
-Declare that this Tor server is controlled or administered by a group or organization identical or similar to that of the other servers, defined by their identity fingerprints or nicknames. When two servers both declare that they are in the same 'family', Tor clients will not use them in the same circuit. (Each server only needs to list the other servers in its family; it doesn't need to list itself, but it won't hurt.)
---------------------------------------------------------------------------------
-Directory
-index: 141
-namingauthoritativedirectory
-0|1
-When this option is set to 1, then the server advertises that it has opinions about nickname-to-fingerprint bindings. It will include these opinions in its published network-status pages, by listing servers with the flag "Named" if a correct binding between that nickname and fingerprint has been registered with the dirserver. Naming dirservers will refuse to accept or publish descriptors that contradict a registered binding. See approved-routers in the FILES section below.
---------------------------------------------------------------------------------
-Client
-index: 94
-natdlistenaddress
-IP[:PORT]
-Bind to this address to listen for NATD connections. (Default: 127.0.0.1).
---------------------------------------------------------------------------------
-Client
-index: 93
-natdport
-PORT
-Allow old versions of ipfw (as included in old versions of FreeBSD, etc.) to send connections through Tor using the NATD protocol. This option is only for people who cannot use TransPort.
---------------------------------------------------------------------------------
-Client
-index: 71
-newcircuitperiod
-NUM
-Every NUM seconds consider whether to build a new circuit. (Default: 30 seconds)
---------------------------------------------------------------------------------
-Relay
-index: 113
-nickname
-name
-Set the server's nickname to 'name'. Nicknames must be between 1 and 19 characters inclusive, and must contain only the characters [a-zA-Z0-9].
---------------------------------------------------------------------------------
-Client
-index: 73
-nodefamily
-node,node,...
-The Tor servers, defined by their identity fingerprints or nicknames, constitute a "family" of similar or co-administered servers, so never use any two of them in the same circuit. Defining a NodeFamily is only needed when a server doesn't list the family itself (with MyFamily). This option can be used multiple times.
---------------------------------------------------------------------------------
-Relay
-index: 114
-numcpus
-num
-How many processes to use at once for decrypting onionskins. (Default: 1)
---------------------------------------------------------------------------------
-Client
-index: 84
-numentryguards
-NUM
-If UseEntryGuards is set to 1, we will try to pick a total of NUM routers as long-term entries for our circuits. (Defaults to 3.)
---------------------------------------------------------------------------------
-Relay
-index: 116
-orlistenaddress
-IP[:PORT]
-Bind to this IP address to listen for connections from Tor clients and servers. If you specify a port, bind to this port rather than the one specified in ORPort. (Default: 0.0.0.0) This directive can be specified multiple times to bind to multiple addresses/ports.
---------------------------------------------------------------------------------
-Relay
-index: 115
-orport
-PORT
-Advertise this port to listen for connections from Tor clients and servers.
---------------------------------------------------------------------------------
-General
-index: 38
-outboundbindaddress
-IP
-Make all outbound connections originate from the IP address specified. This is only useful when you have multiple network interfaces, and you want all of Tor's outgoing connections to use a single one. This setting will be ignored for connections to the loopback addresses (127.0.0.0/8 and ::1).
---------------------------------------------------------------------------------
-General
-index: 6
-perconnbwburst
-N bytes|KB|MB|GB
-If set, do separate rate limiting for each connection from a non-relay. You should never need to change this value, since a network-wide value is published in the consensus and your relay will use that value. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 5
-perconnbwrate
-N bytes|KB|MB|GB
-If set, do separate rate limiting for each connection from a non-relay. You should never need to change this value, since a network-wide value is published in the consensus and your relay will use that value. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 39
-pidfile
-FILE
-On startup, write our PID to FILE. On clean shutdown, remove FILE.
---------------------------------------------------------------------------------
-General
-index: 49
-prefertunneleddirconns
-0|1
-If non-zero, we will avoid directory servers that don't support tunneled directory connections, when possible. (Default: 1)
---------------------------------------------------------------------------------
-General
-index: 40
-protocolwarnings
-0|1
-If 1, Tor will log with severity 'warn' various cases of other parties not following the Tor specification. Otherwise, they are logged with severity 'info'. (Default: 0)
---------------------------------------------------------------------------------
-Hidden Service
-index: 169
-publishhidservdescriptors
-0|1
-If set to 0, Tor will run any hidden services you configure, but it won't advertise them to the rendezvous directory. This option is only useful if you're using a Tor controller that handles hidserv publishing for you. (Default: 1)
---------------------------------------------------------------------------------
-Relay
-index: 117
-publishserverdescriptor
-0|1|v1|v2|v3|bridge|hidserv,...
-This option is only considered if you have an ORPort defined. You can choose multiple arguments, separated by commas. If set to 0, Tor will act as a server but it will not publish its descriptor to the directory authorities. (This is useful if you're testing out your server, or if you're using a Tor controller that handles directory publishing for you.) Otherwise, Tor will publish its descriptor to all directory authorities of the type(s) specified. The value "1" is the default, which means "publish to the appropriate authorities".
---------------------------------------------------------------------------------
-Client
-index: 66
-reachableaddresses
-ADDR[/MASK][:PORT]...
-A comma-separated list of IP addresses and ports that your firewall allows you to connect to. The format is as for the addresses in ExitPolicy, except that "accept" is understood unless "reject" is explicitly provided. For example, 'ReachableAddresses 99.0.0.0/8, reject 18.0.0.0/8:80, accept *:80' means that your firewall allows connections to everything inside net 99, rejects port 80 connections to net 18, and accepts connections to port 80 otherwise. (Default: 'accept *:*'.)
---------------------------------------------------------------------------------
-Client
-index: 67
-reachablediraddresses
-ADDR[/MASK][:PORT]...
-Like ReachableAddresses, a list of addresses and ports. Tor will obey these restrictions when fetching directory information, using standard HTTP GET requests. If not set explicitly then the value of ReachableAddresses is used. If HTTPProxy is set then these connections will go through that proxy.
---------------------------------------------------------------------------------
-Client
-index: 68
-reachableoraddresses
-ADDR[/MASK][:PORT]...
-Like ReachableAddresses, a list of addresses and ports. Tor will obey these restrictions when connecting to Onion Routers, using TLS/SSL. If not set explicitly then the value of ReachableAddresses is used. If HTTPSProxy is set then these connections will go through that proxy. 
-
-The separation between ReachableORAddresses and ReachableDirAddresses is only interesting when you are connecting through proxies (see HTTPProxy and HTTPSProxy). Most proxies limit TLS connections (which Tor uses to connect to Onion Routers) to port 443, and some limit HTTP GET requests (which Tor uses for fetching directory information) to port 80.
---------------------------------------------------------------------------------
-Authority
-index: 150
-recommendedclientversions
-STRING
-STRING is a comma-separated list of Tor versions currently believed to be safe for clients to use. This information is included in version 2 directories. If this is not set then the value of RecommendedVersions is used. When this is set then VersioningAuthoritativeDirectory should be set too.
---------------------------------------------------------------------------------
-Authority
-index: 151
-recommendedserverversions
-STRING
-STRING is a comma-separated list of Tor versions currently believed to be safe for servers to use. This information is included in version 2 directories. If this is not set then the value of RecommendedVersions is used. When this is set then VersioningAuthoritativeDirectory should be set too.
---------------------------------------------------------------------------------
-Authority
-index: 149
-recommendedversions
-STRING
-STRING is a comma-separated list of Tor versions currently believed to be safe. The list is included in each directory, and nodes which pull down the directory learn whether they need to upgrade. This option can appear multiple times: the values from multiple lines are spliced together. When this is set then VersioningAuthoritativeDirectory should be set too.
---------------------------------------------------------------------------------
-Client
-index: 103
-rejectplaintextports
-port,port,...
-Like WarnPlaintextPorts, but instead of warning about risky port uses, Tor will instead refuse to make the connection. (Default: None).
---------------------------------------------------------------------------------
-General
-index: 4
-relaybandwidthburst
-N bytes|KB|MB|GB
-Limit the maximum token bucket size (also known as the burst) for _relayed traffic_ to the given number of bytes in each direction. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 3
-relaybandwidthrate
-N bytes|KB|MB|GB
-If defined, a separate token bucket limits the average incoming bandwidth usage for _relayed traffic_ on this node to the specified number of bytes per second, and the average outgoing bandwidth usage to that same value. Relayed traffic currently is calculated to include answers to directory requests, but that may change in future versions. (Default: 0)
---------------------------------------------------------------------------------
-Hidden Service
-index: 172
-rendpostperiod
-N seconds|minutes|hours|days|weeks
-Every time the specified period elapses, Tor uploads any rendezvous service descriptors to the directory servers. This information is also uploaded whenever it changes. (Default: 20 minutes)
---------------------------------------------------------------------------------
-General
-index: 41
-runasdaemon
-0|1
-If 1, Tor forks and daemonizes to the background. This option has no effect on Windows; instead you should use the --service command-line option. (Default: 0)
---------------------------------------------------------------------------------
-General
-index: 42
-safelogging
-0|1|relay
-Tor can scrub potentially sensitive strings from log messages (e.g. addresses) by replacing them with the string [scrubbed]. This way logs can still be useful, but they don't leave behind personally identifying information about what sites a user might have visited. 
-
-If this option is set to 0, Tor will not perform any scrubbing, if it is set to 1, all potentially sensitive strings are replaced. If it is set to relay, all log messages generated when acting as a relay are sanitized, but all messages generated when acting as a client are not. (Default: 1)
---------------------------------------------------------------------------------
-Client
-index: 85
-safesocks
-0|1
-When this option is enabled, Tor will reject application connections that use unsafe variants of the socks protocol  ones that only provide an IP address, meaning the application is doing a DNS resolve first. Specifically, these are socks4 and socks5 when not doing remote DNS. (Defaults to 0.)
---------------------------------------------------------------------------------
-Relay
-index: 122
-serverdnsallowbrokenconfig
-0|1
-If this option is false, Tor exits immediately if there are problems parsing the system DNS configuration or connecting to nameservers. Otherwise, Tor continues to periodically retry the system nameservers until it eventually succeeds. (Defaults to "1".)
---------------------------------------------------------------------------------
-Relay
-index: 126
-serverdnsallownonrfc953hostnames
-0|1
-When this option is disabled, Tor does not try to resolve hostnames containing illegal characters (like @ and :) rather than sending them to an exit node to be resolved. This helps trap accidental attempts to resolve URLs and so on. This option only affects name lookups that your server does on behalf of clients. (Default: 0)
---------------------------------------------------------------------------------
-Relay
-index: 124
-serverdnsdetecthijacking
-0|1
-When this option is set to 1, we will test periodically to determine whether our local nameservers have been configured to hijack failing DNS requests (usually to an advertising site). If they are, we will attempt to correct this. This option only affects name lookups that your server does on behalf of clients. (Defaults to "1".)
---------------------------------------------------------------------------------
-Relay
-index: 128
-serverdnsrandomizecase
-0|1
-When this option is set, Tor sets the case of each character randomly in outgoing DNS requests, and makes sure that the case matches in DNS replies. This so-called "0x20 hack" helps resist some types of DNS poisoning attack. For more information, see "Increased DNS Forgery Resistance through 0x20-Bit Encoding". This option only affects name lookups that your server does on behalf of clients. (Default: 1)
---------------------------------------------------------------------------------
-Relay
-index: 121
-serverdnsresolvconffile
-filename
-Overrides the default DNS configuration with the configuration in filename. The file format is the same as the standard Unix "resolv.conf" file (7). This option, like all other ServerDNS options, only affects name lookups that your server does on behalf of clients. (Defaults to use the system DNS configuration.)
---------------------------------------------------------------------------------
-Relay
-index: 123
-serverdnssearchdomains
-0|1
-If set to 1, then we will search for addresses in the local search domain. For example, if this system is configured to believe it is in "example.com", and a client tries to connect to "www", the client will be connected to "www.example.com". This option only affects name lookups that your server does on behalf of clients. (Defaults to "0".)
---------------------------------------------------------------------------------
-Relay
-index: 125
-serverdnstestaddresses
-address,address,...
-When we're detecting DNS hijacking, make sure that these valid addresses aren't getting redirected. If they are, then our DNS is completely useless, and we'll reset our exit policy to "reject :". This option only affects name lookups that your server does on behalf of clients. (Defaults to "www.google.com, www.mit.edu, www.yahoo.com, www.slashdot.org".)
---------------------------------------------------------------------------------
-Relay
-index: 118
-shutdownwaitlength
-NUM
-When we get a SIGINT and we're a server, we begin shutting down: we close listeners and start refusing new circuits. After NUM seconds, we exit. If we get a second SIGINT, we exit immedi- ately. (Default: 30 seconds)
---------------------------------------------------------------------------------
-General
-index: 31
-socks4proxy
-host[:port]
-Tor will make all OR connections through the SOCKS 4 proxy at host:port (or host:1080 if port is not specified).
---------------------------------------------------------------------------------
-General
-index: 32
-socks5proxy
-host[:port]
-Tor will make all OR connections through the SOCKS 5 proxy at host:port (or host:1080 if port is not specified).
---------------------------------------------------------------------------------
-General
-index: 34
-socks5proxypassword
-password
-If defined, authenticate to the SOCKS 5 server using username and password in accordance to RFC 1929. Both username and password must be between 1 and 255 characters.
---------------------------------------------------------------------------------
-General
-index: 33
-socks5proxyusername
-username
-
---------------------------------------------------------------------------------
-Client
-index: 76
-sockslistenaddress
-IP[:PORT]
-Bind to this address to listen for connections from Socks-speaking applications. (Default: 127.0.0.1) You can also specify a port (e.g. 192.168.0.1:9100). This directive can be specified multiple times to bind to multiple addresses/ports.
---------------------------------------------------------------------------------
-Client
-index: 77
-sockspolicy
-policy,policy,...
-Set an entrance policy for this server, to limit who can connect to the SocksPort and DNSPort ports. The policies have the same form as exit policies below.
---------------------------------------------------------------------------------
-Client
-index: 75
-socksport
-PORT
-Advertise this port to listen for connections from Socks-speaking applications. Set this to 0 if you don't want to allow application connections. (Default: 9050)
---------------------------------------------------------------------------------
-Client
-index: 78
-sockstimeout
-NUM
-Let a socks connection wait NUM seconds handshaking, and NUM seconds unattached waiting for an appropriate circuit, before we fail it. (Default: 2 minutes.)
---------------------------------------------------------------------------------
-Client
-index: 62
-strictnodes
-0|1
-If 1 and EntryNodes config option is set, Tor will never use any nodes besides those listed in EntryNodes for the first hop of a normal circuit. If 1 and ExitNodes config option is set, Tor will never use any nodes besides those listed in ExitNodes for the last hop of a normal exit circuit. Note that Tor might still use these nodes for non-exit circuits such as one-hop directory fetches or hidden service support circuits.
---------------------------------------------------------------------------------
-Testing
-index: 177
-testingauthdirtimetolearnreachability
-N minutes|hours
-After starting as an authority, do not make claims about whether routers are Running until this much time has passed. Changing this requires that TestingTorNetwork is set. (Default: 30 minutes)
---------------------------------------------------------------------------------
-Testing
-index: 178
-testingestimateddescriptorpropagationtime
-N minutes|hours
-Clients try downloading router descriptors from directory caches after this time. Changing this requires that TestingTorNetwork is set. (Default: 10 minutes) 
-
-SIGNALS
---------------------------------------------------------------------------------
-Testing
-index: 173
-testingtornetwork
-0|1
-If set to 1, Tor adjusts default values of the configuration options below, so that it is easier to set up a testing Tor network. May only be set if non-default set of DirServers is set. Cannot be unset while Tor is running. (Default: 0) 
-
-    ServerDNSAllowBrokenConfig 1
-    DirAllowPrivateAddresses 1
-    EnforceDistinctSubnets 0
-    AssumeReachable 1
-    AuthDirMaxServersPerAddr 0
-    AuthDirMaxServersPerAuthAddr 0
-    ClientDNSRejectInternalAddresses 0
-    ExitPolicyRejectPrivate 0
-    V3AuthVotingInterval 5 minutes
-    V3AuthVoteDelay 20 seconds
-    V3AuthDistDelay 20 seconds
-    TestingV3AuthInitialVotingInterval 5 minutes
-    TestingV3AuthInitialVoteDelay 20 seconds
-    TestingV3AuthInitialDistDelay 20 seconds
-    TestingAuthDirTimeToLearnReachability 0 minutes
-    TestingEstimatedDescriptorPropagationTime 0 minutes
---------------------------------------------------------------------------------
-Testing
-index: 176
-testingv3authinitialdistdelay
-N minutes|hours
-Like TestingV3AuthInitialDistDelay, but for initial voting interval before the first consensus has been created. Changing this requires that TestingTorNetwork is set. (Default: 5 minutes)
---------------------------------------------------------------------------------
-Testing
-index: 175
-testingv3authinitialvotedelay
-N minutes|hours
-Like TestingV3AuthInitialVoteDelay, but for initial voting interval before the first consensus has been created. Changing this requires that TestingTorNetwork is set. (Default: 5 minutes)
---------------------------------------------------------------------------------
-Testing
-index: 174
-testingv3authinitialvotinginterval
-N minutes|hours
-Like V3AuthVotingInterval, but for initial voting interval before the first consensus has been created. Changing this requires that TestingTorNetwork is set. (Default: 30 minutes)
---------------------------------------------------------------------------------
-Client
-index: 86
-testsocks
-0|1
-When this option is enabled, Tor will make a notice-level log entry for each connection to the Socks port indicating whether the request used a safe socks protocol or an unsafe one (see above entry on SafeSocks). This helps to determine whether an application using Tor is possibly leaking DNS requests. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 79
-trackhostexits
-host,.domain,...
-For each value in the comma separated list, Tor will track recent connections to hosts that match this value and attempt to reuse the same exit node for each. If the value is prepended with a '.', it is treated as matching an entire domain. If one of the values is just a '.', it means match everything. This option is useful if you frequently connect to sites that will expire all your authentication cookies (i.e. log you out) if your IP address changes. Note that this option does have the disadvantage of making it more clear that a given history is associated with a single user. However, most people who would wish to observe this will observe it through cookies or other protocol-specific means anyhow.
---------------------------------------------------------------------------------
-Client
-index: 80
-trackhostexitsexpire
-NUM
-Since exit servers go up and down, it is desirable to expire the association between host and exit server after NUM seconds. The default is 1800 seconds (30 minutes).
---------------------------------------------------------------------------------
-Client
-index: 92
-translistenaddress
-IP[:PORT]
-Bind to this address to listen for transparent proxy connections. (Default: 127.0.0.1). This is useful for exporting a transparent proxy server to an entire network.
---------------------------------------------------------------------------------
-Client
-index: 91
-transport
-PORT
-If non-zero, enables transparent proxy support on PORT (by convention, 9040). Requires OS support for transparent proxies, such as BSDs' pf or Linux's IPTables. If you're planning to use Tor as a transparent proxy for a network, you'll want to examine and change VirtualAddrNetwork from the default setting. You'll also want to set the TransListenAddress option for the network you'd like to proxy. (Default: 0).
---------------------------------------------------------------------------------
-General
-index: 48
-tunneldirconns
-0|1
-If non-zero, when a directory server we contact supports it, we will build a one-hop circuit and make an encrypted connection via its ORPort. (Default: 1)
---------------------------------------------------------------------------------
-Client
-index: 81
-updatebridgesfromauthority
-0|1
-When set (along with UseBridges), Tor will try to fetch bridge descriptors from the configured bridge authorities when feasible. It will fall back to a direct request if the authority responds with a 404. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 82
-usebridges
-0|1
-When set, Tor will fetch descriptors for each bridge listed in the "Bridge" config lines, and use these relays as both entry guards and directory guards. (Default: 0)
---------------------------------------------------------------------------------
-Client
-index: 83
-useentryguards
-0|1
-If this option is set to 1, we pick a few long-term entry servers, and try to stick with them. This is desirable because constantly changing servers increases the odds that an adversary who owns some servers will observe a fraction of your paths. (Defaults to 1.)
---------------------------------------------------------------------------------
-General
-index: 43
-user
-UID
-On startup, setuid to this user and setgid to their primary group.
---------------------------------------------------------------------------------
-Directory
-index: 137
-v1authoritativedirectory
-0|1
-When this option is set in addition to AuthoritativeDirectory, Tor generates version 1 directory and running-routers documents (for legacy Tor clients up to 0.1.0.x).
---------------------------------------------------------------------------------
-Directory
-index: 138
-v2authoritativedirectory
-0|1
-When this option is set in addition to AuthoritativeDirectory, Tor generates version 2 network statuses and serves descriptors, etc as described in doc/spec/dir-spec-v2.txt (for Tor clients and servers running 0.1.1.x and 0.1.2.x).
---------------------------------------------------------------------------------
-Authority
-index: 165
-v3authdistdelay
-N minutes|hours
-V3 authoritative directories only. Configures the server's preferred delay between publishing its consensus and signature and assuming it has all the signatures from all the other authorities. Note that the actual time used is not the server's preferred time, but the consensus of all preferences. (Default: 5 minutes.)
---------------------------------------------------------------------------------
-Authority
-index: 166
-v3authnintervalsvalid
-NUM
-V3 authoritative directories only. Configures the number of VotingIntervals for which each consensus should be valid for. Choosing high numbers increases network partitioning risks; choosing low numbers increases directory traffic. Note that the actual number of intervals used is not the server's preferred number, but the consensus of all preferences. Must be at least 2. (Default: 3.)
---------------------------------------------------------------------------------
-Directory
-index: 139
-v3authoritativedirectory
-0|1
-When this option is set in addition to AuthoritativeDirectory, Tor generates version 3 network statuses and serves descriptors, etc as described in doc/spec/dir-spec.txt (for Tor clients and servers running at least 0.2.0.x).
---------------------------------------------------------------------------------
-Authority
-index: 164
-v3authvotedelay
-N minutes|hours
-V3 authoritative directories only. Configures the server's preferred delay between publishing its vote and assuming it has all the votes from all the other authorities. Note that the actual time used is not the server's preferred time, but the consensus of all preferences. (Default: 5 minutes.)
---------------------------------------------------------------------------------
-Authority
-index: 163
-v3authvotinginterval
-N minutes|hours
-V3 authoritative directories only. Configures the server's preferred voting interval. Note that voting will actually happen at an interval chosen by consensus from all the authorities' preferred intervals. This time SHOULD divide evenly into a day. (Default: 1 hour)
---------------------------------------------------------------------------------
-Directory
-index: 140
-versioningauthoritativedirectory
-0|1
-When this option is set to 1, Tor adds information on which versions of Tor are still believed safe for use to the published directory. Each version 1 authority is automatically a versioning authority; version 2 authorities provide this service optionally. See RecommendedVersions, RecommendedClientVersions, and RecommendedServerVersions.
---------------------------------------------------------------------------------
-Client
-index: 87
-virtualaddrnetwork
-Address/bits
-When a controller asks for a virtual (unused) address with the MAPADDRESS command, Tor picks an unassigned address from this range. (Default: 127.192.0.0/10) 
-
-When providing proxy server service to a network of computers using a tool like dns-proxy-tor, change this address to "10.192.0.0/10" or "172.16.0.0/12". The default VirtualAddrNetwork address range on a properly configured machine will route to the loopback interface. For local use, no change to the default VirtualAddrNetwork setting is needed.
---------------------------------------------------------------------------------
-Client
-index: 102
-warnplaintextports
-port,port,...
-Tells Tor to issue a warnings whenever the user tries to make an anonymous connection to one of these ports. This option is designed to alert users to services that risk sending passwords in the clear. (Default: 23,109,110,143).
diff --git a/arm/starter.py b/arm/starter.py
deleted file mode 100644
index fbc9256..0000000
--- a/arm/starter.py
+++ /dev/null
@@ -1,297 +0,0 @@
-"""
-Command line application for monitoring Tor relays, providing real time status
-information. This starts the application, parsing arguments and getting a Tor
-connection.
-"""
-
-import curses
-import locale
-import logging
-import os
-import platform
-import sys
-import time
-import threading
-
-import arm
-import arm.arguments
-import arm.controller
-import arm.util.panel
-import arm.util.tor_config
-import arm.util.tracker
-import arm.util.ui_tools
-
-import stem
-import stem.util.log
-import stem.util.system
-
-from arm.util import log, BASE_DIR, init_controller, msg, uses_settings
-
-
- at uses_settings
-def main(config):
-  config.set('start_time', str(int(time.time())))
-
-  try:
-    args = arm.arguments.parse(sys.argv[1:])
-    config.set('startup.events', args.logged_events)
-  except ValueError as exc:
-    print exc
-    sys.exit(1)
-
-  if args.print_help:
-    print arm.arguments.get_help()
-    sys.exit()
-  elif args.print_version:
-    print arm.arguments.get_version()
-    sys.exit()
-
-  if args.debug_path is not None:
-    try:
-      _setup_debug_logging(args)
-      print msg('debug.saving_to_path', path = args.debug_path)
-    except IOError as exc:
-      print msg('debug.unable_to_write_file', path = args.debug_path, error = exc.strerror)
-      sys.exit(1)
-
-  _load_user_armrc(args.config)
-
-  control_port = (args.control_address, args.control_port)
-  control_socket = args.control_socket
-
-  # If the user explicitely specified an endpoint then just try to connect to
-  # that.
-
-  if args.user_provided_socket and not args.user_provided_port:
-    control_port = None
-  elif args.user_provided_port and not args.user_provided_socket:
-    control_socket = None
-
-  controller = init_controller(
-    control_port = control_port,
-    control_socket = control_socket,
-    password = config.get('tor.password', None),
-    password_prompt = True,
-    chroot_path = config.get('tor.chroot', ''),
-  )
-
-  if controller is None:
-    exit(1)
-
-  _warn_if_root(controller)
-  _warn_if_unable_to_get_pid(controller)
-  _setup_freebsd_chroot(controller)
-  _notify_of_unknown_events()
-  _clear_password()
-  _load_tor_config_descriptions()
-  _use_english_subcommands()
-  _use_unicode()
-  _set_process_name()
-
-  try:
-    curses.wrapper(arm.controller.start_arm)
-  except UnboundLocalError as exc:
-    if os.environ['TERM'] != 'xterm':
-      print msg('setup.unknown_term', term = os.environ['TERM'])
-    else:
-      raise exc
-  except KeyboardInterrupt:
-    pass  # skip printing a stack trace
-  finally:
-    arm.util.panel.HALT_ACTIVITY = True
-    _shutdown_daemons(controller)
-
-
-def _setup_debug_logging(args):
-  """
-  Configures us to log at stem's trace level to a debug log path. This starts
-  it off with some general diagnostic information.
-  """
-
-  debug_dir = os.path.dirname(args.debug_path)
-
-  if not os.path.exists(debug_dir):
-    os.makedirs(debug_dir)
-
-  debug_handler = logging.FileHandler(args.debug_path, mode = 'w')
-  debug_handler.setLevel(stem.util.log.logging_level(stem.util.log.TRACE))
-  debug_handler.setFormatter(logging.Formatter(
-    fmt = '%(asctime)s [%(levelname)s] %(message)s',
-    datefmt = '%m/%d/%Y %H:%M:%S'
-  ))
-
-  logger = stem.util.log.get_logger()
-  logger.addHandler(debug_handler)
-
-  armrc_content = "[file doesn't exist]"
-
-  if os.path.exists(args.config):
-    try:
-      with open(args.config) as armrc_file:
-        armrc_content = armrc_file.read()
-    except IOError as exc:
-      armrc_content = "[unable to read file: %s]" % exc.strerror
-
-  log.trace(
-    'debug.header',
-    arm_version = arm.__version__,
-    stem_version = stem.__version__,
-    python_version = '.'.join(map(str, sys.version_info[:3])),
-    system = platform.system(),
-    platform = ' '.join(platform.dist()),
-    armrc_path = args.config,
-    armrc_content = armrc_content,
-  )
-
-
- at uses_settings
-def _load_user_armrc(path, config):
-  """
-  Loads user's personal armrc if it's available.
-  """
-
-  if os.path.exists(path):
-    try:
-      config.load(path)
-
-      # If the user provided us with a chroot then validate and normalize the
-      # path.
-
-      chroot = config.get('tor.chroot', '').strip().rstrip(os.path.sep)
-
-      if chroot and not os.path.exists(chroot):
-        log.notice('setup.chroot_doesnt_exist', path = chroot)
-        config.set('tor.chroot', '')
-      else:
-        config.set('tor.chroot', chroot)  # use the normalized path
-    except IOError as exc:
-      log.warn('config.unable_to_read_file', error = exc.strerror)
-  else:
-    log.notice('config.nothing_loaded', path = path)
-
-
-def _warn_if_root(controller):
-  """
-  Give a notice if tor or arm are running with root.
-  """
-
-  tor_user = controller.get_user(None)
-
-  if tor_user == 'root':
-    log.notice('setup.tor_is_running_as_root')
-  elif os.getuid() == 0:
-    tor_user = tor_user if tor_user else '<tor user>'
-    log.notice('setup.arm_is_running_as_root', tor_user = tor_user)
-
-
-def _warn_if_unable_to_get_pid(controller):
-  """
-  Provide a warning if we're unable to determine tor's pid. This in turn will
-  limit our ability to query information about the process later.
-  """
-
-  try:
-    controller.get_pid()
-  except ValueError:
-    log.warn('setup.unable_to_determine_pid')
-
-
- at uses_settings
-def _setup_freebsd_chroot(controller, config):
-  """
-  If we're running under FreeBSD then check the system for a chroot path.
-  """
-
-  if not config.get('tor.chroot', None) and platform.system() == 'FreeBSD':
-    jail_chroot = stem.util.system.bsd_jail_path(controller.get_pid(0))
-
-    if jail_chroot and os.path.exists(jail_chroot):
-      log.info('setup.set_freebsd_chroot', path = jail_chroot)
-      config.set('tor.chroot', jail_chroot)
-
-
-def _notify_of_unknown_events():
-  """
-  Provides a notice about any event types tor supports but arm doesn't.
-  """
-
-  missing_events = arm.arguments.missing_event_types()
-
-  if missing_events:
-    log.info('setup.unknown_event_types', event_types = ', '.join(missing_events))
-
-
- at uses_settings
-def _clear_password(config):
-  """
-  Removing the reference to our controller password so the memory can be freed.
-  Without direct memory access this is about the best we can do to clear it.
-  """
-
-  config.set('tor.password', '')
-
-
-def _load_tor_config_descriptions():
-  """
-  Attempt to determine descriptions for tor's configuration options.
-  """
-
-  arm.util.tor_config.load_configuration_descriptions(BASE_DIR)
-
-
-def _use_english_subcommands():
-  """
-  Make subcommands we run (ps, netstat, etc) provide us with English results.
-  This is important so we can parse the output.
-  """
-
-  os.putenv('LANG', 'C')
-
-
- at uses_settings
-def _use_unicode(config):
-  """
-  If using our LANG variable for rendering multi-byte characters lets us
-  get unicode support then then use it. This needs to be done before
-  initializing curses.
-  """
-
-  if not config.get('features.printUnicode', True):
-    return
-
-  is_lang_unicode = 'utf-' in os.getenv('LANG', '').lower()
-
-  if is_lang_unicode and arm.util.ui_tools.is_wide_characters_supported():
-    locale.setlocale(locale.LC_ALL, '')
-
-
-def _set_process_name():
-  """
-  Attempts to rename our process from "python setup.py <input args>" to
-  "arm <input args>".
-  """
-
-  stem.util.system.set_process_name("arm\0%s" % "\0".join(sys.argv[1:]))
-
-
-def _shutdown_daemons(controller):
-  """
-  Stops and joins on worker threads.
-  """
-
-  close_controller = threading.Thread(target = controller.close)
-  close_controller.setDaemon(True)
-  close_controller.start()
-
-  halt_threads = [
-    arm.controller.stop_controller(),
-    arm.util.tracker.stop_trackers(),
-    close_controller,
-  ]
-
-  for thread in halt_threads:
-    thread.join()
-
-
-if __name__ == '__main__':
-  main()
diff --git a/arm/torrc_panel.py b/arm/torrc_panel.py
deleted file mode 100644
index b7fa71a..0000000
--- a/arm/torrc_panel.py
+++ /dev/null
@@ -1,351 +0,0 @@
-"""
-Panel displaying the torrc or armrc with the validation done against it.
-"""
-
-import math
-import curses
-import threading
-
-import arm.popups
-
-from arm.util import panel, tor_config, tor_controller, ui_tools
-
-from stem.control import State
-from stem.util import conf, enum, str_tools
-
-
-def conf_handler(key, value):
-  if key == "features.config.file.max_lines_per_entry":
-    return max(1, value)
-
-
-CONFIG = conf.config_dict("arm", {
-  "features.config.file.showScrollbars": True,
-  "features.config.file.max_lines_per_entry": 8,
-}, conf_handler)
-
-# TODO: The armrc use case is incomplete. There should be equivilant reloading
-# and validation capabilities to the torrc.
-Config = enum.Enum("TORRC", "ARMRC")  # configuration file types that can be displayed
-
-
-class TorrcPanel(panel.Panel):
-  """
-  Renders the current torrc or armrc with syntax highlighting in a scrollable
-  area.
-  """
-
-  def __init__(self, stdscr, config_type):
-    panel.Panel.__init__(self, stdscr, "torrc", 0)
-
-    self.vals_lock = threading.RLock()
-    self.config_type = config_type
-    self.scroll = 0
-    self.show_line_num = True     # shows left aligned line numbers
-    self.strip_comments = False   # drops comments and extra whitespace
-
-    # height of the content when last rendered (the cached value is invalid if
-    # _last_content_height_args is None or differs from the current dimensions)
-
-    self._last_content_height = 1
-    self._last_content_height_args = None
-
-    # listens for tor reload (sighup) events
-
-    controller = tor_controller()
-    controller.add_status_listener(self.reset_listener)
-
-    if controller.is_alive():
-      self.reset_listener(None, State.INIT, None)
-
-  def reset_listener(self, controller, event_type, _):
-    """
-    Reloads and displays the torrc on tor reload (sighup) events.
-    """
-
-    if event_type == State.INIT:
-      # loads the torrc and provides warnings in case of validation errors
-
-      try:
-        loaded_torrc = tor_config.get_torrc()
-        loaded_torrc.load(True)
-        loaded_torrc.log_validation_issues()
-        self.redraw(True)
-      except:
-        pass
-    elif event_type == State.RESET:
-      try:
-        tor_config.get_torrc().load(True)
-        self.redraw(True)
-      except:
-        pass
-
-  def set_comments_visible(self, is_visible):
-    """
-    Sets if comments and blank lines are shown or stripped.
-
-    Arguments:
-      is_visible - displayed comments and blank lines if true, strips otherwise
-    """
-
-    self.strip_comments = not is_visible
-    self._last_content_height_args = None
-    self.redraw(True)
-
-  def set_line_number_visible(self, is_visible):
-    """
-    Sets if line numbers are shown or hidden.
-
-    Arguments:
-      is_visible - displays line numbers if true, hides otherwise
-    """
-
-    self.show_line_num = is_visible
-    self._last_content_height_args = None
-    self.redraw(True)
-
-  def reload_torrc(self):
-    """
-    Reloads the torrc, displaying an indicator of success or failure.
-    """
-
-    try:
-      tor_config.get_torrc().load()
-      self._last_content_height_args = None
-      self.redraw(True)
-      result_msg = "torrc reloaded"
-    except IOError:
-      result_msg = "failed to reload torrc"
-
-    self._last_content_height_args = None
-    self.redraw(True)
-    arm.popups.show_msg(result_msg, 1)
-
-  def handle_key(self, key):
-    with self.vals_lock:
-      if key.is_scroll():
-        page_height = self.get_preferred_size()[0] - 1
-        new_scroll = ui_tools.get_scroll_position(key, self.scroll, page_height, self._last_content_height)
-
-        if self.scroll != new_scroll:
-          self.scroll = new_scroll
-          self.redraw(True)
-      elif key.match('n'):
-        self.set_line_number_visible(not self.show_line_num)
-      elif key.match('s'):
-        self.set_comments_visible(self.strip_comments)
-      elif key.match('r'):
-        self.reload_torrc()
-      else:
-        return False
-
-      return True
-
-  def set_visible(self, is_visible):
-    if not is_visible:
-      self._last_content_height_args = None  # redraws when next displayed
-
-    panel.Panel.set_visible(self, is_visible)
-
-  def get_help(self):
-    return [
-      ('up arrow', 'scroll up a line', None),
-      ('down arrow', 'scroll down a line', None),
-      ('page up', 'scroll up a page', None),
-      ('page down', 'scroll down a page', None),
-      ('s', 'comment stripping', 'on' if self.strip_comments else 'off'),
-      ('n', 'line numbering', 'on' if self.show_line_num else 'off'),
-      ('r', 'reload torrc', None),
-      ('x', 'reset tor (issue sighup)', None),
-    ]
-
-  def draw(self, width, height):
-    self.vals_lock.acquire()
-
-    # If true, we assume that the cached value in self._last_content_height is
-    # still accurate, and stop drawing when there's nothing more to display.
-    # Otherwise the self._last_content_height is suspect, and we'll process all
-    # the content to check if it's right (and redraw again with the corrected
-    # height if not).
-
-    trust_last_content_height = self._last_content_height_args == (width, height)
-
-    # restricts scroll location to valid bounds
-
-    self.scroll = max(0, min(self.scroll, self._last_content_height - height + 1))
-
-    rendered_contents, corrections, conf_location = None, {}, None
-
-    if self.config_type == Config.TORRC:
-      loaded_torrc = tor_config.get_torrc()
-      loaded_torrc.get_lock().acquire()
-      conf_location = loaded_torrc.get_config_location()
-
-      if not loaded_torrc.is_loaded():
-        rendered_contents = ["### Unable to load the torrc ###"]
-      else:
-        rendered_contents = loaded_torrc.get_display_contents(self.strip_comments)
-
-        # constructs a mapping of line numbers to the issue on it
-
-        corrections = dict((line_number, (issue, msg)) for line_number, issue, msg in loaded_torrc.get_corrections())
-
-      loaded_torrc.get_lock().release()
-    else:
-      loaded_armrc = conf.get_config("arm")
-      conf_location = loaded_armrc._path
-      rendered_contents = list(loaded_armrc._raw_contents)
-
-    # offset to make room for the line numbers
-
-    line_number_offset = 0
-
-    if self.show_line_num:
-      if len(rendered_contents) == 0:
-        line_number_offset = 2
-      else:
-        line_number_offset = int(math.log10(len(rendered_contents))) + 2
-
-    # draws left-hand scroll bar if content's longer than the height
-
-    scroll_offset = 0
-
-    if CONFIG["features.config.file.showScrollbars"] and self._last_content_height > height - 1:
-      scroll_offset = 3
-      self.add_scroll_bar(self.scroll, self.scroll + height - 1, self._last_content_height, 1)
-
-    display_line = -self.scroll + 1  # line we're drawing on
-
-    # draws the top label
-
-    if self.is_title_visible():
-      source_label = "Tor" if self.config_type == Config.TORRC else "Arm"
-      location_label = " (%s)" % conf_location if conf_location else ""
-      self.addstr(0, 0, "%s Configuration File%s:" % (source_label, location_label), curses.A_STANDOUT)
-
-    is_multiline = False  # true if we're in the middle of a multiline torrc entry
-
-    for line_number in range(0, len(rendered_contents)):
-      line_text = rendered_contents[line_number]
-      line_text = line_text.rstrip()  # remove ending whitespace
-
-      # blank lines are hidden when stripping comments
-
-      if self.strip_comments and not line_text:
-        continue
-
-      # splits the line into its component (msg, format) tuples
-
-      line_comp = {
-        'option': ['', (curses.A_BOLD, 'green')],
-        'argument': ['', (curses.A_BOLD, 'cyan')],
-        'correction': ['', (curses.A_BOLD, 'cyan')],
-        'comment': ['', ('white',)],
-      }
-
-      # parses the comment
-
-      comment_index = line_text.find("#")
-
-      if comment_index != -1:
-        line_comp['comment'][0] = line_text[comment_index:]
-        line_text = line_text[:comment_index]
-
-      # splits the option and argument, preserving any whitespace around them
-
-      stripped_line = line_text.strip()
-      option_index = stripped_line.find(" ")
-
-      if is_multiline:
-        # part of a multiline entry started on a previous line so everything
-        # is part of the argument
-        line_comp['argument'][0] = line_text
-      elif option_index == -1:
-        # no argument provided
-        line_comp['option'][0] = line_text
-      else:
-        option_text = stripped_line[:option_index]
-        option_end = line_text.find(option_text) + len(option_text)
-        line_comp['option'][0] = line_text[:option_end]
-        line_comp['argument'][0] = line_text[option_end:]
-
-      # flags following lines as belonging to this multiline entry if it ends
-      # with a slash
-
-      if stripped_line:
-        is_multiline = stripped_line.endswith("\\")
-
-      # gets the correction
-
-      if line_number in corrections:
-        line_issue, line_issue_msg = corrections[line_number]
-
-        if line_issue in (tor_config.ValidationError.DUPLICATE, tor_config.ValidationError.IS_DEFAULT):
-          line_comp['option'][1] = (curses.A_BOLD, 'blue')
-          line_comp['argument'][1] = (curses.A_BOLD, 'blue')
-        elif line_issue == tor_config.ValidationError.MISMATCH:
-          line_comp['argument'][1] = (curses.A_BOLD, 'red')
-          line_comp['correction'][0] = ' (%s)' % line_issue_msg
-        else:
-          # For some types of configs the correction field is simply used to
-          # provide extra data (for instance, the type for tor state fields).
-
-          line_comp['correction'][0] = ' (%s)' % line_issue_msg
-          line_comp['correction'][1] = (curses.A_BOLD, 'magenta')
-
-      # draws the line number
-
-      if self.show_line_num and display_line < height and display_line >= 1:
-        line_number_str = ("%%%ii" % (line_number_offset - 1)) % (line_number + 1)
-        self.addstr(display_line, scroll_offset, line_number_str, curses.A_BOLD, 'yellow')
-
-      # draws the rest of the components with line wrap
-
-      cursor_location, line_offset = line_number_offset + scroll_offset, 0
-      max_lines_per_entry = CONFIG["features.config.file.max_lines_per_entry"]
-      display_queue = [line_comp[entry] for entry in ('option', 'argument', 'correction', 'comment')]
-
-      while display_queue:
-        msg, format = display_queue.pop(0)
-
-        max_msg_size, include_break = width - cursor_location, False
-
-        if len(msg) >= max_msg_size:
-          # message is too long - break it up
-
-          if line_offset == max_lines_per_entry - 1:
-            msg = str_tools.crop(msg, max_msg_size)
-          else:
-            include_break = True
-            msg, remainder = str_tools.crop(msg, max_msg_size, 4, 4, str_tools.Ending.HYPHEN, True)
-            display_queue.insert(0, (remainder.strip(), format))
-
-        draw_line = display_line + line_offset
-
-        if msg and draw_line < height and draw_line >= 1:
-          self.addstr(draw_line, cursor_location, msg, *format)
-
-        # If we're done, and have added content to this line, then start
-        # further content on the next line.
-
-        cursor_location += len(msg)
-        include_break |= not display_queue and cursor_location != line_number_offset + scroll_offset
-
-        if include_break:
-          line_offset += 1
-          cursor_location = line_number_offset + scroll_offset
-
-      display_line += max(line_offset, 1)
-
-      if trust_last_content_height and display_line >= height:
-        break
-
-    if not trust_last_content_height:
-      self._last_content_height_args = (width, height)
-      new_content_height = display_line + self.scroll - 1
-
-      if self._last_content_height != new_content_height:
-        self._last_content_height = new_content_height
-        self.redraw(True)
-
-    self.vals_lock.release()
diff --git a/arm/uninstall b/arm/uninstall
deleted file mode 100755
index af68f3d..0000000
--- a/arm/uninstall
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-files="/usr/bin/arm /usr/share/man/man1/arm.1.gz /usr/share/arm"
-
-for i in $files
-do
-  if [ -f $i -o -d $i ]; then
-    rm -rf $i
-    
-    if [ $? = 0 ]; then
-      echo "removed $i"
-    else
-      exit 1
-    fi
-  fi
-done
-
diff --git a/arm/util/__init__.py b/arm/util/__init__.py
deleted file mode 100644
index 9a62617..0000000
--- a/arm/util/__init__.py
+++ /dev/null
@@ -1,203 +0,0 @@
-"""
-General purpose utilities for a variety of tasks supporting arm features and
-safely working with curses (hiding some of the gory details).
-"""
-
-__all__ = [
-  'log',
-  'panel',
-  'text_input',
-  'tor_config',
-  'tracker',
-  'ui_tools',
-]
-
-import calendar
-import collections
-import os
-import sys
-import time
-
-import stem.connection
-import stem.util.conf
-import stem.util.system
-
-from arm.util import log
-
-TOR_CONTROLLER = None
-BASE_DIR = os.path.sep.join(__file__.split(os.path.sep)[:-2])
-
-StateBandwidth = collections.namedtuple('StateBandwidth', (
-  'read_entries',
-  'write_entries',
-  'last_read_time',
-  'last_write_time',
-))
-
-try:
-  uses_settings = stem.util.conf.uses_settings('arm', os.path.join(BASE_DIR, 'config'), lazy_load = False)
-except IOError as exc:
-  print "Unable to load arm's internal configurations: %s" % exc
-  sys.exit(1)
-
-
-def tor_controller():
-  """
-  Singleton for getting our tor controller connection.
-
-  :returns: :class:`~stem.control.Controller` arm is using
-  """
-
-  return TOR_CONTROLLER
-
-
-def init_controller(*args, **kwargs):
-  """
-  Sets the Controller used by arm. This is a passthrough for Stem's
-  :func:`~stem.connection.connect` function.
-
-  :returns: :class:`~stem.control.Controller` arm is using
-  """
-
-  global TOR_CONTROLLER
-  TOR_CONTROLLER = stem.connection.connect(*args, **kwargs)
-  return TOR_CONTROLLER
-
-
-def join(entries, joiner = ' ', size = None):
-  """
-  Joins a series of strings similar to str.join(), but only up to a given size.
-  This returns an empty string if none of the entries will fit. For example...
-
-    >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 18)
-    'This is a looooong'
-
-    >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 17)
-    'This is a'
-
-    >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 2)
-    ''
-
-  :param list entries: strings to be joined
-  :param str joiner: strings to join the entries with
-  :param int size: maximum length the result can be, there's no length
-    limitation if **None**
-
-  :returns: **str** of the joined entries up to the given length
-  """
-
-  if size is None:
-    return joiner.join(entries)
-
-  result = ''
-
-  for entry in entries:
-    new_result = joiner.join((result, entry)) if result else entry
-
-    if len(new_result) > size:
-      break
-    else:
-      result = new_result
-
-  return result
-
-
- at uses_settings
-def msg(message, config, **attr):
-  """
-  Provides the given message.
-
-  :param str message: message handle to log
-  :param dict attr: attributes to format the message with
-
-  :returns: **str** that was requested
-  """
-
-  try:
-    return config.get('msg.%s' % message).format(**attr)
-  except:
-    log.notice('BUG: We attempted to use an undefined string resource (%s)' % message)
-    return ''
-
-
- at uses_settings
-def bandwidth_from_state(config):
-  """
-  Read Tor's state file to determine its recent bandwidth usage. These
-  samplings are at fifteen minute granularity, and can only provide results if
-  we've been running for at least a day. This provides a named tuple with the
-  following...
-
-    * read_entries and write_entries
-
-      List of the average bytes read or written during each fifteen minute
-      period, oldest to newest.
-
-    * last_read_time and last_write_time
-
-      Unix timestamp for when the last entry was recorded.
-
-  :returns: **namedtuple** with the state file's bandwidth informaiton
-
-  :raises: **ValueError** if unable to get the bandwidth information from our
-    state file
-  """
-
-  controller = tor_controller()
-
-  if not controller.is_localhost():
-    raise ValueError('we can only prepopulate bandwidth information for a local tor instance')
-
-  start_time = stem.util.system.start_time(controller.get_pid(None))
-  uptime = time.time() - start_time if start_time else None
-
-  # Only attempt to prepopulate information if we've been running for a day.
-  # Reason is that the state file stores a day's worth of data, and we don't
-  # want to prepopulate with information from a prior tor instance.
-
-  if not uptime:
-    raise ValueError("unable to determine tor's uptime")
-  elif uptime < (24 * 60 * 60):
-    raise ValueError("insufficient uptime, tor must've been running for at least a day")
-
-  # read the user's state file in their data directory (usually '~/.tor')
-
-  data_dir = controller.get_conf('DataDirectory', None)
-
-  if not data_dir:
-    raise ValueError("unable to determine tor's data directory")
-
-  state_path = os.path.join(config.get('tor.chroot', '') + data_dir, 'state')
-
-  try:
-    with open(state_path) as state_file:
-      state_content = state_file.readlines()
-  except IOError as exc:
-    raise ValueError('unable to read the state file at %s, %s' % (state_path, exc))
-
-  # We're interested in two types of entries from our state file...
-  #
-  # * BWHistory*Values - Comma separated list of bytes we read or wrote
-  #   during each fifteen minute period. The last value is an incremental
-  #   counter for our current period, so ignoring that.
-  #
-  # * BWHistory*Ends - When our last sampling was recorded, in UTC.
-
-  attr = {}
-
-  for line in state_content:
-    line = line.strip()
-
-    if line.startswith('BWHistoryReadValues '):
-      attr['read_entries'] = [int(entry) / 900 for entry in line[20:].split(',')[:-1]]
-    elif line.startswith('BWHistoryWriteValues '):
-      attr['write_entries'] = [int(entry) / 900 for entry in line[21:].split(',')[:-1]]
-    elif line.startswith('BWHistoryReadEnds '):
-      attr['last_read_time'] = calendar.timegm(time.strptime(line[18:], '%Y-%m-%d %H:%M:%S')) - 900
-    elif line.startswith('BWHistoryWriteEnds '):
-      attr['last_write_time'] = calendar.timegm(time.strptime(line[19:], '%Y-%m-%d %H:%M:%S')) - 900
-
-  if len(attr) != 4:
-    raise ValueError('bandwidth stats missing from state file')
-
-  return StateBandwidth(**attr)
diff --git a/arm/util/log.py b/arm/util/log.py
deleted file mode 100644
index 72c1d9d..0000000
--- a/arm/util/log.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""
-Logging utilities, primiarily short aliases for logging a message at various
-runlevels.
-"""
-
-import stem.util.log
-
-import arm.util
-
-
-def trace(msg, **attr):
-  _log(stem.util.log.TRACE, msg, **attr)
-
-
-def debug(msg, **attr):
-  _log(stem.util.log.DEBUG, msg, **attr)
-
-
-def info(msg, **attr):
-  _log(stem.util.log.INFO, msg, **attr)
-
-
-def notice(msg, **attr):
-  _log(stem.util.log.NOTICE, msg, **attr)
-
-
-def warn(msg, **attr):
-  _log(stem.util.log.WARN, msg, **attr)
-
-
-def error(msg, **attr):
-  _log(stem.util.log.ERROR, msg, **attr)
-
-
-def _log(runlevel, message, **attr):
-  """
-  Logs the given message, formatted with optional attributes.
-
-  :param stem.util.log.Runlevel runlevel: runlevel at which to log the message
-  :param str message: message handle to log
-  :param dict attr: attributes to format the message with
-  """
-
-  stem.util.log.log(runlevel, arm.util.msg(message, **attr))
diff --git a/arm/util/panel.py b/arm/util/panel.py
deleted file mode 100644
index 9320270..0000000
--- a/arm/util/panel.py
+++ /dev/null
@@ -1,864 +0,0 @@
-"""
-Wrapper for safely working with curses subwindows.
-"""
-
-import copy
-import time
-import curses
-import curses.ascii
-import curses.textpad
-from threading import RLock
-
-from arm.util import text_input, ui_tools
-
-from stem.util import log
-
-# global ui lock governing all panel instances (curses isn't thread save and
-# concurrency bugs produce especially sinister glitches)
-
-CURSES_LOCK = RLock()
-
-SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END)
-
-SPECIAL_KEYS = {
-  'up': curses.KEY_UP,
-  'down': curses.KEY_DOWN,
-  'left': curses.KEY_LEFT,
-  'right': curses.KEY_RIGHT,
-  'home': curses.KEY_HOME,
-  'end': curses.KEY_END,
-  'page_up': curses.KEY_PPAGE,
-  'page_down': curses.KEY_NPAGE,
-  'esc': 27,
-}
-
-
-# tags used by addfstr - this maps to functor/argument combinations since the
-# actual values (in the case of color attributes) might not yet be initialized
-
-def _no_op(arg):
-  return arg
-
-
-FORMAT_TAGS = {
-  "<b>": (_no_op, curses.A_BOLD),
-  "<u>": (_no_op, curses.A_UNDERLINE),
-  "<h>": (_no_op, curses.A_STANDOUT),
-}
-
-for color_label in ui_tools.COLOR_LIST:
-  FORMAT_TAGS["<%s>" % color_label] = (ui_tools.get_color, color_label)
-
-# prevents curses redraws if set
-HALT_ACTIVITY = False
-
-
-class Panel(object):
-  """
-  Wrapper for curses subwindows. This hides most of the ugliness in common
-  curses operations including:
-    - locking when concurrently drawing to multiple windows
-    - gracefully handle terminal resizing
-    - clip text that falls outside the panel
-    - convenience methods for word wrap, in-line formatting, etc
-
-  This uses a design akin to Swing where panel instances provide their display
-  implementation by overwriting the draw() method, and are redrawn with
-  redraw().
-  """
-
-  def __init__(self, parent, name, top, left = 0, height = -1, width = -1):
-    """
-    Creates a durable wrapper for a curses subwindow in the given parent.
-
-    Arguments:
-      parent - parent curses window
-      name   - identifier for the panel
-      top    - positioning of top within parent
-      left   - positioning of the left edge within the parent
-      height - maximum height of panel (uses all available space if -1)
-      width  - maximum width of panel (uses all available space if -1)
-    """
-
-    # The not-so-pythonic getters for these parameters are because some
-    # implementations aren't entirely deterministic (for instance panels
-    # might chose their height based on its parent's current width).
-
-    self.panel_name = name
-    self.parent = parent
-    self.visible = False
-    self.title_visible = True
-
-    # Attributes for pausing. The pause_attr contains variables our get_attr
-    # method is tracking, and the pause buffer has copies of the values from
-    # when we were last unpaused (unused unless we're paused).
-
-    self.paused = False
-    self.pause_attr = []
-    self.pause_buffer = {}
-    self.pause_time = -1
-
-    self.top = top
-    self.left = left
-    self.height = height
-    self.width = width
-
-    # The panel's subwindow instance. This is made available to implementors
-    # via their draw method and shouldn't be accessed directly.
-    #
-    # This is None if either the subwindow failed to be created or needs to be
-    # remade before it's used. The later could be for a couple reasons:
-    # - The subwindow was never initialized.
-    # - Any of the parameters used for subwindow initialization have changed.
-
-    self.win = None
-
-    self.max_y, self.max_x = -1, -1  # subwindow dimensions when last redrawn
-
-  def get_name(self):
-    """
-    Provides panel's identifier.
-    """
-
-    return self.panel_name
-
-  def is_title_visible(self):
-    """
-    True if the title is configured to be visible, False otherwise.
-    """
-
-    return self.title_visible
-
-  def set_title_visible(self, is_visible):
-    """
-    Configures the panel's title to be visible or not when it's next redrawn.
-    This is not guarenteed to be respected (not all panels have a title).
-    """
-
-    self.title_visible = is_visible
-
-  def get_parent(self):
-    """
-    Provides the parent used to create subwindows.
-    """
-
-    return self.parent
-
-  def is_visible(self):
-    """
-    Provides if the panel's configured to be visible or not.
-    """
-
-    return self.visible
-
-  def set_visible(self, is_visible):
-    """
-    Toggles if the panel is visible or not.
-
-    Arguments:
-      is_visible - panel is redrawn when requested if true, skipped otherwise
-    """
-
-    self.visible = is_visible
-
-  def is_paused(self):
-    """
-    Provides if the panel's configured to be paused or not.
-    """
-
-    return self.paused
-
-  def set_pause_attr(self, attr):
-    """
-    Configures the panel to track the given attribute so that get_attr provides
-    the value when it was last unpaused (or its current value if we're
-    currently unpaused). For instance...
-
-    > self.set_pause_attr("myVar")
-    > self.myVar = 5
-    > self.myVar = 6  # self.get_attr("myVar") -> 6
-    > self.set_paused(True)
-    > self.myVar = 7  # self.get_attr("myVar") -> 6
-    > self.set_paused(False)
-    > self.myVar = 7  # self.get_attr("myVar") -> 7
-
-    Arguments:
-      attr - parameter to be tracked for get_attr
-    """
-
-    self.pause_attr.append(attr)
-    self.pause_buffer[attr] = self.copy_attr(attr)
-
-  def get_attr(self, attr):
-    """
-    Provides the value of the given attribute when we were last unpaused. If
-    we're currently unpaused then this is the current value. If untracked this
-    returns None.
-
-    Arguments:
-      attr - local variable to be returned
-    """
-
-    if attr not in self.pause_attr:
-      return None
-    elif self.paused:
-      return self.pause_buffer[attr]
-    else:
-      return self.__dict__.get(attr)
-
-  def copy_attr(self, attr):
-    """
-    Provides a duplicate of the given configuration value, suitable for the
-    pause buffer.
-
-    Arguments:
-      attr - parameter to be provided back
-    """
-
-    current_value = self.__dict__.get(attr)
-    return copy.copy(current_value)
-
-  def set_paused(self, is_pause, suppress_redraw = False):
-    """
-    Toggles if the panel is paused or not. This causes the panel to be redrawn
-    when toggling is pause state unless told to do otherwise. This is
-    important when pausing since otherwise the panel's display could change
-    when redrawn for other reasons.
-
-    This returns True if the panel's pause state was changed, False otherwise.
-
-    Arguments:
-      is_pause        - freezes the state of the pause attributes if true, makes
-                        them editable otherwise
-      suppress_redraw - if true then this will never redraw the panel
-    """
-
-    if is_pause != self.paused:
-      if is_pause:
-        self.pause_time = time.time()
-
-      self.paused = is_pause
-
-      if is_pause:
-        # copies tracked attributes so we know what they were before pausing
-
-        for attr in self.pause_attr:
-          self.pause_buffer[attr] = self.copy_attr(attr)
-
-      if not suppress_redraw:
-        self.redraw(True)
-
-      return True
-    else:
-      return False
-
-  def get_pause_time(self):
-    """
-    Provides the time that we were last paused, returning -1 if we've never
-    been paused.
-    """
-
-    return self.pause_time
-
-  def get_top(self):
-    """
-    Provides the position subwindows are placed at within its parent.
-    """
-
-    return self.top
-
-  def set_top(self, top):
-    """
-    Changes the position where subwindows are placed within its parent.
-
-    Arguments:
-      top - positioning of top within parent
-    """
-
-    if self.top != top:
-      self.top = top
-      self.win = None
-
-  def get_left(self):
-    """
-    Provides the left position where this subwindow is placed within its
-    parent.
-    """
-
-    return self.left
-
-  def set_left(self, left):
-    """
-    Changes the left position where this subwindow is placed within its parent.
-
-    Arguments:
-      left - positioning of top within parent
-    """
-
-    if self.left != left:
-      self.left = left
-      self.win = None
-
-  def get_height(self):
-    """
-    Provides the height used for subwindows (-1 if it isn't limited).
-    """
-
-    return self.height
-
-  def set_height(self, height):
-    """
-    Changes the height used for subwindows. This uses all available space if -1.
-
-    Arguments:
-      height - maximum height of panel (uses all available space if -1)
-    """
-
-    if self.height != height:
-      self.height = height
-      self.win = None
-
-  def get_width(self):
-    """
-    Provides the width used for subwindows (-1 if it isn't limited).
-    """
-
-    return self.width
-
-  def set_width(self, width):
-    """
-    Changes the width used for subwindows. This uses all available space if -1.
-
-    Arguments:
-      width - maximum width of panel (uses all available space if -1)
-    """
-
-    if self.width != width:
-      self.width = width
-      self.win = None
-
-  def get_preferred_size(self):
-    """
-    Provides the dimensions the subwindow would use when next redrawn, given
-    that none of the properties of the panel or parent change before then. This
-    returns a tuple of (height, width).
-    """
-
-    new_height, new_width = self.parent.getmaxyx()
-    set_height, set_width = self.get_height(), self.get_width()
-    new_height = max(0, new_height - self.top)
-    new_width = max(0, new_width - self.left)
-
-    if set_height != -1:
-      new_height = min(new_height, set_height)
-
-    if set_width != -1:
-      new_width = min(new_width, set_width)
-
-    return (new_height, new_width)
-
-  def handle_key(self, key):
-    """
-    Handler for user input. This returns true if the key press was consumed,
-    false otherwise.
-
-    Arguments:
-      key - keycode for the key pressed
-    """
-
-    return False
-
-  def get_help(self):
-    """
-    Provides help information for the controls this page provides. This is a
-    list of tuples of the form...
-    (control, description, status)
-    """
-
-    return []
-
-  def draw(self, width, height):
-    """
-    Draws display's content. This is meant to be overwritten by
-    implementations and not called directly (use redraw() instead). The
-    dimensions provided are the drawable dimensions, which in terms of width is
-    a column less than the actual space.
-
-    Arguments:
-      width  - horizontal space available for content
-      height - vertical space available for content
-    """
-
-    pass
-
-  def redraw(self, force_redraw=False, block=False):
-    """
-    Clears display and redraws its content. This can skip redrawing content if
-    able (ie, the subwindow's unchanged), instead just refreshing the display.
-
-    Arguments:
-      force_redraw - forces the content to be cleared and redrawn if true
-      block       - if drawing concurrently with other panels this determines
-                    if the request is willing to wait its turn or should be
-                    abandoned
-    """
-
-    # skipped if not currently visible or activity has been halted
-
-    if not self.is_visible() or HALT_ACTIVITY:
-      return
-
-    # if the panel's completely outside its parent then this is a no-op
-
-    new_height, new_width = self.get_preferred_size()
-
-    if new_height == 0 or new_width == 0:
-      self.win = None
-      return
-
-    # recreates the subwindow if necessary
-
-    is_new_window = self._reset_subwindow()
-
-    # The reset argument is disregarded in a couple of situations:
-    # - The subwindow's been recreated (obviously it then doesn't have the old
-    #   content to refresh).
-    # - The subwindow's dimensions have changed since last drawn (this will
-    #   likely change the content's layout)
-
-    subwin_max_y, subwin_max_x = self.win.getmaxyx()
-
-    if is_new_window or subwin_max_y != self.max_y or subwin_max_x != self.max_x:
-      force_redraw = True
-
-    self.max_y, self.max_x = subwin_max_y, subwin_max_x
-
-    if not CURSES_LOCK.acquire(block):
-      return
-
-    try:
-      if force_redraw:
-        self.win.erase()  # clears any old contents
-        self.draw(self.max_x, self.max_y)
-      self.win.refresh()
-    finally:
-      CURSES_LOCK.release()
-
-  def hline(self, y, x, length, attr=curses.A_NORMAL):
-    """
-    Draws a horizontal line. This should only be called from the context of a
-    panel's draw method.
-
-    Arguments:
-      y      - vertical location
-      x      - horizontal location
-      length - length the line spans
-      attr   - text attributes
-    """
-
-    if self.win and self.max_x > x and self.max_y > y:
-      try:
-        draw_length = min(length, self.max_x - x)
-        self.win.hline(y, x, curses.ACS_HLINE | attr, draw_length)
-      except:
-        # in edge cases drawing could cause a _curses.error
-        pass
-
-  def vline(self, y, x, length, attr=curses.A_NORMAL):
-    """
-    Draws a vertical line. This should only be called from the context of a
-    panel's draw method.
-
-    Arguments:
-      y      - vertical location
-      x      - horizontal location
-      length - length the line spans
-      attr   - text attributes
-    """
-
-    if self.win and self.max_x > x and self.max_y > y:
-      try:
-        draw_length = min(length, self.max_y - y)
-        self.win.vline(y, x, curses.ACS_VLINE | attr, draw_length)
-      except:
-        # in edge cases drawing could cause a _curses.error
-        pass
-
-  def addch(self, y, x, char, attr=curses.A_NORMAL):
-    """
-    Draws a single character. This should only be called from the context of a
-    panel's draw method.
-
-    Arguments:
-      y    - vertical location
-      x    - horizontal location
-      char - character to be drawn
-      attr - text attributes
-    """
-
-    if self.win and self.max_x > x and self.max_y > y:
-      try:
-        self.win.addch(y, x, char, attr)
-      except:
-        # in edge cases drawing could cause a _curses.error
-        pass
-
-  def addstr(self, y, x, msg, *attributes):
-    """
-    Writes string to subwindow if able. This takes into account screen bounds
-    to avoid making curses upset. This should only be called from the context
-    of a panel's draw method.
-
-    Arguments:
-      y    - vertical location
-      x    - horizontal location
-      msg  - text to be added
-      attr - text attributes
-    """
-
-    format_attr = curses.A_NORMAL
-
-    for attr in attributes:
-      if isinstance(attr, str):
-        format_attr |= ui_tools.get_color(attr)
-      else:
-        format_attr |= attr
-
-    # subwindows need a single character buffer (either in the x or y
-    # direction) from actual content to prevent crash when shrank
-
-    if self.win and self.max_x > x and self.max_y > y:
-      try:
-        drawn_msg = msg[:self.max_x - x]
-        self.win.addstr(y, x, drawn_msg, format_attr)
-        return x + len(drawn_msg)
-      except:
-        # this might produce a _curses.error during edge cases, for instance
-        # when resizing with visible popups
-
-        pass
-
-    return x
-
-  def addfstr(self, y, x, msg):
-    """
-    Writes string to subwindow. The message can contain xhtml-style tags for
-    formatting, including:
-    <b>text</b>               bold
-    <u>text</u>               underline
-    <h>text</h>               highlight
-    <[color]>text</[color]>   use color (see ui_tools.get_color() for constants)
-
-    Tag nesting is supported and tag closing is strictly enforced (raising an
-    exception for invalid formatting). Unrecognized tags are treated as normal
-    text. This should only be called from the context of a panel's draw method.
-
-    Text in multiple color tags (for instance "<blue><red>hello</red></blue>")
-    uses the bitwise OR of those flags (hint: that's probably not what you
-    want).
-
-    Arguments:
-      y    - vertical location
-      x    - horizontal location
-      msg  - formatted text to be added
-    """
-
-    if self.win and self.max_y > y:
-      formatting = [curses.A_NORMAL]
-      expected_close_tags = []
-      unused_msg = msg
-
-      while self.max_x > x and len(unused_msg) > 0:
-        # finds next consumeable tag (left as None if there aren't any left)
-
-        next_tag, tag_start, tag_end = None, -1, -1
-
-        tmp_checked = 0  # portion of the message cleared for having any valid tags
-        expected_tags = FORMAT_TAGS.keys() + expected_close_tags
-
-        while next_tag is None:
-          tag_start = unused_msg.find("<", tmp_checked)
-          tag_end = unused_msg.find(">", tag_start) + 1 if tag_start != -1 else -1
-
-          if tag_start == -1 or tag_end == -1:
-            break  # no more tags to consume
-          else:
-            # check if the tag we've found matches anything being expected
-            if unused_msg[tag_start:tag_end] in expected_tags:
-              next_tag = unused_msg[tag_start:tag_end]
-              break  # found a tag to use
-            else:
-              # not a valid tag - narrow search to everything after it
-              tmp_checked = tag_end
-
-        # splits into text before and after tag
-
-        if next_tag:
-          msg_segment = unused_msg[:tag_start]
-          unused_msg = unused_msg[tag_end:]
-        else:
-          msg_segment = unused_msg
-          unused_msg = ""
-
-        # adds text before tag with current formatting
-
-        attr = 0
-
-        for text_format in formatting:
-          attr |= text_format
-
-        self.win.addstr(y, x, msg_segment[:self.max_x - x - 1], attr)
-        x += len(msg_segment)
-
-        # applies tag attributes for future text
-
-        if next_tag:
-          format_tag = "<" + next_tag[2:] if next_tag.startswith("</") else next_tag
-          format_match = FORMAT_TAGS[format_tag][0](FORMAT_TAGS[format_tag][1])
-
-          if not next_tag.startswith("</"):
-            # open tag - add formatting
-            expected_close_tags.append("</" + next_tag[1:])
-            formatting.append(format_match)
-          else:
-            # close tag - remove formatting
-            expected_close_tags.remove(next_tag)
-            formatting.remove(format_match)
-
-      # only check for unclosed tags if we processed the whole message (if we
-      # stopped processing prematurely it might still be valid)
-
-      if expected_close_tags and not unused_msg:
-        # if we're done then raise an exception for any unclosed tags (tisk, tisk)
-        base_msg = "Unclosed formatting tag%s:" % ("s" if len(expected_close_tags) > 1 else "")
-        raise ValueError("%s: '%s'\n  \"%s\"" % (base_msg, "', '".join(expected_close_tags), msg))
-
-  def getstr(self, y, x, initial_text = "", text_format = None, max_width = None, validator = None):
-    """
-    Provides a text field where the user can input a string, blocking until
-    they've done so and returning the result. If the user presses escape then
-    this terminates and provides back None. This should only be called from
-    the context of a panel's draw method.
-
-    This blanks any content within the space that the input field is rendered
-    (otherwise stray characters would be interpreted as part of the initial
-    input).
-
-    Arguments:
-      y            - vertical location
-      x            - horizontal location
-      initial_text - starting text in this field
-      text_format  - format used for the text
-      max_width    - maximum width for the text field
-      validator    - custom TextInputValidator for handling keybindings
-    """
-
-    if not text_format:
-      text_format = curses.A_NORMAL
-
-    # makes cursor visible
-
-    try:
-      previous_cursor_state = curses.curs_set(1)
-    except curses.error:
-      previous_cursor_state = 0
-
-    # temporary subwindow for user input
-
-    display_width = self.get_preferred_size()[1]
-
-    if max_width:
-      display_width = min(display_width, max_width + x)
-
-    input_subwindow = self.parent.subwin(1, display_width - x, self.top + y, self.left + x)
-
-    # blanks the field's area, filling it with the font in case it's hilighting
-
-    input_subwindow.clear()
-    input_subwindow.bkgd(' ', text_format)
-
-    # prepopulates the initial text
-
-    if initial_text:
-      input_subwindow.addstr(0, 0, initial_text[:display_width - x - 1], text_format)
-
-    # Displays the text field, blocking until the user's done. This closes the
-    # text panel and returns user_input to the initial text if the user presses
-    # escape.
-
-    textbox = curses.textpad.Textbox(input_subwindow)
-
-    if not validator:
-      validator = text_input.BasicValidator()
-
-    textbox.win.attron(text_format)
-    user_input = textbox.edit(lambda key: validator.validate(key, textbox)).strip()
-    textbox.win.attroff(text_format)
-
-    if textbox.lastcmd == curses.ascii.BEL:
-      user_input = None
-
-    # reverts visability settings
-
-    try:
-      curses.curs_set(previous_cursor_state)
-    except curses.error:
-      pass
-
-    return user_input
-
-  def add_scroll_bar(self, top, bottom, size, draw_top = 0, draw_bottom = -1, draw_left = 0):
-    """
-    Draws a left justified scroll bar reflecting position within a vertical
-    listing. This is shorted if necessary, and left undrawn if no space is
-    available. The bottom is squared off, having a layout like:
-     |
-    *|
-    *|
-    *|
-     |
-    -+
-
-    This should only be called from the context of a panel's draw method.
-
-    Arguments:
-      top        - list index for the top-most visible element
-      bottom     - list index for the bottom-most visible element
-      size       - size of the list in which the listed elements are contained
-      draw_top    - starting row where the scroll bar should be drawn
-      draw_bottom - ending row where the scroll bar should end, -1 if it should
-                   span to the bottom of the panel
-      draw_left   - left offset at which to draw the scroll bar
-    """
-
-    if (self.max_y - draw_top) < 2:
-      return  # not enough room
-
-    # sets draw_bottom to be the actual row on which the scrollbar should end
-
-    if draw_bottom == -1:
-      draw_bottom = self.max_y - 1
-    else:
-      draw_bottom = min(draw_bottom, self.max_y - 1)
-
-    # determines scrollbar dimensions
-
-    scrollbar_height = draw_bottom - draw_top
-    slider_top = scrollbar_height * top / size
-    slider_size = scrollbar_height * (bottom - top) / size
-
-    # ensures slider isn't at top or bottom unless really at those extreme bounds
-
-    if top > 0:
-      slider_top = max(slider_top, 1)
-
-    if bottom != size:
-      slider_top = min(slider_top, scrollbar_height - slider_size - 2)
-
-    # avoids a rounding error that causes the scrollbar to be too low when at
-    # the bottom
-
-    if bottom == size:
-      slider_top = scrollbar_height - slider_size - 1
-
-    # draws scrollbar slider
-
-    for i in range(scrollbar_height):
-      if i >= slider_top and i <= slider_top + slider_size:
-        self.addstr(i + draw_top, draw_left, " ", curses.A_STANDOUT)
-      else:
-        self.addstr(i + draw_top, draw_left, " ")
-
-    # draws box around the scroll bar
-
-    self.vline(draw_top, draw_left + 1, draw_bottom - 1)
-    self.addch(draw_bottom, draw_left + 1, curses.ACS_LRCORNER)
-    self.addch(draw_bottom, draw_left, curses.ACS_HLINE)
-
-  def _reset_subwindow(self):
-    """
-    Create a new subwindow instance for the panel if:
-    - Panel currently doesn't have a subwindow (was uninitialized or
-      invalidated).
-    - There's room for the panel to grow vertically (curses automatically
-      lets subwindows regrow horizontally, but not vertically).
-    - The subwindow has been displaced. This is a curses display bug that
-      manifests if the terminal's shrank then re-expanded. Displaced
-      subwindows are never restored to their proper position, resulting in
-      graphical glitches if we draw to them.
-    - The preferred size is smaller than the actual size (should shrink).
-
-    This returns True if a new subwindow instance was created, False otherwise.
-    """
-
-    new_height, new_width = self.get_preferred_size()
-
-    if new_height == 0:
-      return False  # subwindow would be outside its parent
-
-    # determines if a new subwindow should be recreated
-
-    recreate = self.win is None
-
-    if self.win:
-      subwin_max_y, subwin_max_x = self.win.getmaxyx()
-      recreate |= subwin_max_y < new_height           # check for vertical growth
-      recreate |= self.top > self.win.getparyx()[0]   # check for displacement
-      recreate |= subwin_max_x > new_width or subwin_max_y > new_height  # shrinking
-
-    # I'm not sure if recreating subwindows is some sort of memory leak but the
-    # Python curses bindings seem to lack all of the following:
-    # - subwindow deletion (to tell curses to free the memory)
-    # - subwindow moving/resizing (to restore the displaced windows)
-    # so this is the only option (besides removing subwindows entirely which
-    # would mean far more complicated code and no more selective refreshing)
-
-    if recreate:
-      self.win = self.parent.subwin(new_height, new_width, self.top, self.left)
-
-      # note: doing this log before setting win produces an infinite loop
-      log.debug("recreating panel '%s' with the dimensions of %i/%i" % (self.get_name(), new_height, new_width))
-
-    return recreate
-
-
-class KeyInput(object):
-  """
-  Keyboard input by the user.
-  """
-
-  def __init__(self, key):
-    self._key = key  # pressed key as an integer
-
-  def match(self, *keys):
-    """
-    Checks if we have a case insensitive match with the given key. Beside
-    characters, this also recognizes: up, down, left, right, home, end,
-    page_up, page_down, and esc.
-    """
-
-    for key in keys:
-      if key in SPECIAL_KEYS:
-        if self._key == SPECIAL_KEYS[key]:
-          return True
-      elif len(key) == 1:
-        if self._key in (ord(key.lower()), ord(key.upper())):
-          return True
-      else:
-        raise ValueError("%s wasn't among our recognized key codes" % key)
-
-    return False
-
-  def is_scroll(self):
-    """
-    True if the key is used for scrolling, false otherwise.
-    """
-
-    return self._key in SCROLL_KEYS
-
-  def is_selection(self):
-    """
-    True if the key matches the enter or space keys.
-    """
-
-    return self._key in (curses.KEY_ENTER, 10, ord(' '))
diff --git a/arm/util/text_input.py b/arm/util/text_input.py
deleted file mode 100644
index 4faec1e..0000000
--- a/arm/util/text_input.py
+++ /dev/null
@@ -1,213 +0,0 @@
-"""
-Provides input validators that provide text input with various capabilities.
-These can be chained together with the first matching validator taking
-precidence.
-"""
-
-import os
-import curses
-
-PASS = -1
-
-
-class TextInputValidator:
-  """
-  Basic interface for validators. Implementations should override the handle_key
-  method.
-  """
-
-  def __init__(self, next_validator = None):
-    self.next_validator = next_validator
-
-  def validate(self, key, textbox):
-    """
-    Processes the given key input for the textbox. This may modify the
-    textbox's content, cursor position, etc depending on the functionality
-    of the validator. This returns the key that the textbox should interpret,
-    PASS if this validator doesn't want to take any action.
-
-    Arguments:
-      key     - key code input from the user
-      textbox - curses Textbox instance the input came from
-    """
-
-    result = self.handle_key(key, textbox)
-
-    if result != PASS:
-      return result
-    elif self.next_validator:
-      return self.next_validator.validate(key, textbox)
-    else:
-      return key
-
-  def handle_key(self, key, textbox):
-    """
-    Process the given keycode with this validator, returning the keycode for
-    the textbox to process, and PASS if this doesn't want to modify it.
-
-    Arguments:
-      key     - key code input from the user
-      textbox - curses Textbox instance the input came from
-    """
-
-    return PASS
-
-
-class BasicValidator(TextInputValidator):
-  """
-  Interceptor for keystrokes given to a textbox, doing the following:
-  - quits by setting the input to curses.ascii.BEL when escape is pressed
-  - stops the cursor at the end of the box's content when pressing the right
-    arrow
-  - home and end keys move to the start/end of the line
-  """
-
-  def handle_key(self, key, textbox):
-    y, x = textbox.win.getyx()
-
-    if curses.ascii.isprint(key) and x < textbox.maxx:
-      # Shifts the existing text forward so input is an insert method rather
-      # than replacement. The curses.textpad accepts an insert mode flag but
-      # this has a couple issues...
-      # - The flag is only available for Python 2.6+, before that the
-      #   constructor only accepted a subwindow argument as per:
-      #   https://trac.torproject.org/projects/tor/ticket/2354
-      # - The textpad doesn't shift text that has text attributes. This is
-      #   because keycodes read by textbox.win.inch() includes formatting,
-      #   causing the curses.ascii.isprint() check it does to fail.
-
-      current_input = textbox.gather()
-      textbox.win.addstr(y, x + 1, current_input[x:textbox.maxx - 1])
-      textbox.win.move(y, x)  # reverts cursor movement during gather call
-    elif key == 27:
-      # curses.ascii.BEL is a character codes that causes textpad to terminate
-
-      return curses.ascii.BEL
-    elif key == curses.KEY_HOME:
-      textbox.win.move(y, 0)
-      return None
-    elif key in (curses.KEY_END, curses.KEY_RIGHT):
-      msg_length = len(textbox.gather())
-      textbox.win.move(y, x)  # reverts cursor movement during gather call
-
-      if key == curses.KEY_END and msg_length > 0 and x < msg_length - 1:
-        # if we're in the content then move to the end
-
-        textbox.win.move(y, msg_length - 1)
-        return None
-      elif key == curses.KEY_RIGHT and x >= msg_length - 1:
-        # don't move the cursor if there's no content after it
-
-        return None
-    elif key == 410:
-      # if we're resizing the display during text entry then cancel it
-      # (otherwise the input field is filled with nonprintable characters)
-
-      return curses.ascii.BEL
-
-    return PASS
-
-
-class HistoryValidator(TextInputValidator):
-  """
-  This intercepts the up and down arrow keys to scroll through a backlog of
-  previous commands.
-  """
-
-  def __init__(self, command_backlog = [], next_validator = None):
-    TextInputValidator.__init__(self, next_validator)
-
-    # contents that can be scrolled back through, newest to oldest
-
-    self.command_backlog = command_backlog
-
-    # selected item from the backlog, -1 if we're not on a backlog item
-
-    self.selection_index = -1
-
-    # the fields input prior to selecting a backlog item
-
-    self.custom_input = ""
-
-  def handle_key(self, key, textbox):
-    if key in (curses.KEY_UP, curses.KEY_DOWN):
-      offset = 1 if key == curses.KEY_UP else -1
-      new_selection = self.selection_index + offset
-
-      # constrains the new selection to valid bounds
-
-      new_selection = max(-1, new_selection)
-      new_selection = min(len(self.command_backlog) - 1, new_selection)
-
-      # skips if this is a no-op
-
-      if self.selection_index == new_selection:
-        return None
-
-      # saves the previous input if we weren't on the backlog
-
-      if self.selection_index == -1:
-        self.custom_input = textbox.gather().strip()
-
-      if new_selection == -1:
-        new_input = self.custom_input
-      else:
-        new_input = self.command_backlog[new_selection]
-
-      y, _ = textbox.win.getyx()
-      _, max_x = textbox.win.getmaxyx()
-      textbox.win.clear()
-      textbox.win.addstr(y, 0, new_input[:max_x - 1])
-      textbox.win.move(y, min(len(new_input), max_x - 1))
-
-      self.selection_index = new_selection
-      return None
-
-    return PASS
-
-
-class TabCompleter(TextInputValidator):
-  """
-  Provides tab completion based on the current input, finishing if there's only
-  a single match. This expects a functor that accepts the current input and
-  provides matches.
-  """
-
-  def __init__(self, completer, next_validator = None):
-    TextInputValidator.__init__(self, next_validator)
-
-    # functor that accepts a string and gives a list of matches
-
-    self.completer = completer
-
-  def handle_key(self, key, textbox):
-    # Matches against the tab key. The ord('\t') is nine, though strangely none
-    # of the curses.KEY_*TAB constants match this...
-
-    if key == 9:
-      current_contents = textbox.gather().strip()
-      matches = self.completer(current_contents)
-      new_input = None
-
-      if len(matches) == 1:
-        # only a single match, fill it in
-        new_input = matches[0]
-      elif len(matches) > 1:
-        # looks for a common prefix we can complete
-        common_prefix = os.path.commonprefix(matches)  # weird that this comes from path...
-
-        if common_prefix != current_contents:
-          new_input = common_prefix
-
-        # TODO: somehow display matches... this is not gonna be fun
-
-      if new_input:
-        y, _ = textbox.win.getyx()
-        _, max_x = textbox.win.getmaxyx()
-        textbox.win.clear()
-        textbox.win.addstr(y, 0, new_input[:max_x - 1])
-        textbox.win.move(y, min(len(new_input), max_x - 1))
-
-      return None
-
-    return PASS
diff --git a/arm/util/tor_config.py b/arm/util/tor_config.py
deleted file mode 100644
index bec1466..0000000
--- a/arm/util/tor_config.py
+++ /dev/null
@@ -1,1116 +0,0 @@
-"""
-Helper functions for working with tor's configuration file.
-"""
-
-import os
-import time
-import socket
-import threading
-
-import stem.version
-
-from arm.util import tor_controller, ui_tools
-
-from stem.util import conf, enum, log, str_tools, system
-
-# filename used for cached tor config descriptions
-
-CONFIG_DESC_FILENAME = "torConfigDesc.txt"
-
-# messages related to loading the tor configuration descriptions
-
-DESC_LOAD_SUCCESS_MSG = "Loaded configuration descriptions from '%s' (runtime: %0.3f)"
-DESC_LOAD_FAILED_MSG = "Unable to load configuration descriptions (%s)"
-DESC_INTERNAL_LOAD_SUCCESS_MSG = "Falling back to descriptions for Tor %s"
-DESC_INTERNAL_LOAD_FAILED_MSG = "Unable to load fallback descriptions. Categories and help for Tor's configuration options won't be available. (%s)"
-DESC_READ_MAN_SUCCESS_MSG = "Read descriptions for tor's configuration options from its man page (runtime %0.3f)"
-DESC_READ_MAN_FAILED_MSG = "Unable to get the descriptions of Tor's configuration options from its man page (%s)"
-DESC_SAVE_SUCCESS_MSG = "Saved configuration descriptions to '%s' (runtime: %0.3f)"
-DESC_SAVE_FAILED_MSG = "Unable to save configuration descriptions (%s)"
-
-
-def conf_handler(key, value):
-  if key == "torrc.important":
-    # stores lowercase entries to drop case sensitivity
-    return [entry.lower() for entry in value]
-
-
-CONFIG = conf.config_dict("arm", {
-  "features.torrc.validate": True,
-  "torrc.important": [],
-  "torrc.alias": {},
-  "torrc.units.size.b": [],
-  "torrc.units.size.kb": [],
-  "torrc.units.size.mb": [],
-  "torrc.units.size.gb": [],
-  "torrc.units.size.tb": [],
-  "torrc.units.time.sec": [],
-  "torrc.units.time.min": [],
-  "torrc.units.time.hour": [],
-  "torrc.units.time.day": [],
-  "torrc.units.time.week": [],
-  "startup.data_directory": "~/.arm",
-  "features.config.descriptions.enabled": True,
-  "features.config.descriptions.persist": True,
-  "tor.chroot": '',
-}, conf_handler)
-
-
-def general_conf_handler(config, key):
-  value = config.get(key)
-
-  if key.startswith("torrc.summary."):
-    # we'll look for summary keys with a lowercase config name
-    CONFIG[key.lower()] = value
-  elif key.startswith("torrc.units.") and value:
-    # all the torrc.units.* values are comma separated lists
-    return [entry.strip() for entry in value[0].split(",")]
-
-
-conf.get_config("arm").add_listener(general_conf_handler, backfill = True)
-
-# enums and values for numeric torrc entries
-
-ValueType = enum.Enum("UNRECOGNIZED", "SIZE", "TIME")
-SIZE_MULT = {"b": 1, "kb": 1024, "mb": 1048576, "gb": 1073741824, "tb": 1099511627776}
-TIME_MULT = {"sec": 1, "min": 60, "hour": 3600, "day": 86400, "week": 604800}
-
-# enums for issues found during torrc validation:
-# DUPLICATE  - entry is ignored due to being a duplicate
-# MISMATCH   - the value doesn't match tor's current state
-# MISSING    - value differs from its default but is missing from the torrc
-# IS_DEFAULT - the configuration option matches tor's default
-
-ValidationError = enum.Enum("DUPLICATE", "MISMATCH", "MISSING", "IS_DEFAULT")
-
-# descriptions of tor's configuration options fetched from its man page
-
-CONFIG_DESCRIPTIONS_LOCK = threading.RLock()
-CONFIG_DESCRIPTIONS = {}
-
-# categories for tor configuration options
-
-Category = enum.Enum("GENERAL", "CLIENT", "RELAY", "DIRECTORY", "AUTHORITY", "HIDDEN_SERVICE", "TESTING", "UNKNOWN")
-
-TORRC = None  # singleton torrc instance
-MAN_OPT_INDENT = 7  # indentation before options in the man page
-MAN_EX_INDENT = 15  # indentation used for man page examples
-PERSIST_ENTRY_DIVIDER = "-" * 80 + "\n"  # splits config entries when saving to a file
-MULTILINE_PARAM = None  # cached multiline parameters (lazily loaded)
-
-# torrc options that bind to ports
-
-PORT_OPT = ("SocksPort", "ORPort", "DirPort", "ControlPort", "TransPort")
-
-
-class ManPageEntry:
-  """
-  Information provided about a tor configuration option in its man page entry.
-  """
-
-  def __init__(self, option, index, category, arg_usage, description):
-    self.option = option
-    self.index = index
-    self.category = category
-    self.arg_usage = arg_usage
-    self.description = description
-
-
-def get_torrc():
-  """
-  Singleton constructor for a Controller. Be aware that this starts as being
-  unloaded, needing the torrc contents to be loaded before being functional.
-  """
-
-  global TORRC
-
-  if TORRC is None:
-    TORRC = Torrc()
-
-  return TORRC
-
-
-def load_option_descriptions(load_path = None, check_version = True):
-  """
-  Fetches and parses descriptions for tor's configuration options from its man
-  page. This can be a somewhat lengthy call, and raises an IOError if issues
-  occure. When successful loading from a file this returns the version for the
-  contents loaded.
-
-  If available, this can load the configuration descriptions from a file where
-  they were previously persisted to cut down on the load time (latency for this
-  is around 200ms).
-
-  Arguments:
-    load_path     - if set, this attempts to fetch the configuration
-                   descriptions from the given path instead of the man page
-    check_version - discards the results if true and tor's version doens't
-                   match the cached descriptors, otherwise accepts anyway
-  """
-
-  CONFIG_DESCRIPTIONS_LOCK.acquire()
-  CONFIG_DESCRIPTIONS.clear()
-
-  raised_exc = None
-  loaded_version = ""
-
-  try:
-    if load_path:
-      # Input file is expected to be of the form:
-      # <option>
-      # <arg description>
-      # <description, possibly multiple lines>
-      # <PERSIST_ENTRY_DIVIDER>
-      input_file = open(load_path, "r")
-      input_file_contents = input_file.readlines()
-      input_file.close()
-
-      try:
-        version_line = input_file_contents.pop(0).rstrip()
-
-        if version_line.startswith("Tor Version "):
-          file_version = version_line[12:]
-          loaded_version = file_version
-          tor_version = tor_controller().get_info("version", "")
-
-          if check_version and file_version != tor_version:
-            msg = "wrong version, tor is %s but the file's from %s" % (tor_version, file_version)
-            raise IOError(msg)
-        else:
-          raise IOError("unable to parse version")
-
-        while input_file_contents:
-          # gets category enum, failing if it doesn't exist
-          category = input_file_contents.pop(0).rstrip()
-
-          if category not in Category:
-            base_msg = "invalid category in input file: '%s'"
-            raise IOError(base_msg % category)
-
-          # gets the position in the man page
-          index_arg, index_str = -1, input_file_contents.pop(0).rstrip()
-
-          if index_str.startswith("index: "):
-            index_str = index_str[7:]
-
-            if index_str.isdigit():
-              index_arg = int(index_str)
-            else:
-              raise IOError("non-numeric index value: %s" % index_str)
-          else:
-            raise IOError("malformed index argument: %s" % index_str)
-
-          option = input_file_contents.pop(0).rstrip()
-          argument = input_file_contents.pop(0).rstrip()
-
-          description, loaded_line = "", input_file_contents.pop(0)
-
-          while loaded_line != PERSIST_ENTRY_DIVIDER:
-            description += loaded_line
-
-            if input_file_contents:
-              loaded_line = input_file_contents.pop(0)
-            else:
-              break
-
-          CONFIG_DESCRIPTIONS[option.lower()] = ManPageEntry(option, index_arg, category, argument, description.rstrip())
-      except IndexError:
-        CONFIG_DESCRIPTIONS.clear()
-        raise IOError("input file format is invalid")
-    else:
-      man_call_results = system.call("man tor", None)
-
-      if not man_call_results:
-        raise IOError("man page not found")
-
-      # Fetches all options available with this tor instance. This isn't
-      # vital, and the valid_options are left empty if the call fails.
-
-      controller, valid_options = tor_controller(), []
-      config_option_query = controller.get_info("config/names", None)
-
-      if config_option_query:
-        for line in config_option_query.strip().split("\n"):
-          valid_options.append(line[:line.find(" ")].lower())
-
-      option_count, last_option, last_arg = 0, None, None
-      last_category, last_description = Category.GENERAL, ""
-
-      for line in man_call_results:
-        line = ui_tools.get_printable(line)
-        stripped_line = line.strip()
-
-        # we have content, but an indent less than an option (ignore line)
-        # if stripped_line and not line.startswith(" " * MAN_OPT_INDENT): continue
-
-        # line starts with an indent equivilant to a new config option
-
-        is_opt_indent = line.startswith(" " * MAN_OPT_INDENT) and line[MAN_OPT_INDENT] != " "
-
-        is_category_line = not line.startswith(" ") and "OPTIONS" in line
-
-        # if this is a category header or a new option, add an entry using the
-        # buffered results
-
-        if is_opt_indent or is_category_line:
-          # Filters the line based on if the option is recognized by tor or
-          # not. This isn't necessary for arm, so if unable to make the check
-          # then we skip filtering (no loss, the map will just have some extra
-          # noise).
-
-          stripped_description = last_description.strip()
-
-          if last_option and (not valid_options or last_option.lower() in valid_options):
-            CONFIG_DESCRIPTIONS[last_option.lower()] = ManPageEntry(last_option, option_count, last_category, last_arg, stripped_description)
-            option_count += 1
-
-          last_description = ""
-
-          # parses the option and argument
-
-          line = line.strip()
-          div_index = line.find(" ")
-
-          if div_index != -1:
-            last_option, last_arg = line[:div_index], line[div_index + 1:]
-
-          # if this is a category header then switch it
-
-          if is_category_line:
-            if line.startswith("OPTIONS"):
-              last_category = Category.GENERAL
-            elif line.startswith("CLIENT"):
-              last_category = Category.CLIENT
-            elif line.startswith("SERVER"):
-              last_category = Category.RELAY
-            elif line.startswith("DIRECTORY SERVER"):
-              last_category = Category.DIRECTORY
-            elif line.startswith("DIRECTORY AUTHORITY SERVER"):
-              last_category = Category.AUTHORITY
-            elif line.startswith("HIDDEN SERVICE"):
-              last_category = Category.HIDDEN_SERVICE
-            elif line.startswith("TESTING NETWORK"):
-              last_category = Category.TESTING
-            else:
-              log.notice("Unrecognized category in the man page: %s" % line.strip())
-        else:
-          # Appends the text to the running description. Empty lines and lines
-          # starting with a specific indentation are used for formatting, for
-          # instance the ExitPolicy and TestingTorNetwork entries.
-
-          if last_description and last_description[-1] != "\n":
-            last_description += " "
-
-          if not stripped_line:
-            last_description += "\n\n"
-          elif line.startswith(" " * MAN_EX_INDENT):
-            last_description += "    %s\n" % stripped_line
-          else:
-            last_description += stripped_line
-  except IOError as exc:
-    raised_exc = exc
-
-  CONFIG_DESCRIPTIONS_LOCK.release()
-
-  if raised_exc:
-    raise raised_exc
-  else:
-    return loaded_version
-
-
-def save_option_descriptions(path):
-  """
-  Preserves the current configuration descriptors to the given path. This
-  raises an IOError or OSError if unable to do so.
-
-  Arguments:
-    path - location to persist configuration descriptors
-  """
-
-  # make dir if the path doesn't already exist
-
-  base_dir = os.path.dirname(path)
-
-  if not os.path.exists(base_dir):
-    os.makedirs(base_dir)
-
-  output_file = open(path, "w")
-
-  CONFIG_DESCRIPTIONS_LOCK.acquire()
-  sorted_options = CONFIG_DESCRIPTIONS.keys()
-  sorted_options.sort()
-
-  tor_version = tor_controller().get_info("version", "")
-  output_file.write("Tor Version %s\n" % tor_version)
-
-  for i in range(len(sorted_options)):
-    man_entry = get_config_description(sorted_options[i])
-    output_file.write("%s\nindex: %i\n%s\n%s\n%s\n" % (man_entry.category, man_entry.index, man_entry.option, man_entry.arg_usage, man_entry.description))
-
-    if i != len(sorted_options) - 1:
-      output_file.write(PERSIST_ENTRY_DIVIDER)
-
-  output_file.close()
-  CONFIG_DESCRIPTIONS_LOCK.release()
-
-
-def get_config_summary(option):
-  """
-  Provides a short summary description of the configuration option. If none is
-  known then this proivdes None.
-
-  Arguments:
-    option - tor config option
-  """
-
-  return CONFIG.get("torrc.summary.%s" % option.lower())
-
-
-def is_important(option):
-  """
-  Provides True if the option has the 'important' flag in the configuration,
-  False otherwise.
-
-  Arguments:
-    option - tor config option
-  """
-
-  return option.lower() in CONFIG["torrc.important"]
-
-
-def get_config_description(option):
-  """
-  Provides ManPageEntry instances populated with information fetched from the
-  tor man page. This provides None if no such option has been loaded. If the
-  man page is in the process of being loaded then this call blocks until it
-  finishes.
-
-  Arguments:
-    option - tor config option
-  """
-
-  CONFIG_DESCRIPTIONS_LOCK.acquire()
-
-  if option.lower() in CONFIG_DESCRIPTIONS:
-    return_val = CONFIG_DESCRIPTIONS[option.lower()]
-  else:
-    return_val = None
-
-  CONFIG_DESCRIPTIONS_LOCK.release()
-  return return_val
-
-
-def get_config_options():
-  """
-  Provides the configuration options from the loaded man page. This is an empty
-  list if no man page has been loaded.
-  """
-
-  CONFIG_DESCRIPTIONS_LOCK.acquire()
-
-  return_val = [CONFIG_DESCRIPTIONS[opt].option for opt in CONFIG_DESCRIPTIONS]
-
-  CONFIG_DESCRIPTIONS_LOCK.release()
-  return return_val
-
-
-def get_config_location():
-  """
-  Provides the location of the torrc, raising an IOError with the reason if the
-  path can't be determined.
-  """
-
-  controller = tor_controller()
-  config_location = controller.get_info("config-file", None)
-  tor_pid, tor_prefix = controller.controller.get_pid(None), CONFIG['tor.chroot']
-
-  if not config_location:
-    raise IOError("unable to query the torrc location")
-
-  try:
-    tor_cwd = system.cwd(tor_pid)
-    return tor_prefix + system.expand_path(config_location, tor_cwd)
-  except IOError as exc:
-    raise IOError("querying tor's pwd failed because %s" % exc)
-
-
-def get_multiline_parameters():
-  """
-  Provides parameters that can be defined multiple times in the torrc without
-  overwriting the value.
-  """
-
-  # fetches config options with the LINELIST (aka 'LineList'), LINELIST_S (aka
-  # 'Dependent'), and LINELIST_V (aka 'Virtual') types
-
-  global MULTILINE_PARAM
-
-  if MULTILINE_PARAM is None:
-    controller, multiline_entries = tor_controller(), []
-
-    config_option_query = controller.get_info("config/names", None)
-
-    if config_option_query:
-      for line in config_option_query.strip().split("\n"):
-        conf_option, conf_type = line.strip().split(" ", 1)
-
-        if conf_type in ("LineList", "Dependant", "Virtual"):
-          multiline_entries.append(conf_option)
-    else:
-      # unable to query tor connection, so not caching results
-      return ()
-
-    MULTILINE_PARAM = multiline_entries
-
-  return tuple(MULTILINE_PARAM)
-
-
-def get_custom_options(include_value = False):
-  """
-  Provides the torrc parameters that differ from their defaults.
-
-  Arguments:
-    include_value - provides the current value with results if true, otherwise
-                   this just contains the options
-  """
-
-  config_text = tor_controller().get_info("config-text", "").strip()
-  config_lines = config_text.split("\n")
-
-  # removes any duplicates
-
-  config_lines = list(set(config_lines))
-
-  # The "GETINFO config-text" query only provides options that differ
-  # from Tor's defaults with the exception of its Log and Nickname entries
-  # which, even if undefined, returns "Log notice stdout" as per:
-  # https://trac.torproject.org/projects/tor/ticket/2362
-  #
-  # If this is from the deb then it will be "Log notice file /var/log/tor/log"
-  # due to special patching applied to it, as per:
-  # https://trac.torproject.org/projects/tor/ticket/4602
-
-  try:
-    config_lines.remove("Log notice stdout")
-  except ValueError:
-    pass
-
-  try:
-    config_lines.remove("Log notice file /var/log/tor/log")
-  except ValueError:
-    pass
-
-  try:
-    config_lines.remove("Nickname %s" % socket.gethostname())
-  except ValueError:
-    pass
-
-  if include_value:
-    return config_lines
-  else:
-    return [line[:line.find(" ")] for line in config_lines]
-
-
-def save_conf(destination = None, contents = None):
-  """
-  Saves the configuration to the given path. If this is equivilant to
-  issuing a SAVECONF (the contents and destination match what tor's using)
-  then that's done. Otherwise, this writes the contents directly. This raises
-  an IOError if unsuccessful.
-
-  Arguments:
-    destination - path to be saved to, the current config location if None
-    contents    - configuration to be saved, the current config if None
-  """
-
-  if destination:
-    destination = os.path.abspath(destination)
-
-  # fills default config values, and sets is_saveconf to false if they differ
-  # from the arguments
-
-  is_saveconf, start_time = True, time.time()
-
-  current_config = get_custom_options(True)
-
-  if not contents:
-    contents = current_config
-  else:
-    is_saveconf &= contents == current_config
-
-  # The "GETINFO config-text" option was introduced in Tor version 0.2.2.7. If
-  # we're writing custom contents then this is fine, but if we're trying to
-  # save the current configuration then we need to fail if it's unavailable.
-  # Otherwise we'd write a blank torrc as per...
-  # https://trac.torproject.org/projects/tor/ticket/3614
-
-  if contents == ['']:
-    # double check that "GETINFO config-text" is unavailable rather than just
-    # giving an empty result
-
-    if tor_controller().get_info("config-text", None) is None:
-      raise IOError("determining the torrc requires Tor version 0.2.2.7")
-
-  current_location = None
-
-  try:
-    current_location = get_config_location()
-
-    if not destination:
-      destination = current_location
-    else:
-      is_saveconf &= destination == current_location
-  except IOError:
-    pass
-
-  if not destination:
-    raise IOError("unable to determine the torrc's path")
-
-  log_msg = "Saved config by %%s to %s (runtime: %%0.4f)" % destination
-
-  # attempts SAVECONF if we're updating our torrc with the current state
-
-  if is_saveconf:
-    try:
-      tor_controller().save_conf()
-
-      try:
-        get_torrc().load()
-      except IOError:
-        pass
-
-      log.debug(log_msg % ("SAVECONF", time.time() - start_time))
-      return  # if successful then we're done
-    except:
-      pass
-
-  # if the SAVECONF fails or this is a custom save then write contents directly
-
-  try:
-    # make dir if the path doesn't already exist
-
-    base_dir = os.path.dirname(destination)
-
-    if not os.path.exists(base_dir):
-      os.makedirs(base_dir)
-
-    # saves the configuration to the file
-
-    config_file = open(destination, "w")
-    config_file.write("\n".join(contents))
-    config_file.close()
-  except (IOError, OSError) as exc:
-    raise IOError(exc)
-
-  # reloads the cached torrc if overwriting it
-
-  if destination == current_location:
-    try:
-      get_torrc().load()
-    except IOError:
-      pass
-
-  log.debug(log_msg % ("directly writing", time.time() - start_time))
-
-
-def validate(contents = None):
-  """
-  Performs validation on the given torrc contents, providing back a listing of
-  (line number, issue, msg) tuples for issues found. If the issue occures on a
-  multiline torrc entry then the line number is for the last line of the entry.
-
-  Arguments:
-    contents - torrc contents
-  """
-
-  controller = tor_controller()
-  custom_options = get_custom_options()
-  issues_found, seen_options = [], []
-
-  # Strips comments and collapses multiline multi-line entries, for more
-  # information see:
-  # https://trac.torproject.org/projects/tor/ticket/1929
-
-  stripped_contents, multiline_buffer = [], ""
-
-  for line in _strip_comments(contents):
-    if not line:
-      stripped_contents.append("")
-    else:
-      line = multiline_buffer + line
-      multiline_buffer = ""
-
-      if line.endswith("\\"):
-        multiline_buffer = line[:-1]
-        stripped_contents.append("")
-      else:
-        stripped_contents.append(line.strip())
-
-  for line_number in range(len(stripped_contents) - 1, -1, -1):
-    line_text = stripped_contents[line_number]
-
-    if not line_text:
-      continue
-
-    line_comp = line_text.split(None, 1)
-
-    if len(line_comp) == 2:
-      option, value = line_comp
-    else:
-      option, value = line_text, ""
-
-    # Tor is case insensetive when parsing its torrc. This poses a bit of an
-    # issue for us because we want all of our checks to be case insensetive
-    # too but also want messages to match the normal camel-case conventions.
-    #
-    # Using the custom_options to account for this. It contains the tor reported
-    # options (camel case) and is either a matching set or the following defaut
-    # value check will fail. Hence using that hash to correct the case.
-    #
-    # TODO: when refactoring for stem make this less confusing...
-
-    for custom_opt in custom_options:
-      if custom_opt.lower() == option.lower():
-        option = custom_opt
-        break
-
-    # if an aliased option then use its real name
-
-    if option in CONFIG["torrc.alias"]:
-      option = CONFIG["torrc.alias"][option]
-
-    # most parameters are overwritten if defined multiple times
-
-    if option in seen_options and option not in get_multiline_parameters():
-      issues_found.append((line_number, ValidationError.DUPLICATE, option))
-      continue
-    else:
-      seen_options.append(option)
-
-    # checks if the value isn't necessary due to matching the defaults
-
-    if option not in custom_options:
-      issues_found.append((line_number, ValidationError.IS_DEFAULT, option))
-
-    # replace aliases with their recognized representation
-
-    if option in CONFIG["torrc.alias"]:
-      option = CONFIG["torrc.alias"][option]
-
-    # tor appears to replace tabs with a space, for instance:
-    # "accept\t*:563" is read back as "accept *:563"
-
-    value = value.replace("\t", " ")
-
-    # parse value if it's a size or time, expanding the units
-
-    value, value_type = _parse_conf_value(value)
-
-    # issues GETCONF to get the values tor's currently configured to use
-
-    tor_values = controller.get_conf(option, [], True)
-
-    # multiline entries can be comma separated values (for both tor and conf)
-
-    value_list = [value]
-
-    if option in get_multiline_parameters():
-      value_list = [val.strip() for val in value.split(",")]
-
-      fetched_values, tor_values = tor_values, []
-      for fetched_value in fetched_values:
-        for fetched_entry in fetched_value.split(","):
-          fetched_entry = fetched_entry.strip()
-
-          if fetched_entry not in tor_values:
-            tor_values.append(fetched_entry)
-
-    for val in value_list:
-      # checks if both the argument and tor's value are empty
-
-      is_blank_match = not val and not tor_values
-
-      if not is_blank_match and val not in tor_values:
-        # converts corrections to reader friedly size values
-
-        display_values = tor_values
-
-        if value_type == ValueType.SIZE:
-          display_values = [str_tools.size_label(int(val)) for val in tor_values]
-        elif value_type == ValueType.TIME:
-          display_values = [str_tools.time_label(int(val)) for val in tor_values]
-
-        issues_found.append((line_number, ValidationError.MISMATCH, ", ".join(display_values)))
-
-  # checks if any custom options are missing from the torrc
-
-  for option in custom_options:
-    # In new versions the 'DirReqStatistics' option is true by default and
-    # disabled on startup if geoip lookups are unavailable. If this option is
-    # missing then that's most likely the reason.
-    #
-    # https://trac.torproject.org/projects/tor/ticket/4237
-
-    if option == "DirReqStatistics":
-      continue
-
-    if option not in seen_options:
-      issues_found.append((None, ValidationError.MISSING, option))
-
-  return issues_found
-
-
-def _parse_conf_value(conf_arg):
-  """
-  Converts size or time values to their lowest units (bytes or seconds) which
-  is what GETCONF calls provide. The returned is a tuple of the value and unit
-  type.
-
-  Arguments:
-    conf_arg - torrc argument
-  """
-
-  if conf_arg.count(" ") == 1:
-    val, unit = conf_arg.lower().split(" ", 1)
-
-    if not val.isdigit():
-      return conf_arg, ValueType.UNRECOGNIZED
-
-    mult, mult_type = _get_unit_type(unit)
-
-    if mult is not None:
-      return str(int(val) * mult), mult_type
-
-  return conf_arg, ValueType.UNRECOGNIZED
-
-
-def _get_unit_type(unit):
-  """
-  Provides the type and multiplier for an argument's unit. The multiplier is
-  None if the unit isn't recognized.
-
-  Arguments:
-    unit - string representation of a unit
-  """
-
-  for label in SIZE_MULT:
-    if unit in CONFIG["torrc.units.size." + label]:
-      return SIZE_MULT[label], ValueType.SIZE
-
-  for label in TIME_MULT:
-    if unit in CONFIG["torrc.units.time." + label]:
-      return TIME_MULT[label], ValueType.TIME
-
-  return None, ValueType.UNRECOGNIZED
-
-
-def _strip_comments(contents):
-  """
-  Removes comments and extra whitespace from the given torrc contents.
-
-  Arguments:
-    contents - torrc contents
-  """
-
-  stripped_contents = []
-
-  for line in contents:
-    if line and "#" in line:
-      line = line[:line.find("#")]
-
-    stripped_contents.append(line.strip())
-
-  return stripped_contents
-
-
-class Torrc():
-  """
-  Wrapper for the torrc. All getters provide None if the contents are unloaded.
-  """
-
-  def __init__(self):
-    self.contents = None
-    self.config_location = None
-    self.vals_lock = threading.RLock()
-
-    # cached results for the current contents
-    self.displayable_contents = None
-    self.stripped_contents = None
-    self.corrections = None
-
-    # flag to indicate if we've given a load failure warning before
-    self.is_foad_fail_warned = False
-
-  def load(self, log_failure = False):
-    """
-    Loads or reloads the torrc contents, raising an IOError if there's a
-    problem.
-
-    Arguments:
-      log_failure - if the torrc fails to load and we've never provided a
-                   warning for this before then logs a warning
-    """
-
-    self.vals_lock.acquire()
-
-    # clears contents and caches
-    self.contents, self.config_location = None, None
-    self.displayable_contents = None
-    self.stripped_contents = None
-    self.corrections = None
-
-    try:
-      self.config_location = get_config_location()
-      config_file = open(self.config_location, "r")
-      self.contents = config_file.readlines()
-      config_file.close()
-    except IOError as exc:
-      if log_failure and not self.is_foad_fail_warned:
-        log.warn("Unable to load torrc (%s)" % exc.strerror)
-        self.is_foad_fail_warned = True
-
-      self.vals_lock.release()
-      raise exc
-
-    self.vals_lock.release()
-
-  def is_loaded(self):
-    """
-    Provides true if there's loaded contents, false otherwise.
-    """
-
-    return self.contents is not None
-
-  def get_config_location(self):
-    """
-    Provides the location of the loaded configuration contents. This may be
-    available, even if the torrc failed to be loaded.
-    """
-
-    return self.config_location
-
-  def get_contents(self):
-    """
-    Provides the contents of the configuration file.
-    """
-
-    self.vals_lock.acquire()
-    return_val = list(self.contents) if self.contents else None
-    self.vals_lock.release()
-    return return_val
-
-  def get_display_contents(self, strip = False):
-    """
-    Provides the contents of the configuration file, formatted in a rendering
-    frindly fashion:
-    - Tabs print as three spaces. Keeping them as tabs is problematic for
-      layouts since it's counted as a single character, but occupies several
-      cells.
-    - Strips control and unprintable characters.
-
-    Arguments:
-      strip - removes comments and extra whitespace if true
-    """
-
-    self.vals_lock.acquire()
-
-    if not self.is_loaded():
-      return_val = None
-    else:
-      if self.displayable_contents is None:
-        # restricts contents to displayable characters
-        self.displayable_contents = []
-
-        for line_number in range(len(self.contents)):
-          line_text = self.contents[line_number]
-          line_text = line_text.replace("\t", "   ")
-          line_text = ui_tools.get_printable(line_text)
-          self.displayable_contents.append(line_text)
-
-      if strip:
-        if self.stripped_contents is None:
-          self.stripped_contents = _strip_comments(self.displayable_contents)
-
-        return_val = list(self.stripped_contents)
-      else:
-        return_val = list(self.displayable_contents)
-
-    self.vals_lock.release()
-    return return_val
-
-  def get_corrections(self):
-    """
-    Performs validation on the loaded contents and provides back the
-    corrections. If validation is disabled then this won't provide any
-    results.
-    """
-
-    self.vals_lock.acquire()
-
-    if not self.is_loaded():
-      return_val = None
-    else:
-      tor_version = tor_controller().get_version(None)
-      skip_validation = not CONFIG["features.torrc.validate"]
-      skip_validation |= (tor_version is None or not tor_version >= stem.version.Requirement.GETINFO_CONFIG_TEXT)
-
-      if skip_validation:
-        log.info("Skipping torrc validation (requires tor 0.2.2.7-alpha)")
-        return_val = {}
-      else:
-        if self.corrections is None:
-          self.corrections = validate(self.contents)
-
-        return_val = list(self.corrections)
-
-    self.vals_lock.release()
-    return return_val
-
-  def get_lock(self):
-    """
-    Provides the lock governing concurrent access to the contents.
-    """
-
-    return self.vals_lock
-
-  def log_validation_issues(self):
-    """
-    Performs validation on the loaded contents, and logs warnings for issues
-    that are found.
-    """
-
-    corrections = self.get_corrections()
-
-    if corrections:
-      duplicate_options, default_options, mismatch_lines, missing_options = [], [], [], []
-
-      for line_number, issue, msg in corrections:
-        if issue == ValidationError.DUPLICATE:
-          duplicate_options.append("%s (line %i)" % (msg, line_number + 1))
-        elif issue == ValidationError.IS_DEFAULT:
-          default_options.append("%s (line %i)" % (msg, line_number + 1))
-        elif issue == ValidationError.MISMATCH:
-          mismatch_lines.append(line_number + 1)
-        elif issue == ValidationError.MISSING:
-          missing_options.append(msg)
-
-      if duplicate_options or default_options:
-        msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page."
-
-        if duplicate_options:
-          if len(duplicate_options) > 1:
-            msg += "\n- entries ignored due to having duplicates: "
-          else:
-            msg += "\n- entry ignored due to having a duplicate: "
-
-          duplicate_options.sort()
-          msg += ", ".join(duplicate_options)
-
-        if default_options:
-          if len(default_options) > 1:
-            msg += "\n- entries match their default values: "
-          else:
-            msg += "\n- entry matches its default value: "
-
-          default_options.sort()
-          msg += ", ".join(default_options)
-
-        log.notice(msg)
-
-      if mismatch_lines or missing_options:
-        msg = "The torrc differs from what tor's using. You can issue a sighup to reload the torrc values by pressing x."
-
-        if mismatch_lines:
-          if len(mismatch_lines) > 1:
-            msg += "\n- torrc values differ on lines: "
-          else:
-            msg += "\n- torrc value differs on line: "
-
-          mismatch_lines.sort()
-          msg += ", ".join([str(val + 1) for val in mismatch_lines])
-
-        if missing_options:
-          if len(missing_options) > 1:
-            msg += "\n- configuration values are missing from the torrc: "
-          else:
-            msg += "\n- configuration value is missing from the torrc: "
-
-          missing_options.sort()
-          msg += ", ".join(missing_options)
-
-        log.warn(msg)
-
-
-def load_configuration_descriptions(path_prefix):
-  """
-  Attempts to load descriptions for tor's configuration options, fetching them
-  from the man page and persisting them to a file to speed future startups.
-  """
-
-  # It is important that this is loaded before entering the curses context,
-  # otherwise the man call pegs the cpu for around a minute (I'm not sure
-  # why... curses must mess the terminal in a way that's important to man).
-
-  if CONFIG["features.config.descriptions.enabled"]:
-    is_config_descriptions_loaded = False
-
-    # determines the path where cached descriptions should be persisted (left
-    # undefined if caching is disabled)
-
-    descriptor_path = None
-
-    if CONFIG["features.config.descriptions.persist"]:
-      data_dir = CONFIG["startup.data_directory"]
-
-      if not data_dir.endswith("/"):
-        data_dir += "/"
-
-      descriptor_path = os.path.expanduser(data_dir + "cache/") + CONFIG_DESC_FILENAME
-
-    # attempts to load configuration descriptions cached in the data directory
-
-    if descriptor_path:
-      try:
-        load_start_time = time.time()
-        load_option_descriptions(descriptor_path)
-        is_config_descriptions_loaded = True
-
-        log.info(DESC_LOAD_SUCCESS_MSG % (descriptor_path, time.time() - load_start_time))
-      except IOError as exc:
-        log.info(DESC_LOAD_FAILED_MSG % exc.strerror)
-
-    # fetches configuration options from the man page
-
-    if not is_config_descriptions_loaded:
-      try:
-        load_start_time = time.time()
-        load_option_descriptions()
-        is_config_descriptions_loaded = True
-
-        log.info(DESC_READ_MAN_SUCCESS_MSG % (time.time() - load_start_time))
-      except IOError as exc:
-        log.notice(DESC_READ_MAN_FAILED_MSG % exc.strerror)
-
-      # persists configuration descriptions
-
-      if is_config_descriptions_loaded and descriptor_path:
-        try:
-          load_start_time = time.time()
-          save_option_descriptions(descriptor_path)
-          log.info(DESC_SAVE_SUCCESS_MSG % (descriptor_path, time.time() - load_start_time))
-        except IOError as exc:
-          log.notice(DESC_SAVE_FAILED_MSG % exc.strerror)
-        except OSError as exc:
-          log.notice(DESC_SAVE_FAILED_MSG % exc)
-
-    # finally fall back to the cached descriptors provided with arm (this is
-    # often the case for tbb and manual builds)
-
-    if not is_config_descriptions_loaded:
-      try:
-        load_start_time = time.time()
-        loaded_version = load_option_descriptions("%sresources/%s" % (path_prefix, CONFIG_DESC_FILENAME), False)
-        is_config_descriptions_loaded = True
-        log.notice(DESC_INTERNAL_LOAD_SUCCESS_MSG % loaded_version)
-      except IOError as exc:
-        log.error(DESC_INTERNAL_LOAD_FAILED_MSG % exc.strerror)
diff --git a/arm/util/tracker.py b/arm/util/tracker.py
deleted file mode 100644
index b9246a4..0000000
--- a/arm/util/tracker.py
+++ /dev/null
@@ -1,666 +0,0 @@
-"""
-Background tasks for gathering information about the tor process.
-
-::
-
-  get_connection_tracker - provides a ConnectionTracker for our tor process
-  get_resource_tracker - provides a ResourceTracker for our tor process
-  get_port_usage_tracker - provides a PortUsageTracker for our system
-
-  stop_trackers - halts any active trackers
-
-  Daemon - common parent for resolvers
-    |- ConnectionTracker - periodically checks the connections established by tor
-    |  |- get_custom_resolver - provide the custom conntion resolver we're using
-    |  |- set_custom_resolver - overwrites automatic resolver selecion with a custom resolver
-    |  +- get_value - provides our latest connection results
-    |
-    |- ResourceTracker - periodically checks the resource usage of tor
-    |  +- get_value - provides our latest resource usage results
-    |
-    |- PortUsageTracker - provides information about port usage on the local system
-    |  +- get_processes_using_ports - mapping of ports to the processes using it
-    |
-    |- run_counter - number of successful runs
-    |- get_rate - provides the rate at which we run
-    |- set_rate - sets the rate at which we run
-    |- set_paused - pauses or continues work
-    +- stop - stops further work by the daemon
-
-.. data:: Resources
-
-  Resource usage information retrieved about the tor process.
-
-  :var float cpu_sample: average cpu usage since we last checked
-  :var float cpu_average: average cpu usage since we first started tracking the process
-  :var float cpu_total: total cpu time the process has used since starting
-  :var int memory_bytes: memory usage of the process in bytes
-  :var float memory_percent: percentage of our memory used by this process
-  :var float timestamp: unix timestamp for when this information was fetched
-"""
-
-import collections
-import time
-import threading
-
-from stem.control import State
-from stem.util import conf, connection, proc, str_tools, system
-
-from arm.util import log, tor_controller
-
-CONFIG = conf.config_dict('arm', {
-  'queries.connections.rate': 5,
-  'queries.resources.rate': 5,
-  'queries.port_usage.rate': 5,
-})
-
-CONNECTION_TRACKER = None
-RESOURCE_TRACKER = None
-PORT_USAGE_TRACKER = None
-
-Resources = collections.namedtuple('Resources', [
-  'cpu_sample',
-  'cpu_average',
-  'cpu_total',
-  'memory_bytes',
-  'memory_percent',
-  'timestamp',
-])
-
-
-def get_connection_tracker():
-  """
-  Singleton for tracking the connections established by tor.
-  """
-
-  global CONNECTION_TRACKER
-
-  if CONNECTION_TRACKER is None:
-    CONNECTION_TRACKER = ConnectionTracker(CONFIG['queries.connections.rate'])
-
-  return CONNECTION_TRACKER
-
-
-def get_resource_tracker():
-  """
-  Singleton for tracking the resource usage of our tor process.
-  """
-
-  global RESOURCE_TRACKER
-
-  if RESOURCE_TRACKER is None:
-    RESOURCE_TRACKER = ResourceTracker(CONFIG['queries.resources.rate'])
-
-  return RESOURCE_TRACKER
-
-
-def get_port_usage_tracker():
-  """
-  Singleton for tracking the process using a set of ports.
-  """
-
-  global PORT_USAGE_TRACKER
-
-  if PORT_USAGE_TRACKER is None:
-    PORT_USAGE_TRACKER = PortUsageTracker(CONFIG['queries.port_usage.rate'])
-
-  return PORT_USAGE_TRACKER
-
-
-def stop_trackers():
-  """
-  Halts active trackers, providing back the thread shutting them down.
-
-  :returns: **threading.Thread** shutting down the daemons
-  """
-
-  def halt_trackers():
-    trackers = filter(lambda t: t.is_alive(), [
-      get_resource_tracker(),
-      get_connection_tracker(),
-    ])
-
-    for tracker in trackers:
-      tracker.stop()
-
-    for tracker in trackers:
-      tracker.join()
-
-  halt_thread = threading.Thread(target = halt_trackers)
-  halt_thread.setDaemon(True)
-  halt_thread.start()
-  return halt_thread
-
-
-def _resources_via_ps(pid):
-  """
-  Fetches resource usage information about a given process via ps. This returns
-  a tuple of the form...
-
-    (total_cpu_time, uptime, memory_in_bytes, memory_in_percent)
-
-  :param int pid: process to be queried
-
-  :returns: **tuple** with the resource usage information
-
-  :raises: **IOError** if unsuccessful
-  """
-
-  # ps results are of the form...
-  #
-  #     TIME     ELAPSED   RSS %MEM
-  # 3-08:06:32 21-00:00:12 121844 23.5
-  #
-  # ... or if Tor has only recently been started...
-  #
-  #     TIME      ELAPSED    RSS %MEM
-  #  0:04.40        37:57  18772  0.9
-
-  ps_call = system.call("ps -p {pid} -o cputime,etime,rss,%mem".format(pid = pid))
-
-  if ps_call and len(ps_call) >= 2:
-    stats = ps_call[1].strip().split()
-
-    if len(stats) == 4:
-      try:
-        total_cpu_time = str_tools.parse_short_time_label(stats[0])
-        uptime = str_tools.parse_short_time_label(stats[1])
-        memory_bytes = int(stats[2]) * 1024  # ps size is in kb
-        memory_percent = float(stats[3]) / 100.0
-
-        return (total_cpu_time, uptime, memory_bytes, memory_percent)
-      except ValueError:
-        pass
-
-  raise IOError("unrecognized output from ps: %s" % ps_call)
-
-
-def _resources_via_proc(pid):
-  """
-  Fetches resource usage information about a given process via proc. This
-  returns a tuple of the form...
-
-    (total_cpu_time, uptime, memory_in_bytes, memory_in_percent)
-
-  :param int pid: process to be queried
-
-  :returns: **tuple** with the resource usage information
-
-  :raises: **IOError** if unsuccessful
-  """
-
-  utime, stime, start_time = proc.stats(
-    pid,
-    proc.Stat.CPU_UTIME,
-    proc.Stat.CPU_STIME,
-    proc.Stat.START_TIME,
-  )
-
-  total_cpu_time = float(utime) + float(stime)
-  memory_in_bytes = proc.memory_usage(pid)[0]
-  total_memory = proc.physical_memory()
-
-  uptime = time.time() - float(start_time)
-  memory_in_percent = float(memory_in_bytes) / total_memory
-
-  return (total_cpu_time, uptime, memory_in_bytes, memory_in_percent)
-
-
-def _process_for_ports(local_ports, remote_ports):
-  """
-  Provides the name of the process using the given ports.
-
-  :param list local_ports: local port numbers to look up
-  :param list remote_ports: remote port numbers to look up
-
-  :returns: **dict** mapping the ports to the associated process names
-
-  :raises: **IOError** if unsuccessful
-  """
-
-  def _parse_lsof_line(line):
-    line_comp = line.split()
-
-    if not line:
-      return None, None, None  # blank line
-    elif len(line_comp) != 10:
-      raise ValueError('lines are expected to have ten fields')
-    elif line_comp[9] != '(ESTABLISHED)':
-      return None, None, None  # connection isn't established
-
-    cmd = line_comp[0]
-    port_map = line_comp[8]
-
-    if '->' not in port_map:
-      raise ValueError("'%s' is expected to be a '->' separated mapping" % port_map)
-
-    local, remote = port_map.split('->', 1)
-
-    if ':' not in local or ':' not in remote:
-      raise ValueError("'%s' is expected to be 'address:port' entries" % port_map)
-
-    local_port = local.split(':', 1)[1]
-    remote_port = remote.split(':', 1)[1]
-
-    if not connection.is_valid_port(local_port):
-      raise ValueError("'%s' isn't a valid port" % local_port)
-    elif not connection.is_valid_port(remote_port):
-      raise ValueError("'%s' isn't a valid port" % remote_port)
-
-    return int(local_port), int(remote_port), cmd
-
-  # atagar at fenrir:~/Desktop/arm$ lsof -i tcp:51849 -i tcp:37277
-  # COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
-  # tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9051->localhost:37277 (ESTABLISHED)
-  # tor     2001 atagar   15u  IPv4  22024      0t0  TCP localhost:9051->localhost:51849 (ESTABLISHED)
-  # python  2462 atagar    3u  IPv4  14047      0t0  TCP localhost:37277->localhost:9051 (ESTABLISHED)
-  # python  3444 atagar    3u  IPv4  22023      0t0  TCP localhost:51849->localhost:9051 (ESTABLISHED)
-
-  lsof_cmd = 'lsof -nP ' + ' '.join(['-i tcp:%s' % port for port in (local_ports + remote_ports)])
-  lsof_call = system.call(lsof_cmd)
-
-  if lsof_call:
-    results = {}
-
-    if lsof_call[0].startswith('COMMAND  '):
-      lsof_call = lsof_call[1:]  # strip the title line
-
-    for line in lsof_call:
-      try:
-        local_port, remote_port, cmd = _parse_lsof_line(line)
-
-        if local_port in local_ports:
-          results[local_port] = cmd
-        elif remote_port in remote_ports:
-          results[remote_port] = cmd
-      except ValueError as exc:
-        raise IOError("unrecognized output from lsof (%s): %s" % (exc, line))
-
-    return results
-
-  raise IOError("no results from lsof")
-
-
-class Daemon(threading.Thread):
-  """
-  Daemon that can perform a given action at a set rate. Subclasses are expected
-  to implement our _task() method with the work to be done.
-  """
-
-  def __init__(self, rate):
-    super(Daemon, self).__init__()
-    self.setDaemon(True)
-
-    self._process_lock = threading.RLock()
-    self._process_pid = None
-    self._process_name = None
-
-    self._rate = rate
-    self._last_ran = -1  # time when we last ran
-    self._run_counter = 0  # counter for the number of successful runs
-
-    self._is_paused = False
-    self._pause_condition = threading.Condition()
-    self._halt = False  # terminates thread if true
-
-    controller = tor_controller()
-    controller.add_status_listener(self._tor_status_listener)
-    self._tor_status_listener(controller, State.INIT, None)
-
-  def run(self):
-    while not self._halt:
-      time_since_last_ran = time.time() - self._last_ran
-
-      if self._is_paused or time_since_last_ran < self._rate:
-        sleep_duration = max(0.02, self._rate - time_since_last_ran)
-
-        with self._pause_condition:
-          if not self._halt:
-            self._pause_condition.wait(sleep_duration)
-
-        continue  # done waiting, try again
-
-      with self._process_lock:
-        if self._process_pid is not None:
-          is_successful = self._task(self._process_pid, self._process_name)
-        else:
-          is_successful = False
-
-        if is_successful:
-          self._run_counter += 1
-
-      self._last_ran = time.time()
-
-  def _task(self, process_pid, process_name):
-    """
-    Task the resolver is meant to perform. This should be implemented by
-    subclasses.
-
-    :param int process_pid: pid of the process we're tracking
-    :param str process_name: name of the process we're tracking
-
-    :returns: **bool** indicating if our run was successful or not
-    """
-
-    return True
-
-  def run_counter(self):
-    """
-    Provides the number of times we've successful runs so far. This can be used
-    by callers to determine if our results have been seen by them before or
-    not.
-
-    :returns: **int** for the run count we're on
-    """
-
-    return self._run_counter
-
-  def get_rate(self):
-    """
-    Provides the rate at which we perform our task.
-
-    :returns: **float** for the rate in seconds at which we perform our task
-    """
-
-    return self._rate
-
-  def set_rate(self, rate):
-    """
-    Sets the rate at which we perform our task in seconds.
-
-    :param float rate: rate at which to perform work in seconds
-    """
-
-    self._rate = rate
-
-  def set_paused(self, pause):
-    """
-    Either resumes or holds off on doing further work.
-
-    :param bool pause: halts work if **True**, resumes otherwise
-    """
-
-    self._is_paused = pause
-
-  def stop(self):
-    """
-    Halts further work and terminates the thread.
-    """
-
-    with self._pause_condition:
-      self._halt = True
-      self._pause_condition.notifyAll()
-
-  def _tor_status_listener(self, controller, event_type, _):
-    with self._process_lock:
-      if not self._halt and event_type in (State.INIT, State.RESET):
-        tor_pid = controller.get_pid(None)
-        tor_cmd = system.name_by_pid(tor_pid) if tor_pid else None
-
-        self._process_pid = tor_pid
-        self._process_name = tor_cmd if tor_cmd else 'tor'
-
-  def __enter__(self):
-    self.start()
-    return self
-
-  def __exit__(self, exit_type, value, traceback):
-    self.stop()
-    self.join()
-
-
-class ConnectionTracker(Daemon):
-  """
-  Periodically retrieves the connections established by tor.
-  """
-
-  def __init__(self, rate):
-    super(ConnectionTracker, self).__init__(rate)
-
-    self._connections = []
-    self._resolvers = connection.system_resolvers()
-    self._custom_resolver = None
-
-    # Number of times in a row we've either failed with our current resolver or
-    # concluded that our rate is too low.
-
-    self._failure_count = 0
-    self._rate_too_low_count = 0
-
-  def _task(self, process_pid, process_name):
-    if self._custom_resolver:
-      resolver = self._custom_resolver
-      is_default_resolver = False
-    elif self._resolvers:
-      resolver = self._resolvers[0]
-      is_default_resolver = True
-    else:
-      return False  # nothing to resolve with
-
-    try:
-      start_time = time.time()
-
-      self._connections = connection.get_connections(
-        resolver,
-        process_pid = process_pid,
-        process_name = process_name,
-      )
-
-      runtime = time.time() - start_time
-
-      if is_default_resolver:
-        self._failure_count = 0
-
-      # Reduce our rate if connection resolution is taking a long time. This is
-      # most often an issue for extremely busy relays.
-
-      min_rate = 100 * runtime
-
-      if self.get_rate() < min_rate:
-        self._rate_too_low_count += 1
-
-        if self._rate_too_low_count >= 3:
-          min_rate += 1  # little extra padding so we don't frequently update this
-          self.set_rate(min_rate)
-          self._rate_too_low_count = 0
-          log.debug('tracker.lookup_rate_increased', seconds = "%0.1f" % min_rate)
-      else:
-        self._rate_too_low_count = 0
-
-      return True
-    except IOError as exc:
-      log.info('wrap', text = exc)
-
-      # Fail over to another resolver if we've repeatedly been unable to use
-      # this one.
-
-      if is_default_resolver:
-        self._failure_count += 1
-
-        if self._failure_count >= 3:
-          self._resolvers.pop(0)
-          self._failure_count = 0
-
-          if self._resolvers:
-            log.notice(
-              'tracker.unable_to_use_resolver',
-              old_resolver = resolver,
-              new_resolver = self._resolvers[0],
-            )
-          else:
-            log.notice('tracker.unable_to_use_all_resolvers')
-
-      return False
-
-  def get_custom_resolver(self):
-    """
-    Provides the custom resolver the user has selected. This is **None** if
-    we're picking resolvers dynamically.
-
-    :returns: :data:`~stem.util.connection.Resolver` we're overwritten to use
-    """
-
-    return self._custom_resolver
-
-  def set_custom_resolver(self, resolver):
-    """
-    Sets the resolver used for connection resolution. If **None** then this is
-    automatically determined based on what is available.
-
-    :param stem.util.connection.Resolver resolver: resolver to use
-    """
-
-    self._custom_resolver = resolver
-
-  def get_value(self):
-    """
-    Provides a listing of tor's latest connections.
-
-    :returns: **list** of :class:`~stem.util.connection.Connection` we last
-      retrieved, an empty list if our tracker's been stopped
-    """
-
-    if self._halt:
-      return []
-    else:
-      return list(self._connections)
-
-
-class ResourceTracker(Daemon):
-  """
-  Periodically retrieves the resource usage of tor.
-  """
-
-  def __init__(self, rate):
-    super(ResourceTracker, self).__init__(rate)
-
-    self._resources = None
-    self._use_proc = proc.is_available()  # determines if we use proc or ps for lookups
-    self._failure_count = 0  # number of times in a row we've failed to get results
-
-  def get_value(self):
-    """
-    Provides tor's latest resource usage.
-
-    :returns: latest :data:`~arm.util.tracker.Resources` we've polled
-    """
-
-    result = self._resources
-    return result if result else Resources(0.0, 0.0, 0.0, 0, 0.0, 0.0)
-
-  def _task(self, process_pid, process_name):
-    try:
-      resolver = _resources_via_proc if self._use_proc else _resources_via_ps
-      total_cpu_time, uptime, memory_in_bytes, memory_in_percent = resolver(process_pid)
-
-      if self._resources:
-        cpu_sample = (total_cpu_time - self._resources.cpu_total) / self._resources.cpu_total
-      else:
-        cpu_sample = 0.0  # we need a prior datapoint to give a sampling
-
-      self._resources = Resources(
-        cpu_sample = cpu_sample,
-        cpu_average = total_cpu_time / uptime,
-        cpu_total = total_cpu_time,
-        memory_bytes = memory_in_bytes,
-        memory_percent = memory_in_percent,
-        timestamp = time.time(),
-      )
-
-      self._failure_count = 0
-      return True
-    except IOError as exc:
-      self._failure_count += 1
-
-      if self._use_proc:
-        if self._failure_count >= 3:
-          # We've failed three times resolving via proc. Warn, and fall back
-          # to ps resolutions.
-
-          self._use_proc = False
-          self._failure_count = 0
-
-          log.info(
-            'tracker.abort_getting_resources',
-            resolver = 'proc',
-            response = 'falling back to ps',
-            exc = exc,
-          )
-        else:
-          log.debug('tracker.unable_to_get_resources', resolver = 'proc', exc = exc)
-      else:
-        if self._failure_count >= 3:
-          # Give up on further attempts.
-
-          log.info(
-            'tracker.abort_getting_resources',
-            resolver = 'ps',
-            response = 'giving up on getting resource usage information',
-            exc = exc,
-          )
-
-          self.stop()
-        else:
-          log.debug('tracker.unable_to_get_resources', resolver = 'ps', exc = exc)
-
-      return False
-
-
-class PortUsageTracker(Daemon):
-  """
-  Periodically retrieves the processes using a set of ports.
-  """
-
-  def __init__(self, rate):
-    super(PortUsageTracker, self).__init__(rate)
-
-    self._last_requested_ports = []
-    self._processes_for_ports = {}
-    self._failure_count = 0  # number of times in a row we've failed to get results
-
-  def get_processes_using_ports(self, ports):
-    """
-    Registers a given set of ports for further lookups, and returns the last
-    set of 'port => process' mappings we retrieved. Note that this means that
-    we will not return the requested ports unless they're requested again after
-    a successful lookup has been performed.
-
-    :param list ports: port numbers to look up
-
-    :returns: **dict** mapping port numbers to the process using it
-    """
-
-    self._last_requested_ports = ports
-    return self._processes_for_ports
-
-  def _task(self, process_pid, process_name):
-    ports = self._last_requested_ports
-
-    if not ports:
-      return True
-
-    result = {}
-
-    # Use cached results from our last lookup if available.
-
-    for port, process in self._processes_for_ports.items():
-      if port in ports:
-        result[port] = process
-        ports.remove(port)
-
-    try:
-      result.update(_process_for_ports(ports, ports))
-
-      self._processes_for_ports = result
-      self._failure_count = 0
-      return True
-    except IOError as exc:
-      self._failure_count += 1
-
-      if self._failure_count >= 3:
-        log.info('tracker.abort_getting_port_usage', exc = exc)
-        self.stop()
-      else:
-        log.debug('tracker.unable_to_get_port_usages', exc = exc)
-
-      return False
diff --git a/arm/util/ui_tools.py b/arm/util/ui_tools.py
deleted file mode 100644
index a1064ee..0000000
--- a/arm/util/ui_tools.py
+++ /dev/null
@@ -1,400 +0,0 @@
-"""
-Toolkit for working with curses.
-"""
-
-import curses
-
-from curses.ascii import isprint
-
-from arm.util import log, msg
-
-from stem.util import conf, system
-
-COLOR_LIST = {
-  'red': curses.COLOR_RED,
-  'green': curses.COLOR_GREEN,
-  'yellow': curses.COLOR_YELLOW,
-  'blue': curses.COLOR_BLUE,
-  'cyan': curses.COLOR_CYAN,
-  'magenta': curses.COLOR_MAGENTA,
-  'black': curses.COLOR_BLACK,
-  'white': curses.COLOR_WHITE,
-}
-
-DEFAULT_COLOR_ATTR = dict([(color, 0) for color in COLOR_LIST])
-COLOR_ATTR = None
-
-
-def conf_handler(key, value):
-  if key == 'features.color_override':
-    if value not in COLOR_LIST.keys() and value != 'none':
-      raise ValueError(msg('usage.unable_to_set_color_override', color = value))
-
-
-CONFIG = conf.config_dict('arm', {
-  'features.color_override': 'none',
-  'features.colorInterface': True,
-}, conf_handler)
-
-
-def is_color_supported():
-  """
-  Checks if curses presently supports rendering colors.
-
-  :returns: **True** if colors can be rendered, **False** otherwise
-  """
-
-  return _color_attr() != DEFAULT_COLOR_ATTR
-
-
-def get_color(color):
-  """
-  Provides attribute corresponding to a given text color. Supported colors
-  include:
-
-    * red
-    * green
-    * yellow
-    * blue
-    * cyan
-    * magenta
-    * black
-    * white
-
-  If color support isn't available or colors can't be initialized then this uses the
-  terminal's default coloring scheme.
-
-  :param str color: color attributes to be provided
-
-  :returns: **tuple** color pair used by curses to render the color
-  """
-
-  color_override = get_color_override()
-
-  if color_override:
-    color = color_override
-
-  return _color_attr()[color]
-
-
-def set_color_override(color = None):
-  """
-  Overwrites all requests for color with the given color instead.
-
-  :param str color: color to override all requests with, **None** if color
-    requests shouldn't be overwritten
-
-  :raises: **ValueError** if the color name is invalid
-  """
-
-  arm_config = conf.get_config('arm')
-
-  if color is None:
-    arm_config.set('features.color_override', 'none')
-  elif color in COLOR_LIST.keys():
-    arm_config.set('features.color_override', color)
-  else:
-    raise ValueError(msg('usage.unable_to_set_color_override', color = color))
-
-
-def get_color_override():
-  """
-  Provides the override color used by the interface.
-
-  :returns: **str** for the color requrests will be overwritten with, **None**
-    if no override is set
-  """
-
-  color_override = CONFIG.get('features.color_override', 'none')
-
-  if color_override == 'none':
-    return None
-  else:
-    return color_override
-
-
-def _color_attr():
-  """
-  Initializes color mappings usable by curses. This can only be done after
-  calling curses.initscr().
-  """
-
-  global COLOR_ATTR
-
-  if COLOR_ATTR is None:
-    if not CONFIG['features.colorInterface']:
-      COLOR_ATTR = DEFAULT_COLOR_ATTR
-    elif curses.has_colors():
-      color_attr = dict(DEFAULT_COLOR_ATTR)
-
-      for color_pair, color_name in enumerate(COLOR_LIST):
-        foreground_color = COLOR_LIST[color_name]
-        background_color = -1  # allows for default (possibly transparent) background
-        curses.init_pair(color_pair + 1, foreground_color, background_color)
-        color_attr[color_name] = curses.color_pair(color_pair + 1)
-
-      log.info('setup.color_support_available')
-      COLOR_ATTR = color_attr
-    else:
-      log.info('setup.color_support_unavailable')
-      COLOR_ATTR = DEFAULT_COLOR_ATTR
-
-  return COLOR_ATTR
-
-
-def disable_acs():
-  """
-  Replaces the curses ACS characters. This can be preferable if curses is
-  unable to render them...
-
-  http://www.atagar.com/arm/images/acs_display_failure.png
-  """
-
-  for item in curses.__dict__:
-    if item.startswith('ACS_'):
-      curses.__dict__[item] = ord('+')
-
-  # replace a few common border pipes that are better rendered as '|' or
-  # '-' instead
-
-  curses.ACS_SBSB = ord('|')
-  curses.ACS_VLINE = ord('|')
-  curses.ACS_BSBS = ord('-')
-  curses.ACS_HLINE = ord('-')
-
-
-def get_printable(line, keep_newlines = True):
-  """
-  Provides the line back with non-printable characters stripped.
-
-  :param str line: string to be processed
-  :param str keep_newlines: retains newlines if **True**, stripped otherwise
-
-  :returns: **str** of the line with only printable content
-  """
-
-  line = line.replace('\xc2', "'")
-  line = filter(lambda char: isprint(char) or (keep_newlines and char == '\n'), line)
-
-  return line
-
-
-def draw_box(panel, top, left, width, height, attr=curses.A_NORMAL):
-  """
-  Draws a box in the panel with the given bounds.
-
-  Arguments:
-    panel  - panel in which to draw
-    top    - vertical position of the box's top
-    left   - horizontal position of the box's left side
-    width  - width of the drawn box
-    height - height of the drawn box
-    attr   - text attributes
-  """
-
-  # draws the top and bottom
-
-  panel.hline(top, left + 1, width - 2, attr)
-  panel.hline(top + height - 1, left + 1, width - 2, attr)
-
-  # draws the left and right sides
-
-  panel.vline(top + 1, left, height - 2, attr)
-  panel.vline(top + 1, left + width - 1, height - 2, attr)
-
-  # draws the corners
-
-  panel.addch(top, left, curses.ACS_ULCORNER, attr)
-  panel.addch(top, left + width - 1, curses.ACS_URCORNER, attr)
-  panel.addch(top + height - 1, left, curses.ACS_LLCORNER, attr)
-
-
-def get_scroll_position(key, position, page_height, content_height, is_cursor = False):
-  """
-  Parses navigation keys, providing the new scroll possition the panel should
-  use. Position is always between zero and (content_height - page_height). This
-  handles the following keys:
-  Up / Down - scrolls a position up or down
-  Page Up / Page Down - scrolls by the page_height
-  Home - top of the content
-  End - bottom of the content
-
-  This provides the input position if the key doesn't correspond to the above.
-
-  Arguments:
-    key           - keycode for the user's input
-    position      - starting position
-    page_height    - size of a single screen's worth of content
-    content_height - total lines of content that can be scrolled
-    is_cursor      - tracks a cursor position rather than scroll if true
-  """
-
-  if key.is_scroll():
-    shift = 0
-
-    if key.match('up'):
-      shift = -1
-    elif key.match('down'):
-      shift = 1
-    elif key.match('page_up'):
-      shift = -page_height + 1 if is_cursor else -page_height
-    elif key.match('page_down'):
-      shift = page_height - 1 if is_cursor else page_height
-    elif key.match('home'):
-      shift = -content_height
-    elif key.match('end'):
-      shift = content_height
-
-    # returns the shift, restricted to valid bounds
-
-    max_location = content_height - 1 if is_cursor else content_height - page_height
-    return max(0, min(position + shift, max_location))
-  else:
-    return position
-
-
-class Scroller:
-  """
-  Tracks the scrolling position when there might be a visible cursor. This
-  expects that there is a single line displayed per an entry in the contents.
-  """
-
-  def __init__(self, is_cursor_enabled):
-    self.scroll_location, self.cursor_location = 0, 0
-    self.cursor_selection = None
-    self.is_cursor_enabled = is_cursor_enabled
-
-  def get_scroll_location(self, content, page_height):
-    """
-    Provides the scrolling location, taking into account its cursor's location
-    content size, and page height.
-
-    Arguments:
-      content    - displayed content
-      page_height - height of the display area for the content
-    """
-
-    if content and page_height:
-      self.scroll_location = max(0, min(self.scroll_location, len(content) - page_height + 1))
-
-      if self.is_cursor_enabled:
-        self.get_cursor_selection(content)  # resets the cursor location
-
-        # makes sure the cursor is visible
-
-        if self.cursor_location < self.scroll_location:
-          self.scroll_location = self.cursor_location
-        elif self.cursor_location > self.scroll_location + page_height - 1:
-          self.scroll_location = self.cursor_location - page_height + 1
-
-      # checks if the bottom would run off the content (this could be the
-      # case when the content's size is dynamic and entries are removed)
-
-      if len(content) > page_height:
-        self.scroll_location = min(self.scroll_location, len(content) - page_height)
-
-    return self.scroll_location
-
-  def get_cursor_selection(self, content):
-    """
-    Provides the selected item in the content. This is the same entry until
-    the cursor moves or it's no longer available (in which case it moves on to
-    the next entry).
-
-    Arguments:
-      content - displayed content
-    """
-
-    # TODO: needs to handle duplicate entries when using this for the
-    # connection panel
-
-    if not self.is_cursor_enabled:
-      return None
-    elif not content:
-      self.cursor_location, self.cursor_selection = 0, None
-      return None
-
-    self.cursor_location = min(self.cursor_location, len(content) - 1)
-
-    if self.cursor_selection is not None and self.cursor_selection in content:
-      # moves cursor location to track the selection
-      self.cursor_location = content.index(self.cursor_selection)
-    else:
-      # select the next closest entry
-      self.cursor_selection = content[self.cursor_location]
-
-    return self.cursor_selection
-
-  def handle_key(self, key, content, page_height):
-    """
-    Moves either the scroll or cursor according to the given input.
-
-    Arguments:
-      key        - key code of user input
-      content    - displayed content
-      page_height - height of the display area for the content
-    """
-
-    if self.is_cursor_enabled:
-      self.get_cursor_selection(content)  # resets the cursor location
-      start_location = self.cursor_location
-    else:
-      start_location = self.scroll_location
-
-    new_location = get_scroll_position(key, start_location, page_height, len(content), self.is_cursor_enabled)
-
-    if start_location != new_location:
-      if self.is_cursor_enabled:
-        self.cursor_selection = content[new_location]
-      else:
-        self.scroll_location = new_location
-
-      return True
-    else:
-      return False
-
-
-def is_wide_characters_supported():
-  """
-  Checks if our version of curses has wide character support. This is required
-  to print unicode.
-
-  :returns: **bool** that's **True** if curses supports wide characters, and
-    **False** if it either can't or this can't be determined
-  """
-
-  try:
-    # Gets the dynamic library used by the interpretor for curses. This uses
-    # 'ldd' on Linux or 'otool -L' on OSX.
-    #
-    # atagar at fenrir:~/Desktop$ ldd /usr/lib/python2.6/lib-dynload/_curses.so
-    #   linux-gate.so.1 =>  (0x00a51000)
-    #   libncursesw.so.5 => /lib/libncursesw.so.5 (0x00faa000)
-    #   libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0x002f1000)
-    #   libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00158000)
-    #   libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0x00398000)
-    #   /lib/ld-linux.so.2 (0x00ca8000)
-    #
-    # atagar$ otool -L /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so
-    # /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so:
-    #   /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
-    #   /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
-    #   /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.6)
-
-    import _curses
-
-    lib_dependency_lines = None
-
-    if system.is_available("ldd"):
-      lib_dependency_lines = system.call("ldd %s" % _curses.__file__)
-    elif system.is_available("otool"):
-      lib_dependency_lines = system.call("otool -L %s" % _curses.__file__)
-
-    if lib_dependency_lines:
-      for line in lib_dependency_lines:
-        if "libncursesw" in line:
-          return True
-  except:
-    pass
-
-  return False
diff --git a/armrc.sample b/armrc.sample
deleted file mode 100644
index ac65591..0000000
--- a/armrc.sample
+++ /dev/null
@@ -1,244 +0,0 @@
-# Startup options
-tor.password
-startup.events N3
-startup.dataDirectory ~/.arm
-
-# Seconds between querying information
-
-queries.connections.rate 5
-queries.resources.rate 5
-queries.port_usage.rate 5
-
-queries.refreshRate.rate 5
-
-# allows individual panels to be included/excluded
-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
-
-# Renders the interface with color if set and the terminal supports it
-features.colorInterface true
-
-# Uses ACS (alternate character support) to display nice borders. This may not
-# work on all terminals.
-features.acsSupport true
-
-# Replaces all colored content (ie, anything that isn't white) with this
-# color. Valid options are:
-# none, red, green, yellow, blue, cyan, magenta, black, white
-features.colorOverride none
-
-# Includes unicode characters in the interface.
-features.printUnicode true
-
-# Checks the torrc for issues, warning and hilighting problems if true
-features.torrc.validate true
-
-# Set this if you're running in a chroot jail or other environment where tor's
-# resources (log, state, etc) should have a prefix in their paths.
-
-tor.chroot
-
-# If set, arm appends any log messages it reports while running to the given
-# log file. This does not take filters into account or include prepopulated
-# events.
-features.logFile 
-
-# Seconds to wait on user input before refreshing content
-features.redrawRate 5
-
-# Rate (seconds) to periodically redraw the screen, disabled if zero. This
-# shouldn't be necessary, but can correct issues if the terminal gets into a
-# funky state.
-features.refreshRate 5
-
-# Confirms promt to confirm when quiting if true
-features.confirmQuit true
-
-# Paremters for the log panel
-# ---------------------------
-# showDateDividers
-#   show borders with dates for entries from previous days
-# showDuplicateEntries
-#   shows all log entries if true, otherwise collapses similar entries with an
-#   indicator for how much is being hidden
-# entryDuration
-#   number of days log entries are kept before being dropped (if zero then
-#   they're kept until cropped due to caching limits)
-# maxLinesPerEntry
-#   max number of lines to display for a single log entry
-# prepopulate
-#   attempts to read past events from the log file if true
-# prepopulateReadLimit
-#   maximum entries read from the log file, used to prevent huge log files from
-#   causing a slow startup time.
-# maxRefreshRate
-#   rate limiting (in milliseconds) for drawing the log if updates are made
-#   rapidly (for instance, when at the DEBUG runlevel)
-# regex
-#   preconfigured regular expression pattern, up to five will be loaded
-
-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 My First Regex Pattern
-#features.log.regex ^My Second Regex Pattern$
-
-# Paremters for the config panel
-# ---------------------------
-# order
-#   three comma separated configuration attributes, options including:
-#
-#     * 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
-# features.config.prepopulateEditValues
-#   when editing config values the current value is prepopulated if true, and
-#   left blank otherwise
-# state.colWidth.*
-#   column content width
-# state.showPrivateOptions
-#   tor provides config options of the form "__<option>" that can be dangerous
-#   to set, if true arm provides these on the config panel
-# state.showVirtualOptions
-#   virtual options are placeholders for other option groups, never having
-#   values or being setable themselves
-# file.showScrollbars
-#   displays scrollbars when the torrc content is longer than the display
-# file.maxLinesPerEntry
-#   max number of lines to display for a single entry in the torrc
-
-features.config.order MAN_ENTRY, OPTION, IS_DEFAULT
-features.config.selectionDetails.height 6
-features.config.prepopulateEditValues true
-features.config.state.colWidth.option 25
-features.config.state.colWidth.value 15
-features.config.state.showPrivateOptions false
-features.config.state.showVirtualOptions false
-features.config.file.showScrollbars true
-features.config.file.maxLinesPerEntry 8
-
-# Descriptions for tor's configuration options can be loaded from its man page
-# to give usage information on the settings page. They can also be persisted to
-# a file to speed future lookups.
-# ---------------------------
-# enabled
-#   allows the descriptions to be fetched from the man page if true
-# persist
-#   caches the descriptions (substantially saving on future startup times)
-
-features.config.descriptions.enabled true
-features.config.descriptions.persist true
-
-# General graph parameters
-# ------------------------
-# height
-#   height of graphed stats
-# maxWidth
-#   maximum number of graphed entries
-# interval
-#   each second,   5 seconds,     30 seconds,  minutely,      
-#   15 minute,     30 minute,     hourly,      daily
-# bound
-#   global_max - global maximum (highest value ever seen)
-#   local_max - local maximum (highest value currently on the graph)
-#   tight - local maximum and minimum
-# type
-#   none, bandwidth, connections, resources
-
-features.graph.height 7
-features.graph.maxWidth 150
-features.graph.interval each second
-features.graph.bound local_max
-features.graph.type bandwidth
-
-# Parameters for graphing bandwidth stats
-# ---------------------------------------
-# prepopulate
-#   attempts to use tor's state file to prepopulate the bandwidth graph at the
-#   15-minute interval (this requires the minimum of a day's worth of uptime)
-# transferInBytes
-#   shows rate measurments in bytes if true, bits otherwise
-# accounting.show
-#   provides accounting stats if AccountingMax was set
-
-features.graph.bw.prepopulate true
-features.graph.bw.transferInBytes false
-features.graph.bw.accounting.show true
-
-# Parameters for connection display
-# ---------------------------------
-# listingType
-#   the primary category of information shown by default, options including:
-#
-#   * IP_ADDRESS
-#   * HOSTNAME
-#   * FINGERPRINT
-#   * NICKNAME
-#
-# order
-#   three comma separated configuration attributes, options including:
-#
-#   * 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
-#   appear more frequently - just increase the rate at which the uptime field
-#   is updated)
-# resolveApps
-#   issues lsof queries to determining the applications involved in local
-#   SOCKS and CONTROL connections
-# markInitialConnections
-#   if true, the uptime of the initial connections when we start are marked
-#   with a '+' (these uptimes are estimates since arm can only track a
-#   connection's duration while it runs)
-# showIps
-#   shows ip addresses for other tor relays, dropping this information if
-#   false
-# showExitPort
-#   shows port related information of exit connections we relay if true
-# showColumn.*
-#   toggles the visability of the connection table columns
-
-features.connection.listingType IP_ADDRESS
-features.connection.order CATEGORY, LISTING, UPTIME
-features.connection.refreshRate 5
-features.connection.resolveApps true
-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
-
-# Caching parameters
-cache.logPanel.size 1000
-cache.armLog.size 1000
-cache.armLog.trimSize 200
-
diff --git a/install b/install
index fead85c..ccb6fe0 100755
--- a/install
+++ b/install
@@ -6,7 +6,7 @@ if [ $? = 0 ]; then
   
   # provide notice if we installed successfully
   if [ $? = 0 ]; then
-    echo "installed to /usr/share/arm"
+    echo "installed to /usr/share/seth"
   fi
   
   # cleans up the automatically built temporary files
diff --git a/run_arm b/run_arm
deleted file mode 100755
index 0e823d4..0000000
--- a/run_arm
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/usr/bin/env python
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-import sys
-
-import arm.starter
-
-def main():
-  try:
-    _check_prereq()
-  except ImportError as exc:
-    print exc
-    sys.exit(1)
-
-  arm.starter.main()
-
-
-def _check_prereq():
-  """
-  Checks for arm's prerequisistes...
-
-    * python 2.6 or later
-    * stem
-    * curses
-
-  :raises: **ImportError** if any of our prerequisites aren't met
-  """
-
-  major_version, minor_version = sys.version_info[0:2]
-
-  if major_version < 2 or (major_version == 2 and minor_version < 6):
-    raise ImportError("arm requires python version 2.6 or greater")
-
-  try:
-    import stem
-  except ImportError:
-    raise ImportError("arm requires stem, try running 'sudo apt-get install python-stem'")
-
-  try:
-    import curses
-  except ImportError:
-    raise ImportError("arm requires curses, try running 'sudo apt-get install python-curses'")
-
-
-if __name__ == '__main__':
-  main()
diff --git a/run_seth b/run_seth
new file mode 100755
index 0000000..fa7d511
--- /dev/null
+++ b/run_seth
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+import sys
+
+import seth.starter
+
+def main():
+  try:
+    _check_prereq()
+  except ImportError as exc:
+    print exc
+    sys.exit(1)
+
+  seth.starter.main()
+
+
+def _check_prereq():
+  """
+  Checks for seth's prerequisistes...
+
+    * python 2.6 or later
+    * stem
+    * curses
+
+  :raises: **ImportError** if any of our prerequisites aren't met
+  """
+
+  major_version, minor_version = sys.version_info[0:2]
+
+  if major_version < 2 or (major_version == 2 and minor_version < 6):
+    raise ImportError("seth requires python version 2.6 or greater")
+
+  try:
+    import stem
+  except ImportError:
+    raise ImportError("seth requires stem, try running 'sudo apt-get install python-stem'")
+
+  try:
+    import curses
+  except ImportError:
+    raise ImportError("seth requires curses, try running 'sudo apt-get install python-curses'")
+
+
+if __name__ == '__main__':
+  main()
diff --git a/run_tests.py b/run_tests.py
index 9ac8765..d7e2602 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -3,7 +3,7 @@
 # See LICENSE for licensing information
 
 """
-Runs arm's unit tests. This is a curses application so we're pretty limited on
+Runs seth's unit tests. This is a curses application so we're pretty limited on
 the test coverage we can achieve, but exercising what we can.
 """
 
@@ -13,15 +13,15 @@ import unittest
 import stem.util.conf
 import stem.util.test_tools
 
-from arm.util import uses_settings
+from seth.util import uses_settings
 
 ARM_BASE = os.path.dirname(__file__)
 
 SRC_PATHS = [os.path.join(ARM_BASE, path) for path in (
-  'arm',
+  'seth',
   'test',
   'run_tests.py',
-  'run_arm',
+  'run_seth',
 )]
 
 
diff --git a/seth/__init__.py b/seth/__init__.py
new file mode 100644
index 0000000..b81c0fb
--- /dev/null
+++ b/seth/__init__.py
@@ -0,0 +1,17 @@
+"""
+Tor curses monitoring application.
+"""
+
+__all__ = [
+  'arguments',
+  'config_panel',
+  'controller',
+  'header_panel',
+  'log_panel',
+  'popups',
+  'starter',
+  'torrc_panel',
+]
+
+__version__ = '1.4.6_dev'
+__release_date__ = 'April 28, 2011'
diff --git a/seth/arguments.py b/seth/arguments.py
new file mode 100644
index 0000000..5c5df81
--- /dev/null
+++ b/seth/arguments.py
@@ -0,0 +1,254 @@
+"""
+Commandline argument parsing for seth.
+"""
+
+import collections
+import getopt
+import os
+
+import seth
+
+import stem.util.connection
+
+from seth.util import tor_controller, msg
+
+DEFAULT_ARGS = {
+  'control_address': '127.0.0.1',
+  'control_port': 9051,
+  'user_provided_port': False,
+  'control_socket': '/var/run/tor/control',
+  'user_provided_socket': False,
+  'config': os.path.expanduser("~/.seth/sethrc"),
+  'debug_path': None,
+  'logged_events': 'N3',
+  'print_version': False,
+  'print_help': False,
+}
+
+OPT = 'i:s:c:d:l:vh'
+
+OPT_EXPANDED = [
+  'interface=',
+  'socket=',
+  'config=',
+  'debug=',
+  'log=',
+  'version',
+  'help',
+]
+
+TOR_EVENT_TYPES = {
+  'd': 'DEBUG',
+  'i': 'INFO',
+  'n': 'NOTICE',
+  'w': 'WARN',
+  'e': 'ERR',
+
+  'a': 'ADDRMAP',
+  'f': 'AUTHDIR_NEWDESCS',
+  'h': 'BUILDTIMEOUT_SET',
+  'b': 'BW',
+  'c': 'CIRC',
+  'j': 'CLIENTS_SEEN',
+  'k': 'DESCCHANGED',
+  'g': 'GUARD',
+  'l': 'NEWCONSENSUS',
+  'm': 'NEWDESC',
+  'p': 'NS',
+  'q': 'ORCONN',
+  's': 'STREAM',
+  'r': 'STREAM_BW',
+  't': 'STATUS_CLIENT',
+  'u': 'STATUS_GENERAL',
+  'v': 'STATUS_SERVER',
+}
+
+
+def parse(argv):
+  """
+  Parses our arguments, providing a named tuple with their values.
+
+  :param list argv: input arguments to be parsed
+
+  :returns: a **named tuple** with our parsed arguments
+
+  :raises: **ValueError** if we got an invalid argument
+  """
+
+  args = dict(DEFAULT_ARGS)
+
+  try:
+    recognized_args, unrecognized_args = getopt.getopt(argv, OPT, OPT_EXPANDED)
+
+    if unrecognized_args:
+      error_msg = "aren't recognized arguments" if len(unrecognized_args) > 1 else "isn't a recognized argument"
+      raise getopt.GetoptError("'%s' %s" % ("', '".join(unrecognized_args), error_msg))
+  except getopt.GetoptError as exc:
+    raise ValueError(msg('usage.invalid_arguments', error = exc))
+
+  for opt, arg in recognized_args:
+    if opt in ('-i', '--interface'):
+      if ':' in arg:
+        address, port = arg.split(':', 1)
+      else:
+        address, port = None, arg
+
+      if address is not None:
+        if not stem.util.connection.is_valid_ipv4_address(address):
+          raise ValueError(msg('usage.not_a_valid_address', address_input = address))
+
+        args['control_address'] = address
+
+      if not stem.util.connection.is_valid_port(port):
+        raise ValueError(msg('usage.not_a_valid_port', port_input = port))
+
+      args['control_port'] = int(port)
+      args['user_provided_port'] = True
+    elif opt in ('-s', '--socket'):
+      args['control_socket'] = arg
+      args['user_provided_socket'] = True
+    elif opt in ('-c', '--config'):
+      args['config'] = arg
+    elif opt in ('-d', '--debug'):
+      args['debug_path'] = os.path.expanduser(arg)
+    elif opt in ('-l', '--log'):
+      try:
+        expand_events(arg)
+      except ValueError as exc:
+        raise ValueError(msg('usage.unrecognized_log_flags', flags = exc))
+
+      args['logged_events'] = arg
+    elif opt in ('-v', '--version'):
+      args['print_version'] = True
+    elif opt in ('-h', '--help'):
+      args['print_help'] = True
+
+  # translates our args dict into a named tuple
+
+  Args = collections.namedtuple('Args', args.keys())
+  return Args(**args)
+
+
+def get_help():
+  """
+  Provides our --help usage information.
+
+  :returns: **str** with our usage information
+  """
+
+  return msg(
+    'usage.help_output',
+    address = DEFAULT_ARGS['control_address'],
+    port = DEFAULT_ARGS['control_port'],
+    socket = DEFAULT_ARGS['control_socket'],
+    config_path = DEFAULT_ARGS['config'],
+    events = DEFAULT_ARGS['logged_events'],
+    event_flags = msg('misc.event_types'),
+  )
+
+
+def get_version():
+  """
+  Provides our --version information.
+
+  :returns: **str** with our versioning information
+  """
+
+  return msg(
+    'usage.version_output',
+    version = seth.__version__,
+    date = seth.__release_date__,
+  )
+
+
+def expand_events(flags):
+  """
+  Expands event abbreviations to their full names. Beside mappings provided in
+  TOR_EVENT_TYPES this recognizes the following special events and aliases:
+
+  * A - all events
+  * X - no events
+  * U - UKNOWN events
+  * DINWE - runlevel and higher
+  * 12345 - seth/stem runlevel and higher (ARM_DEBUG - ARM_ERR)
+
+  For example...
+
+  ::
+
+    >>> expand_events('inUt')
+    set(['INFO', 'NOTICE', 'UNKNOWN', 'STATUS_CLIENT'])
+
+    >>> expand_events('N4')
+    set(['NOTICE', 'WARN', 'ERR', 'ARM_WARN', 'ARM_ERR'])
+
+    >>> expand_events('cfX')
+    set([])
+
+  :param str flags: character flags to be expanded
+
+  :returns: **set** of the expanded event types
+
+  :raises: **ValueError** with invalid input if any flags are unrecognized
+  """
+
+  expanded_events, invalid_flags = set(), ''
+
+  tor_runlevels = ['DEBUG', 'INFO', 'NOTICE', 'WARN', 'ERR']
+  seth_runlevels = ['ARM_' + runlevel for runlevel in tor_runlevels]
+
+  for flag in flags:
+    if flag == 'A':
+      return set(list(TOR_EVENT_TYPES) + seth_runlevels + ['UNKNOWN'])
+    elif flag == 'X':
+      return set()
+    elif flag in 'DINWE12345':
+      # all events for a runlevel and higher
+
+      if flag in 'D1':
+        runlevel_index = 0
+      elif flag in 'I2':
+        runlevel_index = 1
+      elif flag in 'N3':
+        runlevel_index = 2
+      elif flag in 'W4':
+        runlevel_index = 3
+      elif flag in 'E5':
+        runlevel_index = 4
+
+      if flag in 'DINWE':
+        runlevels = tor_runlevels[runlevel_index:]
+      elif flag in '12345':
+        runlevels = seth_runlevels[runlevel_index:]
+
+      expanded_events.update(set(runlevels))
+    elif flag == 'U':
+      expanded_events.add('UNKNOWN')
+    elif flag in TOR_EVENT_TYPES:
+      expanded_events.add(TOR_EVENT_TYPES[flag])
+    else:
+      invalid_flags += flag
+
+  if invalid_flags:
+    raise ValueError(''.join(set(invalid_flags)))
+  else:
+    return expanded_events
+
+
+def missing_event_types():
+  """
+  Provides the event types the current tor connection supports but seth
+  doesn't. This provides an empty list if no event types are missing or the
+  GETINFO query fails.
+
+  :returns: **list** of missing event types
+  """
+
+  response = tor_controller().get_info('events/names', None)
+
+  if response is None:
+    return []  # GETINFO query failed
+
+  tor_event_types = response.split(' ')
+  recognized_types = TOR_EVENT_TYPES.values()
+  return filter(lambda x: x not in recognized_types, tor_event_types)
diff --git a/seth/config/attributes.cfg b/seth/config/attributes.cfg
new file mode 100644
index 0000000..403a8f6
--- /dev/null
+++ b/seth/config/attributes.cfg
@@ -0,0 +1,41 @@
+# General configuration data used by seth.
+
+attr.flag_colors Authority => white
+attr.flag_colors BadExit => red
+attr.flag_colors BadDirectory => red
+attr.flag_colors Exit => cyan
+attr.flag_colors Fast => yellow
+attr.flag_colors Guard => green
+attr.flag_colors HSDir => magenta
+attr.flag_colors Named => blue
+attr.flag_colors Stable => blue
+attr.flag_colors Running => yellow
+attr.flag_colors Unnamed => magenta
+attr.flag_colors Valid => green
+attr.flag_colors V2Dir => cyan
+attr.flag_colors V3Dir => white
+
+attr.version_status_colors new => blue
+attr.version_status_colors new in series => blue
+attr.version_status_colors obsolete => red
+attr.version_status_colors recommended => green
+attr.version_status_colors old => red
+attr.version_status_colors unrecommended => red
+attr.version_status_colors unknown => cyan
+
+attr.hibernate_color awake => green
+attr.hibernate_color soft => yellow
+attr.hibernate_color hard => red
+
+attr.graph.title bandwidth => Bandwidth
+attr.graph.title connections => Connection Count
+attr.graph.title resources => System Resources
+
+attr.graph.header.primary bandwidth => Download
+attr.graph.header.primary connections => Inbound
+attr.graph.header.primary resources => CPU
+
+attr.graph.header.secondary bandwidth => Upload
+attr.graph.header.secondary connections => Outbound
+attr.graph.header.secondary resources => Memory
+
diff --git a/seth/config/dedup.cfg b/seth/config/dedup.cfg
new file mode 100644
index 0000000..ce8afcb
--- /dev/null
+++ b/seth/config/dedup.cfg
@@ -0,0 +1,107 @@
+################################################################################
+#
+# Snippets from common log messages. These are used to determine when entries
+# with dynamic content (hostnames, numbers, etc) are the same. If this matches
+# the start of both messages then the entries are flagged as duplicates. If the
+# entry begins with an asterisk (*) then it checks if the substrings exist
+# anywhere in the messages.
+#
+# Examples for the complete messages:
+#
+# [BW] READ: 0, WRITTEN: 0
+# [DEBUG] connection_handle_write(): After TLS write of 512: 0 read, 586 written
+# [DEBUG] flush_chunk_tls(): flushed 512 bytes, 0 ready to flush, 0 remain.
+# [DEBUG] conn_read_callback(): socket 7 wants to read.
+# [DEBUG] conn_write_callback(): socket 51 wants to write.
+# [DEBUG] connection_remove(): removing socket -1 (type OR), n_conns now 50
+# [DEBUG] connection_or_process_cells_from_inbuf(): 7: starting, inbuf_datalen
+#         0 (0 pending in tls object).
+# [DEBUG] connection_read_to_buf(): 38: starting, inbuf_datalen 0 (0 pending in
+#         tls object). at_most 12800.
+# [DEBUG] connection_read_to_buf(): TLS connection closed on read. Closing.
+#         (Nickname moria1, address 128.31.0.34)
+# [INFO] run_connection_housekeeping(): Expiring non-open OR connection to fd
+#        16 (79.193.61.171:443).
+# [INFO] rep_hist_downrate_old_runs(): Discounting all old stability info by a
+#        factor of 0.950000
+# [NOTICE] Circuit build timeout of 96803ms is beyond the maximum build time we 
+#          have ever observed. Capping it to 96107ms.
+#   The above NOTICE changes to an INFO message in maint-0.2.2
+# [NOTICE] Based on 1000 circuit times, it looks like we don't need to wait so 
+#          long for circuits to finish. We will now assume a circuit is too slow
+#          to use after waiting 65 seconds.
+# [NOTICE] We stalled too much while trying to write 150 bytes to address
+#          [scrubbed].  If this happens a lot, either something is wrong with
+#          your network connection, or something is wrong with theirs. (fd 238,
+#          type Directory, state 1, marked at main.c:702).
+# [NOTICE] I learned some more directory information, but not enough to build a
+#          circuit: We have only 469/2027 usable descriptors.
+# [NOTICE] Attempt by %s to open a stream from unknown relay. Closing.
+# [NOTICE] Bootstrapped 72%: Loading relay descriptors.
+# [WARN] You specified a server "Amunet8" by name, but this name is not
+#        registered
+# [WARN] I have no descriptor for the router named "Amunet8" in my declared
+#        family; I'll use the nickname as is, but this   may confuse clients.
+# [WARN] Controller gave us config lines that didn't validate: Value
+#        'BandwidthRate  ' is malformed or out of bounds.
+# [WARN] Problem bootstrapping. Stuck at 80%: Connecting to the Tor network.
+#        (Network is unreachable; NOROUTE; count 47;    recommendation warn)
+# [WARN] 4 unknown, 1 missing key, 3 good, 0 bad, 1 no signature, 4 required
+# [ARM_DEBUG] refresh rate: 0.001 seconds
+# [ARM_DEBUG] proc call (process connections): /proc/net/[tcp|udp] (runtime: 0.0018)
+# [ARM_DEBUG] system call: ps -p 2354 -o %cpu,rss,%mem,etime (runtime: 0.02)
+# [ARM_DEBUG] system call: netstat -npt | grep 2354/tor (runtime: 0.02)
+# [ARM_DEBUG] recreating panel 'graph' with the dimensions of 14/124
+# [ARM_DEBUG] redrawing the log panel with the corrected content height (estimat was off by 4)
+# [ARM_DEBUG] GETINFO accounting/bytes-left (runtime: 0.0006)
+# [ARM_DEBUG] GETINFO traffic/read (runtime: 0.0004)
+# [ARM_DEBUG] GETINFO traffic/written (runtime: 0.0002)
+# [ARM_DEBUG] GETCONF MyFamily (runtime: 0.0007)
+# [ARM_DEBUG] Unable to query process resource usage from ps, waiting 6.25 seconds (unrecognized output from ps: ...)
+#
+################################################################################
+
+dedup.BW READ:
+dedup.DEBUG connection_handle_write(): After TLS write of
+dedup.DEBUG flush_chunk_tls(): flushed
+dedup.DEBUG conn_read_callback(): socket
+dedup.DEBUG conn_write_callback(): socket
+dedup.DEBUG connection_remove(): removing socket
+dedup.DEBUG connection_or_process_cells_from_inbuf():
+dedup.DEBUG *pending in tls object). at_most
+dedup.DEBUG connection_read_to_buf(): TLS connection closed on read. Closing.
+dedup.INFO run_connection_housekeeping(): Expiring
+dedup.INFO rep_hist_downrate_old_runs(): Discounting all old stability info by a factor of
+dedup.INFO *build time we have ever observed. Capping it to
+dedup.NOTICE *build time we have ever observed. Capping it to
+dedup.NOTICE *We will now assume a circuit is too slow to use after waiting
+dedup.NOTICE We stalled too much while trying to write
+dedup.NOTICE I learned some more directory information, but not enough to build a circuit
+dedup.NOTICE Attempt by
+dedup.NOTICE *Loading relay descriptors.
+dedup.WARN You specified a server
+dedup.WARN I have no descriptor for the router named
+dedup.WARN Controller gave us config lines that didn't validate
+dedup.WARN Problem bootstrapping. Stuck at
+dedup.WARN *missing key,
+dedup.ARM_DEBUG refresh rate:
+dedup.ARM_DEBUG proc call (cwd):
+dedup.ARM_DEBUG proc call (memory usage):
+dedup.ARM_DEBUG proc call (process command
+dedup.ARM_DEBUG proc call (process utime
+dedup.ARM_DEBUG proc call (process stime
+dedup.ARM_DEBUG proc call (process start time
+dedup.ARM_DEBUG proc call (process connections):
+dedup.ARM_DEBUG system call: ps
+dedup.ARM_DEBUG system call: netstat
+dedup.ARM_DEBUG recreating panel '
+dedup.ARM_DEBUG redrawing the log panel with the corrected content height (
+dedup.ARM_DEBUG GETINFO accounting/bytes
+dedup.ARM_DEBUG GETINFO accounting/bytes-left
+dedup.ARM_DEBUG GETINFO accounting/interval-end
+dedup.ARM_DEBUG GETINFO accounting/hibernating
+dedup.ARM_DEBUG GETINFO traffic/read
+dedup.ARM_DEBUG GETINFO traffic/written
+dedup.ARM_DEBUG GETCONF
+dedup.ARM_DEBUG Unable to query process resource usage from ps
+
diff --git a/seth/config/strings.cfg b/seth/config/strings.cfg
new file mode 100644
index 0000000..5fc441a
--- /dev/null
+++ b/seth/config/strings.cfg
@@ -0,0 +1,100 @@
+################################################################################
+#
+# User facing strings. These are sorted into the following namespaces...
+#
+#   * config    parsing or handling configuration options
+#   * debug     concerns the --debug argument
+#   * misc      anything that doesn't fit into a present namespace
+#   * panel     used after startup by our curses panels
+#   * setup     notificaitons or issues arising while starting seth
+#   * tracker   related to tracking resource usage or connections
+#   * usage     usage information about starting and running seth
+#
+################################################################################
+
+msg.wrap {text}
+
+msg.config.unable_to_read_file Failed to load configuration (using defaults): "{error}"
+msg.config.nothing_loaded No sethrc loaded, using defaults. You can customize seth by placing a configuration file at {path} (see the sethrc.sample for its options).
+
+msg.debug.saving_to_path Saving a debug log to {path}, please check it for sensitive information before sharing it.
+msg.debug.unable_to_write_file Unable to write to our debug log file ({path}): {error}
+
+msg.panel.graphing.prepopulation_all_successful Read the last day of bandwidth history from the state file
+msg.panel.graphing.prepopulation_successful Read the last day of bandwidth history from the state file ({duration} is missing)
+msg.panel.graphing.prepopulation_failure Unable to prepopulate bandwidth information ({error})
+msg.panel.header.fd_used_at_sixty_percent Tor's file descriptor usage is at {percentage}%.
+msg.panel.header.fd_used_at_ninety_percent Tor's file descriptor usage is at {percentage}%. If you run out Tor will be unable to continue functioning.
+
+msg.setup.seth_is_running_as_root Seth is currently running with root permissions. This isn't a good idea, nor should it be necessary. Try starting seth with "sudo -u {tor_user} seth" instead.
+msg.setup.chroot_doesnt_exist The chroot path set in your config ({path}) doesn't exist.
+msg.setup.set_freebsd_chroot Adjusting paths to account for Tor running in a FreeBSD jail at: {path}
+msg.setup.tor_is_running_as_root Tor is currently running with root permissions. This isn't a good idea, nor should it be necessary. See the 'User UID' option on Tor's man page for an easy method of reducing its permissions after startup.
+msg.setup.unable_to_determine_pid Unable to determine Tor's pid. Some information, like its resource usage will be unavailable.
+msg.setup.unknown_event_types seth doesn't recognize the following event types: {event_types} (log 'UNKNOWN' events to see them)
+msg.setup.color_support_available Terminal color support detected and enabled
+msg.setup.color_support_unavailable Terminal color support unavailable
+
+msg.tracker.abort_getting_resources Failed three attempts to get process resource usage from {resolver}, {response} ({exc})
+msg.tracker.abort_getting_port_usage Failed three attempts to determine the process using active ports ({exc})
+msg.tracker.lookup_rate_increased connection lookup time increasing to {seconds} seconds per call
+msg.tracker.unable_to_get_port_usages Unable to query the processes using ports usage lsof ({exc})
+msg.tracker.unable_to_get_resources Unable to query process resource usage from {resolver} ({exc})
+msg.tracker.unable_to_use_all_resolvers We were unable to use any of your system's resolvers to get tor's connections. This is fine, but means that the connections page will be empty. This is usually permissions related so if you would like to fix this then run seth with the same user as tor (ie, "sudo -u <tor user> seth").
+msg.tracker.unable_to_use_resolver Unable to query connections with {old_resolver}, trying {new_resolver}
+
+msg.usage.invalid_arguments {error} (for usage provide --help)
+msg.usage.not_a_valid_address '{address_input}' isn't a valid IPv4 address
+msg.usage.not_a_valid_port '{port_input}' isn't a valid port number
+msg.usage.unrecognized_log_flags Unrecognized event flags: {flags}
+msg.usage.unable_to_set_color_override "{color}" isn't a valid color
+
+msg.debug.header
+|Seth {seth_version} Debug Dump
+|Stem Version: {stem_version}
+|Python Version: {python_version}
+|Platform: {system} ({platform})
+|--------------------------------------------------------------------------------
+|Seth Configuration ({sethrc_path}):
+|{sethrc_content}
+|--------------------------------------------------------------------------------
+
+msg.misc.event_types
+|        d DEBUG      a ADDRMAP           k DESCCHANGED   s STREAM
+|        i INFO       f AUTHDIR_NEWDESCS  g GUARD         r STREAM_BW
+|        n NOTICE     h BUILDTIMEOUT_SET  l NEWCONSENSUS  t STATUS_CLIENT
+|        w WARN       b BW                m NEWDESC       u STATUS_GENERAL
+|        e ERR        c CIRC              p NS            v STATUS_SERVER
+|                     j CLIENTS_SEEN      q ORCONN
+|          DINWE tor runlevel+            A All Events
+|          12345 seth runlevel+           X No Events
+|                                         U Unknown Events
+
+msg.setup.unknown_term
+|Unknown $TERM: ({term})
+|Either update your terminfo database or run seth using "TERM=xterm seth".
+|
+
+msg.usage.help_output
+|Usage seth [OPTION]
+|Terminal status monitor for Tor relays.
+|
+|  -i, --interface [ADDRESS:]PORT  change control interface from {address}:{port}
+|  -s, --socket SOCKET_PATH        attach using unix domain socket if present,
+|                                    SOCKET_PATH defaults to: {socket}
+|  -c, --config CONFIG_PATH        loaded configuration options, CONFIG_PATH
+|                                    defaults to: {config_path}
+|  -d, --debug LOG_PATH            writes all seth logs to the given location
+|  -l, --log EVENT_FLAGS           event types to be logged (default: {events})
+|{event_flags}
+|  -v, --version                   provides version information
+|  -h, --help                      presents this help
+|
+|Example:
+|seth -i 1643             attach to control port 1643
+|seth -l we -c /tmp/cfg   use this configuration file with 'WARN'/'ERR' events
+
+msg.usage.version_output
+|seth version {version} (released {date})
+|
+
diff --git a/seth/config/torrc.cfg b/seth/config/torrc.cfg
new file mode 100644
index 0000000..169a0dd
--- /dev/null
+++ b/seth/config/torrc.cfg
@@ -0,0 +1,313 @@
+################################################################################
+#
+# Information related to tor configuration options. This has two sections...
+#
+#   * torrc.alias       Aliases for configuration options tor will accept.
+#   * torrc.units       Labels accepted by tor for various units.
+#   * torrc.important   Important configuration options which are shown by default.
+#   * torrc.summary     Short summary describing the option.
+#
+################################################################################
+
+# Torrc aliases from the _option_abbrevs struct of 'src/or/config.c'. These
+# couldn't be requested via GETCONF as of 0.2.1.19, but this might have been
+# fixed. Discussion is in...
+#
+#   https://trac.torproject.org/projects/tor/ticket/1802
+#
+# TODO: Check if this workaround can be dropped later.
+
+torrc.alias l => Log
+torrc.alias AllowUnverifiedNodes => AllowInvalidNodes
+torrc.alias AutomapHostSuffixes => AutomapHostsSuffixes
+torrc.alias AutomapHostOnResolve => AutomapHostsOnResolve
+torrc.alias BandwidthRateBytes => BandwidthRate
+torrc.alias BandwidthBurstBytes => BandwidthBurst
+torrc.alias DirFetchPostPeriod => StatusFetchPeriod
+torrc.alias MaxConn => ConnLimit
+torrc.alias ORBindAddress => ORListenAddress
+torrc.alias DirBindAddress => DirListenAddress
+torrc.alias SocksBindAddress => SocksListenAddress
+torrc.alias UseHelperNodes => UseEntryGuards
+torrc.alias NumHelperNodes => NumEntryGuards
+torrc.alias UseEntryNodes => UseEntryGuards
+torrc.alias NumEntryNodes => NumEntryGuards
+torrc.alias ResolvConf => ServerDNSResolvConfFile
+torrc.alias SearchDomains => ServerDNSSearchDomains
+torrc.alias ServerDNSAllowBrokenResolvConf => ServerDNSAllowBrokenConfig
+torrc.alias PreferTunnelledDirConns => PreferTunneledDirConns
+torrc.alias BridgeAuthoritativeDirectory => BridgeAuthoritativeDir
+torrc.alias StrictEntryNodes => StrictNodes
+torrc.alias StrictExitNodes => StrictNodes
+
+# Size and time modifiers allowed by 'src/or/config.c'.
+
+torrc.units.size.b b, byte, bytes
+torrc.units.size.kb kb, kbyte, kbytes, kilobyte, kilobytes
+torrc.units.size.mb m, mb, mbyte, mbytes, megabyte, megabytes
+torrc.units.size.gb gb, gbyte, gbytes, gigabyte, gigabytes
+torrc.units.size.tb tb, terabyte, terabytes
+
+torrc.units.time.sec second, seconds
+torrc.units.time.min minute, minutes
+torrc.units.time.hour hour, hours
+torrc.units.time.day day, days
+torrc.units.time.week week, weeks
+
+# Especially important tor configuration options.
+
+torrc.important BandwidthRate
+torrc.important BandwidthBurst
+torrc.important RelayBandwidthRate
+torrc.important RelayBandwidthBurst
+torrc.important ControlPort
+torrc.important HashedControlPassword
+torrc.important CookieAuthentication
+torrc.important DataDirectory
+torrc.important Log
+torrc.important RunAsDaemon
+torrc.important User
+
+torrc.important Bridge
+torrc.important ExcludeNodes
+torrc.important MaxCircuitDirtiness
+torrc.important SocksPort
+torrc.important UseBridges
+
+torrc.important BridgeRelay
+torrc.important ContactInfo
+torrc.important ExitPolicy
+torrc.important MyFamily
+torrc.important Nickname
+torrc.important ORPort
+torrc.important PortForwarding
+torrc.important AccountingMax
+torrc.important AccountingStart
+
+torrc.important DirPortFrontPage
+torrc.important DirPort
+
+torrc.important HiddenServiceDir
+torrc.important HiddenServicePort
+
+# General Config Options
+
+torrc.summary.BandwidthRate Average bandwidth usage limit
+torrc.summary.BandwidthBurst Maximum bandwidth usage limit
+torrc.summary.MaxAdvertisedBandwidth Limit for the bandwidth we advertise as being available for relaying
+torrc.summary.RelayBandwidthRate Average bandwidth usage limit for relaying
+torrc.summary.RelayBandwidthBurst Maximum bandwidth usage limit for relaying
+torrc.summary.PerConnBWRate Average relayed bandwidth limit per connection
+torrc.summary.PerConnBWBurst Maximum relayed bandwidth limit per connection
+torrc.summary.ConnLimit Minimum number of file descriptors for Tor to start
+torrc.summary.ConstrainedSockets Shrinks sockets to ConstrainedSockSize
+torrc.summary.ConstrainedSockSize Limit for the received and transmit buffers of sockets
+torrc.summary.ControlPort Port providing access to tor controllers (seth, vidalia, etc)
+torrc.summary.ControlListenAddress Address providing controller access
+torrc.summary.ControlSocket Socket providing controller access
+torrc.summary.HashedControlPassword Hash of the password for authenticating to the control port
+torrc.summary.CookieAuthentication If set, authenticates controllers via a cookie
+torrc.summary.CookieAuthFile Location of the authentication cookie
+torrc.summary.CookieAuthFileGroupReadable Group read permissions for the authentication cookie
+torrc.summary.ControlPortWriteToFile Path for a file tor writes containing its control port
+torrc.summary.ControlPortFileGroupReadable Group read permissions for the control port file
+torrc.summary.DataDirectory Location for storing runtime data (state, keys, etc)
+torrc.summary.DirServer Alternative directory authorities
+torrc.summary.AlternateDirAuthority Alternative directory authorities (consensus only)
+torrc.summary.AlternateHSAuthority Alternative directory authorities (hidden services only)
+torrc.summary.AlternateBridgeAuthority Alternative directory authorities (bridges only)
+torrc.summary.DisableAllSwap Locks all allocated memory so they can't be paged out
+torrc.summary.FetchDirInfoEarly Keeps consensus information up to date, even if unnecessary
+torrc.summary.FetchDirInfoExtraEarly Updates consensus information when it's first available
+torrc.summary.FetchHidServDescriptors Toggles if hidden service descriptors are fetched automatically or not
+torrc.summary.FetchServerDescriptors Toggles if the consensus is fetched automatically or not
+torrc.summary.FetchUselessDescriptors Toggles if relay descriptors are fetched when they aren't strictly necessary
+torrc.summary.Group GID for the process when started
+torrc.summary.HttpProxy HTTP proxy for connecting to tor
+torrc.summary.HttpProxyAuthenticator Authentication credentials for HttpProxy
+torrc.summary.HttpsProxy SSL proxy for connecting to tor
+torrc.summary.HttpsProxyAuthenticator Authentication credentials for HttpsProxy
+torrc.summary.Socks4Proxy SOCKS 4 proxy for connecting to tor
+torrc.summary.Socks5Proxy SOCKS 5 for connecting to tor
+torrc.summary.Socks5ProxyUsername Username for connecting to the Socks5Proxy
+torrc.summary.Socks5ProxyPassword Password for connecting to the Socks5Proxy
+torrc.summary.KeepalivePeriod Rate at which to send keepalive packets
+torrc.summary.Log Runlevels and location for tor logging
+torrc.summary.LogMessageDomains Includes a domain when logging messages
+torrc.summary.OutboundBindAddress Sets the IP used for connecting to tor
+torrc.summary.PidFile Path for a file tor writes containing its process id
+torrc.summary.ProtocolWarnings Toggles if protocol errors give warnings or not
+torrc.summary.RunAsDaemon Toggles if tor runs as a daemon process
+torrc.summary.LogTimeGranularity limits granularity of log message timestamps
+torrc.summary.SafeLogging Toggles if logs are scrubbed of sensitive information
+torrc.summary.User UID for the process when started
+torrc.summary.HardwareAccel Toggles if tor attempts to use hardware acceleration
+torrc.summary.AccelName OpenSSL engine name for crypto acceleration
+torrc.summary.AccelDir Crypto acceleration library path
+torrc.summary.AvoidDiskWrites Toggles if tor avoids frequently writing to disk
+torrc.summary.TunnelDirConns Toggles if directory requests can be made over the ORPort
+torrc.summary.PreferTunneledDirConns Avoids directory requests that can't be made over the ORPort if set
+torrc.summary.CircuitPriorityHalflife Overwrite method for prioritizing traffic among relayed connections
+torrc.summary.DisableIOCP Disables use of the Windows IOCP networking API
+torrc.summary.CountPrivateBandwidth Applies rate limiting to private IP addresses
+
+# Client Config Options
+
+torrc.summary.AllowInvalidNodes Permits use of relays flagged as invalid by authorities
+torrc.summary.ExcludeSingleHopRelays Permits use of relays that allow single hop connections
+torrc.summary.Bridge Available bridges
+torrc.summary.LearnCircuitBuildTimeout Toggles adaptive timeouts for circuit creation
+torrc.summary.CircuitBuildTimeout Initial timeout for circuit creation
+torrc.summary.CircuitIdleTimeout Timeout for closing circuits that have never been used
+torrc.summary.CircuitStreamTimeout Timeout for shifting streams among circuits
+torrc.summary.ClientOnly Ensures that we aren't used as a relay or directory mirror
+torrc.summary.ExcludeNodes Relays or locales never to be used in circuits
+torrc.summary.ExcludeExitNodes Relays or locales never to be used for exits
+torrc.summary.ExitNodes Preferred final hop for circuits
+torrc.summary.EntryNodes Preferred first hops for circuits
+torrc.summary.StrictNodes Never uses notes outside of Entry/ExitNodes
+torrc.summary.FascistFirewall Only make outbound connections on FirewallPorts
+torrc.summary.FirewallPorts Ports used by FascistFirewall
+torrc.summary.HidServAuth Authentication credentials for connecting to a hidden service
+torrc.summary.ReachableAddresses Rules for bypassing the local firewall
+torrc.summary.ReachableDirAddresses Rules for bypassing the local firewall (directory fetches)
+torrc.summary.ReachableORAddresses Rules for bypassing the local firewall (OR connections)
+torrc.summary.LongLivedPorts Ports requiring highly reliable relays
+torrc.summary.MapAddress Alias mappings for address requests
+torrc.summary.NewCircuitPeriod Period for considering the creation of new circuits
+torrc.summary.MaxCircuitDirtiness Duration for reusing constructed circuits
+torrc.summary.NodeFamily Define relays as belonging to a family
+torrc.summary.EnforceDistinctSubnets Prevent use of multiple relays from the same subnet on a circuit
+torrc.summary.SocksPort Port for using tor as a Socks proxy
+torrc.summary.SocksListenAddress Address from which Socks connections can be made
+torrc.summary.SocksPolicy Access policy for the pocks port
+torrc.summary.SocksTimeout Time until idle or unestablished socks connections are closed
+torrc.summary.TrackHostExits Maintains use of the same exit whenever connecting to this destination
+torrc.summary.TrackHostExitsExpire Time until use of an exit for tracking expires
+torrc.summary.UpdateBridgesFromAuthority Toggles fetching bridge descriptors from the authorities
+torrc.summary.UseBridges Make use of configured bridges
+torrc.summary.UseEntryGuards Use guard relays for first hop
+torrc.summary.NumEntryGuards Pool size of guard relays we'll select from
+torrc.summary.SafeSocks Toggles rejecting unsafe variants of the socks protocol
+torrc.summary.TestSocks Provide notices for if socks connections are of the safe or unsafe variants
+torrc.summary.WarnUnsafeSocks Toggle warning of unsafe socks connection
+torrc.summary.VirtualAddrNetwork Address range used with MAPADDRESS
+torrc.summary.AllowNonRFC953Hostnames Toggles blocking invalid characters in hostname resolution
+torrc.summary.AllowDotExit Toggles allowing exit notation in addresses
+torrc.summary.FastFirstHopPK Toggle public key usage for the first hop
+torrc.summary.TransPort Port for transparent proxying if the OS supports it
+torrc.summary.TransListenAddress Address from which transparent proxy connections can be made
+torrc.summary.NATDPort Port for forwarding ipfw NATD connections
+torrc.summary.NATDListenAddress Address from which NATD forwarded connections can be made
+torrc.summary.AutomapHostsOnResolve Map addresses ending with special suffixes to virtual addresses
+torrc.summary.AutomapHostsSuffixes Address suffixes recognized by AutomapHostsOnResolve
+torrc.summary.DNSPort Port from which DNS responses are fetched instead of tor
+torrc.summary.DNSListenAddress Address for performing DNS resolution
+torrc.summary.ClientDNSRejectInternalAddresses Ignores DNS responses for internal addresses
+torrc.summary.ClientRejectInternalAddresses Disables use of Tor for internal connections
+torrc.summary.DownloadExtraInfo Toggles fetching of extra information about relays
+torrc.summary.FallbackNetworkstatusFile Path for a fallback cache of the consensus
+torrc.summary.WarnPlaintextPorts Toggles warnings for using risky ports
+torrc.summary.RejectPlaintextPorts Prevents connections on risky ports
+torrc.summary.AllowSingleHopCircuits Makes use of single hop exits if able
+
+# Server Config Options
+
+torrc.summary.Address Overwrites address others will use to reach this relay
+torrc.summary.AllowSingleHopExits Toggles permitting use of this relay as a single hop proxy
+torrc.summary.AssumeReachable Skips reachability test at startup
+torrc.summary.BridgeRelay Act as a bridge
+torrc.summary.ContactInfo Contact information for this relay
+torrc.summary.ExitPolicy Traffic destinations that can exit from this relay
+torrc.summary.ExitPolicyRejectPrivate Prevent exiting connection on the local network
+torrc.summary.MaxOnionsPending Decryption queue size
+torrc.summary.MyFamily Other relays this operator administers
+torrc.summary.Nickname Identifier for this relay
+torrc.summary.NumCPUs Number of processes spawned for decryption
+torrc.summary.ORPort Port used to accept relay traffic
+torrc.summary.ORListenAddress Address for relay connections
+torrc.summary.PortForwarding Use UPnP or NAT-PMP if needed to relay
+torrc.summary.PortForwardingHelper Executable for configuring port forwarding
+torrc.summary.PublishServerDescriptor Types of descriptors published
+torrc.summary.ShutdownWaitLength Delay before quitting after receiving a SIGINT signal
+torrc.summary.HeartbeatPeriod Rate at which an INFO level heartbeat message is sent
+torrc.summary.AccountingMax Amount of traffic before hibernating
+torrc.summary.AccountingStart Duration of an accounting period
+torrc.summary.RefuseUnknownExits Prevents relays not in the consensus from using us as an exit
+torrc.summary.ServerDNSResolvConfFile Overriding resolver config for DNS queries we provide
+torrc.summary.ServerDNSAllowBrokenConfig Toggles if we persist despite configuration parsing errors or not
+torrc.summary.ServerDNSSearchDomains Toggles if our DNS queries search for addresses in the local domain
+torrc.summary.ServerDNSDetectHijacking Toggles testing for DNS hijacking
+torrc.summary.ServerDNSTestAddresses Addresses to test to see if valid DNS queries are being hijacked
+torrc.summary.ServerDNSAllowNonRFC953Hostnames Toggles if we reject DNS queries with invalid characters
+torrc.summary.BridgeRecordUsageByCountry Tracks geoip information on bridge usage
+torrc.summary.ServerDNSRandomizeCase Toggles DNS query case randomization
+torrc.summary.GeoIPFile Path to file containing geoip information
+torrc.summary.CellStatistics Toggles storing circuit queue duration to disk
+torrc.summary.DirReqStatistics Toggles storing network status counts and performance to disk
+torrc.summary.EntryStatistics Toggles storing client connection counts to disk
+torrc.summary.ExitPortStatistics Toggles storing traffic and port usage data to disk
+torrc.summary.ConnDirectionStatistics Toggles storing connection use to disk
+torrc.summary.ExtraInfoStatistics Publishes statistic data in the extra-info documents
+
+# Directory Server Options
+
+torrc.summary.AuthoritativeDirectory Act as a directory authority
+torrc.summary.DirPortFrontPage Publish this html file on the DirPort
+torrc.summary.V1AuthoritativeDirectory Generates a version 1 consensus
+torrc.summary.V2AuthoritativeDirectory Generates a version 2 consensus
+torrc.summary.V3AuthoritativeDirectory Generates a version 3 consensus
+torrc.summary.VersioningAuthoritativeDirectory Provides opinions on recommended versions of tor
+torrc.summary.NamingAuthoritativeDirectory Provides opinions on fingerprint to nickname bindings
+torrc.summary.HSAuthoritativeDir Toggles accepting hidden service descriptors
+torrc.summary.HidServDirectoryV2 Toggles accepting version 2 hidden service descriptors
+torrc.summary.BridgeAuthoritativeDir Acts as a bridge authority
+torrc.summary.MinUptimeHidServDirectoryV2 Required uptime before accepting hidden service directory
+torrc.summary.DirPort Port for directory connections
+torrc.summary.DirListenAddress Address the directory service is bound to
+torrc.summary.DirPolicy Access policy for the DirPort
+torrc.summary.FetchV2Networkstatus Get the obsolete V2 consensus
+
+# Directory Authority Server Options
+
+torrc.summary.RecommendedVersions Tor versions believed to be safe
+torrc.summary.RecommendedClientVersions Tor versions believed to be safe for clients
+torrc.summary.RecommendedServerVersions Tor versions believed to be safe for relays
+torrc.summary.ConsensusParams Params entry of the networkstatus vote
+torrc.summary.DirAllowPrivateAddresses Toggles allowing arbitrary input or non-public IPs in descriptors
+torrc.summary.AuthDirBadDir Relays to be flagged as bad directory caches
+torrc.summary.AuthDirBadExit Relays to be flagged as bad exits
+torrc.summary.AuthDirInvalid Relays from which the valid flag is withheld
+torrc.summary.AuthDirReject Relays to be dropped from the consensus
+torrc.summary.AuthDirListBadDirs Toggles if we provide an opinion on bad directory caches
+torrc.summary.AuthDirListBadExits Toggles if we provide an opinion on bad exits
+torrc.summary.AuthDirRejectUnlisted Rejects further relay descriptors
+torrc.summary.AuthDirMaxServersPerAddr Limit on the number of relays accepted per ip
+torrc.summary.AuthDirMaxServersPerAuthAddr Limit on the number of relays accepted per an authority's ip
+torrc.summary.BridgePassword Password for requesting bridge information
+torrc.summary.V3AuthVotingInterval Consensus voting interval
+torrc.summary.V3AuthVoteDelay Wait time to collect votes of other authorities
+torrc.summary.V3AuthDistDelay Wait time to collect the signatures of other authorities
+torrc.summary.V3AuthNIntervalsValid Number of voting intervals a consensus is valid for
+torrc.summary.V3BandwidthsFile Path to a file containing measured relay bandwidths
+torrc.summary.V3AuthUseLegacyKey Signs consensus with both the current and legacy keys
+torrc.summary.RephistTrackTime Discards old, unchanged reliability informaition
+
+# Hidden Service Options
+
+torrc.summary.HiddenServiceDir Directory contents for the hidden service
+torrc.summary.HiddenServicePort Port the hidden service is provided on
+torrc.summary.PublishHidServDescriptors Toggles automated publishing of the hidden service to the rendezvous directory
+torrc.summary.HiddenServiceVersion Version for published hidden service descriptors
+torrc.summary.HiddenServiceAuthorizeClient Restricts access to the hidden service
+torrc.summary.RendPostPeriod Period at which the rendezvous service descriptors are refreshed
+
+# Testing Network Options
+
+torrc.summary.TestingTorNetwork Overrides other options to be a testing network
+torrc.summary.TestingV3AuthInitialVotingInterval Overrides V3AuthVotingInterval for the first consensus
+torrc.summary.TestingV3AuthInitialVoteDelay Overrides TestingV3AuthInitialVoteDelay for the first consensus
+torrc.summary.TestingV3AuthInitialDistDelay Overrides TestingV3AuthInitialDistDelay for the first consensus
+torrc.summary.TestingAuthDirTimeToLearnReachability Delay until opinions are given about which relays are running or not
+torrc.summary.TestingEstimatedDescriptorPropagationTime Delay before clients attempt to fetch descriptors from directory caches
+
diff --git a/seth/config_panel.py b/seth/config_panel.py
new file mode 100644
index 0000000..e7c1764
--- /dev/null
+++ b/seth/config_panel.py
@@ -0,0 +1,721 @@
+"""
+Panel presenting the configuration state for tor or seth. Options can be edited
+and the resulting configuration files saved.
+"""
+
+import curses
+import threading
+
+import seth.controller
+import popups
+
+from seth.util import panel, tor_config, tor_controller, ui_tools
+
+import stem.control
+
+from stem.util import conf, enum, str_tools
+
+# TODO: The seth use cases are incomplete since they currently can't be
+# modified, have their descriptions fetched, or even get a complete listing
+# of what's available.
+
+State = enum.Enum("TOR", "ARM")  # state to be presented
+
+# mappings of option categories to the color for their entries
+
+CATEGORY_COLOR = {
+  tor_config.Category.GENERAL: "green",
+  tor_config.Category.CLIENT: "blue",
+  tor_config.Category.RELAY: "yellow",
+  tor_config.Category.DIRECTORY: "magenta",
+  tor_config.Category.AUTHORITY: "red",
+  tor_config.Category.HIDDEN_SERVICE: "cyan",
+  tor_config.Category.TESTING: "white",
+  tor_config.Category.UNKNOWN: "white",
+}
+
+# attributes of a ConfigEntry
+
+Field = enum.Enum(
+  "CATEGORY",
+  "OPTION",
+  "VALUE",
+  "TYPE",
+  "ARG_USAGE",
+  "SUMMARY",
+  "DESCRIPTION",
+  "MAN_ENTRY",
+  "IS_DEFAULT",
+)
+
+FIELD_ATTR = {
+  Field.CATEGORY: ("Category", "red"),
+  Field.OPTION: ("Option Name", "blue"),
+  Field.VALUE: ("Value", "cyan"),
+  Field.TYPE: ("Arg Type", "green"),
+  Field.ARG_USAGE: ("Arg Usage", "yellow"),
+  Field.SUMMARY: ("Summary", "green"),
+  Field.DESCRIPTION: ("Description", "white"),
+  Field.MAN_ENTRY: ("Man Page Entry", "blue"),
+  Field.IS_DEFAULT: ("Is Default", "magenta"),
+}
+
+
+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("seth", {
+  "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 get_field_from_label(field_label):
+  """
+  Converts field labels back to their enumeration, raising a ValueError if it
+  doesn't exist.
+  """
+
+  for entry_enum in FIELD_ATTR:
+    if field_label == FIELD_ATTR[entry_enum][0]:
+      return entry_enum
+
+
+class ConfigEntry():
+  """
+  Configuration option in the panel.
+  """
+
+  def __init__(self, option, type, is_default):
+    self.fields = {}
+    self.fields[Field.OPTION] = option
+    self.fields[Field.TYPE] = type
+    self.fields[Field.IS_DEFAULT] = is_default
+
+    # Fetches extra infromation from external sources (the seth config and tor
+    # man page). These are None if unavailable for this config option.
+
+    summary = tor_config.get_config_summary(option)
+    man_entry = tor_config.get_config_description(option)
+
+    if man_entry:
+      self.fields[Field.MAN_ENTRY] = man_entry.index
+      self.fields[Field.CATEGORY] = man_entry.category
+      self.fields[Field.ARG_USAGE] = man_entry.arg_usage
+      self.fields[Field.DESCRIPTION] = man_entry.description
+    else:
+      self.fields[Field.MAN_ENTRY] = 99999  # sorts non-man entries last
+      self.fields[Field.CATEGORY] = tor_config.Category.UNKNOWN
+      self.fields[Field.ARG_USAGE] = ""
+      self.fields[Field.DESCRIPTION] = ""
+
+    # uses the full man page description if a summary is unavailable
+
+    self.fields[Field.SUMMARY] = summary if summary is not None else self.fields[Field.DESCRIPTION]
+
+    # cache of what's displayed for this configuration option
+
+    self.label_cache = None
+    self.label_cache_args = None
+
+  def get(self, field):
+    """
+    Provides back the value in the given field.
+
+    Arguments:
+      field - enum for the field to be provided back
+    """
+
+    if field == Field.VALUE:
+      return self._get_value()
+    else:
+      return self.fields[field]
+
+  def get_all(self, fields):
+    """
+    Provides back a list with the given field values.
+
+    Arguments:
+      field - enums for the fields to be provided back
+    """
+
+    return [self.get(field) for field in fields]
+
+  def get_label(self, option_width, value_width, summary_width):
+    """
+    Provides display string of the configuration entry with the given
+    constraints on the width of the contents.
+
+    Arguments:
+      option_width  - width of the option column
+      value_width   - width of the value column
+      summary_width - width of the summary column
+    """
+
+    # Fetching the display entries is very common so this caches the values.
+    # Doing this substantially drops cpu usage when scrolling (by around 40%).
+
+    arg_set = (option_width, value_width, summary_width)
+
+    if not self.label_cache or self.label_cache_args != arg_set:
+      option_label = str_tools.crop(self.get(Field.OPTION), option_width)
+      value_label = str_tools.crop(self.get(Field.VALUE), value_width)
+      summary_label = str_tools.crop(self.get(Field.SUMMARY), summary_width, None)
+      line_text_layout = "%%-%is %%-%is %%-%is" % (option_width, value_width, summary_width)
+      self.label_cache = line_text_layout % (option_label, value_label, summary_label)
+      self.label_cache_args = arg_set
+
+    return self.label_cache
+
+  def is_unset(self):
+    """
+    True if we have no value, false otherwise.
+    """
+
+    conf_value = tor_controller().get_conf(self.get(Field.OPTION), [], True)
+
+    return not bool(conf_value)
+
+  def _get_value(self):
+    """
+    Provides the current value of the configuration entry, taking advantage of
+    the tor_tools caching to effectively query the accurate value. This uses the
+    value's type to provide a user friendly representation if able.
+    """
+
+    conf_value = ", ".join(tor_controller().get_conf(self.get(Field.OPTION), [], True))
+
+    # provides nicer values for recognized types
+
+    if not conf_value:
+      conf_value = "<none>"
+    elif self.get(Field.TYPE) == "Boolean" and conf_value in ("0", "1"):
+      conf_value = "False" if conf_value == "0" else "True"
+    elif self.get(Field.TYPE) == "DataSize" and conf_value.isdigit():
+      conf_value = str_tools.size_label(int(conf_value))
+    elif self.get(Field.TYPE) == "TimeInterval" and conf_value.isdigit():
+      conf_value = str_tools.time_label(int(conf_value), is_long = True)
+
+    return conf_value
+
+
+class ConfigPanel(panel.Panel):
+  """
+  Renders a listing of the tor or seth configuration state, allowing options to
+  be selected and edited.
+  """
+
+  def __init__(self, stdscr, config_type):
+    panel.Panel.__init__(self, stdscr, "configuration", 0)
+
+    self.config_type = config_type
+    self.conf_contents = []
+    self.conf_important_contents = []
+    self.scroller = ui_tools.Scroller(True)
+    self.vals_lock = threading.RLock()
+
+    # shows all configuration options if true, otherwise only the ones with
+    # the 'important' flag are shown
+
+    self.show_all = False
+
+    # initializes config contents if we're connected
+
+    controller = tor_controller()
+    controller.add_status_listener(self.reset_listener)
+
+    if controller.is_alive():
+      self.reset_listener(None, stem.control.State.INIT, None)
+
+  def reset_listener(self, controller, event_type, _):
+    # fetches configuration options if a new instance, otherewise keeps our
+    # current contents
+
+    if event_type == stem.control.State.INIT:
+      self._load_config_options()
+
+  def _load_config_options(self):
+    """
+    Fetches the configuration options available from tor or seth.
+    """
+
+    self.conf_contents = []
+    self.conf_important_contents = []
+
+    if self.config_type == State.TOR:
+      controller, config_option_lines = tor_controller(), []
+      custom_options = tor_config.get_custom_options()
+      config_option_query = controller.get_info("config/names", None)
+
+      if config_option_query:
+        config_option_lines = config_option_query.strip().split("\n")
+
+      for line in config_option_lines:
+        # lines are of the form "<option> <type>[ <documentation>]", like:
+        # UseEntryGuards Boolean
+        # documentation is aparently only in older versions (for instance,
+        # 0.2.1.25)
+
+        line_comp = line.strip().split(" ")
+        conf_option, conf_type = line_comp[0], line_comp[1]
+
+        # skips private and virtual entries if not configured to show them
+
+        if not CONFIG["features.config.state.showPrivateOptions"] and conf_option.startswith("__"):
+          continue
+        elif not CONFIG["features.config.state.showVirtualOptions"] and conf_type == "Virtual":
+          continue
+
+        self.conf_contents.append(ConfigEntry(conf_option, conf_type, conf_option not in custom_options))
+
+    elif self.config_type == State.ARM:
+      # loaded via the conf utility
+
+      seth_config = conf.get_config("seth")
+
+      for key in seth_config.keys():
+        pass  # TODO: implement
+
+    # mirror listing with only the important configuration options
+
+    self.conf_important_contents = []
+
+    for entry in self.conf_contents:
+      if tor_config.is_important(entry.get(Field.OPTION)):
+        self.conf_important_contents.append(entry)
+
+    # if there aren't any important options then show everything
+
+    if not self.conf_important_contents:
+      self.conf_important_contents = self.conf_contents
+
+    self.set_sort_order()  # initial sorting of the contents
+
+  def get_selection(self):
+    """
+    Provides the currently selected entry.
+    """
+
+    return self.scroller.get_cursor_selection(self._get_config_options())
+
+  def set_filtering(self, is_filtered):
+    """
+    Sets if configuration options are filtered or not.
+
+    Arguments:
+      is_filtered - if true then only relatively important options will be
+                   shown, otherwise everything is shown
+    """
+
+    self.show_all = not is_filtered
+
+  def set_sort_order(self, ordering = None):
+    """
+    Sets the configuration attributes we're sorting by and resorts the
+    contents.
+
+    Arguments:
+      ordering - new ordering, if undefined then this resorts with the last
+                 set ordering
+    """
+
+    self.vals_lock.acquire()
+
+    if ordering:
+      CONFIG["features.config.order"] = ordering
+
+    self.conf_contents.sort(key=lambda i: (i.get_all(CONFIG["features.config.order"])))
+    self.conf_important_contents.sort(key=lambda i: (i.get_all(CONFIG["features.config.order"])))
+    self.vals_lock.release()
+
+  def show_sort_dialog(self):
+    """
+    Provides the sort dialog for our configuration options.
+    """
+
+    # set ordering for config options
+
+    title_label = "Config Option Ordering:"
+    options = [FIELD_ATTR[field][0] for field in Field]
+    old_selection = [FIELD_ATTR[field][0] for field in CONFIG["features.config.order"]]
+    option_colors = dict([FIELD_ATTR[field] for field in Field])
+    results = popups.show_sort_dialog(title_label, options, old_selection, option_colors)
+
+    if results:
+      # converts labels back to enums
+      result_enums = [get_field_from_label(label) for label in results]
+      self.set_sort_order(result_enums)
+
+  def handle_key(self, key):
+    with self.vals_lock:
+      if key.is_scroll():
+        page_height = self.get_preferred_size()[0] - 1
+        detail_panel_height = CONFIG["features.config.selectionDetails.height"]
+
+        if detail_panel_height > 0 and detail_panel_height + 2 <= page_height:
+          page_height -= (detail_panel_height + 1)
+
+        is_changed = self.scroller.handle_key(key, self._get_config_options(), page_height)
+
+        if is_changed:
+          self.redraw(True)
+      elif key.is_selection() and self._get_config_options():
+        # Prompts the user to edit the selected configuration value. The
+        # interface is locked to prevent updates between setting the value
+        # and showing any errors.
+
+        with panel.CURSES_LOCK:
+          selection = self.get_selection()
+          config_option = selection.get(Field.OPTION)
+
+          if selection.is_unset():
+            initial_value = ""
+          else:
+            initial_value = selection.get(Field.VALUE)
+
+          prompt_msg = "%s Value (esc to cancel): " % config_option
+          is_prepopulated = CONFIG["features.config.prepopulateEditValues"]
+          new_value = popups.input_prompt(prompt_msg, initial_value if is_prepopulated else "")
+
+          if new_value is not None and new_value != initial_value:
+            try:
+              if selection.get(Field.TYPE) == "Boolean":
+                # if the value's a boolean then allow for 'true' and 'false' inputs
+
+                if new_value.lower() == "true":
+                  new_value = "1"
+                elif new_value.lower() == "false":
+                  new_value = "0"
+              elif selection.get(Field.TYPE) == "LineList":
+                # set_option accepts list inputs when there's multiple values
+                new_value = new_value.split(",")
+
+              tor_controller().set_conf(config_option, new_value)
+
+              # forces the label to be remade with the new value
+
+              selection.label_cache = None
+
+              # resets the is_default flag
+
+              custom_options = tor_config.get_custom_options()
+              selection.fields[Field.IS_DEFAULT] = config_option not in custom_options
+
+              self.redraw(True)
+            except Exception as exc:
+              popups.show_msg("%s (press any key)" % exc)
+      elif key.match('a'):
+        self.show_all = not self.show_all
+        self.redraw(True)
+      elif key.match('s'):
+        self.show_sort_dialog()
+      elif key.match('v'):
+        self.show_write_dialog()
+      else:
+        return False
+
+      return True
+
+  def show_write_dialog(self):
+    """
+    Provies an interface to confirm if the configuration is saved and, if so,
+    where.
+    """
+
+    # display a popup for saving the current configuration
+
+    config_lines = tor_config.get_custom_options(True)
+    popup, width, height = popups.init(len(config_lines) + 2)
+
+    if not popup:
+      return
+
+    try:
+      # displayed options (truncating the labels if there's limited room)
+
+      if width >= 30:
+        selection_options = ("Save", "Save As...", "Cancel")
+      else:
+        selection_options = ("Save", "Save As", "X")
+
+      # checks if we can show options beside the last line of visible content
+
+      is_option_line_separate = False
+      last_index = min(height - 2, len(config_lines) - 1)
+
+      # if we don't have room to display the selection options and room to
+      # grow then display the selection options on its own line
+
+      if width < (30 + len(config_lines[last_index])):
+        popup.set_height(height + 1)
+        popup.redraw(True)  # recreates the window instance
+        new_height, _ = popup.get_preferred_size()
+
+        if new_height > height:
+          height = new_height
+          is_option_line_separate = True
+
+      selection = 2
+
+      while True:
+        # if the popup has been resized then recreate it (needed for the
+        # proper border height)
+
+        new_height, new_width = popup.get_preferred_size()
+
+        if (height, width) != (new_height, new_width):
+          height, width = new_height, new_width
+          popup.redraw(True)
+
+        # if there isn't room to display the popup then cancel it
+
+        if height <= 2:
+          selection = 2
+          break
+
+        popup.win.erase()
+        popup.win.box()
+        popup.addstr(0, 0, "Configuration being saved:", curses.A_STANDOUT)
+
+        visible_config_lines = height - 3 if is_option_line_separate else height - 2
+
+        for i in range(visible_config_lines):
+          line = str_tools.crop(config_lines[i], width - 2)
+
+          if " " in line:
+            option, arg = line.split(" ", 1)
+            popup.addstr(i + 1, 1, option, curses.A_BOLD, 'green')
+            popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD, 'cyan')
+          else:
+            popup.addstr(i + 1, 1, line, curses.A_BOLD, 'green')
+
+        # draws selection options (drawn right to left)
+
+        draw_x = width - 1
+
+        for i in range(len(selection_options) - 1, -1, -1):
+          option_label = selection_options[i]
+          draw_x -= (len(option_label) + 2)
+
+          # if we've run out of room then drop the option (this will only
+          # occure on tiny displays)
+
+          if draw_x < 1:
+            break
+
+          selection_format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+          popup.addstr(height - 2, draw_x, "[")
+          popup.addstr(height - 2, draw_x + 1, option_label, selection_format, curses.A_BOLD)
+          popup.addstr(height - 2, draw_x + len(option_label) + 1, "]")
+
+          draw_x -= 1  # space gap between the options
+
+        popup.win.refresh()
+
+        key = seth.controller.get_controller().key_input()
+
+        if key.match('left'):
+          selection = max(0, selection - 1)
+        elif key.match('right'):
+          selection = min(len(selection_options) - 1, selection + 1)
+        elif key.is_selection():
+          break
+
+      if selection in (0, 1):
+        loaded_torrc, prompt_canceled = tor_config.get_torrc(), False
+
+        try:
+          config_location = loaded_torrc.get_config_location()
+        except IOError:
+          config_location = ""
+
+        if selection == 1:
+          # prompts user for a configuration location
+          config_location = popups.input_prompt("Save to (esc to cancel): ", config_location)
+
+          if not config_location:
+            prompt_canceled = True
+
+        if not prompt_canceled:
+          try:
+            tor_config.save_conf(config_location, config_lines)
+            msg = "Saved configuration to %s" % config_location
+          except IOError as exc:
+            msg = "Unable to save configuration (%s)" % exc.strerror
+
+          popups.show_msg(msg, 2)
+    finally:
+      popups.finalize()
+
+  def get_help(self):
+    return [
+      ('up arrow', 'scroll up a line', None),
+      ('down arrow', 'scroll down a line', None),
+      ('page up', 'scroll up a page', None),
+      ('page down', 'scroll down a page', None),
+      ('enter', 'edit configuration option', None),
+      ('v', 'save configuration', None),
+      ('a', 'toggle option filtering', None),
+      ('s', 'sort ordering', None),
+    ]
+
+  def draw(self, width, height):
+    self.vals_lock.acquire()
+
+    # panel with details for the current selection
+
+    detail_panel_height = CONFIG["features.config.selectionDetails.height"]
+    is_scrollbar_visible = False
+
+    if detail_panel_height == 0 or detail_panel_height + 2 >= height:
+      # no detail panel
+
+      detail_panel_height = 0
+      scroll_location = self.scroller.get_scroll_location(self._get_config_options(), height - 1)
+      cursor_selection = self.get_selection()
+      is_scrollbar_visible = len(self._get_config_options()) > height - 1
+    else:
+      # Shrink detail panel if there isn't sufficient room for the whole
+      # thing. The extra line is for the bottom border.
+
+      detail_panel_height = min(height - 1, detail_panel_height + 1)
+      scroll_location = self.scroller.get_scroll_location(self._get_config_options(), height - 1 - detail_panel_height)
+      cursor_selection = self.get_selection()
+      is_scrollbar_visible = len(self._get_config_options()) > height - detail_panel_height - 1
+
+      if cursor_selection is not None:
+        self._draw_selection_panel(cursor_selection, width, detail_panel_height, is_scrollbar_visible)
+
+    # draws the top label
+
+    if self.is_title_visible():
+      config_type = "Tor" if self.config_type == State.TOR else "Arm"
+      hidden_msg = "press 'a' to hide most options" if self.show_all else "press 'a' to show all options"
+      title_label = "%s Configuration (%s):" % (config_type, hidden_msg)
+      self.addstr(0, 0, title_label, curses.A_STANDOUT)
+
+    # draws left-hand scroll bar if content's longer than the height
+
+    scroll_offset = 1
+
+    if is_scrollbar_visible:
+      scroll_offset = 3
+      self.add_scroll_bar(scroll_location, scroll_location + height - detail_panel_height - 1, len(self._get_config_options()), 1 + detail_panel_height)
+
+    option_width = CONFIG["features.config.state.colWidth.option"]
+    value_width = CONFIG["features.config.state.colWidth.value"]
+    description_width = max(0, width - scroll_offset - option_width - value_width - 2)
+
+    # if the description column is overly long then use its space for the
+    # value instead
+
+    if description_width > 80:
+      value_width += description_width - 80
+      description_width = 80
+
+    for line_number in range(scroll_location, len(self._get_config_options())):
+      entry = self._get_config_options()[line_number]
+      draw_line = line_number + detail_panel_height + 1 - scroll_location
+
+      line_format = [curses.A_NORMAL if entry.get(Field.IS_DEFAULT) else curses.A_BOLD]
+
+      if entry.get(Field.CATEGORY):
+        line_format += [CATEGORY_COLOR[entry.get(Field.CATEGORY)]]
+
+      if entry == cursor_selection:
+        line_format += [curses.A_STANDOUT]
+
+      line_text = entry.get_label(option_width, value_width, description_width)
+      self.addstr(draw_line, scroll_offset, line_text, *line_format)
+
+      if draw_line >= height:
+        break
+
+    self.vals_lock.release()
+
+  def _get_config_options(self):
+    return self.conf_contents if self.show_all else self.conf_important_contents
+
+  def _draw_selection_panel(self, selection, width, detail_panel_height, is_scrollbar_visible):
+    """
+    Renders a panel for the selected configuration option.
+    """
+
+    # This is a solid border unless the scrollbar is visible, in which case a
+    # 'T' pipe connects the border to the bar.
+
+    ui_tools.draw_box(self, 0, 0, width, detail_panel_height + 1)
+
+    if is_scrollbar_visible:
+      self.addch(detail_panel_height, 1, curses.ACS_TTEE)
+
+    selection_format = (curses.A_BOLD, CATEGORY_COLOR[selection.get(Field.CATEGORY)])
+
+    # first entry:
+    # <option> (<category> Option)
+
+    option_label = " (%s Option)" % selection.get(Field.CATEGORY)
+    self.addstr(1, 2, selection.get(Field.OPTION) + option_label, *selection_format)
+
+    # second entry:
+    # Value: <value> ([default|custom], <type>, usage: <argument usage>)
+
+    if detail_panel_height >= 3:
+      value_attr = []
+      value_attr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
+      value_attr.append(selection.get(Field.TYPE))
+      value_attr.append("usage: %s" % (selection.get(Field.ARG_USAGE)))
+      value_attr_label = ", ".join(value_attr)
+
+      value_label_width = width - 12 - len(value_attr_label)
+      value_label = str_tools.crop(selection.get(Field.VALUE), value_label_width)
+
+      self.addstr(2, 2, "Value: %s (%s)" % (value_label, value_attr_label), *selection_format)
+
+    # remainder is filled with the man page description
+
+    description_height = max(0, detail_panel_height - 3)
+    description_content = "Description: " + selection.get(Field.DESCRIPTION)
+
+    for i in range(description_height):
+      # checks if we're done writing the description
+
+      if not description_content:
+        break
+
+      # there's a leading indent after the first line
+
+      if i > 0:
+        description_content = "  " + description_content
+
+      # we only want to work with content up until the next newline
+
+      if "\n" in description_content:
+        line_content, description_content = description_content.split("\n", 1)
+      else:
+        line_content, description_content = description_content, ""
+
+      if i != description_height - 1:
+        # there's more lines to display
+
+        msg, remainder = str_tools.crop(line_content, width - 3, 4, 4, str_tools.Ending.HYPHEN, True)
+        description_content = remainder.strip() + description_content
+      else:
+        # this is the last line, end it with an ellipse
+
+        msg = str_tools.crop(line_content, width - 3, 4, 4)
+
+      self.addstr(3 + i, 2, msg, *selection_format)
diff --git a/seth/connections/__init__.py b/seth/connections/__init__.py
new file mode 100644
index 0000000..447adf6
--- /dev/null
+++ b/seth/connections/__init__.py
@@ -0,0 +1,12 @@
+"""
+Resources related to our connection panel.
+"""
+
+__all__ = [
+  'circ_entry',
+  'conn_entry',
+  'conn_panel',
+  'count_popup',
+  'descriptor_popup',
+  'entries',
+]
diff --git a/seth/connections/circ_entry.py b/seth/connections/circ_entry.py
new file mode 100644
index 0000000..248ab73
--- /dev/null
+++ b/seth/connections/circ_entry.py
@@ -0,0 +1,253 @@
+"""
+Connection panel entries for client circuits. This includes a header entry
+followed by an entry for each hop in the circuit. For instance:
+
+89.188.20.246:42667    -->  217.172.182.26 (de)       General / Built     8.6m (CIRCUIT)
+|  85.8.28.4 (se)               98FBC3B2B93897A78CDD797EF549E6B62C9A8523    1 / Guard
+|  91.121.204.76 (fr)           546387D93F8D40CFF8842BB9D3A8EC477CEDA984    2 / Middle
++- 217.172.182.26 (de)          5CFA9EA136C0EA0AC096E5CEA7EB674F1207CF86    3 / Exit
+"""
+
+import curses
+
+from seth.connections import entries, conn_entry
+from seth.util import tor_controller
+
+from stem.util import str_tools
+
+ADDRESS_LOOKUP_CACHE = {}
+
+
+class CircEntry(conn_entry.ConnectionEntry):
+  def __init__(self, circuit_id, status, purpose, path):
+    conn_entry.ConnectionEntry.__init__(self, "127.0.0.1", "0", "127.0.0.1", "0")
+
+    self.circuit_id = circuit_id
+    self.status = status
+
+    # drops to lowercase except the first letter
+
+    if len(purpose) >= 2:
+      purpose = purpose[0].upper() + purpose[1:].lower()
+
+    self.lines = [CircHeaderLine(self.circuit_id, purpose)]
+
+    # Overwrites attributes of the initial line to make it more fitting as the
+    # header for our listing.
+
+    self.lines[0].base_type = conn_entry.Category.CIRCUIT
+
+    self.update(status, path)
+
+  def update(self, status, path):
+    """
+    Our status and path can change over time if the circuit is still in the
+    process of being built. Updates these attributes of our relay.
+
+    Arguments:
+      status - new status of the circuit
+      path   - list of fingerprints for the series of relays involved in the
+               circuit
+    """
+
+    self.status = status
+    self.lines = [self.lines[0]]
+    controller = tor_controller()
+
+    if status == "BUILT" and not self.lines[0].is_built:
+      exit_ip, exit_port = get_relay_address(controller, path[-1], ("192.168.0.1", "0"))
+      self.lines[0].set_exit(exit_ip, exit_port, path[-1])
+
+    for i in range(len(path)):
+      relay_fingerprint = path[i]
+      relay_ip, relay_port = get_relay_address(controller, relay_fingerprint, ("192.168.0.1", "0"))
+
+      if i == len(path) - 1:
+        if status == "BUILT":
+          placement_type = "Exit"
+        else:
+          placement_type = "Extending"
+      elif i == 0:
+        placement_type = "Guard"
+      else:
+        placement_type = "Middle"
+
+      placement_label = "%i / %s" % (i + 1, placement_type)
+
+      self.lines.append(CircLine(relay_ip, relay_port, relay_fingerprint, placement_label))
+
+    self.lines[-1].is_last = True
+
+
+class CircHeaderLine(conn_entry.ConnectionLine):
+  """
+  Initial line of a client entry. This has the same basic format as connection
+  lines except that its etc field has circuit attributes.
+  """
+
+  def __init__(self, circuit_id, purpose):
+    conn_entry.ConnectionLine.__init__(self, "127.0.0.1", "0", "0.0.0.0", "0", False, False)
+    self.circuit_id = circuit_id
+    self.purpose = purpose
+    self.is_built = False
+
+  def set_exit(self, exit_address, exit_port, exit_fingerprint):
+    conn_entry.ConnectionLine.__init__(self, "127.0.0.1", "0", exit_address, exit_port, False, False)
+    self.is_built = True
+    self.foreign.fingerprint_overwrite = exit_fingerprint
+
+  def get_type(self):
+    return conn_entry.Category.CIRCUIT
+
+  def get_destination_label(self, max_length, include_locale=False, include_hostname=False):
+    if not self.is_built:
+      return "Building..."
+
+    return conn_entry.ConnectionLine.get_destination_label(self, max_length, include_locale, include_hostname)
+
+  def get_etc_content(self, width, listing_type):
+    """
+    Attempts to provide all circuit related stats. Anything that can't be
+    shown completely (not enough room) is dropped.
+    """
+
+    etc_attr = ["Purpose: %s" % self.purpose, "Circuit ID: %s" % self.circuit_id]
+
+    for i in range(len(etc_attr), -1, -1):
+      etc_label = ", ".join(etc_attr[:i])
+
+      if len(etc_label) <= width:
+        return ("%%-%is" % width) % etc_label
+
+    return ""
+
+  def get_details(self, width):
+    if not self.is_built:
+      detail_format = (curses.A_BOLD, conn_entry.CATEGORY_COLOR[self.get_type()])
+      return [("Building Circuit...", detail_format)]
+    else:
+      return conn_entry.ConnectionLine.get_details(self, width)
+
+
+class CircLine(conn_entry.ConnectionLine):
+  """
+  An individual hop in a circuit. This overwrites the displayed listing, but
+  otherwise makes use of the ConnectionLine attributes (for the detail display,
+  caching, etc).
+  """
+
+  def __init__(self, remote_address, remote_port, remote_fingerprint, placement_label):
+    conn_entry.ConnectionLine.__init__(self, "127.0.0.1", "0", remote_address, remote_port)
+    self.foreign.fingerprint_overwrite = remote_fingerprint
+    self.placement_label = placement_label
+    self.include_port = False
+
+    # determines the sort of left hand bracketing we use
+
+    self.is_last = False
+
+  def get_type(self):
+    return conn_entry.Category.CIRCUIT
+
+  def get_listing_prefix(self):
+    if self.is_last:
+      return (ord(' '), curses.ACS_LLCORNER, curses.ACS_HLINE, ord(' '))
+    else:
+      return (ord(' '), curses.ACS_VLINE, ord(' '), ord(' '))
+
+  def get_listing_entry(self, width, current_time, listing_type):
+    """
+    Provides the [(msg, attr)...] listing for this relay in the circuilt
+    listing. Lines are composed of the following components:
+      <bracket> <dst> <etc> <placement label>
+
+    The dst and etc entries largely match their ConnectionEntry counterparts.
+
+    Arguments:
+      width       - maximum length of the line
+      current_time - the current unix time (ignored)
+      listing_type - primary attribute we're listing connections by
+    """
+
+    return entries.ConnectionPanelLine.get_listing_entry(self, width, current_time, listing_type)
+
+  def _get_listing_entry(self, width, current_time, listing_type):
+    line_format = conn_entry.CATEGORY_COLOR[self.get_type()]
+
+    # The required widths are the sum of the following:
+    # initial space (1 character)
+    # bracketing (3 characters)
+    # placement_label (14 characters)
+    # gap between etc and placement label (5 characters)
+
+    baseline_space = 14 + 5
+
+    dst, etc = "", ""
+
+    if listing_type == entries.ListingType.IP_ADDRESS:
+      # TODO: include hostname when that's available
+      # dst width is derived as:
+      # src (21) + dst (26) + divider (7) + right gap (2) - bracket (3) = 53 char
+
+      dst = "%-53s" % self.get_destination_label(53, include_locale = True)
+
+      # fills the nickname into the empty space here
+
+      dst = "%s%-25s   " % (dst[:25], str_tools.crop(self.foreign.get_nickname(), 25, 0))
+
+      etc = self.get_etc_content(width - baseline_space - len(dst), listing_type)
+    elif listing_type == entries.ListingType.HOSTNAME:
+      # min space for the hostname is 40 characters
+
+      etc = self.get_etc_content(width - baseline_space - 40, listing_type)
+      dst_layout = "%%-%is" % (width - baseline_space - len(etc))
+      dst = dst_layout % self.foreign.get_hostname(self.foreign.get_address())
+    elif listing_type == entries.ListingType.FINGERPRINT:
+      # dst width is derived as:
+      # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
+
+      dst = "%-55s" % self.foreign.get_fingerprint()
+      etc = self.get_etc_content(width - baseline_space - len(dst), listing_type)
+    else:
+      # min space for the nickname is 56 characters
+
+      etc = self.get_etc_content(width - baseline_space - 56, listing_type)
+      dst_layout = "%%-%is" % (width - baseline_space - len(etc))
+      dst = dst_layout % self.foreign.get_nickname()
+
+    return ((dst + etc, line_format),
+            (" " * (width - baseline_space - len(dst) - len(etc) + 5), line_format),
+            ("%-14s" % self.placement_label, line_format))
+
+
+def get_relay_address(controller, relay_fingerprint, default = None):
+  """
+  Provides the (IP Address, ORPort) tuple for a given relay. If the lookup
+  fails then this returns the default.
+
+  Arguments:
+    relay_fingerprint - fingerprint of the relay
+  """
+
+  result = default
+
+  if controller.is_alive():
+    # query the address if it isn't yet cached
+    if relay_fingerprint not in ADDRESS_LOOKUP_CACHE:
+      if relay_fingerprint == controller.get_info("fingerprint", None):
+        # this is us, simply check the config
+        my_address = controller.get_info("address", None)
+        my_or_port = controller.get_conf("ORPort", None)
+
+        if my_address and my_or_port:
+          ADDRESS_LOOKUP_CACHE[relay_fingerprint] = (my_address, my_or_port)
+      else:
+        # check the consensus for the relay
+        relay = controller.get_network_status(relay_fingerprint, None)
+
+        if relay:
+          ADDRESS_LOOKUP_CACHE[relay_fingerprint] = (relay.address, relay.or_port)
+
+    result = ADDRESS_LOOKUP_CACHE.get(relay_fingerprint, default)
+
+  return result
diff --git a/seth/connections/conn_entry.py b/seth/connections/conn_entry.py
new file mode 100644
index 0000000..2b8d235
--- /dev/null
+++ b/seth/connections/conn_entry.py
@@ -0,0 +1,1258 @@
+"""
+Connection panel entries related to actual connections to or from the system
+(ie, results seen by netstat, lsof, etc).
+"""
+
+import time
+import curses
+
+from seth.util import tor_controller
+from seth.connections import entries
+
+import stem.control
+
+from stem.util import conf, connection, enum, str_tools
+
+# Connection Categories:
+#   Inbound      Relay connection, coming to us.
+#   Outbound     Relay connection, leaving us.
+#   Exit         Outbound relay connection leaving the Tor network.
+#   Hidden       Connections to a hidden service we're providing.
+#   Socks        Socks connections for applications using Tor.
+#   Circuit      Circuits our tor client has created.
+#   Directory    Fetching tor consensus information.
+#   Control      Tor controller (seth, vidalia, etc).
+
+Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "HIDDEN", "SOCKS", "CIRCUIT", "DIRECTORY", "CONTROL")
+
+CATEGORY_COLOR = {
+  Category.INBOUND: "green",
+  Category.OUTBOUND: "blue",
+  Category.EXIT: "red",
+  Category.HIDDEN: "magenta",
+  Category.SOCKS: "yellow",
+  Category.CIRCUIT: "cyan",
+  Category.DIRECTORY: "magenta",
+  Category.CONTROL: "red",
+}
+
+# static data for listing format
+# <src>  -->  <dst>  <etc><padding>
+
+LABEL_FORMAT = "%s  -->  %s  %s%s"
+LABEL_MIN_PADDING = 2  # min space between listing label and following data
+
+# sort value for scrubbed ip addresses
+
+SCRUBBED_IP_VAL = 255 ** 4
+
+CONFIG = conf.config_dict("seth", {
+  "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,
+})
+
+FINGERPRINT_TRACKER = None
+
+
+def get_fingerprint_tracker():
+  global FINGERPRINT_TRACKER
+
+  if FINGERPRINT_TRACKER is None:
+    FINGERPRINT_TRACKER = FingerprintTracker()
+
+  return FINGERPRINT_TRACKER
+
+
+class Endpoint:
+  """
+  Collection of attributes associated with a connection endpoint. This is a
+  thin wrapper for torUtil functions, making use of its caching for
+  performance.
+  """
+
+  def __init__(self, address, port):
+    self.address = address
+    self.port = port
+
+    # if true, we treat the port as an definitely not being an ORPort when
+    # searching for matching fingerprints (otherwise we use it to possably
+    # narrow results when unknown)
+
+    self.is_not_or_port = True
+
+    # if set then this overwrites fingerprint lookups
+
+    self.fingerprint_overwrite = None
+
+  def get_address(self):
+    """
+    Provides the IP address of the endpoint.
+    """
+
+    return self.address
+
+  def get_port(self):
+    """
+    Provides the port of the endpoint.
+    """
+
+    return self.port
+
+  def get_hostname(self, default = None):
+    """
+    Provides the hostname associated with the relay's address. This is a
+    non-blocking call and returns None if the address either can't be resolved
+    or hasn't been resolved yet.
+
+    Arguments:
+      default - return value if no hostname is available
+    """
+
+    # TODO: skipping all hostname resolution to be safe for now
+    # try:
+    #   myHostname = hostnames.resolve(self.address)
+    # except:
+    #   # either a ValueError or IOError depending on the source of the lookup failure
+    #   myHostname = None
+    #
+    # if not myHostname: return default
+    # else: return myHostname
+
+    return default
+
+  def get_locale(self, default=None):
+    """
+    Provides the two letter country code for the IP address' locale.
+
+    Arguments:
+      default - return value if no locale information is available
+    """
+
+    controller = tor_controller()
+    return controller.get_info("ip-to-country/%s" % self.address, default)
+
+  def get_fingerprint(self):
+    """
+    Provides the fingerprint of the relay, returning "UNKNOWN" if it can't be
+    determined.
+    """
+
+    if self.fingerprint_overwrite:
+      return self.fingerprint_overwrite
+
+    my_fingerprint = get_fingerprint_tracker().get_relay_fingerprint(self.address)
+
+    # If there were multiple matches and our port is likely the ORPort then
+    # try again with that to narrow the results.
+
+    if not my_fingerprint and not self.is_not_or_port:
+      my_fingerprint = get_fingerprint_tracker().get_relay_fingerprint(self.address, self.port)
+
+    if my_fingerprint:
+      return my_fingerprint
+    else:
+      return "UNKNOWN"
+
+  def get_nickname(self):
+    """
+    Provides the nickname of the relay, retuning "UNKNOWN" if it can't be
+    determined.
+    """
+
+    my_fingerprint = self.get_fingerprint()
+
+    if my_fingerprint != "UNKNOWN":
+      my_nickname = get_fingerprint_tracker().get_relay_nickname(my_fingerprint)
+
+      if my_nickname:
+        return my_nickname
+      else:
+        return "UNKNOWN"
+    else:
+      return "UNKNOWN"
+
+
+class ConnectionEntry(entries.ConnectionPanelEntry):
+  """
+  Represents a connection being made to or from this system. These only
+  concern real connections so it includes the inbound, outbound, directory,
+  application, and controller categories.
+  """
+
+  def __init__(self, local_address, local_port, remote_address, remote_port):
+    entries.ConnectionPanelEntry.__init__(self)
+    self.lines = [ConnectionLine(local_address, local_port, remote_address, remote_port)]
+
+  def get_sort_value(self, attr, listing_type):
+    """
+    Provides the value of a single attribute used for sorting purposes.
+    """
+
+    connection_line = self.lines[0]
+
+    if attr == entries.SortAttr.IP_ADDRESS:
+      if connection_line.is_private():
+        return SCRUBBED_IP_VAL  # orders at the end
+
+      return connection_line.sort_address
+    elif attr == entries.SortAttr.PORT:
+      return connection_line.sort_port
+    elif attr == entries.SortAttr.HOSTNAME:
+      if connection_line.is_private():
+        return ""
+
+      return connection_line.foreign.get_hostname("")
+    elif attr == entries.SortAttr.FINGERPRINT:
+      return connection_line.foreign.get_fingerprint()
+    elif attr == entries.SortAttr.NICKNAME:
+      my_nickname = connection_line.foreign.get_nickname()
+
+      if my_nickname == "UNKNOWN":
+        return "z" * 20  # orders at the end
+      else:
+        return my_nickname.lower()
+    elif attr == entries.SortAttr.CATEGORY:
+      return Category.index_of(connection_line.get_type())
+    elif attr == entries.SortAttr.UPTIME:
+      return connection_line.start_time
+    elif attr == entries.SortAttr.COUNTRY:
+      if connection.is_private_address(self.lines[0].foreign.get_address()):
+        return ""
+      else:
+        return connection_line.foreign.get_locale("")
+    else:
+      return entries.ConnectionPanelEntry.get_sort_value(self, attr, listing_type)
+
+
+class ConnectionLine(entries.ConnectionPanelLine):
+  """
+  Display component of the ConnectionEntry.
+  """
+
+  def __init__(self, local_address, local_port, remote_address, remote_port, include_port=True, include_expanded_addresses=True):
+    entries.ConnectionPanelLine.__init__(self)
+
+    self.local = Endpoint(local_address, local_port)
+    self.foreign = Endpoint(remote_address, remote_port)
+    self.start_time = time.time()
+    self.is_initial_connection = False
+
+    # overwrite the local fingerprint with ours
+
+    controller = tor_controller()
+    self.local.fingerprint_overwrite = controller.get_info("fingerprint", None)
+
+    # True if the connection has matched the properties of a client/directory
+    # connection every time we've checked. The criteria we check is...
+    #   client    - first hop in an established circuit
+    #   directory - matches an established single-hop circuit (probably a
+    #               directory mirror)
+
+    self._possible_client = True
+    self._possible_directory = True
+
+    # attributes for SOCKS, HIDDEN, and CONTROL connections
+
+    self.application_name = None
+    self.application_pid = None
+    self.is_application_resolving = False
+
+    my_or_port = controller.get_conf("ORPort", None)
+    my_dir_port = controller.get_conf("DirPort", None)
+    my_socks_port = controller.get_conf("SocksPort", "9050")
+    my_ctl_port = controller.get_conf("ControlPort", None)
+    my_hidden_service_ports = get_hidden_service_ports(controller)
+
+    # the ORListenAddress can overwrite the ORPort
+
+    listen_addr = controller.get_conf("ORListenAddress", None)
+
+    if listen_addr and ":" in listen_addr:
+      my_or_port = listen_addr[listen_addr.find(":") + 1:]
+
+    if local_port in (my_or_port, my_dir_port):
+      self.base_type = Category.INBOUND
+      self.local.is_not_or_port = False
+    elif local_port == my_socks_port:
+      self.base_type = Category.SOCKS
+    elif remote_port in my_hidden_service_ports:
+      self.base_type = Category.HIDDEN
+    elif local_port == my_ctl_port:
+      self.base_type = Category.CONTROL
+    else:
+      self.base_type = Category.OUTBOUND
+      self.foreign.is_not_or_port = False
+
+    self.cached_type = None
+
+    # includes the port or expanded ip address field when displaying listing
+    # information if true
+
+    self.include_port = include_port
+    self.include_expanded_addresses = include_expanded_addresses
+
+    # cached immutable values used for sorting
+
+    ip_value = 0
+
+    for comp in self.foreign.get_address().split("."):
+      ip_value *= 255
+      ip_value += int(comp)
+
+    self.sort_address = ip_value
+    self.sort_port = int(self.foreign.get_port())
+
+  def get_listing_entry(self, width, current_time, listing_type):
+    """
+    Provides the tuple list for this connection's listing. Lines are composed
+    of the following components:
+      <src>  -->  <dst>     <etc>     <uptime> (<type>)
+
+    ListingType.IP_ADDRESS:
+      src - <internal addr:port> --> <external addr:port>
+      dst - <destination addr:port>
+      etc - <fingerprint> <nickname>
+
+    ListingType.HOSTNAME:
+      src - localhost:<port>
+      dst - <destination hostname:port>
+      etc - <destination addr:port> <fingerprint> <nickname>
+
+    ListingType.FINGERPRINT:
+      src - localhost
+      dst - <destination fingerprint>
+      etc - <nickname> <destination addr:port>
+
+    ListingType.NICKNAME:
+      src - <source nickname>
+      dst - <destination nickname>
+      etc - <fingerprint> <destination addr:port>
+
+    Arguments:
+      width       - maximum length of the line
+      current_time - unix timestamp for what the results should consider to be
+                    the current time
+      listing_type - primary attribute we're listing connections by
+    """
+
+    # fetch our (most likely cached) display entry for the listing
+
+    my_listing = entries.ConnectionPanelLine.get_listing_entry(self, width, current_time, listing_type)
+
+    # fill in the current uptime and return the results
+
+    if CONFIG["features.connection.markInitialConnections"]:
+      time_prefix = "+" if self.is_initial_connection else " "
+    else:
+      time_prefix = ""
+
+    time_label = time_prefix + "%5s" % str_tools.time_label(current_time - self.start_time, 1)
+    my_listing[2] = (time_label, my_listing[2][1])
+
+    return my_listing
+
+  def is_unresolved_application(self):
+    """
+    True if our display uses application information that hasn't yet been resolved.
+    """
+
+    return self.application_name is None and self.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
+
+  def _get_listing_entry(self, width, current_time, listing_type):
+    entry_type = self.get_type()
+
+    # Lines are split into the following components in reverse:
+    # init gap - " "
+    # content  - "<src>  -->  <dst>     <etc>     "
+    # time     - "<uptime>"
+    # preType  - " ("
+    # category - "<type>"
+    # postType - ")   "
+
+    line_format = CATEGORY_COLOR[entry_type]
+    time_width = 6 if CONFIG["features.connection.markInitialConnections"] else 5
+
+    draw_entry = [(" ", line_format),
+                  (self._get_listing_content(width - (12 + time_width) - 1, listing_type), line_format),
+                  (" " * time_width, line_format),
+                  (" (", line_format),
+                  (entry_type.upper(), line_format, curses.A_BOLD),
+                  (")" + " " * (9 - len(entry_type)), line_format)]
+
+    return draw_entry
+
+  def _get_details(self, width):
+    """
+    Provides details on the connection, correlated against available consensus
+    data.
+
+    Arguments:
+      width - available space to display in
+    """
+
+    detail_format = (curses.A_BOLD, CATEGORY_COLOR[self.get_type()])
+    return [(line, detail_format) for line in self._get_detail_content(width)]
+
+  def reset_display(self):
+    entries.ConnectionPanelLine.reset_display(self)
+    self.cached_type = None
+
+  def is_private(self):
+    """
+    Returns true if the endpoint is private, possibly belonging to a client
+    connection or exit traffic.
+    """
+
+    if not CONFIG["features.connection.showIps"]:
+      return True
+
+    # This is used to scrub private information from the interface. Relaying
+    # etiquette (and wiretapping laws) say these are bad things to look at so
+    # DON'T CHANGE THIS UNLESS YOU HAVE A DAMN GOOD REASON!
+
+    my_type = self.get_type()
+
+    if my_type == Category.INBOUND:
+      # if we're a guard or bridge and the connection doesn't belong to a
+      # known relay then it might be client traffic
+
+      controller = tor_controller()
+
+      my_flags = []
+      my_fingerprint = self.get_info("fingerprint", None)
+
+      if my_fingerprint:
+        my_status_entry = self.controller.get_network_status(my_fingerprint)
+
+        if my_status_entry:
+          my_flags = my_status_entry.flags
+
+      if "Guard" in my_flags or controller.get_conf("BridgeRelay", None) == "1":
+        all_matches = get_fingerprint_tracker().get_relay_fingerprint(self.foreign.get_address(), get_all_matches = True)
+
+        return all_matches == []
+    elif my_type == Category.EXIT:
+      # DNS connections exiting us aren't private (since they're hitting our
+      # resolvers). Everything else, however, is.
+
+      # TODO: Ideally this would also double check that it's a UDP connection
+      # (since DNS is the only UDP connections Tor will relay), however this
+      # will take a bit more work to propagate the information up from the
+      # connection resolver.
+
+      return self.foreign.get_port() != "53"
+
+    # for everything else this isn't a concern
+
+    return False
+
+  def get_type(self):
+    """
+    Provides our best guess at the current type of the connection. This
+    depends on consensus results, our current client circuits, etc. Results
+    are cached until this entry's display is reset.
+    """
+
+    # caches both to simplify the calls and to keep the type consistent until
+    # we want to reflect changes
+
+    if not self.cached_type:
+      if self.base_type == Category.OUTBOUND:
+        # Currently the only non-static categories are OUTBOUND vs...
+        # - EXIT since this depends on the current consensus
+        # - CIRCUIT if this is likely to belong to our guard usage
+        # - DIRECTORY if this is a single-hop circuit (directory mirror?)
+        #
+        # The exitability, circuits, and fingerprints are all cached by the
+        # tor_tools util keeping this a quick lookup.
+
+        controller = tor_controller()
+        destination_fingerprint = self.foreign.get_fingerprint()
+
+        if destination_fingerprint == "UNKNOWN":
+          # Not a known relay. This might be an exit connection.
+
+          if is_exiting_allowed(controller, self.foreign.get_address(), self.foreign.get_port()):
+            self.cached_type = Category.EXIT
+        elif self._possible_client or self._possible_directory:
+          # This belongs to a known relay. If we haven't eliminated ourselves as
+          # a possible client or directory connection then check if it still
+          # holds true.
+
+          my_circuits = controller.get_circuits([])
+
+          if self._possible_client:
+            # Checks that this belongs to the first hop in a circuit that's
+            # either unestablished or longer than a single hop (ie, anything but
+            # a built 1-hop connection since those are most likely a directory
+            # mirror).
+
+            for circ in my_circuits:
+              if circ.path and circ.path[0][0] == destination_fingerprint and (circ.status != "BUILT" or len(circ.path) > 1):
+                self.cached_type = Category.CIRCUIT  # matched a probable guard connection
+
+            # if we fell through, we can eliminate ourselves as a guard in the future
+            if not self.cached_type:
+              self._possible_client = False
+
+          if self._possible_directory:
+            # Checks if we match a built, single hop circuit.
+
+            for circ in my_circuits:
+              if circ.path and circ.path[0][0] == destination_fingerprint and circ.status == "BUILT" and len(circ.path) == 1:
+                self.cached_type = Category.DIRECTORY
+
+            # if we fell through, eliminate ourselves as a directory connection
+            if not self.cached_type:
+              self._possible_directory = False
+
+      if not self.cached_type:
+        self.cached_type = self.base_type
+
+    return self.cached_type
+
+  def get_etc_content(self, width, listing_type):
+    """
+    Provides the optional content for the connection.
+
+    Arguments:
+      width       - maximum length of the line
+      listing_type - primary attribute we're listing connections by
+    """
+
+    # for applications show the command/pid
+
+    if self.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
+      display_label = ""
+
+      if self.application_name:
+        if self.application_pid:
+          display_label = "%s (%s)" % (self.application_name, self.application_pid)
+        else:
+          display_label = self.application_name
+      elif self.is_application_resolving:
+        display_label = "resolving..."
+      else:
+        display_label = "UNKNOWN"
+
+      if len(display_label) < width:
+        return ("%%-%is" % width) % display_label
+      else:
+        return ""
+
+    # for everything else display connection/consensus information
+
+    destination_address = self.get_destination_label(26, include_locale = True)
+    etc, used_space = "", 0
+
+    if listing_type == entries.ListingType.IP_ADDRESS:
+      if width > used_space + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+
+        etc += "%-40s  " % self.foreign.get_fingerprint()
+        used_space += 42
+
+      if width > used_space + 10 and CONFIG["features.connection.showColumn.nickname"]:
+        # show nickname (column width: remainder)
+
+        nickname_space = width - used_space
+        nickname_label = str_tools.crop(self.foreign.get_nickname(), nickname_space, 0)
+        etc += ("%%-%is  " % nickname_space) % nickname_label
+        used_space += nickname_space + 2
+    elif listing_type == entries.ListingType.HOSTNAME:
+      if width > used_space + 28 and CONFIG["features.connection.showColumn.destination"]:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % destination_address
+        used_space += 28
+
+      if width > used_space + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.get_fingerprint()
+        used_space += 42
+
+      if width > used_space + 17 and CONFIG["features.connection.showColumn.nickname"]:
+        # show nickname (column width: min 17 characters, uses half of the remainder)
+        nickname_space = 15 + (width - (used_space + 17)) / 2
+        nickname_label = str_tools.crop(self.foreign.get_nickname(), nickname_space, 0)
+        etc += ("%%-%is  " % nickname_space) % nickname_label
+        used_space += (nickname_space + 2)
+    elif listing_type == entries.ListingType.FINGERPRINT:
+      if width > used_space + 17:
+        # show nickname (column width: min 17 characters, consumes any remaining space)
+
+        nickname_space = width - used_space - 2
+
+        # if there's room then also show a column with the destination
+        # ip/port/locale (column width: 28 characters)
+
+        is_locale_included = width > used_space + 45
+        is_locale_included &= CONFIG["features.connection.showColumn.destination"]
+
+        if is_locale_included:
+          nickname_space -= 28
+
+        if CONFIG["features.connection.showColumn.nickname"]:
+          nickname_label = str_tools.crop(self.foreign.get_nickname(), nickname_space, 0)
+          etc += ("%%-%is  " % nickname_space) % nickname_label
+          used_space += nickname_space + 2
+
+        if is_locale_included:
+          etc += "%-26s  " % destination_address
+          used_space += 28
+    else:
+      if width > used_space + 42 and CONFIG["features.connection.showColumn.fingerprint"]:
+        # show fingerprint (column width: 42 characters)
+        etc += "%-40s  " % self.foreign.get_fingerprint()
+        used_space += 42
+
+      if width > used_space + 28 and CONFIG["features.connection.showColumn.destination"]:
+        # show destination ip/port/locale (column width: 28 characters)
+        etc += "%-26s  " % destination_address
+        used_space += 28
+
+    return ("%%-%is" % width) % etc
+
+  def _get_listing_content(self, width, listing_type):
+    """
+    Provides the source, destination, and extra info for our listing.
+
+    Arguments:
+      width       - maximum length of the line
+      listing_type - primary attribute we're listing connections by
+    """
+
+    controller = tor_controller()
+    my_type = self.get_type()
+    destination_address = self.get_destination_label(26, include_locale = True)
+
+    # The required widths are the sum of the following:
+    # - room for LABEL_FORMAT and LABEL_MIN_PADDING (11 characters)
+    # - base data for the listing
+    # - that extra field plus any previous
+
+    used_space = len(LABEL_FORMAT % tuple([""] * 4)) + LABEL_MIN_PADDING
+    local_port = ":%s" % self.local.get_port() if self.include_port else ""
+
+    src, dst, etc = "", "", ""
+
+    if listing_type == entries.ListingType.IP_ADDRESS:
+      my_external_address = controller.get_info("address", self.local.get_address())
+      address_differ = my_external_address != self.local.get_address()
+
+      # Expanding doesn't make sense, if the connection isn't actually
+      # going through Tor's external IP address. As there isn't a known
+      # method for checking if it is, we're checking the type instead.
+      #
+      # This isn't entirely correct. It might be a better idea to check if
+      # the source and destination addresses are both private, but that might
+      # not be perfectly reliable either.
+
+      is_expansion_type = my_type not in (Category.SOCKS, Category.HIDDEN, Category.CONTROL)
+
+      if is_expansion_type:
+        src_address = my_external_address + local_port
+      else:
+        src_address = self.local.get_address() + local_port
+
+      if my_type in (Category.SOCKS, Category.CONTROL):
+        # Like inbound connections these need their source and destination to
+        # be swapped. However, this only applies when listing by IP or hostname
+        # (their fingerprint and nickname are both for us). Reversing the
+        # fields here to keep the same column alignments.
+
+        src = "%-21s" % destination_address
+        dst = "%-26s" % src_address
+      else:
+        src = "%-21s" % src_address  # ip:port = max of 21 characters
+        dst = "%-26s" % destination_address  # ip:port (xx) = max of 26 characters
+
+      used_space += len(src) + len(dst)  # base data requires 47 characters
+
+      # Showing the fingerprint (which has the width of 42) has priority over
+      # an expanded address field. Hence check if we either have space for
+      # both or wouldn't be showing the fingerprint regardless.
+
+      is_expanded_address_visible = width > used_space + 28
+
+      if is_expanded_address_visible and CONFIG["features.connection.showColumn.fingerprint"]:
+        is_expanded_address_visible = width < used_space + 42 or width > used_space + 70
+
+      if address_differ and is_expansion_type and is_expanded_address_visible and self.include_expanded_addresses and CONFIG["features.connection.showColumn.expandedIp"]:
+        # include the internal address in the src (extra 28 characters)
+
+        internal_address = self.local.get_address() + local_port
+
+        # If this is an inbound connection then reverse ordering so it's:
+        # <foreign> --> <external> --> <internal>
+        # when the src and dst are swapped later
+
+        if my_type == Category.INBOUND:
+          src = "%-21s  -->  %s" % (src, internal_address)
+        else:
+          src = "%-21s  -->  %s" % (internal_address, src)
+
+        used_space += 28
+
+      etc = self.get_etc_content(width - used_space, listing_type)
+      used_space += len(etc)
+    elif listing_type == entries.ListingType.HOSTNAME:
+      # 15 characters for source, and a min of 40 reserved for the destination
+      # TODO: when actually functional the src and dst need to be swapped for
+      # SOCKS and CONTROL connections
+
+      src = "localhost%-6s" % local_port
+      used_space += len(src)
+      min_hostname_space = 40
+
+      etc = self.get_etc_content(width - used_space - min_hostname_space, listing_type)
+      used_space += len(etc)
+
+      hostname_space = width - used_space
+      used_space = width  # prevents padding at the end
+
+      if self.is_private():
+        dst = ("%%-%is" % hostname_space) % "<scrubbed>"
+      else:
+        hostname = self.foreign.get_hostname(self.foreign.get_address())
+        port_label = ":%-5s" % self.foreign.get_port() if self.include_port else ""
+
+        # truncates long hostnames and sets dst to <hostname>:<port>
+
+        hostname = str_tools.crop(hostname, hostname_space, 0)
+        dst = ("%%-%is" % hostname_space) % (hostname + port_label)
+    elif listing_type == entries.ListingType.FINGERPRINT:
+      src = "localhost"
+
+      if my_type == Category.CONTROL:
+        dst = "localhost"
+      else:
+        dst = self.foreign.get_fingerprint()
+
+      dst = "%-40s" % dst
+
+      used_space += len(src) + len(dst)  # base data requires 49 characters
+
+      etc = self.get_etc_content(width - used_space, listing_type)
+      used_space += len(etc)
+    else:
+      # base data requires 50 min characters
+      src = self.local.get_nickname()
+
+      if my_type == Category.CONTROL:
+        dst = self.local.get_nickname()
+      else:
+        dst = self.foreign.get_nickname()
+
+      min_base_space = 50
+
+      etc = self.get_etc_content(width - used_space - min_base_space, listing_type)
+      used_space += len(etc)
+
+      base_space = width - used_space
+      used_space = width  # prevents padding at the end
+
+      if len(src) + len(dst) > base_space:
+        src = str_tools.crop(src, base_space / 3)
+        dst = str_tools.crop(dst, base_space - len(src))
+
+      # pads dst entry to its max space
+
+      dst = ("%%-%is" % (base_space - len(src))) % dst
+
+    if my_type == Category.INBOUND:
+      src, dst = dst, src
+
+    padding = " " * (width - used_space + LABEL_MIN_PADDING)
+
+    return LABEL_FORMAT % (src, dst, etc, padding)
+
+  def _get_detail_content(self, width):
+    """
+    Provides a list with detailed information for this connection.
+
+    Arguments:
+      width - max length of lines
+    """
+
+    lines = [""] * 7
+    lines[0] = "address: %s" % self.get_destination_label(width - 11)
+    lines[1] = "locale: %s" % ("??" if self.is_private() else self.foreign.get_locale("??"))
+
+    # Remaining data concerns the consensus results, with three possible cases:
+    # - if there's a single match then display its details
+    # - if there's multiple potential relays then list all of the combinations
+    #   of ORPorts / Fingerprints
+    # - if no consensus data is available then say so (probably a client or
+    #   exit connection)
+
+    fingerprint = self.foreign.get_fingerprint()
+    controller = tor_controller()
+
+    if fingerprint != "UNKNOWN":
+      # single match - display information available about it
+
+      ns_entry = controller.get_info("ns/id/%s" % fingerprint, None)
+      desc_entry = controller.get_info("desc/id/%s" % fingerprint, None)
+
+      # append the fingerprint to the second line
+
+      lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint)
+
+      if ns_entry:
+        # example consensus entry:
+        # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0
+        # s Exit Fast Guard Named Running Stable Valid
+        # w Bandwidth=2540
+        # p accept 20-23,43,53,79-81,88,110,143,194,443
+
+        ns_lines = ns_entry.split("\n")
+
+        first_line_comp = ns_lines[0].split(" ")
+
+        if len(first_line_comp) >= 9:
+          _, nickname, _, _, published_date, published_time, _, or_port, dir_port = first_line_comp[:9]
+        else:
+          nickname, published_date, published_time, or_port, dir_port = "", "", "", "", ""
+
+        flags = "unknown"
+
+        if len(ns_lines) >= 2 and ns_lines[1].startswith("s "):
+          flags = ns_lines[1][2:]
+
+        exit_policy = None
+        descriptor = controller.get_server_descriptor(fingerprint, None)
+
+        if descriptor:
+          exit_policy = descriptor.exit_policy
+
+        if exit_policy:
+          policy_label = exit_policy.summary()
+        else:
+          policy_label = "unknown"
+
+        dir_port_label = "" if dir_port == "0" else "dirport: %s" % dir_port
+        lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, or_port, dir_port_label)
+        lines[3] = "published: %s %s" % (published_time, published_date)
+        lines[4] = "flags: %s" % flags.replace(" ", ", ")
+        lines[5] = "exit policy: %s" % policy_label
+
+      if desc_entry:
+        tor_version, platform, contact = "", "", ""
+
+        for desc_line in desc_entry.split("\n"):
+          if desc_line.startswith("platform"):
+            # has the tor version and platform, ex:
+            # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64
+
+            tor_version = desc_line[13:desc_line.find(" ", 13)]
+            platform = desc_line[desc_line.rfind(" on ") + 4:]
+          elif desc_line.startswith("contact"):
+            contact = desc_line[8:]
+
+            # clears up some highly common obscuring
+
+            for alias in (" at ", " AT "):
+              contact = contact.replace(alias, "@")
+
+            for alias in (" dot ", " DOT "):
+              contact = contact.replace(alias, ".")
+
+            break  # contact lines come after the platform
+
+        lines[3] = "%-35s os: %-14s version: %s" % (lines[3], platform, tor_version)
+
+        # contact information is an optional field
+
+        if contact:
+          lines[6] = "contact: %s" % contact
+    else:
+      all_matches = get_fingerprint_tracker().get_relay_fingerprint(self.foreign.get_address(), get_all_matches = True)
+
+      if all_matches:
+        # multiple matches
+        lines[2] = "Multiple matches, possible fingerprints are:"
+
+        for i in range(len(all_matches)):
+          is_last_line = i == 3
+
+          relay_port, relay_fingerprint = all_matches[i]
+          line_text = "%i. or port: %-5s fingerprint: %s" % (i, relay_port, relay_fingerprint)
+
+          # if there's multiple lines remaining at the end then give a count
+
+          remaining_relays = len(all_matches) - i
+
+          if is_last_line and remaining_relays > 1:
+            line_text = "... %i more" % remaining_relays
+
+          lines[3 + i] = line_text
+
+          if is_last_line:
+            break
+      else:
+        # no consensus entry for this ip address
+        lines[2] = "No consensus data found"
+
+    # crops any lines that are too long
+
+    for i in range(len(lines)):
+      lines[i] = str_tools.crop(lines[i], width - 2)
+
+    return lines
+
+  def get_destination_label(self, max_length, include_locale = False, include_hostname = False):
+    """
+    Provides a short description of the destination. This is made up of two
+    components, the base <ip addr>:<port> and an extra piece of information in
+    parentheses. The IP address is scrubbed from private connections.
+
+    Extra information is...
+    - the port's purpose for exit connections
+    - the locale and/or hostname if set to do so, the address isn't private,
+      and isn't on the local network
+    - nothing otherwise
+
+    Arguments:
+      max_length       - maximum length of the string returned
+      include_locale   - possibly includes the locale
+      include_hostname - possibly includes the hostname
+    """
+
+    # the port and port derived data can be hidden by config or without include_port
+
+    include_port = self.include_port and (CONFIG["features.connection.showExitPort"] or self.get_type() != Category.EXIT)
+
+    # destination of the connection
+
+    address_label = "<scrubbed>" if self.is_private() else self.foreign.get_address()
+    port_label = ":%s" % self.foreign.get_port() if include_port else ""
+    destination_address = address_label + port_label
+
+    # Only append the extra info if there's at least a couple characters of
+    # space (this is what's needed for the country codes).
+
+    if len(destination_address) + 5 <= max_length:
+      space_available = max_length - len(destination_address) - 3
+
+      if self.get_type() == Category.EXIT and include_port:
+        purpose = connection.port_usage(self.foreign.get_port())
+
+        if purpose:
+          # BitTorrent is a common protocol to truncate, so just use "Torrent"
+          # if there's not enough room.
+
+          if len(purpose) > space_available and purpose == "BitTorrent":
+            purpose = "Torrent"
+
+          # crops with a hyphen if too long
+
+          purpose = str_tools.crop(purpose, space_available, ending = str_tools.Ending.HYPHEN)
+
+          destination_address += " (%s)" % purpose
+      elif not connection.is_private_address(self.foreign.get_address()):
+        extra_info = []
+        controller = tor_controller()
+
+        if include_locale and not controller.is_geoip_unavailable():
+          foreign_locale = self.foreign.get_locale("??")
+          extra_info.append(foreign_locale)
+          space_available -= len(foreign_locale) + 2
+
+        if include_hostname:
+          destination_hostname = self.foreign.get_hostname()
+
+          if destination_hostname:
+            # determines the full space available, taking into account the ", "
+            # dividers if there's multiple pieces of extra data
+
+            max_hostname_space = space_available - 2 * len(extra_info)
+            destination_hostname = str_tools.crop(destination_hostname, max_hostname_space)
+            extra_info.append(destination_hostname)
+            space_available -= len(destination_hostname)
+
+        if extra_info:
+          destination_address += " (%s)" % ", ".join(extra_info)
+
+    return destination_address[:max_length]
+
+
+def get_hidden_service_ports(controller, default = []):
+  """
+  Provides the target ports hidden services are configured to use.
+
+  Arguments:
+    default - value provided back if unable to query the hidden service ports
+  """
+
+  result = []
+  hs_options = controller.get_conf_map("HiddenServiceOptions", {})
+
+  for entry in hs_options.get("HiddenServicePort", []):
+    # HiddenServicePort entries are of the form...
+    #
+    #   VIRTPORT [TARGET]
+    #
+    # ... with the TARGET being an address, port, or address:port. If the
+    # target port isn't defined then uses the VIRTPORT.
+
+    hs_port = None
+
+    if ' ' in entry:
+      virtport, target = entry.split(' ', 1)
+
+      if ':' in target:
+        hs_port = target.split(':', 1)[1]  # target is an address:port
+      elif target.isdigit():
+        hs_port = target  # target is a port
+      else:
+        hs_port = virtport  # target is an address
+    else:
+      hs_port = entry  # just has the virtual port
+
+    if hs_port.isdigit():
+      result.append(hs_port)
+
+  if result:
+    return result
+  else:
+    return default
+
+
+def is_exiting_allowed(controller, ip_address, port):
+  """
+  Checks if the given destination can be exited to by this relay, returning
+  True if so and False otherwise.
+  """
+
+  result = False
+
+  if controller.is_alive():
+    # If we allow any exiting then this could be relayed DNS queries,
+    # otherwise the policy is checked. Tor still makes DNS connections to
+    # test when exiting isn't allowed, but nothing is relayed over them.
+    # I'm registering these as non-exiting to avoid likely user confusion:
+    # https://trac.torproject.org/projects/tor/ticket/965
+
+    our_policy = controller.get_exit_policy(None)
+
+    if our_policy and our_policy.is_exiting_allowed() and port == "53":
+      result = True
+    else:
+      result = our_policy and our_policy.can_exit_to(ip_address, port)
+
+  return result
+
+
+class FingerprintTracker:
+  def __init__(self):
+    # mappings of ip -> [(port, fingerprint), ...]
+
+    self._fingerprint_mappings = None
+
+    # lookup cache with (ip, port) -> fingerprint mappings
+
+    self._fingerprint_lookup_cache = {}
+
+    # lookup cache with fingerprint -> nickname mappings
+
+    self._nickname_lookup_cache = {}
+
+    controller = tor_controller()
+
+    controller.add_event_listener(self.new_consensus_event, stem.control.EventType.NEWCONSENSUS)
+    controller.add_event_listener(self.new_desc_event, stem.control.EventType.NEWDESC)
+
+  def new_consensus_event(self, event):
+    self._fingerprint_lookup_cache = {}
+    self._nickname_lookup_cache = {}
+
+    if self._fingerprint_mappings is not None:
+      self._fingerprint_mappings = self._get_fingerprint_mappings(event.desc)
+
+  def new_desc_event(self, event):
+    # If we're tracking ip address -> fingerprint mappings then update with
+    # the new relays.
+
+    self._fingerprint_lookup_cache = {}
+
+    if self._fingerprint_mappings is not None:
+      desc_fingerprints = [fingerprint for (fingerprint, nickname) in event.relays]
+
+      for fingerprint in desc_fingerprints:
+        # gets consensus data for the new descriptor
+
+        try:
+          desc = tor_controller().get_network_status(fingerprint)
+        except stem.ControllerError:
+          continue
+
+        # updates fingerprintMappings with new data
+
+        if desc.address in self._fingerprint_mappings:
+          # if entry already exists with the same orport, remove it
+
+          orport_match = None
+
+          for entry_port, entry_fingerprint in self._fingerprint_mappings[desc.address]:
+            if entry_port == desc.or_port:
+              orport_match = (entry_port, entry_fingerprint)
+              break
+
+          if orport_match:
+            self._fingerprint_mappings[desc.address].remove(orport_match)
+
+          # add the new entry
+
+          self._fingerprint_mappings[desc.address].append((desc.or_port, desc.fingerprint))
+        else:
+          self._fingerprint_mappings[desc.address] = [(desc.or_port, desc.fingerprint)]
+
+  def get_relay_fingerprint(self, relay_address, relay_port = None, get_all_matches = False):
+    """
+    Provides the fingerprint associated with the given address. If there's
+    multiple potential matches or the mapping is unknown then this returns
+    None. This disambiguates the fingerprint if there's multiple relays on
+    the same ip address by several methods, one of them being to pick relays
+    we have a connection with.
+
+    Arguments:
+      relay_address  - address of relay to be returned
+      relay_port     - orport of relay (to further narrow the results)
+      get_all_matches - ignores the relay_port and provides all of the
+                      (port, fingerprint) tuples matching the given
+                      address
+    """
+
+    result = None
+    controller = tor_controller()
+
+    if controller.is_alive():
+      if get_all_matches:
+        # populates the ip -> fingerprint mappings if not yet available
+        if self._fingerprint_mappings is None:
+          self._fingerprint_mappings = self._get_fingerprint_mappings()
+
+        if relay_address in self._fingerprint_mappings:
+          result = self._fingerprint_mappings[relay_address]
+        else:
+          result = []
+      else:
+        # query the fingerprint if it isn't yet cached
+        if (relay_address, relay_port) not in self._fingerprint_lookup_cache:
+          relay_fingerprint = self._get_relay_fingerprint(controller, relay_address, relay_port)
+          self._fingerprint_lookup_cache[(relay_address, relay_port)] = relay_fingerprint
+
+        result = self._fingerprint_lookup_cache[(relay_address, relay_port)]
+
+    return result
+
+  def get_relay_nickname(self, relay_fingerprint):
+    """
+    Provides the nickname associated with the given relay. This provides None
+    if no such relay exists, and "Unnamed" if the name hasn't been set.
+
+    Arguments:
+      relay_fingerprint - fingerprint of the relay
+    """
+
+    result = None
+    controller = tor_controller()
+
+    if controller.is_alive():
+      # query the nickname if it isn't yet cached
+      if relay_fingerprint not in self._nickname_lookup_cache:
+        if relay_fingerprint == controller.get_info("fingerprint", None):
+          # this is us, simply check the config
+          my_nickname = controller.get_conf("Nickname", "Unnamed")
+          self._nickname_lookup_cache[relay_fingerprint] = my_nickname
+        else:
+          ns_entry = controller.get_network_status(relay_fingerprint, None)
+
+          if ns_entry:
+            self._nickname_lookup_cache[relay_fingerprint] = ns_entry.nickname
+
+      result = self._nickname_lookup_cache[relay_fingerprint]
+
+    return result
+
+  def _get_relay_fingerprint(self, controller, relay_address, relay_port):
+    """
+    Provides the fingerprint associated with the address/port combination.
+
+    Arguments:
+      relay_address - address of relay to be returned
+      relay_port    - orport of relay (to further narrow the results)
+    """
+
+    # If we were provided with a string port then convert to an int (so
+    # lookups won't mismatch based on type).
+
+    if isinstance(relay_port, str):
+      relay_port = int(relay_port)
+
+    # checks if this matches us
+
+    if relay_address == controller.get_info("address", None):
+      if not relay_port or str(relay_port) == controller.get_conf("ORPort", None):
+        return controller.get_info("fingerprint", None)
+
+    # if we haven't yet populated the ip -> fingerprint mappings then do so
+
+    if self._fingerprint_mappings is None:
+      self._fingerprint_mappings = self._get_fingerprint_mappings()
+
+    potential_matches = self._fingerprint_mappings.get(relay_address)
+
+    if not potential_matches:
+      return None  # no relay matches this ip address
+
+    if len(potential_matches) == 1:
+      # There's only one relay belonging to this ip address. If the port
+      # matches then we're done.
+
+      match = potential_matches[0]
+
+      if relay_port and match[0] != relay_port:
+        return None
+      else:
+        return match[1]
+    elif relay_port:
+      # Multiple potential matches, so trying to match based on the port.
+      for entry_port, entry_fingerprint in potential_matches:
+        if entry_port == relay_port:
+          return entry_fingerprint
+
+    return None
+
+  def _get_fingerprint_mappings(self, descriptors = None):
+    """
+    Provides IP address to (port, fingerprint) tuple mappings for all of the
+    currently cached relays.
+
+    Arguments:
+      descriptors - router status entries (fetched if not provided)
+    """
+
+    results = {}
+    controller = tor_controller()
+
+    if controller.is_alive():
+      # fetch the current network status if not provided
+
+      if not descriptors:
+        try:
+          descriptors = controller.get_network_statuses()
+        except stem.ControllerError:
+          descriptors = []
+
+      # construct mappings of ips to relay data
+
+      for desc in descriptors:
+        results.setdefault(desc.address, []).append((desc.or_port, desc.fingerprint))
+
+    return results
diff --git a/seth/connections/conn_panel.py b/seth/connections/conn_panel.py
new file mode 100644
index 0000000..b29de38
--- /dev/null
+++ b/seth/connections/conn_panel.py
@@ -0,0 +1,674 @@
+"""
+Listing of the currently established connections tor has made.
+"""
+
+import re
+import time
+import curses
+import threading
+
+import seth.popups
+import seth.util.tracker
+
+from seth.connections import count_popup, descriptor_popup, entries, conn_entry, circ_entry
+from seth.util import panel, tor_controller, tracker, ui_tools
+
+from stem.control import State
+from stem.util import conf, connection, enum
+
+# height of the detail panel content, not counting top and bottom border
+
+DETAILS_HEIGHT = 7
+
+# listing types
+
+Listing = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+
+def conf_handler(key, value):
+  if key == "features.connection.listing_type":
+    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("seth", {
+  "features.connection.resolveApps": True,
+  "features.connection.listing_type": 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):
+  """
+  Listing of connections tor is making, with information correlated against
+  the current consensus and other data sources.
+  """
+
+  def __init__(self, stdscr):
+    panel.Panel.__init__(self, stdscr, "connections", 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+
+    # 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 seth doesn't allow for that atm)
+
+    if not CONFIG["features.connection.showIps"] and CONFIG["features.connection.listing_type"] == 0:
+      seth_config = conf.get_config("seth")
+      seth_config.set("features.connection.listing_type", Listing.keys()[Listing.index_of(Listing.FINGERPRINT)])
+
+    self._scroller = ui_tools.Scroller(True)
+    self._title = "Connections:"  # title line of the panel
+    self._entries = []            # last fetched display entries
+    self._entry_lines = []        # individual lines rendered from the entries listing
+    self._show_details = False    # presents the details panel if true
+
+    self._last_update = -1        # time the content was last revised
+    self._is_tor_running = True   # indicates if tor is currently running or not
+    self._halt_time = None        # time when tor was stopped
+    self._halt = False            # terminates thread if true
+    self._cond = threading.Condition()  # used for pausing the thread
+    self.vals_lock = threading.RLock()
+
+    # Tracks exiting port and client country statistics
+
+    self._client_locale_usage = {}
+    self._exit_port_usage = {}
+
+    # If we're a bridge and been running over a day then prepopulates with the
+    # last day's clients.
+
+    controller = tor_controller()
+    bridge_clients = controller.get_info("status/clients-seen", None)
+
+    if bridge_clients:
+      # Response has a couple arguments...
+      # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8
+
+      country_summary = None
+
+      for arg in bridge_clients.split():
+        if arg.startswith("CountrySummary="):
+          country_summary = arg[15:]
+          break
+
+      if country_summary:
+        for entry in country_summary.split(","):
+          if re.match("^..=[0-9]+$", entry):
+            locale, count = entry.split("=", 1)
+            self._client_locale_usage[locale] = int(count)
+
+    # Last sampling received from the ConnectionResolver, used to detect when
+    # it changes.
+
+    self._last_resource_fetch = -1
+
+    # resolver for the command/pid associated with SOCKS, HIDDEN, and CONTROL connections
+
+    self._app_resolver = tracker.get_port_usage_tracker()
+
+    # rate limits appResolver queries to once per update
+
+    self.app_resolve_since_update = False
+
+    # mark the initially exitsing connection uptimes as being estimates
+
+    for entry in self._entries:
+      if isinstance(entry, conn_entry.ConnectionEntry):
+        entry.getLines()[0].is_initial_connection = True
+
+    # listens for when tor stops so we know to stop reflecting changes
+
+    controller.add_status_listener(self.tor_state_listener)
+
+  def tor_state_listener(self, controller, event_type, _):
+    """
+    Freezes the connection contents when Tor stops.
+    """
+
+    self._is_tor_running = event_type in (State.INIT, State.RESET)
+
+    if self._is_tor_running:
+      self._halt_time = None
+    else:
+      self._halt_time = time.time()
+
+    self.redraw(True)
+
+  def get_pause_time(self):
+    """
+    Provides the time Tor stopped if it isn't running. Otherwise this is the
+    time we were last paused.
+    """
+
+    if self._halt_time:
+      return self._halt_time
+    else:
+      return panel.Panel.get_pause_time(self)
+
+  def set_sort_order(self, ordering = None):
+    """
+    Sets the connection attributes we're sorting by and resorts the contents.
+
+    Arguments:
+      ordering - new ordering, if undefined then this resorts with the last
+                 set ordering
+    """
+
+    self.vals_lock.acquire()
+
+    if ordering:
+      seth_config = conf.get_config("seth")
+
+      ordering_keys = [entries.SortAttr.keys()[entries.SortAttr.index_of(v)] for v in ordering]
+      seth_config.set("features.connection.order", ", ".join(ordering_keys))
+
+    self._entries.sort(key = lambda i: (i.get_sort_values(CONFIG["features.connection.order"], self.get_listing_type())))
+
+    self._entry_lines = []
+
+    for entry in self._entries:
+      self._entry_lines += entry.getLines()
+
+    self.vals_lock.release()
+
+  def get_listing_type(self):
+    """
+    Provides the priority content we list connections by.
+    """
+
+    return CONFIG["features.connection.listing_type"]
+
+  def set_listing_type(self, listing_type):
+    """
+    Sets the priority information presented by the panel.
+
+    Arguments:
+      listing_type - Listing instance for the primary information to be shown
+    """
+
+    if self.get_listing_type() == listing_type:
+      return
+
+    self.vals_lock.acquire()
+
+    seth_config = conf.get_config("seth")
+    seth_config.set("features.connection.listing_type", Listing.keys()[Listing.index_of(listing_type)])
+
+    # if we're sorting by the listing then we need to resort
+
+    if entries.SortAttr.LISTING in CONFIG["features.connection.order"]:
+      self.set_sort_order()
+
+    self.vals_lock.release()
+
+  def is_clients_allowed(self):
+    """
+    True if client connections are permissable, false otherwise.
+    """
+
+    controller = tor_controller()
+
+    my_flags = []
+    my_fingerprint = self.get_info("fingerprint", None)
+
+    if my_fingerprint:
+      my_status_entry = self.controller.get_network_status(my_fingerprint)
+
+      if my_status_entry:
+        my_flags = my_status_entry.flags
+
+    return "Guard" in my_flags or controller.get_conf("BridgeRelay", None) == "1"
+
+  def is_exits_allowed(self):
+    """
+    True if exit connections are permissable, false otherwise.
+    """
+
+    controller = tor_controller()
+
+    if not controller.get_conf("ORPort", None):
+      return False  # no ORPort
+
+    policy = controller.get_exit_policy(None)
+
+    return policy and policy.is_exiting_allowed()
+
+  def show_sort_dialog(self):
+    """
+    Provides the sort dialog for our connections.
+    """
+
+    # set ordering for connection options
+
+    title_label = "Connection Ordering:"
+    options = list(entries.SortAttr)
+    old_selection = CONFIG["features.connection.order"]
+    option_colors = dict([(attr, entries.SORT_COLORS[attr]) for attr in options])
+    results = seth.popups.show_sort_dialog(title_label, options, old_selection, option_colors)
+
+    if results:
+      self.set_sort_order(results)
+
+  def handle_key(self, key):
+    with self.vals_lock:
+      if key.is_scroll():
+        page_height = self.get_preferred_size()[0] - 1
+
+        if self._show_details:
+          page_height -= (DETAILS_HEIGHT + 1)
+
+        is_changed = self._scroller.handle_key(key, self._entry_lines, page_height)
+
+        if is_changed:
+          self.redraw(True)
+      elif key.is_selection():
+        self._show_details = not self._show_details
+        self.redraw(True)
+      elif key.match('s'):
+        self.show_sort_dialog()
+      elif key.match('u'):
+        # provides a menu to pick the connection resolver
+
+        title = "Resolver Util:"
+        options = ["auto"] + list(connection.Resolver)
+        conn_resolver = seth.util.tracker.get_connection_tracker()
+
+        current_overwrite = conn_resolver.get_custom_resolver()
+
+        if current_overwrite is None:
+          old_selection = 0
+        else:
+          old_selection = options.index(current_overwrite)
+
+        selection = seth.popups.show_menu(title, options, old_selection)
+
+        # applies new setting
+
+        if selection != -1:
+          selected_option = options[selection] if selection != 0 else None
+          conn_resolver.set_custom_resolver(selected_option)
+      elif key.match('l'):
+        # provides a menu to pick the primary information we list connections by
+
+        title = "List By:"
+        options = list(entries.ListingType)
+
+        # dropping the HOSTNAME listing type until we support displaying that content
+
+        options.remove(seth.connections.entries.ListingType.HOSTNAME)
+
+        old_selection = options.index(self.get_listing_type())
+        selection = seth.popups.show_menu(title, options, old_selection)
+
+        # applies new setting
+
+        if selection != -1:
+          self.set_listing_type(options[selection])
+      elif key.match('d'):
+        # presents popup for raw consensus data
+        descriptor_popup.show_descriptor_popup(self)
+      elif key.match('c') and self.is_clients_allowed():
+        count_popup.showCountDialog(count_popup.CountType.CLIENT_LOCALE, self._client_locale_usage)
+      elif key.match('e') and self.is_exits_allowed():
+        count_popup.showCountDialog(count_popup.CountType.EXIT_PORT, self._exit_port_usage)
+      else:
+        return False
+
+      return True
+
+  def run(self):
+    """
+    Keeps connections listing updated, checking for new entries at a set rate.
+    """
+
+    last_draw = time.time() - 1
+
+    # Fetches out initial connection results. The wait is so this doesn't
+    # run during seth's interface initialization (otherwise there's a
+    # noticeable pause before the first redraw).
+
+    self._cond.acquire()
+    self._cond.wait(0.2)
+    self._cond.release()
+    self._update()             # populates initial entries
+    self._resolve_apps(False)  # resolves initial applications
+
+    while not self._halt:
+      current_time = time.time()
+
+      if self.is_paused() or not self._is_tor_running or current_time - last_draw < CONFIG["features.connection.refreshRate"]:
+        self._cond.acquire()
+
+        if not self._halt:
+          self._cond.wait(0.2)
+
+        self._cond.release()
+      else:
+        # updates content if their's new results, otherwise just redraws
+
+        self._update()
+        self.redraw(True)
+
+        # we may have missed multiple updates due to being paused, showing
+        # another panel, etc so last_draw might need to jump multiple ticks
+
+        draw_ticks = (time.time() - last_draw) / CONFIG["features.connection.refreshRate"]
+        last_draw += CONFIG["features.connection.refreshRate"] * draw_ticks
+
+  def get_help(self):
+    resolver_util = seth.util.tracker.get_connection_tracker().get_custom_resolver()
+
+    options = [
+      ('up arrow', 'scroll up a line', None),
+      ('down arrow', 'scroll down a line', None),
+      ('page up', 'scroll up a page', None),
+      ('page down', 'scroll down a page', None),
+      ('enter', 'show connection details', None),
+      ('d', 'raw consensus descriptor', None),
+    ]
+
+    if self.is_clients_allowed():
+      options.append(('c', 'client locale usage summary', None))
+
+    if self.is_exits_allowed():
+      options.append(('e', 'exit port usage summary', None))
+
+    options.append(('l', 'listed identity', self.get_listing_type().lower()))
+    options.append(('s', 'sort ordering', None))
+    options.append(('u', 'resolving utility', 'auto' if resolver_util is None else resolver_util))
+    return options
+
+  def get_selection(self):
+    """
+    Provides the currently selected connection entry.
+    """
+
+    return self._scroller.get_cursor_selection(self._entry_lines)
+
+  def draw(self, width, height):
+    self.vals_lock.acquire()
+
+    # if we don't have any contents then refuse to show details
+
+    if not self._entries:
+      self._show_details = False
+
+    # extra line when showing the detail panel is for the bottom border
+
+    detail_panel_offset = DETAILS_HEIGHT + 1 if self._show_details else 0
+    is_scrollbar_visible = len(self._entry_lines) > height - detail_panel_offset - 1
+
+    scroll_location = self._scroller.get_scroll_location(self._entry_lines, height - detail_panel_offset - 1)
+    cursor_selection = self.get_selection()
+
+    # draws the detail panel if currently displaying it
+
+    if self._show_details and cursor_selection:
+      # This is a solid border unless the scrollbar is visible, in which case a
+      # 'T' pipe connects the border to the bar.
+
+      ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
+
+      if is_scrollbar_visible:
+        self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
+
+      draw_entries = cursor_selection.get_details(width)
+
+      for i in range(min(len(draw_entries), DETAILS_HEIGHT)):
+        self.addstr(1 + i, 2, draw_entries[i][0], *draw_entries[i][1])
+
+    # title label with connection counts
+
+    if self.is_title_visible():
+      title = "Connection Details:" if self._show_details else self._title
+      self.addstr(0, 0, title, curses.A_STANDOUT)
+
+    scroll_offset = 0
+
+    if is_scrollbar_visible:
+      scroll_offset = 2
+      self.add_scroll_bar(scroll_location, scroll_location + height - detail_panel_offset - 1, len(self._entry_lines), 1 + detail_panel_offset)
+
+    if self.is_paused() or not self._is_tor_running:
+      current_time = self.get_pause_time()
+    else:
+      current_time = time.time()
+
+    for line_number in range(scroll_location, len(self._entry_lines)):
+      entry_line = self._entry_lines[line_number]
+
+      # if this is an unresolved SOCKS, HIDDEN, or CONTROL entry then queue up
+      # resolution for the applicaitions they belong to
+
+      if isinstance(entry_line, conn_entry.ConnectionLine) and entry_line.is_unresolved_application():
+        self._resolve_apps()
+
+      # hilighting if this is the selected line
+
+      extra_format = curses.A_STANDOUT if entry_line == cursor_selection else curses.A_NORMAL
+
+      draw_line = line_number + detail_panel_offset + 1 - scroll_location
+
+      prefix = entry_line.get_listing_prefix()
+
+      for i in range(len(prefix)):
+        self.addch(draw_line, scroll_offset + i, prefix[i])
+
+      x_offset = scroll_offset + len(prefix)
+      draw_entry = entry_line.get_listing_entry(width - scroll_offset - len(prefix), current_time, self.get_listing_type())
+
+      for msg, attr in draw_entry:
+        attr |= extra_format
+        self.addstr(draw_line, x_offset, msg, *attr)
+        x_offset += len(msg)
+
+      if draw_line >= height:
+        break
+
+    self.vals_lock.release()
+
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+
+    self._cond.acquire()
+    self._halt = True
+    self._cond.notifyAll()
+    self._cond.release()
+
+  def _update(self):
+    """
+    Fetches the newest resolved connections.
+    """
+
+    self.app_resolve_since_update = False
+
+    # if we don't have an initialized resolver then this is a no-op
+
+    if not seth.util.tracker.get_connection_tracker().is_alive():
+      return
+
+    conn_resolver = seth.util.tracker.get_connection_tracker()
+    current_resolution_count = conn_resolver.run_counter()
+
+    self.vals_lock.acquire()
+
+    new_entries = []  # the new results we'll display
+
+    # Fetches new connections and client circuits...
+    # new_connections  [(local ip, local port, foreign ip, foreign port)...]
+    # new_circuits     {circuit_id => (status, purpose, path)...}
+
+    new_connections = [(conn.local_address, conn.local_port, conn.remote_address, conn.remote_port) for conn in conn_resolver.get_value()]
+    new_circuits = {}
+
+    for circ in tor_controller().get_circuits():
+      # Skips established single-hop circuits (these are for directory
+      # fetches, not client circuits)
+
+      if not (circ.status == "BUILT" and len(circ.path) == 1):
+        new_circuits[circ.id] = (circ.status, circ.purpose, [entry[0] for entry in circ.path])
+
+    # Populates new_entries with any of our old entries that still exist.
+    # This is both for performance and to keep from resetting the uptime
+    # attributes. Note that CircEntries are a ConnectionEntry subclass so
+    # we need to check for them first.
+
+    for old_entry in self._entries:
+      if isinstance(old_entry, circ_entry.CircEntry):
+        new_entry = new_circuits.get(old_entry.circuit_id)
+
+        if new_entry:
+          old_entry.update(new_entry[0], new_entry[2])
+          new_entries.append(old_entry)
+          del new_circuits[old_entry.circuit_id]
+      elif isinstance(old_entry, conn_entry.ConnectionEntry):
+        connection_line = old_entry.getLines()[0]
+        conn_attr = (connection_line.local.get_address(), connection_line.local.get_port(),
+                     connection_line.foreign.get_address(), connection_line.foreign.get_port())
+
+        if conn_attr in new_connections:
+          new_entries.append(old_entry)
+          new_connections.remove(conn_attr)
+
+    # Reset any display attributes for the entries we're keeping
+
+    for entry in new_entries:
+      entry.reset_display()
+
+    # Adds any new connection and circuit entries.
+
+    for local_address, local_port, remote_address, remote_port in new_connections:
+      new_conn_entry = conn_entry.ConnectionEntry(local_address, local_port, remote_address, remote_port)
+      new_conn_line = new_conn_entry.getLines()[0]
+
+      if new_conn_line.get_type() != conn_entry.Category.CIRCUIT:
+        new_entries.append(new_conn_entry)
+
+        # updates exit port and client locale usage information
+        if new_conn_line.is_private():
+          if new_conn_line.get_type() == conn_entry.Category.INBOUND:
+            # client connection, update locale information
+
+            client_locale = new_conn_line.foreign.get_locale()
+
+            if client_locale:
+              self._client_locale_usage[client_locale] = self._client_locale_usage.get(client_locale, 0) + 1
+          elif new_conn_line.get_type() == conn_entry.Category.EXIT:
+            exit_port = new_conn_line.foreign.get_port()
+            self._exit_port_usage[exit_port] = self._exit_port_usage.get(exit_port, 0) + 1
+
+    for circuit_id in new_circuits:
+      status, purpose, path = new_circuits[circuit_id]
+      new_entries.append(circ_entry.CircEntry(circuit_id, status, purpose, path))
+
+    # Counts the relays in each of the categories. This also flushes the
+    # type cache for all of the connections (in case its changed since last
+    # fetched).
+
+    category_types = list(conn_entry.Category)
+    type_counts = dict((type, 0) for type in category_types)
+
+    for entry in new_entries:
+      if isinstance(entry, conn_entry.ConnectionEntry):
+        type_counts[entry.getLines()[0].get_type()] += 1
+      elif isinstance(entry, circ_entry.CircEntry):
+        type_counts[conn_entry.Category.CIRCUIT] += 1
+
+    # makes labels for all the categories with connections (ie,
+    # "21 outbound", "1 control", etc)
+
+    count_labels = []
+
+    for category in category_types:
+      if type_counts[category] > 0:
+        count_labels.append("%i %s" % (type_counts[category], category.lower()))
+
+    if count_labels:
+      self._title = "Connections (%s):" % ", ".join(count_labels)
+    else:
+      self._title = "Connections:"
+
+    self._entries = new_entries
+
+    self._entry_lines = []
+
+    for entry in self._entries:
+      self._entry_lines += entry.getLines()
+
+    self.set_sort_order()
+    self._last_resource_fetch = current_resolution_count
+    self.vals_lock.release()
+
+  def _resolve_apps(self, flag_query = True):
+    """
+    Triggers an asynchronous query for all unresolved SOCKS, HIDDEN, and
+    CONTROL entries.
+
+    Arguments:
+      flag_query - sets a flag to prevent further call from being respected
+                  until the next update if true
+    """
+
+    if self.app_resolve_since_update or not CONFIG["features.connection.resolveApps"]:
+      return
+
+    unresolved_lines = [l for l in self._entry_lines if isinstance(l, conn_entry.ConnectionLine) and l.is_unresolved_application()]
+
+    # get the ports used for unresolved applications
+
+    app_ports = []
+
+    for line in unresolved_lines:
+      app_conn = line.local if line.get_type() == conn_entry.Category.HIDDEN else line.foreign
+      app_ports.append(app_conn.get_port())
+
+    # Queue up resolution for the unresolved ports (skips if it's still working
+    # on the last query).
+
+    if app_ports and not self._app_resolver.is_alive():
+      self._app_resolver.get_processes_using_ports(app_ports)
+
+    # Fetches results. If the query finishes quickly then this is what we just
+    # asked for, otherwise these belong to an earlier resolution.
+    #
+    # The application resolver might have given up querying (for instance, if
+    # the lsof lookups aren't working on this platform or lacks permissions).
+    # The is_application_resolving flag lets the unresolved entries indicate if there's
+    # a lookup in progress for them or not.
+
+    time.sleep(0.2)  # TODO: previous resolver only blocked while awaiting a lookup
+    app_results = self._app_resolver.get_processes_using_ports(app_ports)
+
+    for line in unresolved_lines:
+      is_local = line.get_type() == conn_entry.Category.HIDDEN
+      line_port = line.local.get_port() if is_local else line.foreign.get_port()
+
+      if line_port in app_results:
+        # sets application attributes if there's a result with this as the
+        # inbound port
+
+        for inbound_port, outbound_port, cmd, pid in app_results[line_port]:
+          app_port = outbound_port if is_local else inbound_port
+
+          if line_port == app_port:
+            line.application_name = cmd
+            line.application_pid = pid
+            line.is_application_resolving = False
+      else:
+        line.is_application_resolving = self._app_resolver.is_alive
+
+    if flag_query:
+      self.app_resolve_since_update = True
diff --git a/seth/connections/count_popup.py b/seth/connections/count_popup.py
new file mode 100644
index 0000000..ef51c17
--- /dev/null
+++ b/seth/connections/count_popup.py
@@ -0,0 +1,111 @@
+"""
+Provides a dialog with client locale or exiting port counts.
+"""
+
+import curses
+import operator
+
+import seth.controller
+import seth.popups
+
+from stem.util import connection, enum, log
+
+CountType = enum.Enum("CLIENT_LOCALE", "EXIT_PORT")
+EXIT_USAGE_WIDTH = 15
+
+
+def showCountDialog(count_type, counts):
+  """
+  Provides a dialog with bar graphs and percentages for the given set of
+  counts. Pressing any key closes the dialog.
+
+  Arguments:
+    count_type - type of counts being presented
+    counts    - mapping of labels to counts
+  """
+
+  is_no_stats = not counts
+  no_stats_msg = "Usage stats aren't available yet, press any key..."
+
+  if is_no_stats:
+    popup, width, height = seth.popups.init(3, len(no_stats_msg) + 4)
+  else:
+    popup, width, height = seth.popups.init(4 + max(1, len(counts)), 80)
+
+  if not popup:
+    return
+
+  try:
+    control = seth.controller.get_controller()
+
+    popup.win.box()
+
+    # dialog title
+
+    if count_type == CountType.CLIENT_LOCALE:
+      title = "Client Locales"
+    elif count_type == CountType.EXIT_PORT:
+      title = "Exiting Port Usage"
+    else:
+      title = ""
+      log.warn("Unrecognized count type: %s" % count_type)
+
+    popup.addstr(0, 0, title, curses.A_STANDOUT)
+
+    if is_no_stats:
+      popup.addstr(1, 2, no_stats_msg, curses.A_BOLD, 'cyan')
+    else:
+      sorted_counts = sorted(counts.iteritems(), key=operator.itemgetter(1))
+      sorted_counts.reverse()
+
+      # constructs string formatting for the max key and value display width
+
+      key_width, val_width, value_total = 3, 1, 0
+
+      for k, v in sorted_counts:
+        key_width = max(key_width, len(k))
+        val_width = max(val_width, len(str(v)))
+        value_total += v
+
+      # extra space since we're adding usage informaion
+
+      if count_type == CountType.EXIT_PORT:
+        key_width += EXIT_USAGE_WIDTH
+
+      label_format = "%%-%is %%%ii (%%%%%%-2i)" % (key_width, val_width)
+
+      for i in range(height - 4):
+        k, v = sorted_counts[i]
+
+        # includes a port usage column
+
+        if count_type == CountType.EXIT_PORT:
+          usage = connection.port_usage(k)
+
+          if usage:
+            key_format = "%%-%is   %%s" % (key_width - EXIT_USAGE_WIDTH)
+            k = key_format % (k, usage[:EXIT_USAGE_WIDTH - 3])
+
+        label = label_format % (k, v, v * 100 / value_total)
+        popup.addstr(i + 1, 2, label, curses.A_BOLD, 'green')
+
+        # All labels have the same size since they're based on the max widths.
+        # If this changes then this'll need to be the max label width.
+
+        label_width = len(label)
+
+        # draws simple bar graph for percentages
+
+        fill_width = v * (width - 4 - label_width) / value_total
+
+        for j in range(fill_width):
+          popup.addstr(i + 1, 3 + label_width + j, " ", curses.A_STANDOUT, 'red')
+
+      popup.addstr(height - 2, 2, "Press any key...")
+
+    popup.win.refresh()
+
+    curses.cbreak()
+    control.key_input()
+  finally:
+    seth.popups.finalize()
diff --git a/seth/connections/descriptor_popup.py b/seth/connections/descriptor_popup.py
new file mode 100644
index 0000000..2c8b47c
--- /dev/null
+++ b/seth/connections/descriptor_popup.py
@@ -0,0 +1,273 @@
+"""
+Popup providing the raw descriptor and consensus information for a relay.
+"""
+
+import math
+import curses
+
+import seth.popups
+import seth.connections.conn_entry
+
+from seth.util import panel, tor_controller, ui_tools
+
+from stem.util import str_tools
+
+# field keywords used to identify areas for coloring
+
+LINE_NUM_COLOR = "yellow"
+HEADER_COLOR = "cyan"
+HEADER_PREFIX = ["ns/id/", "desc/id/"]
+
+SIG_COLOR = "red"
+SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
+SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
+
+UNRESOLVED_MSG = "No consensus data available"
+ERROR_MSG = "Unable to retrieve data"
+
+
+def show_descriptor_popup(conn_panel):
+  """
+  Presents consensus descriptor in popup window with the following controls:
+  Up, Down, Page Up, Page Down - scroll descriptor
+  Right, Left - next / previous connection
+  Enter, Space, d, D - close popup
+
+  Arguments:
+    conn_panel - connection panel providing the dialog
+  """
+
+  # hides the title of the connection panel
+
+  conn_panel.set_title_visible(False)
+  conn_panel.redraw(True)
+
+  control = seth.controller.get_controller()
+  panel.CURSES_LOCK.acquire()
+  is_done = False
+
+  try:
+    while not is_done:
+      selection = conn_panel.get_selection()
+
+      if not selection:
+        break
+
+      fingerprint = selection.foreign.get_fingerprint()
+
+      if fingerprint == "UNKNOWN":
+        fingerprint = None
+
+      display_text = get_display_text(fingerprint)
+      display_color = seth.connections.conn_entry.CATEGORY_COLOR[selection.get_type()]
+      show_line_number = fingerprint is not None
+
+      # determines the maximum popup size the display_text can fill
+
+      popup_height, popup_width = get_preferred_size(display_text, conn_panel.max_x, show_line_number)
+
+      popup, _, height = seth.popups.init(popup_height, popup_width)
+
+      if not popup:
+        break
+
+      scroll, is_changed = 0, True
+
+      try:
+        while not is_done:
+          if is_changed:
+            draw(popup, fingerprint, display_text, display_color, scroll, show_line_number)
+            is_changed = False
+
+          key = control.key_input()
+
+          if key.is_scroll():
+            # TODO: This is a bit buggy in that scrolling is by display_text
+            # lines rather than the displayed lines, causing issues when
+            # content wraps. The result is that we can't have a scrollbar and
+            # can't scroll to the bottom if there's a multi-line being
+            # displayed. However, trying to correct this introduces a big can
+            # of worms and after hours decided that this isn't worth the
+            # effort...
+
+            new_scroll = ui_tools.get_scroll_position(key, scroll, height - 2, len(display_text))
+
+            if scroll != new_scroll:
+              scroll, is_changed = new_scroll, True
+          elif key.is_selection() or key.match('d'):
+            is_done = True  # closes popup
+          elif key.match('left', 'right'):
+            # navigation - pass on to conn_panel and recreate popup
+
+            conn_panel.handle_key(panel.KeyInput(curses.KEY_UP) if key.match('left') else panel.KeyInput(curses.KEY_DOWN))
+            break
+      finally:
+        seth.popups.finalize()
+  finally:
+    conn_panel.set_title_visible(True)
+    conn_panel.redraw(True)
+    panel.CURSES_LOCK.release()
+
+
+def get_display_text(fingerprint):
+  """
+  Provides the descriptor and consensus entry for a relay. This is a list of
+  lines to be displayed by the dialog.
+  """
+
+  if not fingerprint:
+    return [UNRESOLVED_MSG]
+
+  controller, description = tor_controller(), []
+
+  description.append("ns/id/%s" % fingerprint)
+  consensus_entry = controller.get_info("ns/id/%s" % fingerprint, None)
+
+  if consensus_entry:
+    description += consensus_entry.split("\n")
+  else:
+    description += [ERROR_MSG, ""]
+
+  description.append("desc/id/%s" % fingerprint)
+  descriptor_entry = controller.get_info("desc/id/%s" % fingerprint, None)
+
+  if descriptor_entry:
+    description += descriptor_entry.split("\n")
+  else:
+    description += [ERROR_MSG]
+
+  return description
+
+
+def get_preferred_size(text, max_width, show_line_number):
+  """
+  Provides the (height, width) tuple for the preferred size of the given text.
+  """
+
+  width, height = 0, len(text) + 2
+  line_number_width = int(math.log10(len(text))) + 1
+
+  for line in text:
+    # width includes content, line number field, and border
+
+    line_width = len(line) + 5
+
+    if show_line_number:
+      line_width += line_number_width
+
+    width = max(width, line_width)
+
+    # tracks number of extra lines that will be taken due to text wrap
+    height += (line_width - 2) / max_width
+
+  return (height, width)
+
+
+def draw(popup, fingerprint, display_text, display_color, scroll, show_line_number):
+  popup.win.erase()
+  popup.win.box()
+  x_offset = 2
+
+  if fingerprint:
+    title = "Consensus Descriptor (%s):" % fingerprint
+  else:
+    title = "Consensus Descriptor:"
+
+  popup.addstr(0, 0, title, curses.A_STANDOUT)
+
+  line_number_width = int(math.log10(len(display_text))) + 1
+  is_encryption_block = False   # flag indicating if we're currently displaying a key
+
+  # checks if first line is in an encryption block
+
+  for i in range(0, scroll):
+    line_text = display_text[i].strip()
+
+    if line_text in SIG_START_KEYS:
+      is_encryption_block = True
+    elif line_text in SIG_END_KEYS:
+      is_encryption_block = False
+
+  draw_line, page_height = 1, popup.max_y - 2
+
+  for i in range(scroll, scroll + page_height):
+    line_text = display_text[i].strip()
+    x_offset = 2
+
+    if show_line_number:
+      line_number_label = ("%%%ii" % line_number_width) % (i + 1)
+
+      popup.addstr(draw_line, x_offset, line_number_label, curses.A_BOLD, LINE_NUM_COLOR)
+      x_offset += line_number_width + 1
+
+    # Most consensus and descriptor lines are keyword/value pairs. Both are
+    # shown with the same color, but the keyword is bolded.
+
+    keyword, value = line_text, ""
+    draw_format = display_color
+
+    if line_text.startswith(HEADER_PREFIX[0]) or line_text.startswith(HEADER_PREFIX[1]):
+      keyword, value = line_text, ""
+      draw_format = HEADER_COLOR
+    elif line_text == UNRESOLVED_MSG or line_text == ERROR_MSG:
+      keyword, value = line_text, ""
+    elif line_text in SIG_START_KEYS:
+      keyword, value = line_text, ""
+      is_encryption_block = True
+      draw_format = SIG_COLOR
+    elif line_text in SIG_END_KEYS:
+      keyword, value = line_text, ""
+      is_encryption_block = False
+      draw_format = SIG_COLOR
+    elif is_encryption_block:
+      keyword, value = "", line_text
+      draw_format = SIG_COLOR
+    elif " " in line_text:
+      div_index = line_text.find(" ")
+      keyword, value = line_text[:div_index], line_text[div_index:]
+
+    display_queue = [(keyword, (draw_format, curses.A_BOLD)), (value, (draw_format,))]
+    cursor_location = x_offset
+
+    while display_queue:
+      msg, msg_format = display_queue.pop(0)
+
+      if not msg:
+        continue
+
+      max_msg_size = popup.max_x - 1 - cursor_location
+
+      if len(msg) >= max_msg_size:
+        # needs to split up the line
+
+        msg, remainder = str_tools.crop(msg, max_msg_size, None, ending = None, get_remainder = True)
+
+        if x_offset == cursor_location and msg == "":
+          # first word is longer than the line
+
+          msg = str_tools.crop(remainder, max_msg_size)
+
+          if " " in remainder:
+            remainder = remainder.split(" ", 1)[1]
+          else:
+            remainder = ""
+
+        popup.addstr(draw_line, cursor_location, msg, *msg_format)
+        cursor_location = x_offset
+
+        if remainder:
+          display_queue.insert(0, (remainder.strip(), msg_format))
+          draw_line += 1
+      else:
+        popup.addstr(draw_line, cursor_location, msg, *msg_format)
+        cursor_location += len(msg)
+
+      if draw_line > page_height:
+        break
+
+    draw_line += 1
+
+    if draw_line > page_height:
+      break
+
+  popup.win.refresh()
diff --git a/seth/connections/entries.py b/seth/connections/entries.py
new file mode 100644
index 0000000..85bce32
--- /dev/null
+++ b/seth/connections/entries.py
@@ -0,0 +1,179 @@
+"""
+Interface for entries in the connection panel. These consist of two parts: the
+entry itself (ie, Tor connection, client circuit, etc) and the lines it
+consists of in the listing.
+"""
+
+from stem.util import enum
+
+# attributes we can list entries by
+
+ListingType = enum.Enum(("IP_ADDRESS", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
+
+SortAttr = enum.Enum("CATEGORY", "UPTIME", "LISTING", "IP_ADDRESS", "PORT", "HOSTNAME", "FINGERPRINT", "NICKNAME", "COUNTRY")
+
+SORT_COLORS = {
+  SortAttr.CATEGORY: "red",
+  SortAttr.UPTIME: "yellow",
+  SortAttr.LISTING: "green",
+  SortAttr.IP_ADDRESS: "blue",
+  SortAttr.PORT: "blue",
+  SortAttr.HOSTNAME: "magenta",
+  SortAttr.FINGERPRINT: "cyan",
+  SortAttr.NICKNAME: "cyan",
+  SortAttr.COUNTRY: "blue",
+}
+
+# maximum number of ports a system can have
+
+PORT_COUNT = 65536
+
+
+class ConnectionPanelEntry:
+  """
+  Common parent for connection panel entries. This consists of a list of lines
+  in the panel listing. This caches results until the display indicates that
+  they should be flushed.
+  """
+
+  def __init__(self):
+    self.lines = []
+    self.flush_cache = True
+
+  def getLines(self):
+    """
+    Provides the individual lines in the connection listing.
+    """
+
+    if self.flush_cache:
+      self.lines = self._get_lines(self.lines)
+      self.flush_cache = False
+
+    return self.lines
+
+  def _get_lines(self, old_results):
+    # implementation of getLines
+
+    for line in old_results:
+      line.reset_display()
+
+    return old_results
+
+  def get_sort_values(self, sort_attrs, listing_type):
+    """
+    Provides the value used in comparisons to sort based on the given
+    attribute.
+
+    Arguments:
+      sort_attrs   - list of SortAttr values for the field being sorted on
+      listing_type - ListingType enumeration for the attribute we're listing
+                    entries by
+    """
+
+    return [self.get_sort_value(attr, listing_type) for attr in sort_attrs]
+
+  def get_sort_value(self, attr, listing_type):
+    """
+    Provides the value of a single attribute used for sorting purposes.
+
+    Arguments:
+      attr        - list of SortAttr values for the field being sorted on
+      listing_type - ListingType enumeration for the attribute we're listing
+                    entries by
+    """
+
+    if attr == SortAttr.LISTING:
+      if listing_type == ListingType.IP_ADDRESS:
+        # uses the IP address as the primary value, and port as secondary
+        sort_value = self.get_sort_value(SortAttr.IP_ADDRESS, listing_type) * PORT_COUNT
+        sort_value += self.get_sort_value(SortAttr.PORT, listing_type)
+        return sort_value
+      elif listing_type == ListingType.HOSTNAME:
+        return self.get_sort_value(SortAttr.HOSTNAME, listing_type)
+      elif listing_type == ListingType.FINGERPRINT:
+        return self.get_sort_value(SortAttr.FINGERPRINT, listing_type)
+      elif listing_type == ListingType.NICKNAME:
+        return self.get_sort_value(SortAttr.NICKNAME, listing_type)
+
+    return ""
+
+  def reset_display(self):
+    """
+    Flushes cached display results.
+    """
+
+    self.flush_cache = True
+
+
+class ConnectionPanelLine:
+  """
+  Individual line in the connection panel listing.
+  """
+
+  def __init__(self):
+    # cache for displayed information
+    self._listing_cache = None
+    self._listing_cache_args = (None, None)
+
+    self._details_cache = None
+    self._details_cache_args = None
+
+    self._descriptor_cache = None
+    self._descriptor_cache_args = None
+
+  def get_listing_prefix(self):
+    """
+    Provides a list of characters to be appended before the listing entry.
+    """
+
+    return ()
+
+  def get_listing_entry(self, width, current_time, listing_type):
+    """
+    Provides a [(msg, attr)...] tuple list for contents to be displayed in the
+    connection panel listing.
+
+    Arguments:
+      width       - available space to display in
+      current_time - unix timestamp for what the results should consider to be
+                    the current time (this may be ignored due to caching)
+      listing_type - ListingType enumeration for the highest priority content
+                    to be displayed
+    """
+
+    if self._listing_cache_args != (width, listing_type):
+      self._listing_cache = self._get_listing_entry(width, current_time, listing_type)
+      self._listing_cache_args = (width, listing_type)
+
+    return self._listing_cache
+
+  def _get_listing_entry(self, width, current_time, listing_type):
+    # implementation of get_listing_entry
+    return None
+
+  def get_details(self, width):
+    """
+    Provides a list of [(msg, attr)...] tuple listings with detailed
+    information for this connection.
+
+    Arguments:
+      width - available space to display in
+    """
+
+    if self._details_cache_args != width:
+      self._details_cache = self._get_details(width)
+      self._details_cache_args = width
+
+    return self._details_cache
+
+  def _get_details(self, width):
+    # implementation of get_details
+    return []
+
+  def reset_display(self):
+    """
+    Flushes cached display results.
+    """
+
+    self._listing_cache_args = (None, None)
+    self._details_cache_args = None
diff --git a/seth/controller.py b/seth/controller.py
new file mode 100644
index 0000000..0ef0d9b
--- /dev/null
+++ b/seth/controller.py
@@ -0,0 +1,657 @@
+"""
+Main interface loop for seth, periodically redrawing the screen and issuing
+user input to the proper panels.
+"""
+
+import os
+import time
+import curses
+import threading
+
+import seth.arguments
+import seth.menu.menu
+import seth.popups
+import seth.header_panel
+import seth.log_panel
+import seth.config_panel
+import seth.torrc_panel
+import seth.graph_panel
+import seth.connections.conn_panel
+import seth.util.tracker
+
+import stem
+
+from stem.control import State
+
+from seth.util import panel, tor_config, tor_controller, ui_tools
+
+from stem.util import conf, log, system
+
+ARM_CONTROLLER = None
+
+
+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("seth", {
+  "startup.events": "N3",
+  "startup.data_directory": "~/.seth",
+  "features.acsSupport": True,
+  "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,
+  "start_time": 0,
+}, conf_handler)
+
+
+def get_controller():
+  """
+  Provides the seth controller instance.
+  """
+
+  return ARM_CONTROLLER
+
+
+def stop_controller():
+  """
+  Halts our Controller, providing back the thread doing so.
+  """
+
+  def halt_controller():
+    control = get_controller()
+
+    if control:
+      for panel_impl in control.get_daemon_panels():
+        panel_impl.stop()
+
+      for panel_impl in control.get_daemon_panels():
+        panel_impl.join()
+
+  halt_thread = threading.Thread(target = halt_controller)
+  halt_thread.start()
+  return halt_thread
+
+
+def init_controller(stdscr, start_time):
+  """
+  Spawns the controller, and related panels for it.
+
+  Arguments:
+    stdscr - curses window
+  """
+
+  global ARM_CONTROLLER
+
+  # initializes the panels
+
+  sticky_panels = [
+    seth.header_panel.HeaderPanel(stdscr, start_time),
+    LabelPanel(stdscr),
+  ]
+
+  page_panels, first_page_panels = [], []
+
+  # first page: graph and log
+  if CONFIG["features.panels.show.graph"]:
+    first_page_panels.append(seth.graph_panel.GraphPanel(stdscr))
+
+  if CONFIG["features.panels.show.log"]:
+    expanded_events = seth.arguments.expand_events(CONFIG["startup.events"])
+    first_page_panels.append(seth.log_panel.LogPanel(stdscr, expanded_events))
+
+  if first_page_panels:
+    page_panels.append(first_page_panels)
+
+  # second page: connections
+  if CONFIG["features.panels.show.connection"]:
+    page_panels.append([seth.connections.conn_panel.ConnectionPanel(stdscr)])
+
+    # The DisableDebuggerAttachment will prevent our connection panel from really
+    # functioning. It'll have circuits, but little else. If this is the case then
+    # notify the user and tell them what they can do to fix it.
+
+    controller = tor_controller()
+
+    if controller.get_conf("DisableDebuggerAttachment", None) == "1":
+      log.notice("Tor is preventing system utilities like netstat and lsof from working. This means that seth can't provide you with connection information. You can change this by adding 'DisableDebuggerAttachment 0' to your torrc and restarting tor. For more information see...\nhttps://trac.torproject.org/3313")
+      seth.util.tracker.get_connection_tracker().set_paused(True)
+    else:
+      # Configures connection resoultions. This is paused/unpaused according to
+      # if Tor's connected or not.
+
+      controller.add_status_listener(conn_reset_listener)
+
+      tor_pid = controller.get_pid(None)
+
+      if tor_pid:
+        # use the tor pid to help narrow connection results
+        tor_cmd = system.name_by_pid(tor_pid)
+
+        if tor_cmd is None:
+          tor_cmd = "tor"
+
+        resolver = seth.util.tracker.get_connection_tracker()
+        log.info("Operating System: %s, Connection Resolvers: %s" % (os.uname()[0], ", ".join(resolver._resolvers)))
+        resolver.start()
+      else:
+        # constructs singleton resolver and, if tor isn't connected, initizes
+        # it to be paused
+
+        seth.util.tracker.get_connection_tracker().set_paused(not controller.is_alive())
+
+  # third page: config
+
+  if CONFIG["features.panels.show.config"]:
+    page_panels.append([seth.config_panel.ConfigPanel(stdscr, seth.config_panel.State.TOR)])
+
+  # fourth page: torrc
+
+  if CONFIG["features.panels.show.torrc"]:
+    page_panels.append([seth.torrc_panel.TorrcPanel(stdscr, seth.torrc_panel.Config.TORRC)])
+
+  # initializes the controller
+
+  ARM_CONTROLLER = Controller(stdscr, sticky_panels, page_panels)
+
+
+class LabelPanel(panel.Panel):
+  """
+  Panel that just displays a single line of text.
+  """
+
+  def __init__(self, stdscr):
+    panel.Panel.__init__(self, stdscr, "msg", 0, height=1)
+    self.msg_text = ""
+    self.msg_attr = curses.A_NORMAL
+
+  def set_message(self, msg, attr = None):
+    """
+    Sets the message being displayed by the panel.
+
+    Arguments:
+      msg  - string to be displayed
+      attr - attribute for the label, normal text if undefined
+    """
+
+    if attr is None:
+      attr = curses.A_NORMAL
+
+    self.msg_text = msg
+    self.msg_attr = attr
+
+  def draw(self, width, height):
+    self.addstr(0, 0, self.msg_text, self.msg_attr)
+
+
+class Controller:
+  """
+  Tracks the global state of the interface
+  """
+
+  def __init__(self, stdscr, sticky_panels, page_panels):
+    """
+    Creates a new controller instance. Panel lists are ordered as they appear,
+    top to bottom on the page.
+
+    Arguments:
+      stdscr       - curses window
+      sticky_panels - panels shown at the top of each page
+      page_panels   - list of pages, each being a list of the panels on it
+    """
+
+    self._screen = stdscr
+    self._sticky_panels = sticky_panels
+    self._page_panels = page_panels
+    self._page = 0
+    self._is_paused = False
+    self._force_redraw = False
+    self._is_done = False
+    self._last_drawn = 0
+    self.set_msg()  # initializes our control message
+
+  def get_screen(self):
+    """
+    Provides our curses window.
+    """
+
+    return self._screen
+
+  def key_input(self):
+    """
+    Gets keystroke from the user.
+    """
+
+    return panel.KeyInput(self.get_screen().getch())
+
+  def get_page_count(self):
+    """
+    Provides the number of pages the interface has. This may be zero if all
+    page panels have been disabled.
+    """
+
+    return len(self._page_panels)
+
+  def get_page(self):
+    """
+    Provides the number belonging to this page. Page numbers start at zero.
+    """
+
+    return self._page
+
+  def set_page(self, page_number):
+    """
+    Sets the selected page, raising a ValueError if the page number is invalid.
+
+    Arguments:
+      page_number - page number to be selected
+    """
+
+    if page_number < 0 or page_number >= self.get_page_count():
+      raise ValueError("Invalid page number: %i" % page_number)
+
+    if page_number != self._page:
+      self._page = page_number
+      self._force_redraw = True
+      self.set_msg()
+
+  def next_page(self):
+    """
+    Increments the page number.
+    """
+
+    self.set_page((self._page + 1) % len(self._page_panels))
+
+  def prev_page(self):
+    """
+    Decrements the page number.
+    """
+
+    self.set_page((self._page - 1) % len(self._page_panels))
+
+  def is_paused(self):
+    """
+    True if the interface is paused, false otherwise.
+    """
+
+    return self._is_paused
+
+  def set_paused(self, is_pause):
+    """
+    Sets the interface to be paused or unpaused.
+    """
+
+    if is_pause != self._is_paused:
+      self._is_paused = is_pause
+      self._force_redraw = True
+      self.set_msg()
+
+      for panel_impl in self.get_all_panels():
+        panel_impl.set_paused(is_pause)
+
+  def get_panel(self, name):
+    """
+    Provides the panel with the given identifier. This returns None if no such
+    panel exists.
+
+    Arguments:
+      name - name of the panel to be fetched
+    """
+
+    for panel_impl in self.get_all_panels():
+      if panel_impl.get_name() == name:
+        return panel_impl
+
+    return None
+
+  def get_sticky_panels(self):
+    """
+    Provides the panels visibile at the top of every page.
+    """
+
+    return list(self._sticky_panels)
+
+  def get_display_panels(self, page_number = None, include_sticky = True):
+    """
+    Provides all panels belonging to a page and sticky content above it. This
+    is ordered they way they are presented (top to bottom) on the page.
+
+    Arguments:
+      page_number    - page number of the panels to be returned, the current
+                      page if None
+      include_sticky - includes sticky panels in the results if true
+    """
+
+    return_page = self._page if page_number is None else page_number
+
+    if self._page_panels:
+      if include_sticky:
+        return self._sticky_panels + self._page_panels[return_page]
+      else:
+        return list(self._page_panels[return_page])
+    else:
+      return self._sticky_panels if include_sticky else []
+
+  def get_daemon_panels(self):
+    """
+    Provides thread panels.
+    """
+
+    thread_panels = []
+
+    for panel_impl in self.get_all_panels():
+      if isinstance(panel_impl, threading.Thread):
+        thread_panels.append(panel_impl)
+
+    return thread_panels
+
+  def get_all_panels(self):
+    """
+    Provides all panels in the interface.
+    """
+
+    all_panels = list(self._sticky_panels)
+
+    for page in self._page_panels:
+      all_panels += list(page)
+
+    return all_panels
+
+  def redraw(self, force = True):
+    """
+    Redraws the displayed panel content.
+
+    Arguments:
+      force - redraws reguardless of if it's needed if true, otherwise ignores
+              the request when there arne't changes to be displayed
+    """
+
+    force |= self._force_redraw
+    self._force_redraw = False
+
+    current_time = time.time()
+
+    if CONFIG["features.refreshRate"] != 0:
+      if self._last_drawn + CONFIG["features.refreshRate"] <= current_time:
+        force = True
+
+    display_panels = self.get_display_panels()
+
+    occupied_content = 0
+
+    for panel_impl in display_panels:
+      panel_impl.set_top(occupied_content)
+      occupied_content += panel_impl.get_height()
+
+    # apparently curses may cache display contents unless we explicitely
+    # request a redraw here...
+    # https://trac.torproject.org/projects/tor/ticket/2830#comment:9
+
+    if force:
+      self._screen.clear()
+
+    for panel_impl in display_panels:
+      panel_impl.redraw(force)
+
+    if force:
+      self._last_drawn = current_time
+
+  def request_redraw(self):
+    """
+    Requests that all content is redrawn when the interface is next rendered.
+    """
+
+    self._force_redraw = True
+
+  def get_last_redraw_time(self):
+    """
+    Provides the time when the content was last redrawn, zero if the content
+    has never been drawn.
+    """
+
+    return self._last_drawn
+
+  def set_msg(self, msg = None, attr = None, redraw = False):
+    """
+    Sets the message displayed in the interfaces control panel. This uses our
+    default prompt if no arguments are provided.
+
+    Arguments:
+      msg    - string to be displayed
+      attr   - attribute for the label, normal text if undefined
+      redraw - redraws right away if true, otherwise redraws when display
+               content is next normally drawn
+    """
+
+    if msg is None:
+      msg = ""
+
+      if attr is None:
+        if not self._is_paused:
+          msg = "page %i / %i - m: menu, p: pause, h: page help, q: quit" % (self._page + 1, len(self._page_panels))
+          attr = curses.A_NORMAL
+        else:
+          msg = "Paused"
+          attr = curses.A_STANDOUT
+
+    control_panel = self.get_panel("msg")
+    control_panel.set_message(msg, attr)
+
+    if redraw:
+      control_panel.redraw(True)
+    else:
+      self._force_redraw = True
+
+  def get_data_directory(self):
+    """
+    Provides the path where seth's resources are being placed. The path ends
+    with a slash and is created if it doesn't already exist.
+    """
+
+    data_dir = os.path.expanduser(CONFIG["startup.data_directory"])
+
+    if not data_dir.endswith("/"):
+      data_dir += "/"
+
+    if not os.path.exists(data_dir):
+      os.makedirs(data_dir)
+
+    return data_dir
+
+  def is_done(self):
+    """
+    True if seth should be terminated, false otherwise.
+    """
+
+    return self._is_done
+
+  def quit(self):
+    """
+    Terminates seth after the input is processed. Optionally if we're connected
+    to a seth generated tor instance then this may check if that should be shut
+    down too.
+    """
+
+    self._is_done = True
+
+    # check if the torrc has a "ARM_SHUTDOWN" comment flag, if so then shut
+    # down the instance
+
+    is_shutdown_flag_present = False
+    torrc_contents = tor_config.get_torrc().get_contents()
+
+    if torrc_contents:
+      for line in torrc_contents:
+        if "# ARM_SHUTDOWN" in line:
+          is_shutdown_flag_present = True
+          break
+
+    if is_shutdown_flag_present:
+      try:
+        tor_controller().close()
+      except IOError as exc:
+        seth.popups.show_msg(str(exc), 3, curses.A_BOLD)
+
+
+def heartbeat_check(is_unresponsive):
+  """
+  Logs if its been ten seconds since the last BW event.
+
+  Arguments:
+    is_unresponsive - flag for if we've indicated to be responsive or not
+  """
+
+  controller = tor_controller()
+  last_heartbeat = controller.get_latest_heartbeat()
+
+  if controller.is_alive():
+    if not is_unresponsive and (time.time() - last_heartbeat) >= 10:
+      is_unresponsive = True
+      log.notice("Relay unresponsive (last heartbeat: %s)" % time.ctime(last_heartbeat))
+    elif is_unresponsive and (time.time() - last_heartbeat) < 10:
+      # really shouldn't happen (meant Tor froze for a bit)
+      is_unresponsive = False
+      log.notice("Relay resumed")
+
+  return is_unresponsive
+
+
+def conn_reset_listener(controller, event_type, _):
+  """
+  Pauses connection resolution when tor's shut down, and resumes with the new
+  pid if started again.
+  """
+
+  resolver = seth.util.tracker.get_connection_tracker()
+
+  if resolver.is_alive():
+    resolver.set_paused(event_type == State.CLOSED)
+
+    if event_type in (State.INIT, State.RESET):
+      # Reload the torrc contents. If the torrc panel is present then it will
+      # do this instead since it wants to do validation and redraw _after_ the
+      # new contents are loaded.
+
+      if get_controller().get_panel("torrc") is None:
+        tor_config.get_torrc().load(True)
+
+
+def start_seth(stdscr):
+  """
+  Main draw loop context.
+
+  Arguments:
+    stdscr    - curses window
+  """
+
+  init_controller(stdscr, CONFIG['start_time'])
+  control = get_controller()
+
+  if not CONFIG["features.acsSupport"]:
+    ui_tools.disable_acs()
+
+  # provides notice about any unused config keys
+
+  for key in conf.get_config("seth").unused_keys():
+    log.notice("Unused configuration entry: %s" % key)
+
+  # tells daemon panels to start
+
+  for panel_impl in control.get_daemon_panels():
+    panel_impl.start()
+
+  # allows for background transparency
+
+  try:
+    curses.use_default_colors()
+  except curses.error:
+    pass
+
+  # makes the cursor invisible
+
+  try:
+    curses.curs_set(0)
+  except curses.error:
+    pass
+
+  # logs the initialization time
+
+  log.info("seth started (initialization took %0.3f seconds)" % (time.time() - CONFIG['start_time']))
+
+  # main draw loop
+
+  override_key = None      # uses this rather than waiting on user input
+  is_unresponsive = False  # flag for heartbeat responsiveness check
+
+  while not control.is_done():
+    display_panels = control.get_display_panels()
+    is_unresponsive = heartbeat_check(is_unresponsive)
+
+    # sets panel visability
+
+    for panel_impl in control.get_all_panels():
+      panel_impl.set_visible(panel_impl in display_panels)
+
+    # redraws the interface if it's needed
+
+    control.redraw(False)
+    stdscr.refresh()
+
+    # wait for user keyboard input until timeout, unless an override was set
+
+    if override_key:
+      key, override_key = override_key, None
+    else:
+      curses.halfdelay(CONFIG["features.redrawRate"] * 10)
+      key = panel.KeyInput(stdscr.getch())
+
+    if key.match('right'):
+      control.next_page()
+    elif key.match('left'):
+      control.prev_page()
+    elif key.match('p'):
+      control.set_paused(not control.is_paused())
+    elif key.match('m'):
+      seth.menu.menu.show_menu()
+    elif key.match('q'):
+      # provides prompt to confirm that seth should exit
+
+      if CONFIG["features.confirmQuit"]:
+        msg = "Are you sure (q again to confirm)?"
+        confirmation_key = seth.popups.show_msg(msg, attr = curses.A_BOLD)
+        quit_confirmed = confirmation_key.match('q')
+      else:
+        quit_confirmed = True
+
+      if quit_confirmed:
+        control.quit()
+    elif key.match('x'):
+      # provides prompt to confirm that seth should issue a sighup
+
+      msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?"
+      confirmation_key = seth.popups.show_msg(msg, attr = curses.A_BOLD)
+
+      if confirmation_key in (ord('x'), ord('X')):
+        try:
+          tor_controller().signal(stem.Signal.RELOAD)
+        except IOError as exc:
+          log.error("Error detected when reloading tor: %s" % exc.strerror)
+    elif key.match('h'):
+      override_key = seth.popups.show_help_popup()
+    elif key == ord('l') - 96:
+      # force redraw when ctrl+l is pressed
+      control.redraw(True)
+    else:
+      for panel_impl in display_panels:
+        is_keystroke_consumed = panel_impl.handle_key(key)
+
+        if is_keystroke_consumed:
+          break
diff --git a/seth/demo_glyphs.py b/seth/demo_glyphs.py
new file mode 100755
index 0000000..7781139
--- /dev/null
+++ b/seth/demo_glyphs.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Displays all ACS options with their corresponding representation. These are
+undocumented in the pydocs. For more information see the following man page:
+
+http://www.mkssoftware.com/docs/man5/terminfo.5.asp
+"""
+
+import curses
+
+
+def main():
+  try:
+    curses.wrapper(_show_glyphs)
+  except KeyboardInterrupt:
+    pass  # quit
+
+
+def _show_glyphs(stdscr):
+  """
+  Renders a chart with the ACS glyphs.
+  """
+
+  try:
+    curses.use_default_colors()  # allow semi-transparent backgrounds
+  except curses.error:
+    pass
+
+  try:
+    curses.curs_set(0)  # attempt to make the cursor invisible
+  except curses.error:
+    pass
+
+  height, width = stdscr.getmaxyx()
+  columns = width / 30
+
+  if columns == 0:
+    return  # not wide enough to show anything
+
+  # mapping of keycodes to their ACS option names (for instance, ACS_LTEE)
+
+  acs_options = dict((v, k) for (k, v) in curses.__dict__.items() if k.startswith('ACS_'))
+
+  stdscr.addstr(0, 0, 'Curses Glyphs:', curses.A_STANDOUT)
+  x, y = 0, 2
+
+  for keycode in sorted(acs_options.keys()):
+    stdscr.addstr(y, x * 30, '%s (%i)' % (acs_options[keycode], keycode))
+    stdscr.addch(y, (x * 30) + 25, keycode)
+
+    x += 1
+
+    if x >= columns:
+      x, y = 0, y + 1
+
+      if y >= height:
+        break
+
+  stdscr.getch()  # quit on keyboard input
+
+
+if __name__ == '__main__':
+  main()
diff --git a/seth/graph_panel.py b/seth/graph_panel.py
new file mode 100644
index 0000000..1f06890
--- /dev/null
+++ b/seth/graph_panel.py
@@ -0,0 +1,734 @@
+"""
+Graphs of tor related statistics. For example...
+
+Downloaded (0.0 B/sec):           Uploaded (0.0 B/sec):
+  34                                30
+                            *                                 *
+                    **  *   *                          *      **
+      *   *  *      ** **   **          ***  **       ** **   **
+     *********      ******  ******     *********      ******  ******
+   0 ************ ****************   0 ************ ****************
+         25s  50   1m   1.6  2.0           25s  50   1m   1.6  2.0
+"""
+
+import copy
+import curses
+import time
+
+import seth.controller
+import seth.popups
+import seth.util.tracker
+
+from seth.util import bandwidth_from_state, join, msg, panel, tor_controller
+
+from stem.control import EventType, Listener
+from stem.util import conf, enum, log, str_tools, system
+
+GraphStat = enum.Enum(('BANDWIDTH', 'bandwidth'), ('CONNECTIONS', 'connections'), ('SYSTEM_RESOURCES', 'resources'))
+Interval = enum.Enum(('EACH_SECOND', 'each second'), ('FIVE_SECONDS', '5 seconds'), ('THIRTY_SECONDS', '30 seconds'), ('MINUTELY', 'minutely'), ('FIFTEEN_MINUTE', '15 minute'), ('THIRTY_MINUTE', '30 minute'), ('HOURLY', 'hourly'), ('DAILY', 'daily'))
+Bounds = enum.Enum(('GLOBAL_MAX', 'global_max'), ('LOCAL_MAX', 'local_max'), ('TIGHT', 'tight'))
+
+INTERVAL_SECONDS = {
+  Interval.EACH_SECOND: 1,
+  Interval.FIVE_SECONDS: 5,
+  Interval.THIRTY_SECONDS: 30,
+  Interval.MINUTELY: 60,
+  Interval.FIFTEEN_MINUTE: 900,
+  Interval.THIRTY_MINUTE: 1800,
+  Interval.HOURLY: 3600,
+  Interval.DAILY: 86400,
+}
+
+PRIMARY_COLOR, SECONDARY_COLOR = 'green', 'cyan'
+
+ACCOUNTING_RATE = 5
+DEFAULT_CONTENT_HEIGHT = 4  # space needed for labeling above and below the graph
+WIDE_LABELING_GRAPH_COL = 50  # minimum graph columns to use wide spacing for x-axis labels
+COLLAPSE_WIDTH = 135  # width at which to move optional stats from the title to x-axis label
+
+
+def conf_handler(key, value):
+  if key == 'features.graph.height':
+    return max(1, value)
+  elif key == 'features.graph.max_width':
+    return max(1, value)
+  elif key == 'features.graph.type':
+    if value != 'none' and value not in GraphStat:
+      log.warn("'%s' isn't a valid graph type, options are: none, %s" % (CONFIG['features.graph.type'], ', '.join(GraphStat)))
+      return CONFIG['features.graph.type']  # keep the default
+  elif key == 'features.graph.interval':
+    if value not in Interval:
+      log.warn("'%s' isn't a valid graphing interval, options are: %s" % (value, ', '.join(Interval)))
+      return CONFIG['features.graph.interval']  # keep the default
+  elif key == 'features.graph.bound':
+    if value not in Bounds:
+      log.warn("'%s' isn't a valid graph bounds, options are: %s" % (value, ', '.join(Bounds)))
+      return CONFIG['features.graph.bound']  # keep the default
+
+
+CONFIG = conf.config_dict('seth', {
+  'attr.hibernate_color': {},
+  'attr.graph.title': {},
+  'attr.graph.header.primary': {},
+  'attr.graph.header.secondary': {},
+  'features.graph.height': 7,
+  'features.graph.type': GraphStat.BANDWIDTH,
+  'features.graph.interval': Interval.EACH_SECOND,
+  'features.graph.bound': Bounds.LOCAL_MAX,
+  'features.graph.max_width': 150,
+  'features.panels.show.connection': True,
+  'features.graph.bw.prepopulate': True,
+  'features.graph.bw.transferInBytes': False,
+  'features.graph.bw.accounting.show': True,
+  'tor.chroot': '',
+}, conf_handler)
+
+
+class Stat(object):
+  """
+  Graphable statistical information.
+
+  :var int latest_value: last value we recorded
+  :var int total: sum of all values we've recorded
+  :var int tick: number of events we've processed
+  :var float start_time: unix timestamp for when we started
+  :var dict values: mapping of intervals to an array of samplings from newest to oldest
+  :var dict max_value: mapping of intervals to the maximum value it has had
+  """
+
+  def __init__(self, clone = None):
+    if clone:
+      self.latest_value = clone.latest_value
+      self.total = clone.total
+      self.tick = clone.tick
+      self.start_time = clone.start_time
+      self.values = copy.deepcopy(clone.values)
+      self.max_value = dict(clone.max_value)
+      self._in_process_value = dict(clone._in_process_value)
+    else:
+      self.latest_value = 0
+      self.total = 0
+      self.tick = 0
+      self.start_time = time.time()
+      self.values = dict([(i, CONFIG['features.graph.max_width'] * [0]) for i in Interval])
+      self.max_value = dict([(i, 0) for i in Interval])
+      self._in_process_value = dict([(i, 0) for i in Interval])
+
+  def average(self, by_time = False):
+    return self.total / (time.time() - self.start_time) if by_time else self.total / max(1, self.tick)
+
+  def update(self, new_value):
+    self.latest_value = new_value
+    self.total += new_value
+    self.tick += 1
+
+    for interval in Interval:
+      interval_seconds = INTERVAL_SECONDS[interval]
+      self._in_process_value[interval] += new_value
+
+      if self.tick % interval_seconds == 0:
+        new_entry = self._in_process_value[interval] / interval_seconds
+        self.values[interval] = [new_entry] + self.values[interval][:-1]
+        self.max_value[interval] = max(self.max_value[interval], new_entry)
+        self._in_process_value[interval] = 0
+
+
+class GraphCategory(object):
+  """
+  Category for the graph. This maintains two subgraphs, updating them each
+  second with updated stats.
+
+  :var Stat primary: first subgraph
+  :var Stat secondary: second subgraph
+  :var list title_stats: additional information to include in the graph title
+  :var list primary_header_stats: additional information for the primary header
+  :var list secondary_header_stats: additional information for the secondary header
+  """
+
+  def __init__(self, clone = None):
+    if clone:
+      self.primary = Stat(clone.primary)
+      self.secondary = Stat(clone.secondary)
+      self.title_stats = list(clone.title_stats)
+      self.primary_header_stats = list(clone.primary_header_stats)
+      self.secondary_header_stats = list(clone.secondary_header_stats)
+    else:
+      self.primary = Stat()
+      self.secondary = Stat()
+      self.title_stats = []
+      self.primary_header_stats = []
+      self.secondary_header_stats = []
+
+  def y_axis_label(self, value, is_primary):
+    """
+    Provides the label we should display on our y-axis.
+
+    :param int value: value being shown on the y-axis
+    :param bool is_primary: True if this is the primary attribute, False if
+      it's the secondary
+
+    :returns: **str** with our y-axis label
+    """
+
+    return str(value)
+
+  def bandwidth_event(self, event):
+    """
+    Called when it's time to process another event. All graphs use tor BW
+    events to keep in sync with each other (this happens once per second).
+    """
+
+    pass
+
+
+class BandwidthStats(GraphCategory):
+  """
+  Tracks tor's bandwidth usage.
+  """
+
+  def __init__(self, clone = None):
+    GraphCategory.__init__(self, clone)
+
+    if not clone:
+      # We both show our 'total' attributes and use it to determine our average.
+      #
+      # If we can get *both* our start time and the totals from tor (via 'GETINFO
+      # traffic/*') then that's ideal, but if not then just track the total for
+      # the time seth is run.
+
+      controller = tor_controller()
+
+      read_total = controller.get_info('traffic/read', None)
+      write_total = controller.get_info('traffic/written', None)
+      start_time = system.start_time(controller.get_pid(None))
+
+      if read_total and write_total and start_time:
+        self.primary.total = int(read_total)
+        self.secondary.total = int(write_total)
+        self.primary.start_time = self.secondary.start_time = start_time
+
+  def y_axis_label(self, value, is_primary):
+    return self._size_label(value, 0)
+
+  def bandwidth_event(self, event):
+    self.primary.update(event.read)
+    self.secondary.update(event.written)
+
+    self.primary_header_stats = [
+      '%-14s' % ('%s/sec' % self._size_label(self.primary.latest_value)),
+      '- avg: %s/sec' % self._size_label(self.primary.average(by_time = True)),
+      ', total: %s' % self._size_label(self.primary.total),
+    ]
+
+    self.secondary_header_stats = [
+      '%-14s' % ('%s/sec' % self._size_label(self.secondary.latest_value)),
+      '- avg: %s/sec' % self._size_label(self.secondary.average(by_time = True)),
+      ', total: %s' % self._size_label(self.secondary.total),
+    ]
+
+    controller = tor_controller()
+
+    stats = []
+    bw_rate = controller.get_effective_rate(None)
+    bw_burst = controller.get_effective_rate(None, burst = True)
+
+    if bw_rate and bw_burst:
+      bw_rate_label = self._size_label(bw_rate)
+      bw_burst_label = self._size_label(bw_burst)
+
+      # if both are using rounded values then strip off the '.0' decimal
+
+      if '.0' in bw_rate_label and '.0' in bw_burst_label:
+        bw_rate_label = bw_rate_label.split('.', 1)[0]
+        bw_burst_label = bw_burst_label.split('.', 1)[0]
+
+      stats.append('limit: %s/s' % bw_rate_label)
+      stats.append('burst: %s/s' % bw_burst_label)
+
+    my_router_status_entry = controller.get_network_status(default = None)
+    measured_bw = getattr(my_router_status_entry, 'bandwidth', None)
+
+    if measured_bw:
+      stats.append('measured: %s/s' % self._size_label(measured_bw))
+    else:
+      my_server_descriptor = controller.get_server_descriptor(default = None)
+      observed_bw = getattr(my_server_descriptor, 'observed_bandwidth', None)
+
+      if observed_bw:
+        stats.append('observed: %s/s' % self._size_label(observed_bw))
+
+    self.title_stats = stats
+
+  def prepopulate_from_state(self):
+    """
+    Attempts to use tor's state file to prepopulate values for the 15 minute
+    interval via the BWHistoryReadValues/BWHistoryWriteValues values.
+
+    :returns: **float** for the number of seconds of data missing
+
+    :raises: **ValueError** if unable to get the bandwidth information from our
+      state file
+    """
+
+    def update_values(stat, entries, latest_time):
+      # fill missing entries with the last value
+
+      missing_entries = int((time.time() - latest_time) / 900)
+      entries = entries + [entries[-1]] * missing_entries
+
+      # pad if too short and truncate if too long
+
+      entry_count = CONFIG['features.graph.max_width']
+      entries = [0] * (entry_count - len(entries)) + entries[-entry_count:]
+
+      stat.values[Interval.FIFTEEN_MINUTE] = entries
+      stat.max_value[Interval.FIFTEEN_MINUTE] = max(entries)
+      stat.latest_value = entries[-1] * 900
+
+    stats = bandwidth_from_state()
+
+    update_values(self.primary, stats.read_entries, stats.last_read_time)
+    update_values(self.secondary, stats.write_entries, stats.last_write_time)
+
+    return time.time() - min(stats.last_read_time, stats.last_write_time)
+
+  def _size_label(self, byte_count, decimal = 1):
+    """
+    Alias for str_tools.size_label() that accounts for if the user prefers bits
+    or bytes.
+    """
+
+    return str_tools.size_label(byte_count, decimal, is_bytes = CONFIG['features.graph.bw.transferInBytes'])
+
+
+class ConnectionStats(GraphCategory):
+  """
+  Tracks number of inbound and outbound connections.
+  """
+
+  def bandwidth_event(self, event):
+    inbound_count, outbound_count = 0, 0
+
+    controller = tor_controller()
+    or_ports = controller.get_ports(Listener.OR, [])
+    dir_ports = controller.get_ports(Listener.DIR, [])
+    control_ports = controller.get_ports(Listener.CONTROL, [])
+
+    for entry in seth.util.tracker.get_connection_tracker().get_value():
+      if entry.local_port in or_ports or entry.local_port in dir_ports:
+        inbound_count += 1
+      elif entry.local_port in control_ports:
+        pass  # control connection
+      else:
+        outbound_count += 1
+
+    self.primary.update(inbound_count)
+    self.secondary.update(outbound_count)
+
+    self.primary_header_stats = [str(self.primary.latest_value), ', avg: %s' % self.primary.average()]
+    self.secondary_header_stats = [str(self.secondary.latest_value), ', avg: %s' % self.secondary.average()]
+
+
+class ResourceStats(GraphCategory):
+  """
+  Tracks cpu and memory usage of the tor process.
+  """
+
+  def y_axis_label(self, value, is_primary):
+    return '%i%%' % value if is_primary else str_tools.size_label(value)
+
+  def bandwidth_event(self, event):
+    resources = seth.util.tracker.get_resource_tracker().get_value()
+    self.primary.update(resources.cpu_sample * 100)  # decimal percentage to whole numbers
+    self.secondary.update(resources.memory_bytes)
+
+    self.primary_header_stats = ['%0.1f%%' % self.primary.latest_value, ', avg: %0.1f%%' % self.primary.average()]
+    self.secondary_header_stats = [str_tools.size_label(self.secondary.latest_value, 1), ', avg: %s' % str_tools.size_label(self.secondary.average(), 1)]
+
+
+class GraphPanel(panel.Panel):
+  """
+  Panel displaying graphical information of GraphCategory instances.
+  """
+
+  def __init__(self, stdscr):
+    panel.Panel.__init__(self, stdscr, 'graph', 0)
+
+    self._displayed_stat = None if CONFIG['features.graph.type'] == 'none' else CONFIG['features.graph.type']
+    self._update_interval = CONFIG['features.graph.interval']
+    self._bounds = CONFIG['features.graph.bound']
+    self._graph_height = CONFIG['features.graph.height']
+
+    self._accounting_stats = None
+
+    self._stats = {
+      GraphStat.BANDWIDTH: BandwidthStats(),
+      GraphStat.SYSTEM_RESOURCES: ResourceStats(),
+    }
+
+    if CONFIG['features.panels.show.connection']:
+      self._stats[GraphStat.CONNECTIONS] = ConnectionStats()
+    elif self._displayed_stat == GraphStat.CONNECTIONS:
+      log.warn("The connection graph is unavailble when you set 'features.panels.show.connection false'.")
+      self._displayed_stat = GraphStat.BANDWIDTH
+
+    self.set_pause_attr('_stats')
+    self.set_pause_attr('_accounting_stats')
+
+    # prepopulates bandwidth values from state file
+
+    controller = tor_controller()
+
+    if controller.is_alive() and CONFIG['features.graph.bw.prepopulate']:
+      try:
+        missing_seconds = self._stats[GraphStat.BANDWIDTH].prepopulate_from_state()
+
+        if missing_seconds:
+          log.notice(msg('panel.graphing.prepopulation_successful', duration = str_tools.time_label(missing_seconds, 0, True)))
+        else:
+          log.notice(msg('panel.graphing.prepopulation_all_successful'))
+
+        self.update_interval = Interval.FIFTEEN_MINUTE
+      except ValueError as exc:
+        log.info(msg('panel.graphing.prepopulation_failure', error = exc))
+
+    controller.add_event_listener(self._update_accounting, EventType.BW)
+    controller.add_event_listener(self._update_stats, EventType.BW)
+    controller.add_status_listener(lambda *args: self.redraw(True))
+
+  @property
+  def displayed_stat(self):
+    return self._displayed_stat
+
+  @displayed_stat.setter
+  def displayed_stat(self, value):
+    if value is not None and value not in self._stats.keys():
+      raise ValueError("%s isn't a graphed statistic" % value)
+
+    self._displayed_stat = value
+
+  def stat_options(self):
+    return self._stats.keys()
+
+  @property
+  def update_interval(self):
+    return self._update_interval
+
+  @update_interval.setter
+  def update_interval(self, value):
+    if value not in Interval:
+      raise ValueError("%s isn't a valid graphing update interval" % value)
+
+    self._update_interval = value
+
+  @property
+  def bounds_type(self):
+    return self._bounds
+
+  @bounds_type.setter
+  def bounds_type(self, value):
+    if value not in Bounds:
+      raise ValueError("%s isn't a valid type of bounds" % value)
+
+    self._bounds = value
+
+  def get_height(self):
+    """
+    Provides the height of the content.
+    """
+
+    if not self.displayed_stat:
+      return 0
+
+    height = DEFAULT_CONTENT_HEIGHT + self._graph_height
+
+    if self.displayed_stat == GraphStat.BANDWIDTH and self._accounting_stats:
+      height += 3
+
+    return height
+
+  def set_graph_height(self, new_graph_height):
+    self._graph_height = max(1, new_graph_height)
+
+  def resize_graph(self):
+    """
+    Prompts for user input to resize the graph panel. Options include...
+      down arrow - grow graph
+      up arrow - shrink graph
+      enter / space - set size
+    """
+
+    control = seth.controller.get_controller()
+
+    with panel.CURSES_LOCK:
+      try:
+        while True:
+          msg = 'press the down/up to resize the graph, and enter when done'
+          control.set_msg(msg, curses.A_BOLD, True)
+          curses.cbreak()
+          key = control.key_input()
+
+          if key.match('down'):
+            # don't grow the graph if it's already consuming the whole display
+            # (plus an extra line for the graph/log gap)
+
+            max_height = self.parent.getmaxyx()[0] - self.top
+            current_height = self.get_height()
+
+            if current_height < max_height + 1:
+              self.set_graph_height(self._graph_height + 1)
+          elif key.match('up'):
+            self.set_graph_height(self._graph_height - 1)
+          elif key.is_selection():
+            break
+
+          control.redraw()
+      finally:
+        control.set_msg()
+
+  def handle_key(self, key):
+    if key.match('r'):
+      self.resize_graph()
+    elif key.match('b'):
+      # uses the next boundary type
+      self.bounds_type = Bounds.next(self.bounds_type)
+      self.redraw(True)
+    elif key.match('s'):
+      # provides a menu to pick the graphed stats
+
+      available_stats = self._stats.keys()
+      available_stats.sort()
+
+      # uses sorted, camel cased labels for the options
+
+      options = ['None']
+
+      for label in available_stats:
+        words = label.split()
+        options.append(' '.join(word[0].upper() + word[1:] for word in words))
+
+      if self.displayed_stat:
+        initial_selection = available_stats.index(self.displayed_stat) + 1
+      else:
+        initial_selection = 0
+
+      selection = seth.popups.show_menu('Graphed Stats:', options, initial_selection)
+
+      # applies new setting
+
+      if selection == 0:
+        self.displayed_stat = None
+      elif selection != -1:
+        self.displayed_stat = available_stats[selection - 1]
+    elif key.match('i'):
+      # provides menu to pick graph panel update interval
+
+      selection = seth.popups.show_menu('Update Interval:', list(Interval), list(Interval).index(self.update_interval))
+
+      if selection != -1:
+        self.update_interval = list(Interval)[selection]
+    else:
+      return False
+
+    return True
+
+  def get_help(self):
+    return [
+      ('r', 'resize graph', None),
+      ('s', 'graphed stats', self.displayed_stat if self.displayed_stat else 'none'),
+      ('b', 'graph bounds', self.bounds_type.replace('_', ' ')),
+      ('i', 'graph update interval', self.update_interval),
+    ]
+
+  def draw(self, width, height):
+    if not self.displayed_stat:
+      return
+
+    param = self.get_attr('_stats')[self.displayed_stat]
+    graph_column = min((width - 10) / 2, CONFIG['features.graph.max_width'])
+
+    if self.is_title_visible():
+      title = CONFIG['attr.graph.title'].get(self.displayed_stat, '')
+      title_stats = join(param.title_stats, ', ', width - len(title) - 4)
+      title = '%s (%s):' % (title, title_stats) if title_stats else '%s:' % title
+      self.addstr(0, 0, title, curses.A_STANDOUT)
+
+    # top labels
+
+    primary_header = CONFIG['attr.graph.header.primary'].get(self.displayed_stat, '')
+    primary_header_stats = join(param.primary_header_stats, '', (width / 2) - len(primary_header) - 4)
+    left = '%s (%s):' % (primary_header, primary_header_stats) if primary_header_stats else '%s:' % primary_header
+    self.addstr(1, 0, left, curses.A_BOLD, PRIMARY_COLOR)
+
+    secondary_header = CONFIG['attr.graph.header.secondary'].get(self.displayed_stat, '')
+    secondary_header_stats = join(param.secondary_header_stats, '', (width / 2) - len(secondary_header) - 4)
+    right = '%s (%s):' % (secondary_header, secondary_header_stats) if secondary_header_stats else '%s:' % secondary_header
+    self.addstr(1, graph_column + 5, right, curses.A_BOLD, SECONDARY_COLOR)
+
+    # determines max/min value on the graph
+
+    if self.bounds_type == Bounds.GLOBAL_MAX:
+      primary_max_bound = param.primary.max_value[self.update_interval]
+      secondary_max_bound = param.secondary.max_value[self.update_interval]
+    else:
+      # both Bounds.LOCAL_MAX and Bounds.TIGHT use local maxima
+      if graph_column < 2:
+        # nothing being displayed
+        primary_max_bound, secondary_max_bound = 0, 0
+      else:
+        primary_max_bound = max(param.primary.values[self.update_interval][:graph_column])
+        secondary_max_bound = max(param.secondary.values[self.update_interval][:graph_column])
+
+    primary_min_bound = secondary_min_bound = 0
+
+    if self.bounds_type == Bounds.TIGHT:
+      primary_min_bound = min(param.primary.values[self.update_interval][:graph_column])
+      secondary_min_bound = min(param.secondary.values[self.update_interval][:graph_column])
+
+      # if the max = min (ie, all values are the same) then use zero lower
+      # bound so a graph is still displayed
+
+      if primary_min_bound == primary_max_bound:
+        primary_min_bound = 0
+
+      if secondary_min_bound == secondary_max_bound:
+        secondary_min_bound = 0
+
+    # displays upper and lower bounds
+
+    # TODO: we need to get the longest y_axis_label() result so we can offset
+    # following content by that
+
+    self.addstr(2, 0, param.y_axis_label(primary_max_bound, True), PRIMARY_COLOR)
+    self.addstr(self._graph_height + 1, 0, param.y_axis_label(primary_min_bound, True), PRIMARY_COLOR)
+
+    self.addstr(2, graph_column + 5, param.y_axis_label(secondary_max_bound, False), SECONDARY_COLOR)
+    self.addstr(self._graph_height + 1, graph_column + 5, param.y_axis_label(secondary_min_bound, False), SECONDARY_COLOR)
+
+    # displays intermediate bounds on every other row
+
+    ticks = (self._graph_height - 3) / 2
+
+    for i in range(ticks):
+      row = self._graph_height - (2 * i) - 3
+
+      if self._graph_height % 2 == 0 and i >= (ticks / 2):
+        row -= 1
+
+      if primary_min_bound != primary_max_bound:
+        primary_val = (primary_max_bound - primary_min_bound) * (self._graph_height - row - 1) / (self._graph_height - 1)
+
+        if primary_val not in (primary_min_bound, primary_max_bound):
+          self.addstr(row + 2, 0, param.y_axis_label(primary_val, True), PRIMARY_COLOR)
+
+      if secondary_min_bound != secondary_max_bound:
+        secondary_val = (secondary_max_bound - secondary_min_bound) * (self._graph_height - row - 1) / (self._graph_height - 1)
+
+        if secondary_val not in (secondary_min_bound, secondary_max_bound):
+          self.addstr(row + 2, graph_column + 5, param.y_axis_label(secondary_val, False), SECONDARY_COLOR)
+
+    # creates bar graph (both primary and secondary)
+
+    for col in range(graph_column):
+      column_count = int(param.primary.values[self.update_interval][col]) - primary_min_bound
+      column_height = int(min(self._graph_height, self._graph_height * column_count / (max(1, primary_max_bound) - primary_min_bound)))
+
+      for row in range(column_height):
+        self.addstr(self._graph_height + 1 - row, col + 5, ' ', curses.A_STANDOUT, PRIMARY_COLOR)
+
+      column_count = int(param.secondary.values[self.update_interval][col]) - secondary_min_bound
+      column_height = int(min(self._graph_height, self._graph_height * column_count / (max(1, secondary_max_bound) - secondary_min_bound)))
+
+      for row in range(column_height):
+        self.addstr(self._graph_height + 1 - row, col + graph_column + 10, ' ', curses.A_STANDOUT, SECONDARY_COLOR)
+
+    # bottom labeling of x-axis
+
+    interval_sec = INTERVAL_SECONDS[self.update_interval]
+
+    interval_spacing = 10 if graph_column >= WIDE_LABELING_GRAPH_COL else 5
+    units_label, decimal_precision = None, 0
+
+    for i in range((graph_column - 4) / interval_spacing):
+      loc = (i + 1) * interval_spacing
+      time_label = str_tools.time_label(loc * interval_sec, decimal_precision)
+
+      if not units_label:
+        units_label = time_label[-1]
+      elif units_label != time_label[-1]:
+        # upped scale so also up precision of future measurements
+        units_label = time_label[-1]
+        decimal_precision += 1
+      else:
+        # if constrained on space then strips labeling since already provided
+        time_label = time_label[:-1]
+
+      self.addstr(self._graph_height + 2, 4 + loc, time_label, PRIMARY_COLOR)
+      self.addstr(self._graph_height + 2, graph_column + 10 + loc, time_label, SECONDARY_COLOR)
+
+    # if display is narrow, overwrites x-axis labels with avg / total stats
+
+    labeling_line = DEFAULT_CONTENT_HEIGHT + self._graph_height - 2
+
+    if self.displayed_stat == GraphStat.BANDWIDTH and width <= COLLAPSE_WIDTH:
+      # clears line
+
+      self.addstr(labeling_line, 0, ' ' * width)
+      graph_column = min((width - 10) / 2, CONFIG['features.graph.max_width'])
+
+      runtime = time.time() - param.start_time
+      primary_footer = 'total: %s, avg: %s/sec' % (param._size_label(param.primary.total), param._size_label(param.primary.total / runtime))
+      secondary_footer = 'total: %s, avg: %s/sec' % (param._size_label(param.secondary.total), param._size_label(param.secondary.total / runtime))
+
+      self.addstr(labeling_line, 1, primary_footer, PRIMARY_COLOR)
+      self.addstr(labeling_line, graph_column + 6, secondary_footer, SECONDARY_COLOR)
+
+    # provides accounting stats if enabled
+
+    accounting_stats = self.get_attr('_accounting_stats')
+
+    if self.displayed_stat == GraphStat.BANDWIDTH and accounting_stats:
+      if tor_controller().is_alive():
+        hibernate_color = CONFIG['attr.hibernate_color'].get(accounting_stats.status, 'red')
+
+        x, y = 0, labeling_line + 2
+        x = self.addstr(y, x, 'Accounting (', curses.A_BOLD)
+        x = self.addstr(y, x, accounting_stats.status, curses.A_BOLD, hibernate_color)
+        x = self.addstr(y, x, ')', curses.A_BOLD)
+
+        self.addstr(y, 35, 'Time to reset: %s' % str_tools.short_time_label(accounting_stats.time_until_reset))
+
+        self.addstr(y + 1, 2, '%s / %s' % (accounting_stats.read_bytes, accounting_stats.read_limit), PRIMARY_COLOR)
+        self.addstr(y + 1, 37, '%s / %s' % (accounting_stats.written_bytes, accounting_stats.write_limit), SECONDARY_COLOR)
+      else:
+        self.addstr(labeling_line + 2, 0, 'Accounting:', curses.A_BOLD)
+        self.addstr(labeling_line + 2, 12, 'Connection Closed...')
+
+  def copy_attr(self, attr):
+    if attr == '_stats':
+      return dict([(key, type(self._stats[key])(self._stats[key])) for key in self._stats])
+    else:
+      return panel.Panel.copy_attr(self, attr)
+
+  def _update_accounting(self, event):
+    if not CONFIG['features.graph.bw.accounting.show']:
+      self._accounting_stats = None
+    elif not self._accounting_stats or time.time() - self._accounting_stats.retrieved >= ACCOUNTING_RATE:
+      old_accounting_stats = self._accounting_stats
+      self._accounting_stats = tor_controller().get_accounting_stats(None)
+
+      # if we either added or removed accounting info then redraw the whole
+      # screen to account for resizing
+
+      if bool(old_accounting_stats) != bool(self._accounting_stats):
+        seth.controller.get_controller().redraw()
+
+  def _update_stats(self, event):
+    for stat in self._stats.values():
+      stat.bandwidth_event(event)
+
+    param = self.get_attr('_stats')[self.displayed_stat]
+    update_rate = INTERVAL_SECONDS[self.update_interval]
+
+    if param.primary.tick % update_rate == 0:
+      self.redraw(True)
diff --git a/seth/header_panel.py b/seth/header_panel.py
new file mode 100644
index 0000000..b0ece7d
--- /dev/null
+++ b/seth/header_panel.py
@@ -0,0 +1,480 @@
+"""
+Top panel for every page, containing basic system and tor related information.
+This expands the information it presents to two columns if there's room
+available.
+"""
+
+import collections
+import os
+import time
+import curses
+import threading
+
+import seth.controller
+import seth.popups
+
+import stem
+
+from stem.control import Listener
+from stem.util import conf, log, proc, str_tools, system
+
+from seth.util import msg, tor_controller, panel, tracker
+
+MIN_DUAL_COL_WIDTH = 141  # minimum width where we'll show two columns
+SHOW_FD_THRESHOLD = 60  # show file descriptor usage if usage is over this percentage
+UPDATE_RATE = 5  # rate in seconds at which we refresh
+
+CONFIG = conf.config_dict('seth', {
+  'attr.flag_colors': {},
+  'attr.version_status_colors': {},
+})
+
+
+class HeaderPanel(panel.Panel, threading.Thread):
+  """
+  Top area containing tor settings and system information.
+  """
+
+  def __init__(self, stdscr, start_time):
+    panel.Panel.__init__(self, stdscr, 'header', 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+
+    self._vals = get_sampling()
+
+    self._pause_condition = threading.Condition()
+    self._halt = False  # terminates thread if true
+
+    tor_controller().add_status_listener(self.reset_listener)
+
+  def is_wide(self, width = None):
+    """
+    True if we should show two columns of information, False otherwise.
+    """
+
+    if width is None:
+      width = self.get_parent().getmaxyx()[1]
+
+    return width >= MIN_DUAL_COL_WIDTH
+
+  def get_height(self):
+    """
+    Provides the height of the content, which is dynamically determined by the
+    panel's maximum width.
+    """
+
+    if self._vals.is_relay:
+      return 4 if self.is_wide() else 6
+    else:
+      return 3 if self.is_wide() else 4
+
+  def send_newnym(self):
+    """
+    Requests a new identity and provides a visual queue.
+    """
+
+    controller = tor_controller()
+
+    if not controller.is_newnym_available():
+      return
+
+    controller.signal(stem.Signal.NEWNYM)
+
+    # If we're wide then the newnym label in this panel will give an
+    # indication that the signal was sent. Otherwise use a msg.
+
+    if not self.is_wide():
+      seth.popups.show_msg('Requesting a new identity', 1)
+
+  def handle_key(self, key):
+    if key.match('n'):
+      self.send_newnym()
+    elif key.match('r') and not self._vals.is_connected:
+      # TODO: This is borked. Not quite sure why but our attempt to call
+      # PROTOCOLINFO fails with a socket error, followed by completely freezing
+      # seth. This is exposing two bugs...
+      #
+      # * This should be working. That's a stem issue.
+      # * Our interface shouldn't be locking up. That's an seth issue.
+
+      return True
+
+      controller = tor_controller()
+
+      try:
+        controller.connect()
+
+        try:
+          controller.authenticate()  # TODO: should account for our chroot
+        except stem.connection.MissingPassword:
+          password = seth.popups.input_prompt('Controller Password: ')
+
+          if password:
+            controller.authenticate(password)
+
+        log.notice("Reconnected to Tor's control port")
+        seth.popups.show_msg('Tor reconnected', 1)
+      except Exception as exc:
+        seth.popups.show_msg('Unable to reconnect (%s)' % exc, 3)
+        controller.close()
+    else:
+      return False
+
+    return True
+
+  def draw(self, width, height):
+    vals = self._vals  # local reference to avoid concurrency concerns
+    is_wide = self.is_wide(width)
+
+    # space available for content
+
+    left_width = max(width / 2, 77) if is_wide else width
+    right_width = width - left_width
+
+    self._draw_platform_section(0, 0, left_width, vals)
+
+    if vals.is_connected:
+      self._draw_ports_section(0, 1, left_width, vals)
+    else:
+      self._draw_disconnected(0, 1, left_width, vals)
+
+    if is_wide:
+      self._draw_resource_usage(left_width, 0, right_width, vals)
+
+      if vals.is_relay:
+        self._draw_fingerprint_and_fd_usage(left_width, 1, right_width, vals)
+        self._draw_flags(0, 2, left_width, vals)
+        self._draw_exit_policy(left_width, 2, right_width, vals)
+      elif vals.is_connected:
+        self._draw_newnym_option(left_width, 1, right_width, vals)
+    else:
+      self._draw_resource_usage(0, 2, left_width, vals)
+
+      if vals.is_relay:
+        self._draw_fingerprint_and_fd_usage(0, 3, left_width, vals)
+        self._draw_flags(0, 4, left_width, vals)
+
+  def _draw_platform_section(self, x, y, width, vals):
+    """
+    Section providing the user's hostname, platform, and version information...
+
+      seth - odin (Linux 3.5.0-52-generic)        Tor 0.2.5.1-alpha-dev (unrecommended)
+      |------ platform (40 characters) ------|   |----------- tor version -----------|
+    """
+
+    initial_x, space_left = x, min(width, 40)
+
+    x = self.addstr(y, x, vals.format('seth - {hostname}', space_left))
+    space_left -= x - initial_x
+
+    if space_left >= 10:
+      self.addstr(y, x, ' (%s)' % vals.format('{platform}', space_left - 3))
+
+    x, space_left = initial_x + 43, width - 43
+
+    if vals.version != 'Unknown' and space_left >= 10:
+      x = self.addstr(y, x, vals.format('Tor {version}', space_left))
+      space_left -= x - 43 - initial_x
+
+      if space_left >= 7 + len(vals.version_status):
+        version_color = CONFIG['attr.version_status_colors'].get(vals.version_status, 'white')
+
+        x = self.addstr(y, x, ' (')
+        x = self.addstr(y, x, vals.version_status, version_color)
+        self.addstr(y, x, ')')
+
+  def _draw_ports_section(self, x, y, width, vals):
+    """
+    Section providing our nickname, address, and port information...
+
+      Unnamed - 0.0.0.0:7000, Control Port (cookie): 9051
+    """
+
+    if not vals.is_relay:
+      x = self.addstr(y, x, 'Relaying Disabled', 'cyan')
+    else:
+      x = self.addstr(y, x, vals.format('{nickname} - {address}:{or_port}'))
+
+      if vals.dir_port != '0':
+        x = self.addstr(y, x, vals.format(', Dir Port: {dir_port}'))
+
+    if vals.control_port:
+      if width >= x + 19 + len(vals.control_port) + len(vals.auth_type):
+        auth_color = 'red' if vals.auth_type == 'open' else 'green'
+
+        x = self.addstr(y, x, ', Control Port (')
+        x = self.addstr(y, x, vals.auth_type, auth_color)
+        self.addstr(y, x, vals.format('): {control_port}'))
+      else:
+        self.addstr(y, x, vals.format(', Control Port: {control_port}'))
+    elif vals.socket_path:
+      self.addstr(y, x, vals.format(', Control Socket: {socket_path}'))
+
+  def _draw_disconnected(self, x, y, width, vals):
+    """
+    Message indicating that tor is disconnected...
+
+      Tor Disconnected (15:21 07/13/2014, press r to reconnect)
+    """
+
+    x = self.addstr(y, x, 'Tor Disconnected', curses.A_BOLD, 'red')
+    self.addstr(y, x, vals.format(' ({last_heartbeat}, press r to reconnect)'))
+
+  def _draw_resource_usage(self, x, y, width, vals):
+    """
+    System resource usage of the tor process...
+
+      cpu: 0.0% tor, 1.0% seth    mem: 0 (0.0%)       pid: 16329  uptime: 12-20:42:07
+    """
+
+    if vals.start_time:
+      if not vals.is_connected:
+        now = vals.connection_time
+      elif self.is_paused():
+        now = self.get_pause_time()
+      else:
+        now = time.time()
+
+      uptime = str_tools.short_time_label(now - vals.start_time)
+    else:
+      uptime = ''
+
+    sys_fields = (
+      (0, vals.format('cpu: {tor_cpu}% tor, {seth_cpu}% seth')),
+      (27, vals.format('mem: {memory} ({memory_percent}%)')),
+      (47, vals.format('pid: {pid}')),
+      (59, 'uptime: %s' % uptime),
+    )
+
+    for (start, label) in sys_fields:
+      if width >= start + len(label):
+        self.addstr(y, x + start, label)
+      else:
+        break
+
+  def _draw_fingerprint_and_fd_usage(self, x, y, width, vals):
+    """
+    Presents our fingerprint, and our file descriptor usage if we're running
+    out...
+
+      fingerprint: 1A94D1A794FCB2F8B6CBC179EF8FDD4008A98D3B, file desc: 900 / 1000 (90%)
+    """
+
+    initial_x, space_left = x, width
+
+    x = self.addstr(y, x, vals.format('fingerprint: {fingerprint}', width))
+    space_left -= x - initial_x
+
+    if space_left >= 30 and vals.fd_used and vals.fd_limit != -1:
+      fd_percent = 100 * vals.fd_used / vals.fd_limit
+
+      if fd_percent >= SHOW_FD_THRESHOLD:
+        if fd_percent >= 95:
+          percentage_format = (curses.A_BOLD, 'red')
+        elif fd_percent >= 90:
+          percentage_format = ('red',)
+        elif fd_percent >= 60:
+          percentage_format = ('yellow',)
+        else:
+          percentage_format = ()
+
+        x = self.addstr(y, x, ', file descriptors' if space_left >= 37 else ', file desc')
+        x = self.addstr(y, x, vals.format(': {fd_used} / {fd_limit} ('))
+        x = self.addstr(y, x, '%i%%' % fd_percent, *percentage_format)
+        self.addstr(y, x, ')')
+
+  def _draw_flags(self, x, y, width, vals):
+    """
+    Presents flags held by our relay...
+
+      flags: Running, Valid
+    """
+
+    x = self.addstr(y, x, 'flags: ')
+
+    if vals.flags:
+      for i, flag in enumerate(vals.flags):
+        flag_color = CONFIG['attr.flag_colors'].get(flag, 'white')
+        x = self.addstr(y, x, flag, curses.A_BOLD, flag_color)
+
+        if i < len(vals.flags) - 1:
+          x = self.addstr(y, x, ', ')
+    else:
+      self.addstr(y, x, 'none', curses.A_BOLD, 'cyan')
+
+  def _draw_exit_policy(self, x, y, width, vals):
+    """
+    Presents our exit policy...
+
+      exit policy: reject *:*
+    """
+
+    x = self.addstr(y, x, 'exit policy: ')
+
+    if not vals.exit_policy:
+      return
+
+    rules = list(vals.exit_policy.strip_private().strip_default())
+
+    for i, rule in enumerate(rules):
+      policy_color = 'green' if rule.is_accept else 'red'
+      x = self.addstr(y, x, str(rule), curses.A_BOLD, policy_color)
+
+      if i < len(rules) - 1:
+        x = self.addstr(y, x, ', ')
+
+    if vals.exit_policy.has_default():
+      if rules:
+        x = self.addstr(y, x, ', ')
+
+      self.addstr(y, x, '<default>', curses.A_BOLD, 'cyan')
+
+  def _draw_newnym_option(self, x, y, width, vals):
+    """
+    Provide a notice for requiesting a new identity, and time until it's next
+    available if in the process of building circuits.
+    """
+
+    if vals.newnym_wait == 0:
+      self.addstr(y, x, "press 'n' for a new identity")
+    else:
+      plural = 's' if vals.newnym_wait > 1 else ''
+      self.addstr(y, x, 'building circuits, available again in %i second%s' % (vals.newnym_wait, plural))
+
+  def run(self):
+    """
+    Keeps stats updated, checking for new information at a set rate.
+    """
+
+    last_ran = -1
+
+    while not self._halt:
+      if self.is_paused() or not self._vals.is_connected or (time.time() - last_ran) < UPDATE_RATE:
+        with self._pause_condition:
+          if not self._halt:
+            self._pause_condition.wait(0.2)
+
+        continue  # done waiting, try again
+
+      self._update()
+      last_ran = time.time()
+
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+
+    with self._pause_condition:
+      self._halt = True
+      self._pause_condition.notifyAll()
+
+  def reset_listener(self, controller, event_type, _):
+    self._update()
+
+  def _update(self):
+    previous_height = self.get_height()
+    self._vals = get_sampling(self._vals)
+
+    if self._vals.fd_used and self._vals.fd_limit != -1:
+      fd_percent = 100 * self._vals.fd_used / self._vals.fd_limit
+
+      if fd_percent >= 90:
+        log_msg = msg('panel.header.fd_used_at_ninety_percent', percentage = fd_percent)
+        log.log_once('fd_used_at_ninety_percent', log.WARN, log_msg)
+        log.DEDUPLICATION_MESSAGE_IDS.add('fd_used_at_sixty_percent')
+      elif fd_percent >= 60:
+        log_msg = msg('panel.header.fd_used_at_sixty_percent', percentage = fd_percent)
+        log.log_once('fd_used_at_sixty_percent', log.NOTICE, log_msg)
+
+    if previous_height != self.get_height():
+      # We're toggling between being a relay and client, causing the height
+      # of this panel to change. Redraw all content so we don't get
+      # overlapping content.
+
+      seth.controller.get_controller().redraw()
+    else:
+      self.redraw(True)  # just need to redraw ourselves
+
+
+def get_sampling(last_sampling = None):
+  controller = tor_controller()
+  retrieved = time.time()
+
+  pid = controller.get_pid('')
+  tor_resources = tracker.get_resource_tracker().get_value()
+  seth_total_cpu_time = sum(os.times()[:3])
+
+  or_listeners = controller.get_listeners(Listener.OR, [])
+  control_listeners = controller.get_listeners(Listener.CONTROL, [])
+
+  if controller.get_conf('HashedControlPassword', None):
+    auth_type = 'password'
+  elif controller.get_conf('CookieAuthentication', None) == '1':
+    auth_type = 'cookie'
+  else:
+    auth_type = 'open'
+
+  try:
+    fd_used = proc.file_descriptors_used(pid)
+  except IOError:
+    fd_used = None
+
+  if last_sampling:
+    seth_cpu_delta = seth_total_cpu_time - last_sampling.seth_total_cpu_time
+    seth_time_delta = retrieved - last_sampling.retrieved
+
+    python_cpu_time = seth_cpu_delta / seth_time_delta
+    sys_call_cpu_time = 0.0  # TODO: add a wrapper around call() to get this
+
+    seth_cpu = python_cpu_time + sys_call_cpu_time
+  else:
+    seth_cpu = 0.0
+
+  attr = {
+    'retrieved': retrieved,
+    'is_connected': controller.is_alive(),
+    'connection_time': controller.connection_time(),
+    'last_heartbeat': time.strftime('%H:%M %m/%d/%Y', time.localtime(controller.get_latest_heartbeat())),
+
+    'fingerprint': controller.get_info('fingerprint', 'Unknown'),
+    'nickname': controller.get_conf('Nickname', ''),
+    'newnym_wait': controller.get_newnym_wait(),
+    'exit_policy': controller.get_exit_policy(None),
+    'flags': getattr(controller.get_network_status(default = None), 'flags', []),
+
+    'version': str(controller.get_version('Unknown')).split()[0],
+    'version_status': controller.get_info('status/version/current', 'Unknown'),
+
+    'address': or_listeners[0][0] if (or_listeners and or_listeners[0][0] != '0.0.0.0') else controller.get_info('address', 'Unknown'),
+    'or_port': or_listeners[0][1] if or_listeners else '',
+    'dir_port': controller.get_conf('DirPort', '0'),
+    'control_port': str(control_listeners[0][1]) if control_listeners else None,
+    'socket_path': controller.get_conf('ControlSocket', None),
+    'is_relay': bool(or_listeners),
+
+    'auth_type': auth_type,
+    'pid': pid,
+    'start_time': system.start_time(pid),
+    'fd_limit': int(controller.get_info('process/descriptor-limit', '-1')),
+    'fd_used': fd_used,
+
+    'seth_total_cpu_time': seth_total_cpu_time,
+    'tor_cpu': '%0.1f' % (100 * tor_resources.cpu_sample),
+    'seth_cpu': seth_cpu,
+    'memory': str_tools.size_label(tor_resources.memory_bytes) if tor_resources.memory_bytes > 0 else 0,
+    'memory_percent': '%0.1f' % (100 * tor_resources.memory_percent),
+
+    'hostname': os.uname()[1],
+    'platform': '%s %s' % (os.uname()[0], os.uname()[2]),  # [platform name] [version]
+  }
+
+  class Sampling(collections.namedtuple('Sampling', attr.keys())):
+    def format(self, message, crop_width = None):
+      formatted_msg = message.format(**super(Sampling, self).__dict__)
+
+      if crop_width:
+        formatted_msg = str_tools.crop(formatted_msg, crop_width)
+
+      return formatted_msg
+
+  return Sampling(**attr)
diff --git a/seth/log_panel.py b/seth/log_panel.py
new file mode 100644
index 0000000..eb9a0c8
--- /dev/null
+++ b/seth/log_panel.py
@@ -0,0 +1,1369 @@
+"""
+Panel providing a chronological log of events its been configured to listen
+for. This provides prepopulation from the log file and supports filtering by
+regular expressions.
+"""
+
+import re
+import os
+import time
+import curses
+import logging
+import threading
+
+import stem
+from stem.control import State
+from stem.response import events
+from stem.util import conf, log, str_tools, system
+
+import seth.arguments
+import seth.popups
+from seth import __version__
+from seth.util import panel, tor_controller, ui_tools
+
+RUNLEVEL_EVENT_COLOR = {
+  log.DEBUG: "magenta",
+  log.INFO: "blue",
+  log.NOTICE: "green",
+  log.WARN: "yellow",
+  log.ERR: "red",
+}
+
+DAYBREAK_EVENT = "DAYBREAK"  # special event for marking when the date changes
+TIMEZONE_OFFSET = time.altzone if time.localtime()[8] else time.timezone
+
+ENTRY_INDENT = 2  # spaces an entry's message is indented after the first line
+
+
+def conf_handler(key, value):
+  if key == "features.log.max_lines_per_entry":
+    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.log_panel.size":
+    return max(1000, value)
+
+
+CONFIG = conf.config_dict("seth", {
+  "features.log_file": "",
+  "features.log.showDateDividers": True,
+  "features.log.showDuplicateEntries": False,
+  "features.log.entryDuration": 7,
+  "features.log.max_lines_per_entry": 6,
+  "features.log.prepopulate": True,
+  "features.log.prepopulateReadLimit": 5000,
+  "features.log.maxRefreshRate": 300,
+  "features.log.regex": [],
+  "cache.log_panel.size": 1000,
+  "msg.misc.event_types": '',
+  "tor.chroot": '',
+}, conf_handler)
+
+DUPLICATE_MSG = " [%i duplicate%s hidden]"
+
+# The height of the drawn content is estimated based on the last time we redrew
+# the panel. It's chiefly used for scrolling and the bar indicating its
+# position. Letting the estimate be too inaccurate results in a display bug, so
+# redraws the display if it's off by this threshold.
+
+CONTENT_HEIGHT_REDRAW_THRESHOLD = 3
+
+# static starting portion of common log entries, fetched from the config when
+# needed if None
+
+COMMON_LOG_MESSAGES = None
+
+# cached values and the arguments that generated it for the get_daybreaks and
+# get_duplicates functions
+
+CACHED_DAYBREAKS_ARGUMENTS = (None, None)  # events, current day
+CACHED_DAYBREAKS_RESULT = None
+CACHED_DUPLICATES_ARGUMENTS = None  # events
+CACHED_DUPLICATES_RESULT = None
+
+# duration we'll wait for the deduplication function before giving up (in ms)
+
+DEDUPLICATION_TIMEOUT = 100
+
+# maximum number of regex filters we'll remember
+
+MAX_REGEX_FILTERS = 5
+
+
+def days_since(timestamp = None):
+  """
+  Provides the number of days since the epoch converted to local time (rounded
+  down).
+
+  Arguments:
+    timestamp - unix timestamp to convert, current time if undefined
+  """
+
+  if timestamp is None:
+    timestamp = time.time()
+
+  return int((timestamp - TIMEZONE_OFFSET) / 86400)
+
+
+def load_log_messages():
+  """
+  Fetches a mapping of common log messages to their runlevels from the config.
+  """
+
+  global COMMON_LOG_MESSAGES
+  seth_config = conf.get_config("seth")
+
+  COMMON_LOG_MESSAGES = {}
+
+  for conf_key in seth_config.keys():
+    if conf_key.startswith("dedup."):
+      event_type = conf_key[4:].upper()
+      messages = seth_config.get(conf_key, [])
+      COMMON_LOG_MESSAGES[event_type] = messages
+
+
+def get_log_file_entries(runlevels, read_limit = None, add_limit = None):
+  """
+  Parses tor's log file for past events matching the given runlevels, providing
+  a list of log entries (ordered newest to oldest). Limiting the number of read
+  entries is suggested to avoid parsing everything from logs in the GB and TB
+  range.
+
+  Arguments:
+    runlevels - event types (DEBUG - ERR) to be returned
+    read_limit - max lines of the log file that'll be read (unlimited if None)
+    add_limit  - maximum entries to provide back (unlimited if None)
+  """
+
+  start_time = time.time()
+
+  if not runlevels:
+    return []
+
+  # checks tor's configuration for the log file's location (if any exists)
+
+  logging_types, logging_location = None, None
+
+  for logging_entry in tor_controller().get_conf("Log", [], True):
+    # looks for an entry like: notice file /var/log/tor/notices.log
+
+    entry_comp = logging_entry.split()
+
+    if entry_comp[1] == "file":
+      logging_types, logging_location = entry_comp[0], entry_comp[2]
+      break
+
+  if not logging_location:
+    return []
+
+  # includes the prefix for tor paths
+
+  logging_location = CONFIG['tor.chroot'] + logging_location
+
+  # if the runlevels argument is a superset of the log file then we can
+  # limit the read contents to the add_limit
+
+  runlevels = list(log.Runlevel)
+  logging_types = logging_types.upper()
+
+  if add_limit and (not read_limit or read_limit > add_limit):
+    if "-" in logging_types:
+      div_index = logging_types.find("-")
+      start_index = runlevels.index(logging_types[:div_index])
+      end_index = runlevels.index(logging_types[div_index + 1:])
+      log_file_run_levels = runlevels[start_index:end_index + 1]
+    else:
+      start_index = runlevels.index(logging_types)
+      log_file_run_levels = runlevels[start_index:]
+
+    # checks if runlevels we're reporting are a superset of the file's contents
+
+    is_file_subset = True
+
+    for runlevel_type in log_file_run_levels:
+      if runlevel_type not in runlevels:
+        is_file_subset = False
+        break
+
+    if is_file_subset:
+      read_limit = add_limit
+
+  # tries opening the log file, cropping results to avoid choking on huge logs
+
+  lines = []
+
+  try:
+    if read_limit:
+      lines = system.call("tail -n %i %s" % (read_limit, logging_location))
+
+      if not lines:
+        raise IOError()
+    else:
+      log_file = open(logging_location, "r")
+      lines = log_file.readlines()
+      log_file.close()
+  except IOError:
+    log.warn("Unable to read tor's log file: %s" % logging_location)
+
+  if not lines:
+    return []
+
+  logged_events = []
+  current_unix_time, current_local_time = time.time(), time.localtime()
+
+  for i in range(len(lines) - 1, -1, -1):
+    line = lines[i]
+
+    # entries look like:
+    # Jul 15 18:29:48.806 [notice] Parsing GEOIP file.
+
+    line_comp = line.split()
+
+    # Checks that we have all the components we expect. This could happen if
+    # we're either not parsing a tor log or in weird edge cases (like being
+    # out of disk space)
+
+    if len(line_comp) < 4:
+      continue
+
+    event_type = line_comp[3][1:-1].upper()
+
+    if event_type in runlevels:
+      # converts timestamp to unix time
+
+      timestamp = " ".join(line_comp[:3])
+
+      # strips the decimal seconds
+
+      if "." in timestamp:
+        timestamp = timestamp[:timestamp.find(".")]
+
+      # Ignoring wday and yday since they aren't used.
+      #
+      # Pretend the year is 2012, because 2012 is a leap year, and parsing a
+      # date with strptime fails if Feb 29th is passed without a year that's
+      # actually a leap year. We can't just use the current year, because we
+      # might be parsing old logs which didn't get rotated.
+      #
+      # https://trac.torproject.org/projects/tor/ticket/5265
+
+      timestamp = "2012 " + timestamp
+      event_time_comp = list(time.strptime(timestamp, "%Y %b %d %H:%M:%S"))
+      event_time_comp[8] = current_local_time.tm_isdst
+      event_time = time.mktime(event_time_comp)  # converts local to unix time
+
+      # The above is gonna be wrong if the logs are for the previous year. If
+      # the event's in the future then correct for this.
+
+      if event_time > current_unix_time + 60:
+        event_time_comp[0] -= 1
+        event_time = time.mktime(event_time_comp)
+
+      event_msg = " ".join(line_comp[4:])
+      logged_events.append(LogEntry(event_time, event_type, event_msg, RUNLEVEL_EVENT_COLOR[event_type]))
+
+    if "opening log file" in line:
+      break  # this entry marks the start of this tor instance
+
+  if add_limit:
+    logged_events = logged_events[:add_limit]
+
+  log.info("Read %i entries from tor's log file: %s (read limit: %i, runtime: %0.3f)" % (len(logged_events), logging_location, read_limit, time.time() - start_time))
+
+  return logged_events
+
+
+def get_daybreaks(events, ignore_time_for_cache = False):
+  """
+  Provides the input events back with special 'DAYBREAK_EVENT' markers inserted
+  whenever the date changed between log entries (or since the most recent
+  event). The timestamp matches the beginning of the day for the following
+  entry.
+
+  Arguments:
+    events             - chronologically ordered listing of events
+    ignore_time_for_cache - skips taking the day into consideration for providing
+                         cached results if true
+  """
+
+  global CACHED_DAYBREAKS_ARGUMENTS, CACHED_DAYBREAKS_RESULT
+
+  if not events:
+    return []
+
+  new_listing = []
+  current_day = days_since()
+  last_day = current_day
+
+  if CACHED_DAYBREAKS_ARGUMENTS[0] == events and \
+    (ignore_time_for_cache or CACHED_DAYBREAKS_ARGUMENTS[1] == current_day):
+    return list(CACHED_DAYBREAKS_RESULT)
+
+  for entry in events:
+    event_day = days_since(entry.timestamp)
+
+    if event_day != last_day:
+      marker_timestamp = (event_day * 86400) + TIMEZONE_OFFSET
+      new_listing.append(LogEntry(marker_timestamp, DAYBREAK_EVENT, "", "white"))
+
+    new_listing.append(entry)
+    last_day = event_day
+
+  CACHED_DAYBREAKS_ARGUMENTS = (list(events), current_day)
+  CACHED_DAYBREAKS_RESULT = list(new_listing)
+
+  return new_listing
+
+
+def get_duplicates(events):
+  """
+  Deduplicates a list of log entries, providing back a tuple listing with the
+  log entry and count of duplicates following it. Entries in different days are
+  not considered to be duplicates. This times out, returning None if it takes
+  longer than DEDUPLICATION_TIMEOUT.
+
+  Arguments:
+    events - chronologically ordered listing of events
+  """
+
+  global CACHED_DUPLICATES_ARGUMENTS, CACHED_DUPLICATES_RESULT
+
+  if CACHED_DUPLICATES_ARGUMENTS == events:
+    return list(CACHED_DUPLICATES_RESULT)
+
+  # loads common log entries from the config if they haven't been
+
+  if COMMON_LOG_MESSAGES is None:
+    load_log_messages()
+
+  start_time = time.time()
+  events_remaining = list(events)
+  return_events = []
+
+  while events_remaining:
+    entry = events_remaining.pop(0)
+    duplicate_indices = is_duplicate(entry, events_remaining, True)
+
+    # checks if the call timeout has been reached
+
+    if (time.time() - start_time) > DEDUPLICATION_TIMEOUT / 1000.0:
+      return None
+
+    # drops duplicate entries
+
+    duplicate_indices.reverse()
+
+    for i in duplicate_indices:
+      del events_remaining[i]
+
+    return_events.append((entry, len(duplicate_indices)))
+
+  CACHED_DUPLICATES_ARGUMENTS = list(events)
+  CACHED_DUPLICATES_RESULT = list(return_events)
+
+  return return_events
+
+
+def is_duplicate(event, event_set, get_duplicates = False):
+  """
+  True if the event is a duplicate for something in the event_set, false
+  otherwise. If the get_duplicates flag is set this provides the indices of
+  the duplicates instead.
+
+  Arguments:
+    event         - event to search for duplicates of
+    event_set      - set to look for the event in
+    get_duplicates - instead of providing back a boolean this gives a list of
+                    the duplicate indices in the event_set
+  """
+
+  duplicate_indices = []
+
+  for i in range(len(event_set)):
+    forward_entry = event_set[i]
+
+    # if showing dates then do duplicate detection for each day, rather
+    # than globally
+
+    if forward_entry.type == DAYBREAK_EVENT:
+      break
+
+    if event.type == forward_entry.type:
+      is_duplicate = False
+
+      if event.msg == forward_entry.msg:
+        is_duplicate = True
+      elif event.type in COMMON_LOG_MESSAGES:
+        for common_msg in COMMON_LOG_MESSAGES[event.type]:
+          # if it starts with an asterisk then check the whole message rather
+          # than just the start
+
+          if common_msg[0] == "*":
+            is_duplicate = common_msg[1:] in event.msg and common_msg[1:] in forward_entry.msg
+          else:
+            is_duplicate = event.msg.startswith(common_msg) and forward_entry.msg.startswith(common_msg)
+
+          if is_duplicate:
+            break
+
+      if is_duplicate:
+        if get_duplicates:
+          duplicate_indices.append(i)
+        else:
+          return True
+
+  if get_duplicates:
+    return duplicate_indices
+  else:
+    return False
+
+
+class LogEntry():
+  """
+  Individual log file entry, having the following attributes:
+    timestamp - unix timestamp for when the event occurred
+    event_type - event type that occurred ("INFO", "BW", "ARM_WARN", etc)
+    msg       - message that was logged
+    color     - color of the log entry
+  """
+
+  def __init__(self, timestamp, event_type, msg, color):
+    self.timestamp = timestamp
+    self.type = event_type
+    self.msg = msg
+    self.color = color
+    self._display_message = None
+
+  def get_display_message(self, include_date = False):
+    """
+    Provides the entry's message for the log.
+
+    Arguments:
+      include_date - appends the event's date to the start of the message
+    """
+
+    if include_date:
+      # not the common case so skip caching
+      entry_time = time.localtime(self.timestamp)
+      time_label = "%i/%i/%i %02i:%02i:%02i" % (entry_time[1], entry_time[2], entry_time[0], entry_time[3], entry_time[4], entry_time[5])
+      return "%s [%s] %s" % (time_label, self.type, self.msg)
+
+    if not self._display_message:
+      entry_time = time.localtime(self.timestamp)
+      self._display_message = "%02i:%02i:%02i [%s] %s" % (entry_time[3], entry_time[4], entry_time[5], self.type, self.msg)
+
+    return self._display_message
+
+
+class LogPanel(panel.Panel, threading.Thread, logging.Handler):
+  """
+  Listens for and displays tor, seth, and stem events. This can prepopulate
+  from tor's log file if it exists.
+  """
+
+  def __init__(self, stdscr, logged_events):
+    panel.Panel.__init__(self, stdscr, "log", 0)
+    logging.Handler.__init__(self, level = log.logging_level(log.DEBUG))
+
+    self.setFormatter(logging.Formatter(
+      fmt = '%(asctime)s [%(levelname)s] %(message)s',
+      datefmt = '%m/%d/%Y %H:%M:%S'),
+    )
+
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+
+    # Make sure that the msg.* messages are loaded. Lazy loading it later is
+    # fine, but this way we're sure it happens before warning about unused
+    # config options.
+
+    load_log_messages()
+
+    # regex filters the user has defined
+
+    self.filter_options = []
+
+    for filter in CONFIG["features.log.regex"]:
+      # checks if we can't have more filters
+
+      if len(self.filter_options) >= MAX_REGEX_FILTERS:
+        break
+
+      try:
+        re.compile(filter)
+        self.filter_options.append(filter)
+      except re.error as exc:
+        log.notice("Invalid regular expression pattern (%s): %s" % (exc, filter))
+
+    self.logged_events = []  # needs to be set before we receive any events
+
+    # restricts the input to the set of events we can listen to, and
+    # configures the controller to liten to them
+
+    self.logged_events = self.set_event_listening(logged_events)
+
+    self.set_pause_attr("msg_log")       # tracks the message log when we're paused
+    self.msg_log = []                    # log entries, sorted by the timestamp
+    self.regex_filter = None             # filter for presented log events (no filtering if None)
+    self.last_content_height = 0         # height of the rendered content when last drawn
+    self.log_file = None                 # file log messages are saved to (skipped if None)
+    self.scroll = 0
+
+    self._last_update = -1               # time the content was last revised
+    self._halt = False                   # terminates thread if true
+    self._cond = threading.Condition()   # used for pausing/resuming the thread
+
+    # restricts concurrent write access to attributes used to draw the display
+    # and pausing:
+    # msg_log, logged_events, regex_filter, scroll
+
+    self.vals_lock = threading.RLock()
+
+    # cached parameters (invalidated if arguments for them change)
+    # last set of events we've drawn with
+
+    self._last_logged_events = []
+
+    # _get_title (args: logged_events, regex_filter pattern, width)
+
+    self._title_cache = None
+    self._title_args = (None, None, None)
+
+    self.reprepopulate_events()
+
+    # leaving last_content_height as being too low causes initialization problems
+
+    self.last_content_height = len(self.msg_log)
+
+    # adds listeners for tor and stem events
+
+    controller = tor_controller()
+    controller.add_status_listener(self._reset_listener)
+
+    # opens log file if we'll be saving entries
+
+    if CONFIG["features.log_file"]:
+      log_path = CONFIG["features.log_file"]
+
+      try:
+        # make dir if the path doesn't already exist
+
+        base_dir = os.path.dirname(log_path)
+
+        if not os.path.exists(base_dir):
+          os.makedirs(base_dir)
+
+        self.log_file = open(log_path, "a")
+        log.notice("seth %s opening log file (%s)" % (__version__, log_path))
+      except IOError as exc:
+        log.error("Unable to write to log file: %s" % exc.strerror)
+        self.log_file = None
+      except OSError as exc:
+        log.error("Unable to write to log file: %s" % exc)
+        self.log_file = None
+
+    stem_logger = log.get_logger()
+    stem_logger.addHandler(self)
+
+  def emit(self, record):
+    if record.levelname == "WARNING":
+      record.levelname = "WARN"
+
+    event_color = RUNLEVEL_EVENT_COLOR[record.levelname]
+    self.register_event(LogEntry(int(record.created), "ARM_%s" % record.levelname, record.msg, event_color))
+
+  def reprepopulate_events(self):
+    """
+    Clears the event log and repopulates it from the seth and tor backlogs.
+    """
+
+    self.vals_lock.acquire()
+
+    # clears the event log
+
+    self.msg_log = []
+
+    # fetches past tor events from log file, if available
+
+    if CONFIG["features.log.prepopulate"]:
+      set_runlevels = list(set.intersection(set(self.logged_events), set(list(log.Runlevel))))
+      read_limit = CONFIG["features.log.prepopulateReadLimit"]
+      add_limit = CONFIG["cache.log_panel.size"]
+
+      for entry in get_log_file_entries(set_runlevels, read_limit, add_limit):
+        self.msg_log.append(entry)
+
+    # crops events that are either too old, or more numerous than the caching size
+
+    self._trim_events(self.msg_log)
+
+    self.vals_lock.release()
+
+  def set_duplicate_visability(self, is_visible):
+    """
+    Sets if duplicate log entries are collaped or expanded.
+
+    Arguments:
+      is_visible - if true all log entries are shown, otherwise they're
+                   deduplicated
+    """
+
+    seth_config = conf.get_config("seth")
+    seth_config.set("features.log.showDuplicateEntries", str(is_visible))
+
+  def register_tor_event(self, event):
+    """
+    Translates a stem.response.event.Event instance into a LogEvent, and calls
+    register_event().
+    """
+
+    msg, color = ' '.join(str(event).split(' ')[1:]), "white"
+
+    if isinstance(event, events.CircuitEvent):
+      color = "yellow"
+    elif isinstance(event, events.BandwidthEvent):
+      color = "cyan"
+      msg = "READ: %i, WRITTEN: %i" % (event.read, event.written)
+    elif isinstance(event, events.LogEvent):
+      color = RUNLEVEL_EVENT_COLOR[event.runlevel]
+      msg = event.message
+    elif isinstance(event, events.NetworkStatusEvent):
+      color = "blue"
+    elif isinstance(event, events.NewConsensusEvent):
+      color = "magenta"
+    elif isinstance(event, events.GuardEvent):
+      color = "yellow"
+    elif event.type not in seth.arguments.TOR_EVENT_TYPES.values():
+      color = "red"  # unknown event type
+
+    self.register_event(LogEntry(event.arrived_at, event.type, msg, color))
+
+  def register_event(self, event):
+    """
+    Notes event and redraws log. If paused it's held in a temporary buffer.
+
+    Arguments:
+      event - LogEntry for the event that occurred
+    """
+
+    if event.type not in self.logged_events:
+      return
+
+    # strips control characters to avoid screwing up the terminal
+
+    event.msg = ui_tools.get_printable(event.msg)
+
+    # note event in the log file if we're saving them
+
+    if self.log_file:
+      try:
+        self.log_file.write(event.get_display_message(True) + "\n")
+        self.log_file.flush()
+      except IOError as exc:
+        log.error("Unable to write to log file: %s" % exc.strerror)
+        self.log_file = None
+
+    self.vals_lock.acquire()
+    self.msg_log.insert(0, event)
+    self._trim_events(self.msg_log)
+
+    # notifies the display that it has new content
+
+    if not self.regex_filter or self.regex_filter.search(event.get_display_message()):
+      self._cond.acquire()
+      self._cond.notifyAll()
+      self._cond.release()
+
+    self.vals_lock.release()
+
+  def set_logged_events(self, event_types):
+    """
+    Sets the event types recognized by the panel.
+
+    Arguments:
+      event_types - event types to be logged
+    """
+
+    if event_types == self.logged_events:
+      return
+
+    self.vals_lock.acquire()
+
+    # configures the controller to listen for these tor events, and provides
+    # back a subset without anything we're failing to listen to
+
+    set_types = self.set_event_listening(event_types)
+    self.logged_events = set_types
+    self.redraw(True)
+    self.vals_lock.release()
+
+  def get_filter(self):
+    """
+    Provides our currently selected regex filter.
+    """
+
+    return self.filter_options[0] if self.regex_filter else None
+
+  def set_filter(self, log_filter):
+    """
+    Filters log entries according to the given regular expression.
+
+    Arguments:
+      log_filter - regular expression used to determine which messages are
+                  shown, None if no filter should be applied
+    """
+
+    if log_filter == self.regex_filter:
+      return
+
+    self.vals_lock.acquire()
+    self.regex_filter = log_filter
+    self.redraw(True)
+    self.vals_lock.release()
+
+  def make_filter_selection(self, selected_option):
+    """
+    Makes the given filter selection, applying it to the log and reorganizing
+    our filter selection.
+
+    Arguments:
+      selected_option - regex filter we've already added, None if no filter
+                       should be applied
+    """
+
+    if selected_option:
+      try:
+        self.set_filter(re.compile(selected_option))
+
+        # move selection to top
+
+        self.filter_options.remove(selected_option)
+        self.filter_options.insert(0, selected_option)
+      except re.error as exc:
+        # shouldn't happen since we've already checked validity
+
+        log.warn("Invalid regular expression ('%s': %s) - removing from listing" % (selected_option, exc))
+        self.filter_options.remove(selected_option)
+    else:
+      self.set_filter(None)
+
+  def show_filter_prompt(self):
+    """
+    Prompts the user to add a new regex filter.
+    """
+
+    regex_input = seth.popups.input_prompt("Regular expression: ")
+
+    if regex_input:
+      try:
+        self.set_filter(re.compile(regex_input))
+
+        if regex_input in self.filter_options:
+          self.filter_options.remove(regex_input)
+
+        self.filter_options.insert(0, regex_input)
+      except re.error as exc:
+        seth.popups.show_msg("Unable to compile expression: %s" % exc, 2)
+
+  def show_event_selection_prompt(self):
+    """
+    Prompts the user to select the events being listened for.
+    """
+
+    # allow user to enter new types of events to log - unchanged if left blank
+
+    popup, width, height = seth.popups.init(11, 80)
+
+    if popup:
+      try:
+        # displays the available flags
+
+        popup.win.box()
+        popup.addstr(0, 0, "Event Types:", curses.A_STANDOUT)
+        event_lines = CONFIG['msg.misc.event_types'].split("\n")
+
+        for i in range(len(event_lines)):
+          popup.addstr(i + 1, 1, event_lines[i][6:])
+
+        popup.win.refresh()
+
+        user_input = seth.popups.input_prompt("Events to log: ")
+
+        if user_input:
+          user_input = user_input.replace(' ', '')  # strips spaces
+
+          try:
+            self.set_logged_events(seth.arguments.expand_events(user_input))
+          except ValueError as exc:
+            seth.popups.show_msg("Invalid flags: %s" % str(exc), 2)
+      finally:
+        seth.popups.finalize()
+
+  def show_snapshot_prompt(self):
+    """
+    Lets user enter a path to take a snapshot, canceling if left blank.
+    """
+
+    path_input = seth.popups.input_prompt("Path to save log snapshot: ")
+
+    if path_input:
+      try:
+        self.save_snapshot(path_input)
+        seth.popups.show_msg("Saved: %s" % path_input, 2)
+      except IOError as exc:
+        seth.popups.show_msg("Unable to save snapshot: %s" % exc.strerror, 2)
+
+  def clear(self):
+    """
+    Clears the contents of the event log.
+    """
+
+    self.vals_lock.acquire()
+    self.msg_log = []
+    self.redraw(True)
+    self.vals_lock.release()
+
+  def save_snapshot(self, path):
+    """
+    Saves the log events currently being displayed to the given path. This
+    takes filers into account. This overwrites the file if it already exists,
+    and raises an IOError if there's a problem.
+
+    Arguments:
+      path - path where to save the log snapshot
+    """
+
+    path = os.path.abspath(os.path.expanduser(path))
+
+    # make dir if the path doesn't already exist
+
+    base_dir = os.path.dirname(path)
+
+    try:
+      if not os.path.exists(base_dir):
+        os.makedirs(base_dir)
+    except OSError as exc:
+      raise IOError("unable to make directory '%s'" % base_dir)
+
+    snapshot_file = open(path, "w")
+    self.vals_lock.acquire()
+
+    try:
+      for entry in self.msg_log:
+        is_visible = not self.regex_filter or self.regex_filter.search(entry.get_display_message())
+
+        if is_visible:
+          snapshot_file.write(entry.get_display_message(True) + "\n")
+
+      self.vals_lock.release()
+    except Exception as exc:
+      self.vals_lock.release()
+      raise exc
+
+  def handle_key(self, key):
+    if key.is_scroll():
+      page_height = self.get_preferred_size()[0] - 1
+      new_scroll = ui_tools.get_scroll_position(key, self.scroll, page_height, self.last_content_height)
+
+      if self.scroll != new_scroll:
+        self.vals_lock.acquire()
+        self.scroll = new_scroll
+        self.redraw(True)
+        self.vals_lock.release()
+    elif key.match('u'):
+      self.vals_lock.acquire()
+      self.set_duplicate_visability(not CONFIG["features.log.showDuplicateEntries"])
+      self.redraw(True)
+      self.vals_lock.release()
+    elif key.match('c'):
+      msg = "This will clear the log. Are you sure (c again to confirm)?"
+      key_press = seth.popups.show_msg(msg, attr = curses.A_BOLD)
+
+      if key_press.match('c'):
+        self.clear()
+    elif key.match('f'):
+      # Provides menu to pick regular expression filters or adding new ones:
+      # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
+
+      options = ["None"] + self.filter_options + ["New..."]
+      old_selection = 0 if not self.regex_filter else 1
+
+      # does all activity under a curses lock to prevent redraws when adding
+      # new filters
+
+      panel.CURSES_LOCK.acquire()
+
+      try:
+        selection = seth.popups.show_menu("Log Filter:", options, old_selection)
+
+        # applies new setting
+
+        if selection == 0:
+          self.set_filter(None)
+        elif selection == len(options) - 1:
+          # selected 'New...' option - prompt user to input regular expression
+          self.show_filter_prompt()
+        elif selection != -1:
+          self.make_filter_selection(self.filter_options[selection - 1])
+      finally:
+        panel.CURSES_LOCK.release()
+
+      if len(self.filter_options) > MAX_REGEX_FILTERS:
+        del self.filter_options[MAX_REGEX_FILTERS:]
+    elif key.match('e'):
+      self.show_event_selection_prompt()
+    elif key.match('a'):
+      self.show_snapshot_prompt()
+    else:
+      return False
+
+    return True
+
+  def get_help(self):
+    return [
+      ('up arrow', 'scroll log up a line', None),
+      ('down arrow', 'scroll log down a line', None),
+      ('a', 'save snapshot of the log', None),
+      ('e', 'change logged events', None),
+      ('f', 'log regex filter', 'enabled' if self.regex_filter else 'disabled'),
+      ('u', 'duplicate log entries', 'visible' if CONFIG['features.log.showDuplicateEntries'] else 'hidden'),
+      ('c', 'clear event log', None),
+    ]
+
+  def draw(self, width, height):
+    """
+    Redraws message log. Entries stretch to use available space and may
+    contain up to two lines. Starts with newest entries.
+    """
+
+    current_log = self.get_attr("msg_log")
+
+    self.vals_lock.acquire()
+    self._last_logged_events, self._last_update = list(current_log), time.time()
+
+    # draws the top label
+
+    if self.is_title_visible():
+      self.addstr(0, 0, self._get_title(width), curses.A_STANDOUT)
+
+    # restricts scroll location to valid bounds
+
+    self.scroll = max(0, min(self.scroll, self.last_content_height - height + 1))
+
+    # draws left-hand scroll bar if content's longer than the height
+
+    msg_indent, divider_indent = 1, 0  # offsets for scroll bar
+    is_scroll_bar_visible = self.last_content_height > height - 1
+
+    if is_scroll_bar_visible:
+      msg_indent, divider_indent = 3, 2
+      self.add_scroll_bar(self.scroll, self.scroll + height - 1, self.last_content_height, 1)
+
+    # draws log entries
+
+    line_count = 1 - self.scroll
+    seen_first_date_divider = False
+    divider_attr, duplicate_attr = (curses.A_BOLD, 'yellow'), (curses.A_BOLD, 'green')
+
+    is_dates_shown = self.regex_filter is None and CONFIG["features.log.showDateDividers"]
+    event_log = get_daybreaks(current_log, self.is_paused()) if is_dates_shown else list(current_log)
+
+    if not CONFIG["features.log.showDuplicateEntries"]:
+      deduplicated_log = get_duplicates(event_log)
+
+      if deduplicated_log is None:
+        log.warn("Deduplication took too long. Its current implementation has difficulty handling large logs so disabling it to keep the interface responsive.")
+        self.set_duplicate_visability(True)
+        deduplicated_log = [(entry, 0) for entry in event_log]
+    else:
+      deduplicated_log = [(entry, 0) for entry in event_log]
+
+    # determines if we have the minimum width to show date dividers
+
+    show_daybreaks = width - divider_indent >= 3
+
+    while deduplicated_log:
+      entry, duplicate_count = deduplicated_log.pop(0)
+
+      if self.regex_filter and not self.regex_filter.search(entry.get_display_message()):
+        continue  # filter doesn't match log message - skip
+
+      # checks if we should be showing a divider with the date
+
+      if entry.type == DAYBREAK_EVENT:
+        # bottom of the divider
+
+        if seen_first_date_divider:
+          if line_count >= 1 and line_count < height and show_daybreaks:
+            self.addch(line_count, divider_indent, curses.ACS_LLCORNER, *divider_attr)
+            self.hline(line_count, divider_indent + 1, width - divider_indent - 2, *divider_attr)
+            self.addch(line_count, width - 1, curses.ACS_LRCORNER, *divider_attr)
+
+          line_count += 1
+
+        # top of the divider
+
+        if line_count >= 1 and line_count < height and show_daybreaks:
+          time_label = time.strftime(" %B %d, %Y ", time.localtime(entry.timestamp))
+          self.addch(line_count, divider_indent, curses.ACS_ULCORNER, *divider_attr)
+          self.addch(line_count, divider_indent + 1, curses.ACS_HLINE, *divider_attr)
+          self.addstr(line_count, divider_indent + 2, time_label, curses.A_BOLD, *divider_attr)
+
+          line_length = width - divider_indent - len(time_label) - 3
+          self.hline(line_count, divider_indent + len(time_label) + 2, line_length, *divider_attr)
+          self.addch(line_count, divider_indent + len(time_label) + 2 + line_length, curses.ACS_URCORNER, *divider_attr)
+
+        seen_first_date_divider = True
+        line_count += 1
+      else:
+        # entry contents to be displayed, tuples of the form:
+        # (msg, formatting, includeLinebreak)
+
+        display_queue = []
+
+        msg_comp = entry.get_display_message().split("\n")
+
+        for i in range(len(msg_comp)):
+          font = curses.A_BOLD if "ERR" in entry.type else curses.A_NORMAL  # emphasizes ERR messages
+          display_queue.append((msg_comp[i].strip(), (font, entry.color), i != len(msg_comp) - 1))
+
+        if duplicate_count:
+          plural_label = "s" if duplicate_count > 1 else ""
+          duplicate_msg = DUPLICATE_MSG % (duplicate_count, plural_label)
+          display_queue.append((duplicate_msg, duplicate_attr, False))
+
+        cursor_location, line_offset = msg_indent, 0
+        max_entries_per_line = CONFIG["features.log.max_lines_per_entry"]
+
+        while display_queue:
+          msg, format, include_break = display_queue.pop(0)
+          draw_line = line_count + line_offset
+
+          if line_offset == max_entries_per_line:
+            break
+
+          max_msg_size = width - cursor_location - 1
+
+          if len(msg) > max_msg_size:
+            # message is too long - break it up
+            if line_offset == max_entries_per_line - 1:
+              msg = str_tools.crop(msg, max_msg_size)
+            else:
+              msg, remainder = str_tools.crop(msg, max_msg_size, 4, 4, str_tools.Ending.HYPHEN, True)
+              display_queue.insert(0, (remainder.strip(), format, include_break))
+
+            include_break = True
+
+          if draw_line < height and draw_line >= 1:
+            if seen_first_date_divider and width - divider_indent >= 3 and show_daybreaks:
+              self.addch(draw_line, divider_indent, curses.ACS_VLINE, *divider_attr)
+              self.addch(draw_line, width - 1, curses.ACS_VLINE, *divider_attr)
+
+            self.addstr(draw_line, cursor_location, msg, *format)
+
+          cursor_location += len(msg)
+
+          if include_break or not display_queue:
+            line_offset += 1
+            cursor_location = msg_indent + ENTRY_INDENT
+
+        line_count += line_offset
+
+      # if this is the last line and there's room, then draw the bottom of the divider
+
+      if not deduplicated_log and seen_first_date_divider:
+        if line_count < height and show_daybreaks:
+          self.addch(line_count, divider_indent, curses.ACS_LLCORNER, *divider_attr)
+          self.hline(line_count, divider_indent + 1, width - divider_indent - 2, *divider_attr)
+          self.addch(line_count, width - 1, curses.ACS_LRCORNER, *divider_attr)
+
+        line_count += 1
+
+    # redraw the display if...
+    # - last_content_height was off by too much
+    # - we're off the bottom of the page
+
+    new_content_height = line_count + self.scroll - 1
+    content_height_delta = abs(self.last_content_height - new_content_height)
+    force_redraw, force_redraw_reason = True, ""
+
+    if content_height_delta >= CONTENT_HEIGHT_REDRAW_THRESHOLD:
+      force_redraw_reason = "estimate was off by %i" % content_height_delta
+    elif new_content_height > height and self.scroll + height - 1 > new_content_height:
+      force_redraw_reason = "scrolled off the bottom of the page"
+    elif not is_scroll_bar_visible and new_content_height > height - 1:
+      force_redraw_reason = "scroll bar wasn't previously visible"
+    elif is_scroll_bar_visible and new_content_height <= height - 1:
+      force_redraw_reason = "scroll bar shouldn't be visible"
+    else:
+      force_redraw = False
+
+    self.last_content_height = new_content_height
+
+    if force_redraw:
+      log.debug("redrawing the log panel with the corrected content height (%s)" % force_redraw_reason)
+      self.redraw(True)
+
+    self.vals_lock.release()
+
+  def redraw(self, force_redraw=False, block=False):
+    # determines if the content needs to be redrawn or not
+    panel.Panel.redraw(self, force_redraw, block)
+
+  def run(self):
+    """
+    Redraws the display, coalescing updates if events are rapidly logged (for
+    instance running at the DEBUG runlevel) while also being immediately
+    responsive if additions are less frequent.
+    """
+
+    last_day = days_since()  # used to determine if the date has changed
+
+    while not self._halt:
+      current_day = days_since()
+      time_since_reset = time.time() - self._last_update
+      max_log_update_rate = CONFIG["features.log.maxRefreshRate"] / 1000.0
+
+      sleep_time = 0
+
+      if (self.msg_log == self._last_logged_events and last_day == current_day) or self.is_paused():
+        sleep_time = 5
+      elif time_since_reset < max_log_update_rate:
+        sleep_time = max(0.05, max_log_update_rate - time_since_reset)
+
+      if sleep_time:
+        self._cond.acquire()
+
+        if not self._halt:
+          self._cond.wait(sleep_time)
+
+        self._cond.release()
+      else:
+        last_day = current_day
+        self.redraw(True)
+
+        # makes sure that we register this as an update, otherwise lacking the
+        # curses lock can cause a busy wait here
+
+        self._last_update = time.time()
+
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+
+    self._cond.acquire()
+    self._halt = True
+    self._cond.notifyAll()
+    self._cond.release()
+
+  def set_event_listening(self, events):
+    """
+    Configures the events Tor listens for, filtering non-tor events from what we
+    request from the controller. This returns a sorted list of the events we
+    successfully set.
+
+    Arguments:
+      events - event types to attempt to set
+    """
+
+    events = set(events)  # drops duplicates
+
+    # accounts for runlevel naming difference
+
+    if "ERROR" in events:
+      events.add("ERR")
+      events.remove("ERROR")
+
+    if "WARNING" in events:
+      events.add("WARN")
+      events.remove("WARNING")
+
+    tor_events = events.intersection(set(seth.arguments.TOR_EVENT_TYPES.values()))
+    seth_events = events.intersection(set(["ARM_%s" % runlevel for runlevel in log.Runlevel.keys()]))
+
+    # adds events unrecognized by seth if we're listening to the 'UNKNOWN' type
+
+    if "UNKNOWN" in events:
+      tor_events.update(set(seth.arguments.missing_event_types()))
+
+    controller = tor_controller()
+    controller.remove_event_listener(self.register_tor_event)
+
+    for event_type in list(tor_events):
+      try:
+        controller.add_event_listener(self.register_tor_event, event_type)
+      except stem.ProtocolError:
+        tor_events.remove(event_type)
+
+    # provides back the input set minus events we failed to set
+
+    return sorted(tor_events.union(seth_events))
+
+  def _reset_listener(self, controller, event_type, _):
+    # if we're attaching to a new tor instance then clears the log and
+    # prepopulates it with the content belonging to this instance
+
+    if event_type == State.INIT:
+      self.reprepopulate_events()
+      self.redraw(True)
+    elif event_type == State.CLOSED:
+      log.notice("Tor control port closed")
+
+  def _get_title(self, width):
+    """
+    Provides the label used for the panel, looking like:
+      Events (ARM NOTICE - ERR, BW - filter: prepopulate):
+
+    This truncates the attributes (with an ellipse) if too long, and condenses
+    runlevel ranges if there's three or more in a row (for instance ARM_INFO,
+    ARM_NOTICE, and ARM_WARN becomes "ARM_INFO - WARN").
+
+    Arguments:
+      width - width constraint the label needs to fix in
+    """
+
+    # usually the attributes used to make the label are decently static, so
+    # provide cached results if they're unchanged
+
+    self.vals_lock.acquire()
+    current_pattern = self.regex_filter.pattern if self.regex_filter else None
+    is_unchanged = self._title_args[0] == self.logged_events
+    is_unchanged &= self._title_args[1] == current_pattern
+    is_unchanged &= self._title_args[2] == width
+
+    if is_unchanged:
+      self.vals_lock.release()
+      return self._title_cache
+
+    events_list = list(self.logged_events)
+
+    if not events_list:
+      if not current_pattern:
+        panel_label = "Events:"
+      else:
+        label_pattern = str_tools.crop(current_pattern, width - 18)
+        panel_label = "Events (filter: %s):" % label_pattern
+    else:
+      # does the following with all runlevel types (tor, seth, and stem):
+      # - pulls to the start of the list
+      # - condenses range if there's three or more in a row (ex. "ARM_INFO - WARN")
+      # - condense further if there's identical runlevel ranges for multiple
+      #   types (ex. "NOTICE - ERR, ARM_NOTICE - ERR" becomes "TOR/ARM NOTICE - ERR")
+
+      tmp_runlevels = []  # runlevels pulled from the list (just the runlevel part)
+      runlevel_ranges = []  # tuple of type, start_level, end_level for ranges to be consensed
+
+      # reverses runlevels and types so they're appended in the right order
+
+      reversed_runlevels = list(log.Runlevel)
+      reversed_runlevels.reverse()
+
+      for prefix in ("ARM_", ""):
+        # blank ending runlevel forces the break condition to be reached at the end
+        for runlevel in reversed_runlevels + [""]:
+          event_type = prefix + runlevel
+          if runlevel and event_type in events_list:
+            # runlevel event found, move to the tmp list
+            events_list.remove(event_type)
+            tmp_runlevels.append(runlevel)
+          elif tmp_runlevels:
+            # adds all tmp list entries to the start of events_list
+            if len(tmp_runlevels) >= 3:
+              # save condense sequential runlevels to be added later
+              runlevel_ranges.append((prefix, tmp_runlevels[-1], tmp_runlevels[0]))
+            else:
+              # adds runlevels individaully
+              for tmp_runlevel in tmp_runlevels:
+                events_list.insert(0, prefix + tmp_runlevel)
+
+            tmp_runlevels = []
+
+      # adds runlevel ranges, condensing if there's identical ranges
+
+      for i in range(len(runlevel_ranges)):
+        if runlevel_ranges[i]:
+          prefix, start_level, end_level = runlevel_ranges[i]
+
+          # check for matching ranges
+
+          matches = []
+
+          for j in range(i + 1, len(runlevel_ranges)):
+            if runlevel_ranges[j] and runlevel_ranges[j][1] == start_level and runlevel_ranges[j][2] == end_level:
+              matches.append(runlevel_ranges[j])
+              runlevel_ranges[j] = None
+
+          if matches:
+            # strips underscores and replaces empty entries with "TOR"
+
+            prefixes = [entry[0] for entry in matches] + [prefix]
+
+            for k in range(len(prefixes)):
+              if prefixes[k] == "":
+                prefixes[k] = "TOR"
+              else:
+                prefixes[k] = prefixes[k].replace("_", "")
+
+            events_list.insert(0, "%s %s - %s" % ("/".join(prefixes), start_level, end_level))
+          else:
+            events_list.insert(0, "%s%s - %s" % (prefix, start_level, end_level))
+
+      # truncates to use an ellipsis if too long, for instance:
+
+      attr_label = ", ".join(events_list)
+
+      if current_pattern:
+        attr_label += " - filter: %s" % current_pattern
+
+      attr_label = str_tools.crop(attr_label, width - 10, 1)
+
+      if attr_label:
+        attr_label = " (%s)" % attr_label
+
+      panel_label = "Events%s:" % attr_label
+
+    # cache results and return
+
+    self._title_cache = panel_label
+    self._title_args = (list(self.logged_events), current_pattern, width)
+    self.vals_lock.release()
+
+    return panel_label
+
+  def _trim_events(self, event_listing):
+    """
+    Crops events that have either:
+    - grown beyond the cache limit
+    - outlived the configured log duration
+
+    Argument:
+      event_listing - listing of log entries
+    """
+
+    cache_size = CONFIG["cache.log_panel.size"]
+
+    if len(event_listing) > cache_size:
+      del event_listing[cache_size:]
+
+    log_ttl = CONFIG["features.log.entryDuration"]
+
+    if log_ttl > 0:
+      current_day = days_since()
+
+      breakpoint = None  # index at which to crop from
+
+      for i in range(len(event_listing) - 1, -1, -1):
+        days_since_event = current_day - days_since(event_listing[i].timestamp)
+
+        if days_since_event > log_ttl:
+          breakpoint = i  # older than the ttl
+        else:
+          break
+
+      # removes entries older than the ttl
+
+      if breakpoint is not None:
+        del event_listing[breakpoint:]
diff --git a/seth/menu/__init__.py b/seth/menu/__init__.py
new file mode 100644
index 0000000..54644fe
--- /dev/null
+++ b/seth/menu/__init__.py
@@ -0,0 +1,9 @@
+"""
+Resources for displaying the menu.
+"""
+
+__all__ = [
+  'actions',
+  'item',
+  'menu',
+]
diff --git a/seth/menu/actions.py b/seth/menu/actions.py
new file mode 100644
index 0000000..acea413
--- /dev/null
+++ b/seth/menu/actions.py
@@ -0,0 +1,327 @@
+"""
+Generates the menu for seth, binding options with their related actions.
+"""
+
+import functools
+
+import seth.popups
+import seth.controller
+import seth.menu.item
+import seth.graph_panel
+import seth.util.tracker
+
+from seth.util import tor_controller, ui_tools
+
+import stem
+import stem.util.connection
+
+from stem.util import conf, str_tools
+
+CONFIG = conf.config_dict('seth', {
+  'features.log.showDuplicateEntries': False,
+})
+
+
+def make_menu():
+  """
+  Constructs the base menu and all of its contents.
+  """
+
+  base_menu = seth.menu.item.Submenu("")
+  base_menu.add(make_actions_menu())
+  base_menu.add(make_view_menu())
+
+  control = seth.controller.get_controller()
+
+  for page_panel in control.get_display_panels(include_sticky = False):
+    if page_panel.get_name() == "graph":
+      base_menu.add(make_graph_menu(page_panel))
+    elif page_panel.get_name() == "log":
+      base_menu.add(make_log_menu(page_panel))
+    elif page_panel.get_name() == "connections":
+      base_menu.add(make_connections_menu(page_panel))
+    elif page_panel.get_name() == "configuration":
+      base_menu.add(make_configuration_menu(page_panel))
+    elif page_panel.get_name() == "torrc":
+      base_menu.add(make_torrc_menu(page_panel))
+
+  base_menu.add(make_help_menu())
+
+  return base_menu
+
+
+def make_actions_menu():
+  """
+  Submenu consisting of...
+    Close Menu
+    New Identity
+    Pause / Unpause
+    Reset Tor
+    Exit
+  """
+
+  control = seth.controller.get_controller()
+  controller = tor_controller()
+  header_panel = control.get_panel("header")
+  actions_menu = seth.menu.item.Submenu("Actions")
+  actions_menu.add(seth.menu.item.MenuItem("Close Menu", None))
+  actions_menu.add(seth.menu.item.MenuItem("New Identity", header_panel.send_newnym))
+
+  if controller.is_alive():
+    actions_menu.add(seth.menu.item.MenuItem("Stop Tor", controller.close))
+
+  actions_menu.add(seth.menu.item.MenuItem("Reset Tor", functools.partial(controller.signal, stem.Signal.RELOAD)))
+
+  if control.is_paused():
+    label, arg = "Unpause", False
+  else:
+    label, arg = "Pause", True
+
+  actions_menu.add(seth.menu.item.MenuItem(label, functools.partial(control.set_paused, arg)))
+  actions_menu.add(seth.menu.item.MenuItem("Exit", control.quit))
+
+  return actions_menu
+
+
+def make_view_menu():
+  """
+  Submenu consisting of...
+    [X] <Page 1>
+    [ ] <Page 2>
+    [ ] etc...
+        Color (Submenu)
+  """
+
+  view_menu = seth.menu.item.Submenu("View")
+  control = seth.controller.get_controller()
+
+  if control.get_page_count() > 0:
+    page_group = seth.menu.item.SelectionGroup(control.set_page, control.get_page())
+
+    for i in range(control.get_page_count()):
+      page_panels = control.get_display_panels(page_number = i, include_sticky = False)
+      label = " / ".join([str_tools._to_camel_case(panel.get_name()) for panel in page_panels])
+
+      view_menu.add(seth.menu.item.SelectionMenuItem(label, page_group, i))
+
+  if ui_tools.is_color_supported():
+    color_menu = seth.menu.item.Submenu("Color")
+    color_group = seth.menu.item.SelectionGroup(ui_tools.set_color_override, ui_tools.get_color_override())
+
+    color_menu.add(seth.menu.item.SelectionMenuItem("All", color_group, None))
+
+    for color in ui_tools.COLOR_LIST:
+      color_menu.add(seth.menu.item.SelectionMenuItem(str_tools._to_camel_case(color), color_group, color))
+
+    view_menu.add(color_menu)
+
+  return view_menu
+
+
+def make_help_menu():
+  """
+  Submenu consisting of...
+    Hotkeys
+    About
+  """
+
+  help_menu = seth.menu.item.Submenu("Help")
+  help_menu.add(seth.menu.item.MenuItem("Hotkeys", seth.popups.show_help_popup))
+  help_menu.add(seth.menu.item.MenuItem("About", seth.popups.show_about_popup))
+  return help_menu
+
+
+def make_graph_menu(graph_panel):
+  """
+  Submenu for the graph panel, consisting of...
+    [X] <Stat 1>
+    [ ] <Stat 2>
+    [ ] <Stat 2>
+        Resize...
+        Interval (Submenu)
+        Bounds (Submenu)
+
+  Arguments:
+    graph_panel - instance of the graph panel
+  """
+
+  graph_menu = seth.menu.item.Submenu("Graph")
+
+  # stats options
+
+  stat_group = seth.menu.item.SelectionGroup(functools.partial(setattr, graph_panel, 'displayed_stat'), graph_panel.displayed_stat)
+  available_stats = graph_panel.stat_options()
+  available_stats.sort()
+
+  for stat_key in ["None"] + available_stats:
+    label = str_tools._to_camel_case(stat_key, divider = " ")
+    stat_key = None if stat_key == "None" else stat_key
+    graph_menu.add(seth.menu.item.SelectionMenuItem(label, stat_group, stat_key))
+
+  # resizing option
+
+  graph_menu.add(seth.menu.item.MenuItem("Resize...", graph_panel.resize_graph))
+
+  # interval submenu
+
+  interval_menu = seth.menu.item.Submenu("Interval")
+  interval_group = seth.menu.item.SelectionGroup(functools.partial(setattr, graph_panel, 'update_interval'), graph_panel.update_interval)
+
+  for interval in seth.graph_panel.Interval:
+    interval_menu.add(seth.menu.item.SelectionMenuItem(interval, interval_group, interval))
+
+  graph_menu.add(interval_menu)
+
+  # bounds submenu
+
+  bounds_menu = seth.menu.item.Submenu("Bounds")
+  bounds_group = seth.menu.item.SelectionGroup(functools.partial(setattr, graph_panel, 'bounds_type'), graph_panel.bounds_type)
+
+  for bounds_type in seth.graph_panel.Bounds:
+    bounds_menu.add(seth.menu.item.SelectionMenuItem(bounds_type, bounds_group, bounds_type))
+
+  graph_menu.add(bounds_menu)
+
+  return graph_menu
+
+
+def make_log_menu(log_panel):
+  """
+  Submenu for the log panel, consisting of...
+    Events...
+    Snapshot...
+    Clear
+    Show / Hide Duplicates
+    Filter (Submenu)
+
+  Arguments:
+    log_panel - instance of the log panel
+  """
+
+  log_menu = seth.menu.item.Submenu("Log")
+
+  log_menu.add(seth.menu.item.MenuItem("Events...", log_panel.show_event_selection_prompt))
+  log_menu.add(seth.menu.item.MenuItem("Snapshot...", log_panel.show_snapshot_prompt))
+  log_menu.add(seth.menu.item.MenuItem("Clear", log_panel.clear))
+
+  if CONFIG["features.log.showDuplicateEntries"]:
+    label, arg = "Hide", False
+  else:
+    label, arg = "Show", True
+
+  log_menu.add(seth.menu.item.MenuItem("%s Duplicates" % label, functools.partial(log_panel.set_duplicate_visability, arg)))
+
+  # filter submenu
+
+  filter_menu = seth.menu.item.Submenu("Filter")
+  filter_group = seth.menu.item.SelectionGroup(log_panel.make_filter_selection, log_panel.get_filter())
+
+  filter_menu.add(seth.menu.item.SelectionMenuItem("None", filter_group, None))
+
+  for option in log_panel.filter_options:
+    filter_menu.add(seth.menu.item.SelectionMenuItem(option, filter_group, option))
+
+  filter_menu.add(seth.menu.item.MenuItem("New...", log_panel.show_filter_prompt))
+  log_menu.add(filter_menu)
+
+  return log_menu
+
+
+def make_connections_menu(conn_panel):
+  """
+  Submenu for the connections panel, consisting of...
+    [X] IP Address
+    [ ] Fingerprint
+    [ ] Nickname
+        Sorting...
+        Resolver (Submenu)
+
+  Arguments:
+    conn_panel - instance of the connections panel
+  """
+
+  connections_menu = seth.menu.item.Submenu("Connections")
+
+  # listing options
+
+  listing_group = seth.menu.item.SelectionGroup(conn_panel.set_listing_type, conn_panel.get_listing_type())
+
+  listing_options = list(seth.connections.entries.ListingType)
+  listing_options.remove(seth.connections.entries.ListingType.HOSTNAME)
+
+  for option in listing_options:
+    connections_menu.add(seth.menu.item.SelectionMenuItem(option, listing_group, option))
+
+  # sorting option
+
+  connections_menu.add(seth.menu.item.MenuItem("Sorting...", conn_panel.show_sort_dialog))
+
+  # resolver submenu
+
+  conn_resolver = seth.util.tracker.get_connection_tracker()
+  resolver_menu = seth.menu.item.Submenu("Resolver")
+  resolver_group = seth.menu.item.SelectionGroup(conn_resolver.set_custom_resolver, conn_resolver.get_custom_resolver())
+
+  resolver_menu.add(seth.menu.item.SelectionMenuItem("auto", resolver_group, None))
+
+  for option in stem.util.connection.Resolver:
+    resolver_menu.add(seth.menu.item.SelectionMenuItem(option, resolver_group, option))
+
+  connections_menu.add(resolver_menu)
+
+  return connections_menu
+
+
+def make_configuration_menu(config_panel):
+  """
+  Submenu for the configuration panel, consisting of...
+    Save Config...
+    Sorting...
+    Filter / Unfilter Options
+
+  Arguments:
+    config_panel - instance of the configuration panel
+  """
+
+  config_menu = seth.menu.item.Submenu("Configuration")
+  config_menu.add(seth.menu.item.MenuItem("Save Config...", config_panel.show_write_dialog))
+  config_menu.add(seth.menu.item.MenuItem("Sorting...", config_panel.show_sort_dialog))
+
+  if config_panel.show_all:
+    label, arg = "Filter", True
+  else:
+    label, arg = "Unfilter", False
+
+  config_menu.add(seth.menu.item.MenuItem("%s Options" % label, functools.partial(config_panel.set_filtering, arg)))
+
+  return config_menu
+
+
+def make_torrc_menu(torrc_panel):
+  """
+  Submenu for the torrc panel, consisting of...
+    Reload
+    Show / Hide Comments
+    Show / Hide Line Numbers
+
+  Arguments:
+    torrc_panel - instance of the torrc panel
+  """
+
+  torrc_menu = seth.menu.item.Submenu("Torrc")
+  torrc_menu.add(seth.menu.item.MenuItem("Reload", torrc_panel.reload_torrc))
+
+  if torrc_panel.strip_comments:
+    label, arg = "Show", True
+  else:
+    label, arg = "Hide", False
+
+  torrc_menu.add(seth.menu.item.MenuItem("%s Comments" % label, functools.partial(torrc_panel.set_comments_visible, arg)))
+
+  if torrc_panel.show_line_num:
+    label, arg = "Hide", False
+  else:
+    label, arg = "Show", True
+  torrc_menu.add(seth.menu.item.MenuItem("%s Line Numbers" % label, functools.partial(torrc_panel.set_line_number_visible, arg)))
+
+  return torrc_menu
diff --git a/seth/menu/item.py b/seth/menu/item.py
new file mode 100644
index 0000000..bc1ba58
--- /dev/null
+++ b/seth/menu/item.py
@@ -0,0 +1,207 @@
+"""
+Menu item, representing an option in the drop-down menu.
+"""
+
+import seth.controller
+
+
+class MenuItem():
+  """
+  Option in a drop-down menu.
+  """
+
+  def __init__(self, label, callback):
+    self._label = label
+    self._callback = callback
+    self._parent = None
+
+  def get_label(self):
+    """
+    Provides a tuple of three strings representing the prefix, label, and
+    suffix for this item.
+    """
+
+    return ("", self._label, "")
+
+  def get_parent(self):
+    """
+    Provides the Submenu we're contained within.
+    """
+
+    return self._parent
+
+  def get_hierarchy(self):
+    """
+    Provides a list with all of our parents, up to the root.
+    """
+
+    my_hierarchy = [self]
+    while my_hierarchy[-1].get_parent():
+      my_hierarchy.append(my_hierarchy[-1].get_parent())
+
+    my_hierarchy.reverse()
+    return my_hierarchy
+
+  def get_root(self):
+    """
+    Provides the base submenu we belong to.
+    """
+
+    if self._parent:
+      return self._parent.get_root()
+    else:
+      return self
+
+  def select(self):
+    """
+    Performs the callback for the menu item, returning true if we should close
+    the menu and false otherwise.
+    """
+
+    if self._callback:
+      control = seth.controller.get_controller()
+      control.set_msg()
+      control.redraw()
+      self._callback()
+    return True
+
+  def next(self):
+    """
+    Provides the next option for the submenu we're in, raising a ValueError
+    if we don't have a parent.
+    """
+
+    return self._get_sibling(1)
+
+  def prev(self):
+    """
+    Provides the previous option for the submenu we're in, raising a ValueError
+    if we don't have a parent.
+    """
+
+    return self._get_sibling(-1)
+
+  def _get_sibling(self, offset):
+    """
+    Provides our sibling with a given index offset from us, raising a
+    ValueError if we don't have a parent.
+
+    Arguments:
+      offset - index offset for the sibling to be returned
+    """
+
+    if self._parent:
+      my_siblings = self._parent.get_children()
+
+      try:
+        my_index = my_siblings.index(self)
+        return my_siblings[(my_index + offset) % len(my_siblings)]
+      except ValueError:
+        # We expect a bidirectional references between submenus and their
+        # children. If we don't have this then our menu's screwed up.
+
+        msg = "The '%s' submenu doesn't contain '%s' (children: '%s')" % (self, self._parent, "', '".join(my_siblings))
+        raise ValueError(msg)
+    else:
+      raise ValueError("Menu option '%s' doesn't have a parent" % self)
+
+  def __str__(self):
+    return self._label
+
+
+class Submenu(MenuItem):
+  """
+  Menu item that lists other menu options.
+  """
+
+  def __init__(self, label):
+    MenuItem.__init__(self, label, None)
+    self._children = []
+
+  def get_label(self):
+    """
+    Provides our label with a ">" suffix to indicate that we have suboptions.
+    """
+
+    my_label = MenuItem.get_label(self)[1]
+    return ("", my_label, " >")
+
+  def add(self, menu_item):
+    """
+    Adds the given menu item to our listing. This raises a ValueError if the
+    item already has a parent.
+
+    Arguments:
+      menu_item - menu option to be added
+    """
+
+    if menu_item.get_parent():
+      raise ValueError("Menu option '%s' already has a parent" % menu_item)
+    else:
+      menu_item._parent = self
+      self._children.append(menu_item)
+
+  def get_children(self):
+    """
+    Provides the menu and submenus we contain.
+    """
+
+    return list(self._children)
+
+  def is_empty(self):
+    """
+    True if we have no children, false otherwise.
+    """
+
+    return not bool(self._children)
+
+  def select(self):
+    return False
+
+
+class SelectionGroup():
+  """
+  Radio button groups that SelectionMenuItems can belong to.
+  """
+
+  def __init__(self, action, selected_arg):
+    self.action = action
+    self.selected_arg = selected_arg
+
+
+class SelectionMenuItem(MenuItem):
+  """
+  Menu item with an associated group which determines the selection. This is
+  for the common single argument getter/setter pattern.
+  """
+
+  def __init__(self, label, group, arg):
+    MenuItem.__init__(self, label, None)
+    self._group = group
+    self._arg = arg
+
+  def is_selected(self):
+    """
+    True if we're the selected item, false otherwise.
+    """
+
+    return self._arg == self._group.selected_arg
+
+  def get_label(self):
+    """
+    Provides our label with a "[X]" prefix if selected and "[ ]" if not.
+    """
+
+    my_label = MenuItem.get_label(self)[1]
+    my_prefix = "[X] " if self.is_selected() else "[ ] "
+    return (my_prefix, my_label, "")
+
+  def select(self):
+    """
+    Performs the group's setter action with our argument.
+    """
+
+    if not self.is_selected():
+      self._group.action(self._arg)
+
+    return True
diff --git a/seth/menu/menu.py b/seth/menu/menu.py
new file mode 100644
index 0000000..02dcfb0
--- /dev/null
+++ b/seth/menu/menu.py
@@ -0,0 +1,192 @@
+"""
+Display logic for presenting the menu.
+"""
+
+import curses
+
+import seth.popups
+import seth.controller
+import seth.menu.item
+import seth.menu.actions
+
+from seth.util import ui_tools
+
+
+class MenuCursor:
+  """
+  Tracks selection and key handling in the menu.
+  """
+
+  def __init__(self, initial_selection):
+    self._selection = initial_selection
+    self._is_done = False
+
+  def is_done(self):
+    """
+    Provides true if a selection has indicated that we should close the menu.
+    False otherwise.
+    """
+
+    return self._is_done
+
+  def get_selection(self):
+    """
+    Provides the currently selected menu item.
+    """
+
+    return self._selection
+
+  def handle_key(self, key):
+    is_selection_submenu = isinstance(self._selection, seth.menu.item.Submenu)
+    selection_hierarchy = self._selection.get_hierarchy()
+
+    if key.is_selection():
+      if is_selection_submenu:
+        if not self._selection.is_empty():
+          self._selection = self._selection.get_children()[0]
+      else:
+        self._is_done = self._selection.select()
+    elif key.match('up'):
+      self._selection = self._selection.prev()
+    elif key.match('down'):
+      self._selection = self._selection.next()
+    elif key.match('left'):
+      if len(selection_hierarchy) <= 3:
+        # shift to the previous main submenu
+
+        prev_submenu = selection_hierarchy[1].prev()
+        self._selection = prev_submenu.get_children()[0]
+      else:
+        # go up a submenu level
+
+        self._selection = self._selection.get_parent()
+    elif key.match('right'):
+      if is_selection_submenu:
+        # open submenu (same as making a selection)
+
+        if not self._selection.is_empty():
+          self._selection = self._selection.get_children()[0]
+      else:
+        # shift to the next main submenu
+
+        next_submenu = selection_hierarchy[1].next()
+        self._selection = next_submenu.get_children()[0]
+    elif key.match('esc', 'm'):
+      self._is_done = True
+
+
+def show_menu():
+  popup, _, _ = seth.popups.init(1, below_static = False)
+
+  if not popup:
+    return
+
+  control = seth.controller.get_controller()
+
+  try:
+    # generates the menu and uses the initial selection of the first item in
+    # the file menu
+
+    menu = seth.menu.actions.make_menu()
+    cursor = MenuCursor(menu.get_children()[0].get_children()[0])
+
+    while not cursor.is_done():
+      # sets the background color
+
+      popup.win.clear()
+      popup.win.bkgd(' ', curses.A_STANDOUT | ui_tools.get_color("red"))
+      selection_hierarchy = cursor.get_selection().get_hierarchy()
+
+      # provide a message saying how to close the menu
+
+      control.set_msg("Press m or esc to close the menu.", curses.A_BOLD, True)
+
+      # renders the menu bar, noting where the open submenu is positioned
+
+      draw_left, selection_left = 0, 0
+
+      for top_level_item in menu.get_children():
+        draw_format = curses.A_BOLD
+
+        if top_level_item == selection_hierarchy[1]:
+          draw_format |= curses.A_UNDERLINE
+          selection_left = draw_left
+
+        draw_label = " %s " % top_level_item.get_label()[1]
+        popup.addstr(0, draw_left, draw_label, draw_format)
+        popup.addch(0, draw_left + len(draw_label), curses.ACS_VLINE)
+
+        draw_left += len(draw_label) + 1
+
+      # recursively shows opened submenus
+
+      _draw_submenu(cursor, 1, 1, selection_left)
+
+      popup.win.refresh()
+
+      curses.cbreak()
+      cursor.handle_key(control.key_input())
+
+      # redraws the rest of the interface if we're rendering on it again
+
+      if not cursor.is_done():
+        control.redraw()
+  finally:
+    control.set_msg()
+    seth.popups.finalize()
+
+
+def _draw_submenu(cursor, level, top, left):
+  selection_hierarchy = cursor.get_selection().get_hierarchy()
+
+  # checks if there's nothing to display
+
+  if len(selection_hierarchy) < level + 2:
+    return
+
+  # fetches the submenu and selection we're displaying
+
+  submenu = selection_hierarchy[level]
+  selection = selection_hierarchy[level + 1]
+
+  # gets the size of the prefix, middle, and suffix columns
+
+  all_label_sets = [entry.get_label() for entry in submenu.get_children()]
+  prefix_col_size = max([len(entry[0]) for entry in all_label_sets])
+  middle_col_size = max([len(entry[1]) for entry in all_label_sets])
+  suffix_col_size = max([len(entry[2]) for entry in all_label_sets])
+
+  # formatted string so we can display aligned menu entries
+
+  label_format = " %%-%is%%-%is%%-%is " % (prefix_col_size, middle_col_size, suffix_col_size)
+  menu_width = len(label_format % ("", "", ""))
+
+  popup, _, _ = seth.popups.init(len(submenu.get_children()), menu_width, top, left, below_static = False)
+
+  if not popup:
+    return
+
+  try:
+    # sets the background color
+
+    popup.win.bkgd(' ', curses.A_STANDOUT | ui_tools.get_color("red"))
+
+    draw_top, selection_top = 0, 0
+
+    for menu_item in submenu.get_children():
+      if menu_item == selection:
+        draw_format = (curses.A_BOLD, 'white')
+        selection_top = draw_top
+      else:
+        draw_format = (curses.A_NORMAL,)
+
+      popup.addstr(draw_top, 0, label_format % menu_item.get_label(), *draw_format)
+      draw_top += 1
+
+    popup.win.refresh()
+
+    # shows the next submenu
+
+    _draw_submenu(cursor, level + 1, top + selection_top, left + menu_width)
+  finally:
+    seth.popups.finalize()
diff --git a/seth/popups.py b/seth/popups.py
new file mode 100644
index 0000000..15b18b3
--- /dev/null
+++ b/seth/popups.py
@@ -0,0 +1,392 @@
+"""
+Functions for displaying popups in the interface.
+"""
+
+import curses
+
+import seth.controller
+
+from seth import __version__, __release_date__
+from seth.util import panel, ui_tools
+
+
+def init(height = -1, width = -1, top = 0, left = 0, below_static = True):
+  """
+  Preparation for displaying a popup. This creates a popup with a valid
+  subwindow instance. If that's successful then the curses lock is acquired
+  and this returns a tuple of the...
+  (popup, draw width, draw height)
+  Otherwise this leaves curses unlocked and returns None.
+
+  Arguments:
+    height      - maximum height of the popup
+    width       - maximum width of the popup
+    top         - top position, relative to the sticky content
+    left        - left position from the screen
+    below_static - positions popup below static content if true
+  """
+
+  control = seth.controller.get_controller()
+
+  if below_static:
+    sticky_height = sum([sticky_panel.get_height() for sticky_panel in control.get_sticky_panels()])
+  else:
+    sticky_height = 0
+
+  popup = panel.Panel(control.get_screen(), "popup", top + sticky_height, left, height, width)
+  popup.set_visible(True)
+
+  # Redraws the popup to prepare a subwindow instance. If none is spawned then
+  # the panel can't be drawn (for instance, due to not being visible).
+
+  popup.redraw(True)
+
+  if popup.win is not None:
+    panel.CURSES_LOCK.acquire()
+    return (popup, popup.max_x - 1, popup.max_y)
+  else:
+    return (None, 0, 0)
+
+
+def finalize():
+  """
+  Cleans up after displaying a popup, releasing the cureses lock and redrawing
+  the rest of the display.
+  """
+
+  seth.controller.get_controller().request_redraw()
+  panel.CURSES_LOCK.release()
+
+
+def input_prompt(msg, initial_value = ""):
+  """
+  Prompts the user to enter a string on the control line (which usually
+  displays the page number and basic controls).
+
+  Arguments:
+    msg          - message to prompt the user for input with
+    initial_value - initial value of the field
+  """
+
+  panel.CURSES_LOCK.acquire()
+  control = seth.controller.get_controller()
+  msg_panel = control.get_panel("msg")
+  msg_panel.set_message(msg)
+  msg_panel.redraw(True)
+  user_input = msg_panel.getstr(0, len(msg), initial_value)
+  control.set_msg()
+  panel.CURSES_LOCK.release()
+
+  return user_input
+
+
+def show_msg(msg, max_wait = -1, attr = curses.A_STANDOUT):
+  """
+  Displays a single line message on the control line for a set time. Pressing
+  any key will end the message. This returns the key pressed.
+
+  Arguments:
+    msg     - message to be displayed to the user
+    max_wait - time to show the message, indefinite if -1
+    attr    - attributes with which to draw the message
+  """
+
+  with panel.CURSES_LOCK:
+    control = seth.controller.get_controller()
+    control.set_msg(msg, attr, True)
+
+    if max_wait == -1:
+      curses.cbreak()
+    else:
+      curses.halfdelay(max_wait * 10)
+
+    key_press = control.key_input()
+    control.set_msg()
+    return key_press
+
+
+def show_help_popup():
+  """
+  Presents a popup with instructions for the current page's hotkeys. This
+  returns the user input used to close the popup. If the popup didn't close
+  properly, this is an arrow, enter, or scroll key then this returns None.
+  """
+
+  popup, _, height = init(9, 80)
+
+  if not popup:
+    return
+
+  exit_key = None
+
+  try:
+    control = seth.controller.get_controller()
+    page_panels = control.get_display_panels()
+
+    # the first page is the only one with multiple panels, and it looks better
+    # with the log entries first, so reversing the order
+
+    page_panels.reverse()
+
+    help_options = []
+
+    for entry in page_panels:
+      help_options += entry.get_help()
+
+    # test doing afterward in case of overwriting
+
+    popup.win.box()
+    popup.addstr(0, 0, "Page %i Commands:" % (control.get_page() + 1), curses.A_STANDOUT)
+
+    for i in range(len(help_options)):
+      if i / 2 >= height - 2:
+        break
+
+      # draws entries in the form '<key>: <description>[ (<selection>)]', for
+      # instance...
+      # u: duplicate log entries (hidden)
+
+      key, description, selection = help_options[i]
+
+      if key:
+        description = ": " + description
+
+      row = (i / 2) + 1
+      col = 2 if i % 2 == 0 else 41
+
+      popup.addstr(row, col, key, curses.A_BOLD)
+      col += len(key)
+      popup.addstr(row, col, description)
+      col += len(description)
+
+      if selection:
+        popup.addstr(row, col, " (")
+        popup.addstr(row, col + 2, selection, curses.A_BOLD)
+        popup.addstr(row, col + 2 + len(selection), ")")
+
+    # tells user to press a key if the lower left is unoccupied
+
+    if len(help_options) < 13 and height == 9:
+      popup.addstr(7, 2, "Press any key...")
+
+    popup.win.refresh()
+    curses.cbreak()
+    exit_key = control.key_input()
+  finally:
+    finalize()
+
+  if not exit_key.is_selection() and not exit_key.is_scroll() and \
+    not exit_key.match('left', 'right'):
+    return exit_key
+  else:
+    return None
+
+
+def show_about_popup():
+  """
+  Presents a popup with author and version information.
+  """
+
+  popup, _, height = init(9, 80)
+
+  if not popup:
+    return
+
+  try:
+    control = seth.controller.get_controller()
+
+    popup.win.box()
+    popup.addstr(0, 0, "About:", curses.A_STANDOUT)
+    popup.addstr(1, 2, "seth, version %s (released %s)" % (__version__, __release_date__), curses.A_BOLD)
+    popup.addstr(2, 4, "Written by Damian Johnson (atagar at torproject.org)")
+    popup.addstr(3, 4, "Project page: www.atagar.com/seth")
+    popup.addstr(5, 2, "Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)")
+    popup.addstr(7, 2, "Press any key...")
+    popup.win.refresh()
+
+    curses.cbreak()
+    control.key_input()
+  finally:
+    finalize()
+
+
+def show_sort_dialog(title, options, old_selection, option_colors):
+  """
+  Displays a sorting dialog of the form:
+
+    Current Order: <previous selection>
+    New Order: <selections made>
+
+    <option 1>    <option 2>    <option 3>   Cancel
+
+  Options are colored when among the "Current Order" or "New Order", but not
+  when an option below them. If cancel is selected or the user presses escape
+  then this returns None. Otherwise, the new ordering is provided.
+
+  Arguments:
+    title   - title displayed for the popup window
+    options      - ordered listing of option labels
+    old_selection - current ordering
+    option_colors - mappings of options to their color
+  """
+
+  popup, _, _ = init(9, 80)
+
+  if not popup:
+    return
+
+  new_selections = []  # new ordering
+
+  try:
+    cursor_location = 0     # index of highlighted option
+    curses.cbreak()         # wait indefinitely for key presses (no timeout)
+
+    selection_options = list(options)
+    selection_options.append("Cancel")
+
+    while len(new_selections) < len(old_selection):
+      popup.win.erase()
+      popup.win.box()
+      popup.addstr(0, 0, title, curses.A_STANDOUT)
+
+      _draw_sort_selection(popup, 1, 2, "Current Order: ", old_selection, option_colors)
+      _draw_sort_selection(popup, 2, 2, "New Order: ", new_selections, option_colors)
+
+      # presents remaining options, each row having up to four options with
+      # spacing of nineteen cells
+
+      row, col = 4, 0
+
+      for i in range(len(selection_options)):
+        option_format = curses.A_STANDOUT if cursor_location == i else curses.A_NORMAL
+        popup.addstr(row, col * 19 + 2, selection_options[i], option_format)
+        col += 1
+
+        if col == 4:
+          row, col = row + 1, 0
+
+      popup.win.refresh()
+
+      key = seth.controller.get_controller().key_input()
+
+      if key.match('left'):
+        cursor_location = max(0, cursor_location - 1)
+      elif key.match('right'):
+        cursor_location = min(len(selection_options) - 1, cursor_location + 1)
+      elif key.match('up'):
+        cursor_location = max(0, cursor_location - 4)
+      elif key.match('down'):
+        cursor_location = min(len(selection_options) - 1, cursor_location + 4)
+      elif key.is_selection():
+        selection = selection_options[cursor_location]
+
+        if selection == "Cancel":
+          break
+        else:
+          new_selections.append(selection)
+          selection_options.remove(selection)
+          cursor_location = min(cursor_location, len(selection_options) - 1)
+      elif key == 27:
+        break  # esc - cancel
+  finally:
+    finalize()
+
+  if len(new_selections) == len(old_selection):
+    return new_selections
+  else:
+    return None
+
+
+def _draw_sort_selection(popup, y, x, prefix, options, option_colors):
+  """
+  Draws a series of comma separated sort selections. The whole line is bold
+  and sort options also have their specified color. Example:
+
+    Current Order: Man Page Entry, Option Name, Is Default
+
+  Arguments:
+    popup        - panel in which to draw sort selection
+    y            - vertical location
+    x            - horizontal location
+    prefix       - initial string description
+    options      - sort options to be shown
+    option_colors - mappings of options to their color
+  """
+
+  popup.addstr(y, x, prefix, curses.A_BOLD)
+  x += len(prefix)
+
+  for i in range(len(options)):
+    sort_type = options[i]
+    sort_color = ui_tools.get_color(option_colors.get(sort_type, "white"))
+    popup.addstr(y, x, sort_type, sort_color | curses.A_BOLD)
+    x += len(sort_type)
+
+    # comma divider between options, if this isn't the last
+
+    if i < len(options) - 1:
+      popup.addstr(y, x, ", ", curses.A_BOLD)
+      x += 2
+
+
+def show_menu(title, options, old_selection):
+  """
+  Provides menu with options laid out in a single column. User can cancel
+  selection with the escape key, in which case this proives -1. Otherwise this
+  returns the index of the selection.
+
+  Arguments:
+    title        - title displayed for the popup window
+    options      - ordered listing of options to display
+    old_selection - index of the initially selected option (uses the first
+                   selection without a carrot if -1)
+  """
+
+  max_width = max(map(len, options)) + 9
+  popup, _, _ = init(len(options) + 2, max_width)
+
+  if not popup:
+    return
+
+  selection = old_selection if old_selection != -1 else 0
+
+  try:
+    # hides the title of the first panel on the page
+
+    control = seth.controller.get_controller()
+    top_panel = control.get_display_panels(include_sticky = False)[0]
+    top_panel.set_title_visible(False)
+    top_panel.redraw(True)
+
+    curses.cbreak()   # wait indefinitely for key presses (no timeout)
+
+    while True:
+      popup.win.erase()
+      popup.win.box()
+      popup.addstr(0, 0, title, curses.A_STANDOUT)
+
+      for i in range(len(options)):
+        label = options[i]
+        format = curses.A_STANDOUT if i == selection else curses.A_NORMAL
+        tab = "> " if i == old_selection else "  "
+        popup.addstr(i + 1, 2, tab)
+        popup.addstr(i + 1, 4, " %s " % label, format)
+
+      popup.win.refresh()
+
+      key = control.key_input()
+
+      if key.match('up'):
+        selection = max(0, selection - 1)
+      elif key.match('down'):
+        selection = min(len(options) - 1, selection + 1)
+      elif key.is_selection():
+        break
+      elif key.match('esc'):
+        selection = -1
+        break
+  finally:
+    top_panel.set_title_visible(True)
+    finalize()
+
+  return selection
diff --git a/seth/resources/arm.1 b/seth/resources/arm.1
new file mode 100644
index 0000000..7ed7c5c
--- /dev/null
+++ b/seth/resources/arm.1
@@ -0,0 +1,69 @@
+.TH seth 1 "27 August 2010"
+.SH NAME
+seth - Terminal Tor status monitor
+
+.SH SYNOPSIS
+seth [\fIOPTION\fR]
+
+.SH DESCRIPTION
+The anonymizing relay monitor (seth) is a terminal status monitor for Tor
+relays, intended for command-line aficionados, ssh connections, and anyone
+stuck with a tty terminal. This works much like top does for system usage,
+providing real time statistics for:
+  * bandwidth, cpu, and memory usage
+  * relay's current configuration
+  * logged events
+  * connection details (ip, hostname, fingerprint, and consensus data)
+  * etc
+
+Defaults and interface properties are configurable via a user provided
+configuration file (for an example see the provided \fBsethrc.sample\fR).
+Releases and information are available at \fIhttp://www.atagar.com/seth\fR.
+
+.SH OPTIONS
+.TP
+\fB\-i\fR, \fB\-\-interface [ADDRESS:]PORT\fR
+tor control port seth should attach to (default is \fB127.0.0.1:9051\fR)
+
+.TP
+\fB\-c\fR, \fB\-\-config CONFIG_PATH\fR
+user provided configuration file (default is \fB~/.seth/sethrc\fR)
+
+.TP
+\fB\-d\fR, \fB\-\-debug\fR
+writes all seth logs to ~/.seth/log
+
+.TP
+\fB\-e\fR, \fB\-\-event EVENT_FLAGS\fR
+flags for tor, seth, and torctl events to be logged (default is \fBN3\fR)
+
+  d DEBUG      a ADDRMAP           k DESCCHANGED   s STREAM
+  i INFO       f AUTHDIR_NEWDESCS  g GUARD         r STREAM_BW
+  n NOTICE     h BUILDTIMEOUT_SET  l NEWCONSENSUS  t STATUS_CLIENT
+  w WARN       b BW                m NEWDESC       u STATUS_GENERAL
+  e ERR        c CIRC              p NS            v STATUS_SERVER
+               j CLIENTS_SEEN      q ORCONN
+    DINWE tor runlevel+            A All Events
+    12345 seth runlevel+            X No Events
+    67890 torctl runlevel+         U Unknown Events
+
+.TP
+\fB\-v\fR, \fB\-\-version\fR
+provides version information
+
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+provides usage information
+
+.SH FILES
+.TP
+\fB~/.seth/sethrc\fR
+Your personal seth configuration file
+
+.TP
+\fB/usr/share/doc/seth/sethrc.sample\fR
+Sample sethrc configuration file that documents all options
+
+.SH AUTHOR
+Written by Damian Johnson (atagar at torproject.org)
+
diff --git a/seth/resources/tor-arm.desktop b/seth/resources/tor-arm.desktop
new file mode 100644
index 0000000..a4d37ac
--- /dev/null
+++ b/seth/resources/tor-arm.desktop
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Name=Tor monitor
+Name[es]=Monitor de Tor
+Comment=Status monitor for Tor routers
+Comment[es]=Monitor de estado para routers Tor
+GenericName=Monitor
+GenericName[es]=Monitor
+Exec=seth -g
+Icon=tor-seth
+Terminal=false
+Type=Application
+Categories=System;Monitor;GTK;
diff --git a/seth/resources/tor-arm.svg b/seth/resources/tor-arm.svg
new file mode 100644
index 0000000..8e710ab
--- /dev/null
+++ b/seth/resources/tor-arm.svg
@@ -0,0 +1,1074 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.0"
+   width="128"
+   height="128"
+   id="svg2"
+   inkscape:version="0.48.1 r9760"
+   sodipodi:docname="utilities-system-monitor.svg">
+  <metadata
+     id="metadata261">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1024"
+     inkscape:window-height="550"
+     id="namedview259"
+     showgrid="false"
+     inkscape:zoom="2.3828125"
+     inkscape:cx="64"
+     inkscape:cy="63.692344"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient4199">
+      <stop
+         style="stop-color:white;stop-opacity:1"
+         offset="0"
+         id="stop4201" />
+      <stop
+         style="stop-color:white;stop-opacity:0"
+         offset="1"
+         id="stop4203" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4167">
+      <stop
+         style="stop-color:#171717;stop-opacity:1"
+         offset="0"
+         id="stop4169" />
+      <stop
+         style="stop-color:#777;stop-opacity:1"
+         offset="1"
+         id="stop4171" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4159">
+      <stop
+         style="stop-color:white;stop-opacity:1"
+         offset="0"
+         id="stop4161" />
+      <stop
+         style="stop-color:white;stop-opacity:0"
+         offset="1"
+         id="stop4163" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4142">
+      <stop
+         style="stop-color:#e5ff00;stop-opacity:1"
+         offset="0"
+         id="stop4144" />
+      <stop
+         style="stop-color:#e5ff00;stop-opacity:0"
+         offset="1"
+         id="stop4146" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3399">
+      <stop
+         style="stop-color:yellow;stop-opacity:1"
+         offset="0"
+         id="stop3401" />
+      <stop
+         style="stop-color:yellow;stop-opacity:0"
+         offset="1"
+         id="stop3403" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3391">
+      <stop
+         style="stop-color:#ffff1d;stop-opacity:1"
+         offset="0"
+         id="stop3393" />
+      <stop
+         style="stop-color:#ffff6f;stop-opacity:0"
+         offset="1"
+         id="stop3395" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3383">
+      <stop
+         style="stop-color:yellow;stop-opacity:1"
+         offset="0"
+         id="stop3385" />
+      <stop
+         style="stop-color:yellow;stop-opacity:0"
+         offset="1"
+         id="stop3387" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4111">
+      <stop
+         style="stop-color:black;stop-opacity:1"
+         offset="0"
+         id="stop4113" />
+      <stop
+         style="stop-color:black;stop-opacity:0"
+         offset="1"
+         id="stop4115" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4031">
+      <stop
+         style="stop-color:#292929;stop-opacity:1"
+         offset="0"
+         id="stop4033" />
+      <stop
+         style="stop-color:#e9e9e9;stop-opacity:1"
+         offset="1"
+         id="stop4035" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient4002">
+      <stop
+         style="stop-color:lime;stop-opacity:1"
+         offset="0"
+         id="stop4004" />
+      <stop
+         style="stop-color:#f0ff80;stop-opacity:0"
+         offset="1"
+         id="stop4006" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3785">
+      <stop
+         style="stop-color:black;stop-opacity:1"
+         offset="0"
+         id="stop3787" />
+      <stop
+         style="stop-color:black;stop-opacity:0"
+         offset="1"
+         id="stop3789" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3761">
+      <stop
+         style="stop-color:#f6f6f6;stop-opacity:1"
+         offset="0"
+         id="stop3763" />
+      <stop
+         style="stop-color:#5a5a5a;stop-opacity:1"
+         offset="1"
+         id="stop3765" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3749">
+      <stop
+         style="stop-color:#181818;stop-opacity:1"
+         offset="0"
+         id="stop3751" />
+      <stop
+         style="stop-color:#ababab;stop-opacity:1"
+         offset="1"
+         id="stop3753" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3737">
+      <stop
+         style="stop-color:gray;stop-opacity:1"
+         offset="0"
+         id="stop3739" />
+      <stop
+         style="stop-color:#232323;stop-opacity:1"
+         offset="1"
+         id="stop3741" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3729">
+      <stop
+         style="stop-color:#ededed;stop-opacity:1"
+         offset="0"
+         id="stop3731" />
+      <stop
+         style="stop-color:#bcbcbc;stop-opacity:1"
+         offset="1"
+         id="stop3733" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3570">
+      <stop
+         style="stop-color:black;stop-opacity:1"
+         offset="0"
+         id="stop3572" />
+      <stop
+         style="stop-color:black;stop-opacity:0"
+         offset="1"
+         id="stop3574" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3470">
+      <stop
+         style="stop-color:#ddd;stop-opacity:1"
+         offset="0"
+         id="stop3472" />
+      <stop
+         style="stop-color:#fbfbfb;stop-opacity:1"
+         offset="1"
+         id="stop3474" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3452">
+      <stop
+         style="stop-color:#979797;stop-opacity:1"
+         offset="0"
+         id="stop3454" />
+      <stop
+         style="stop-color:#454545;stop-opacity:1"
+         offset="1"
+         id="stop3456" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3440">
+      <stop
+         style="stop-color:black;stop-opacity:1"
+         offset="0"
+         id="stop3442" />
+      <stop
+         style="stop-color:black;stop-opacity:0"
+         offset="1"
+         id="stop3444" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3384">
+      <stop
+         style="stop-color:black;stop-opacity:1"
+         offset="0"
+         id="stop3386" />
+      <stop
+         style="stop-color:black;stop-opacity:0"
+         offset="1"
+         id="stop3388" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3292">
+      <stop
+         style="stop-color:#5e5e5e;stop-opacity:1"
+         offset="0"
+         id="stop3294" />
+      <stop
+         style="stop-color:#292929;stop-opacity:1"
+         offset="1"
+         id="stop3296" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3275">
+      <stop
+         style="stop-color:#323232;stop-opacity:1"
+         offset="0"
+         id="stop3277" />
+      <stop
+         style="stop-color:#1a1a1a;stop-opacity:1"
+         offset="1"
+         id="stop3279" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3265">
+      <stop
+         style="stop-color:white;stop-opacity:1"
+         offset="0"
+         id="stop3267" />
+      <stop
+         style="stop-color:white;stop-opacity:0"
+         offset="1"
+         id="stop3269" />
+    </linearGradient>
+    <filter
+       id="filter3162">
+      <feGaussianBlur
+         id="feGaussianBlur3164"
+         stdDeviation="0.14753906"
+         inkscape:collect="always" />
+    </filter>
+    <filter
+       id="filter3193">
+      <feGaussianBlur
+         id="feGaussianBlur3195"
+         stdDeviation="0.12753906"
+         inkscape:collect="always" />
+    </filter>
+    <filter
+       id="filter3247"
+       height="1.60944"
+       y="-0.30472"
+       width="1.03826"
+       x="-0.019130022">
+      <feGaussianBlur
+         id="feGaussianBlur3249"
+         stdDeviation="0.89273437"
+         inkscape:collect="always" />
+    </filter>
+    <radialGradient
+       cx="64"
+       cy="7.1979251"
+       r="56"
+       fx="64"
+       fy="7.1979251"
+       id="radialGradient3271"
+       xlink:href="#linearGradient3265"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.236503,0,0,0.798045,-15.13621,10.25573)" />
+    <radialGradient
+       cx="56"
+       cy="65.961678"
+       r="44"
+       fx="56"
+       fy="64.752823"
+       id="radialGradient3281"
+       xlink:href="#linearGradient3292"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.206761,0,0,2.057714,-67.57862,-106.9325)" />
+    <radialGradient
+       cx="56"
+       cy="60"
+       r="44"
+       fx="56"
+       fy="99.821198"
+       id="radialGradient3287"
+       xlink:href="#linearGradient3275"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.497439,3.473066e-8,-3.238492e-8,1.3963,-27.85656,-45.05228)" />
+    <radialGradient
+       cx="56"
+       cy="60"
+       r="44"
+       fx="56"
+       fy="99.821198"
+       id="radialGradient3289"
+       xlink:href="#linearGradient3275"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.497439,3.473066e-8,-3.238492e-8,1.3963,-27.85656,-44.05228)" />
+    <clipPath
+       id="clipPath3361">
+      <rect
+         width="88"
+         height="72"
+         rx="5.0167508"
+         ry="5.0167508"
+         x="12"
+         y="24"
+         style="opacity:1;fill:url(#radialGradient3365);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect3363" />
+    </clipPath>
+    <radialGradient
+       cx="56"
+       cy="65.961678"
+       r="44"
+       fx="56"
+       fy="64.752823"
+       id="radialGradient3365"
+       xlink:href="#linearGradient3292"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.206761,0,0,2.057714,-67.57862,-106.9325)" />
+    <linearGradient
+       x1="52.513512"
+       y1="97"
+       x2="52.513512"
+       y2="74.244766"
+       id="linearGradient3390"
+       xlink:href="#linearGradient3384"
+       gradientUnits="userSpaceOnUse" />
+    <clipPath
+       id="clipPath3402">
+      <rect
+         width="88"
+         height="72"
+         rx="5.0167508"
+         ry="5.0167508"
+         x="12"
+         y="24"
+         style="opacity:1;fill:url(#radialGradient3406);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect3404" />
+    </clipPath>
+    <radialGradient
+       cx="56"
+       cy="65.961678"
+       r="44"
+       fx="56"
+       fy="64.752823"
+       id="radialGradient3406"
+       xlink:href="#linearGradient3292"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.206761,0,0,2.057714,-67.57862,-106.9325)" />
+    <filter
+       id="filter3424">
+      <feGaussianBlur
+         id="feGaussianBlur3426"
+         stdDeviation="0.23507812"
+         inkscape:collect="always" />
+    </filter>
+    <filter
+       id="filter3430">
+      <feGaussianBlur
+         id="feGaussianBlur3432"
+         stdDeviation="0.23507812"
+         inkscape:collect="always" />
+    </filter>
+    <linearGradient
+       x1="100"
+       y1="92.763115"
+       x2="100"
+       y2="60"
+       id="linearGradient3446"
+       xlink:href="#linearGradient3440"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="100"
+       y1="92.763115"
+       x2="100"
+       y2="60"
+       id="linearGradient3450"
+       xlink:href="#linearGradient3440"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(0,-120)" />
+    <radialGradient
+       cx="108.33566"
+       cy="25.487402"
+       r="4.171701"
+       fx="108.33566"
+       fy="25.487402"
+       id="radialGradient3458"
+       xlink:href="#linearGradient3452"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.432375,0,0,1.432375,-46.84166,-11.02012)" />
+    <linearGradient
+       x1="110.75722"
+       y1="32.559616"
+       x2="106.72433"
+       y2="24.216215"
+       id="linearGradient3476"
+       xlink:href="#linearGradient3470"
+       gradientUnits="userSpaceOnUse" />
+    <filter
+       id="filter3549"
+       height="1.348368"
+       y="-0.17418399"
+       width="1.1806649"
+       x="-0.090332433">
+      <feGaussianBlur
+         id="feGaussianBlur3551"
+         stdDeviation="0.099971814"
+         inkscape:collect="always" />
+    </filter>
+    <filter
+       id="filter3553"
+       height="1.2047423"
+       y="-0.10237114"
+       width="1.2103517"
+       x="-0.10517583">
+      <feGaussianBlur
+         id="feGaussianBlur3555"
+         stdDeviation="0.099971814"
+         inkscape:collect="always" />
+    </filter>
+    <filter
+       id="filter3557"
+       height="1.348368"
+       y="-0.17418399"
+       width="1.1806649"
+       x="-0.090332433">
+      <feGaussianBlur
+         id="feGaussianBlur3559"
+         stdDeviation="0.099971814"
+         inkscape:collect="always" />
+    </filter>
+    <filter
+       id="filter3561"
+       height="1.2047423"
+       y="-0.10237114"
+       width="1.2103517"
+       x="-0.10517583">
+      <feGaussianBlur
+         id="feGaussianBlur3563"
+         stdDeviation="0.099971814"
+         inkscape:collect="always" />
+    </filter>
+    <linearGradient
+       x1="111.58585"
+       y1="31.213261"
+       x2="116.79939"
+       y2="35.079716"
+       id="linearGradient3576"
+       xlink:href="#linearGradient3570"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-0.559618,-0.203498)" />
+    <filter
+       id="filter3590">
+      <feGaussianBlur
+         id="feGaussianBlur3592"
+         stdDeviation="0.29695312"
+         inkscape:collect="always" />
+    </filter>
+    <linearGradient
+       x1="111.58585"
+       y1="31.213261"
+       x2="116.79939"
+       y2="35.079716"
+       id="linearGradient3671"
+       xlink:href="#linearGradient3570"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-0.559618,-0.203498)" />
+    <radialGradient
+       cx="108.33566"
+       cy="25.487402"
+       r="4.171701"
+       fx="108.33566"
+       fy="25.487402"
+       id="radialGradient3673"
+       xlink:href="#linearGradient3452"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.432375,0,0,1.432375,-46.84166,-11.02012)" />
+    <linearGradient
+       x1="110.75722"
+       y1="32.559616"
+       x2="106.72433"
+       y2="24.216215"
+       id="linearGradient3675"
+       xlink:href="#linearGradient3470"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="111.58585"
+       y1="31.213261"
+       x2="116.79939"
+       y2="35.079716"
+       id="linearGradient3711"
+       xlink:href="#linearGradient3570"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-0.559618,-0.203498)" />
+    <radialGradient
+       cx="108.33566"
+       cy="25.487402"
+       r="4.171701"
+       fx="108.33566"
+       fy="25.487402"
+       id="radialGradient3713"
+       xlink:href="#linearGradient3452"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.432375,0,0,1.432375,-46.84166,-11.02012)" />
+    <linearGradient
+       x1="110.75722"
+       y1="32.559616"
+       x2="106.72433"
+       y2="24.216215"
+       id="linearGradient3715"
+       xlink:href="#linearGradient3470"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="110"
+       y1="84"
+       x2="110"
+       y2="72.081078"
+       id="linearGradient3735"
+       xlink:href="#linearGradient3729"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="110"
+       y1="84"
+       x2="110"
+       y2="88"
+       id="linearGradient3743"
+       xlink:href="#linearGradient3737"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="110"
+       y1="84"
+       x2="110"
+       y2="72.081078"
+       id="linearGradient3747"
+       xlink:href="#linearGradient3729"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.2,0,-90.8)" />
+    <radialGradient
+       cx="110"
+       cy="87.735802"
+       r="4"
+       fx="110"
+       fy="87.735802"
+       id="radialGradient3755"
+       xlink:href="#linearGradient3749"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(5.343975,0,0,6.161922,-477.8373,-454.2492)" />
+    <linearGradient
+       x1="113.34818"
+       y1="79.669319"
+       x2="118.02862"
+       y2="79.669319"
+       id="linearGradient3791"
+       xlink:href="#linearGradient3785"
+       gradientUnits="userSpaceOnUse" />
+    <filter
+       id="filter3853"
+       height="1.1794737"
+       y="-0.089736843"
+       width="1.6153383"
+       x="-0.30766916">
+      <feGaussianBlur
+         id="feGaussianBlur3855"
+         stdDeviation="0.54783699"
+         inkscape:collect="always" />
+    </filter>
+    <linearGradient
+       x1="98.899841"
+       y1="40.170177"
+       x2="98.899841"
+       y2="104.503"
+       id="linearGradient4008"
+       xlink:href="#linearGradient4002"
+       gradientUnits="userSpaceOnUse" />
+    <clipPath
+       id="clipPath4019">
+      <rect
+         width="88"
+         height="72"
+         rx="5.0167508"
+         ry="5.0167508"
+         x="12"
+         y="24"
+         style="opacity:0.65263157;fill:url(#linearGradient4023);fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect4021" />
+    </clipPath>
+    <linearGradient
+       x1="100"
+       y1="92.763115"
+       x2="100"
+       y2="60"
+       id="linearGradient4023"
+       xlink:href="#linearGradient3440"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="100"
+       y1="92.763115"
+       x2="100"
+       y2="72.820351"
+       id="linearGradient4027"
+       xlink:href="#linearGradient3440"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="100"
+       y1="65.697929"
+       x2="95.716316"
+       y2="65.697929"
+       id="linearGradient4099"
+       xlink:href="#linearGradient3440"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="100"
+       y1="65.697929"
+       x2="95.909744"
+       y2="65.697929"
+       id="linearGradient4103"
+       xlink:href="#linearGradient3440"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-112,0)" />
+    <linearGradient
+       x1="48.9221"
+       y1="24"
+       x2="48.9221"
+       y2="30.250481"
+       id="linearGradient4107"
+       xlink:href="#linearGradient3440"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(-112,0)" />
+    <radialGradient
+       cx="64"
+       cy="73.977821"
+       r="52"
+       fx="64"
+       fy="73.977821"
+       id="radialGradient4119"
+       xlink:href="#linearGradient4111"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.285229,0,74.89936)"
+       spreadMethod="reflect" />
+    <filter
+       id="filter4137"
+       height="1.5494737"
+       y="-0.27473684"
+       width="1.0634008"
+       x="-0.031700405">
+      <feGaussianBlur
+         id="feGaussianBlur4139"
+         stdDeviation="1.3736842"
+         inkscape:collect="always" />
+    </filter>
+    <clipPath
+       id="clipPath3379">
+      <rect
+         width="88"
+         height="72"
+         rx="5.0167508"
+         ry="5.0167508"
+         x="-100"
+         y="23"
+         transform="scale(-1,1)"
+         style="opacity:0.32105264;fill:black;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="rect3381" />
+    </clipPath>
+    <linearGradient
+       x1="100.11033"
+       y1="69.474098"
+       x2="-17.198158"
+       y2="69.474098"
+       id="linearGradient3389"
+       xlink:href="#linearGradient3383"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="101.41602"
+       y1="64.334373"
+       x2="-35.975773"
+       y2="64.334373"
+       id="linearGradient3397"
+       xlink:href="#linearGradient3391"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="99.727539"
+       y1="63.027271"
+       x2="-3.3565123"
+       y2="63.027271"
+       id="linearGradient3405"
+       xlink:href="#linearGradient3399"
+       gradientUnits="userSpaceOnUse" />
+    <filter
+       id="filter3411"
+       height="1.3350769"
+       y="-0.16753846"
+       width="1.0821887"
+       x="-0.04109434">
+      <feGaussianBlur
+         id="feGaussianBlur3413"
+         stdDeviation="1.815"
+         inkscape:collect="always" />
+    </filter>
+    <filter
+       id="filter4138"
+       height="1.252"
+       y="-0.126"
+       width="1.252"
+       x="-0.126">
+      <feGaussianBlur
+         id="feGaussianBlur4140"
+         stdDeviation="0.21"
+         inkscape:collect="always" />
+    </filter>
+    <radialGradient
+       cx="18"
+       cy="102"
+       r="2"
+       fx="18"
+       fy="102"
+       id="radialGradient4148"
+       xlink:href="#linearGradient4142"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(3.25543,0,0,3.25543,-40.59774,-230.0538)" />
+    <linearGradient
+       x1="20.930662"
+       y1="96.872108"
+       x2="23.156008"
+       y2="105.17721"
+       id="linearGradient4165"
+       xlink:href="#linearGradient4159"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       x1="34.736519"
+       y1="106.93066"
+       x2="21.263483"
+       y2="100"
+       id="linearGradient4173"
+       xlink:href="#linearGradient4167"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.666667,0,0,1,5.333334,0)" />
+    <filter
+       id="filter4190">
+      <feGaussianBlur
+         id="feGaussianBlur4192"
+         stdDeviation="2.6020349"
+         inkscape:collect="always" />
+    </filter>
+    <linearGradient
+       x1="29.355932"
+       y1="27.119223"
+       x2="35.527592"
+       y2="50.152176"
+       id="linearGradient4205"
+       xlink:href="#linearGradient4199"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3440"
+       id="linearGradient3238"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.4101341,0,0,1.4101341,-142.94128,-20.830999)"
+       x1="100"
+       y1="65.697929"
+       x2="95.909744"
+       y2="65.697929" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3440"
+       id="linearGradient3240"
+       gradientUnits="userSpaceOnUse"
+       x1="100"
+       y1="65.697929"
+       x2="95.716316"
+       y2="65.697929"
+       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3440"
+       id="linearGradient3242"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.4101341,0,0,1.4101341,-142.94128,-20.830999)"
+       x1="48.9221"
+       y1="24"
+       x2="48.9221"
+       y2="30.250481" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient4199"
+       id="linearGradient3244"
+       gradientUnits="userSpaceOnUse"
+       x1="29.355932"
+       y1="27.119223"
+       x2="35.527592"
+       y2="50.152176"
+       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3440"
+       id="linearGradient3246"
+       gradientUnits="userSpaceOnUse"
+       x1="100"
+       y1="92.763115"
+       x2="100"
+       y2="72.820351"
+       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3383"
+       id="linearGradient3248"
+       gradientUnits="userSpaceOnUse"
+       x1="100.11033"
+       y1="69.474098"
+       x2="-17.198158"
+       y2="69.474098" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3391"
+       id="linearGradient3250"
+       gradientUnits="userSpaceOnUse"
+       x1="101.41602"
+       y1="64.334373"
+       x2="-35.975773"
+       y2="64.334373" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3399"
+       id="linearGradient3252"
+       gradientUnits="userSpaceOnUse"
+       x1="99.727539"
+       y1="63.027271"
+       x2="-3.3565123"
+       y2="63.027271" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3440"
+       id="linearGradient3254"
+       gradientUnits="userSpaceOnUse"
+       x1="100"
+       y1="92.763115"
+       x2="100"
+       y2="60"
+       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3440"
+       id="linearGradient3256"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.4101341,0,0,1.4101341,-14.993741,-148.3851)"
+       x1="100"
+       y1="92.763115"
+       x2="100"
+       y2="60" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3292"
+       id="radialGradient3258"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(3.111829,0,0,2.9016527,-110.28866,-171.62017)"
+       cx="56"
+       cy="65.961678"
+       fx="56"
+       fy="64.752823"
+       r="44" />
+  </defs>
+  <g
+     id="layer1">
+    <rect
+       width="124.0918"
+       height="101.52966"
+       rx="7.0742917"
+       ry="7.0742917"
+       x="1.9278687"
+       y="13.01222"
+       style="fill:url(#radialGradient3258);fill-opacity:1;stroke:none"
+       id="rect3273" />
+    <g
+       style="opacity:0.25789478;fill:#ff7e00;stroke:#d3d7cf"
+       clip-path="url(#clipPath3361)"
+       id="g3349"
+       transform="matrix(1.4101341,0,0,1.4101341,-14.993741,-20.830999)">
+      <path
+         d="m 24.5,19.5 0,80"
+         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         id="path3300"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 40.5,19.5 0,80"
+         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         id="path3307"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 56.5,19.5 0,80"
+         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         id="path3309"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 72.5,19.5 0,80"
+         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         id="path3311"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 88.5,19.5 0,80"
+         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         id="path3317"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 0.5,60.5 110.61729,0"
+         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         id="path3325"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 0.5,79.5 110.61729,0"
+         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         id="path3327"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 0.5,40.5 110.61729,0"
+         style="fill:#ff7e00;fill-rule:evenodd;stroke:#d3d7cf;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         id="path3329"
+         inkscape:connector-curvature="0" />
+    </g>
+    <rect
+       width="124.0918"
+       height="101.52966"
+       rx="7.0742917"
+       ry="7.0742917"
+       x="1.9278687"
+       y="-114.54188"
+       transform="scale(1,-1)"
+       style="opacity:0.32105264;fill:url(#linearGradient3256);fill-opacity:1;stroke:none"
+       id="rect3448" />
+    <rect
+       width="124.0918"
+       height="101.52966"
+       rx="7.0742917"
+       ry="7.0742917"
+       x="1.9278687"
+       y="13.01222"
+       style="opacity:0.43684214;fill:url(#linearGradient3254);fill-opacity:1;stroke:none"
+       id="rect4025" />
+    <g
+       transform="matrix(1.4101341,0,0,1.4101341,-14.993741,-19.420865)"
+       clip-path="url(#clipPath3379)"
+       id="g4010">
+      <path
+         d="M 16.246914,126.84803 -2.6446783,98.771282 12,79.49 l 12,0 12,-24 16,0 12,16 12,0 8,-12 15.306836,0 5.779584,0 -0.0494,65.38272"
+         style="opacity:0.28494622;fill:url(#linearGradient3248);fill-opacity:1;fill-rule:evenodd;stroke:none"
+         id="path3431"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 4,59.49 8,20 12,0 12,-24 16,0 12,16 12,0 8,-12 15.306836,0 8.693164,0"
+         style="fill:none;stroke:url(#linearGradient3250);stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         id="path3413"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 4,59.49 8,20 12,0 12,-24 16,0 12,16 12,0 8,-12 15.306836,0 8.693164,0"
+         style="fill:none;stroke:url(#linearGradient3252);stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         id="path3857"
+         inkscape:connector-curvature="0" />
+    </g>
+    <rect
+       width="124.0918"
+       height="101.52966"
+       rx="7.0742917"
+       ry="7.0742917"
+       x="1.9278687"
+       y="13.01222"
+       style="opacity:0.32105264;fill:url(#linearGradient3246);fill-opacity:1;stroke:none"
+       id="rect3438" />
+    <path
+       d="m 9.0226062,13.012221 c -3.9191562,0 -7.0947373,3.175579 -7.0947373,7.094737 l 0,30.62635 C 25.678508,39.547637 58.966862,32.577831 95.833988,32.577831 c 10.395432,0 20.489952,0.541015 30.185682,1.586401 l 0,-14.057274 c 0,-3.919156 -3.17558,-7.094737 -7.09474,-7.094737 l -109.9023238,0 z"
+       style="opacity:0.225;fill:url(#linearGradient3244);fill-opacity:1;stroke:none"
+       id="rect4194"
+       inkscape:connector-curvature="0" />
+    <rect
+       width="124.0918"
+       height="101.52966"
+       rx="7.0742917"
+       ry="7.0742917"
+       x="-126.01967"
+       y="13.01222"
+       transform="scale(-1,1)"
+       style="opacity:0.32105264;fill:url(#linearGradient3242);fill-opacity:1;stroke:none"
+       id="rect4105" />
+    <rect
+       width="124.0918"
+       height="101.52966"
+       rx="7.0742917"
+       ry="7.0742917"
+       x="1.9278687"
+       y="13.01222"
+       style="opacity:0.32105264;fill:url(#linearGradient3240);fill-opacity:1;stroke:none"
+       id="rect4097" />
+    <rect
+       width="124.0918"
+       height="101.52966"
+       rx="7.0742917"
+       ry="7.0742917"
+       x="-126.01967"
+       y="13.01222"
+       transform="scale(-1,1)"
+       style="opacity:0.32105264;fill:url(#linearGradient3238);fill-opacity:1;stroke:none"
+       id="rect4101" />
+  </g>
+</svg>
diff --git a/seth/resources/torConfigDesc.txt b/seth/resources/torConfigDesc.txt
new file mode 100644
index 0000000..9fa83e0
--- /dev/null
+++ b/seth/resources/torConfigDesc.txt
@@ -0,0 +1,1123 @@
+Tor Version 0.2.2.13-alpha
+General
+index: 46
+acceldir
+DIR
+Specify this option if using dynamic hardware acceleration and the engine implementation library resides somewhere other than the OpenSSL default.
+--------------------------------------------------------------------------------
+General
+index: 45
+accelname
+NAME
+When using OpenSSL hardware crypto acceleration attempt to load the dynamic engine of this name. This must be used for any dynamic hardware engine. Names can be verified with the openssl engine command.
+--------------------------------------------------------------------------------
+Relay
+index: 119
+accountingmax
+N bytes|KB|MB|GB|TB
+Never send more than the specified number of bytes in a given accounting period, or receive more than that number in the period. For example, with AccountingMax set to 1 GB, a server could send 900 MB and receive 800 MB and continue running. It will only hibernate once one of the two reaches 1 GB. When the number of bytes is exhausted, Tor will hibernate until some time in the next accounting period. To prevent all servers from waking at the same time, Tor will also wait until a random point in each period before waking up. If you have bandwidth cost issues, enabling hibernation is preferable to setting a low bandwidth, since it provides users with a collection of fast servers that are up some of the time, which is more useful than a set of slow servers that are always "available".
+--------------------------------------------------------------------------------
+Relay
+index: 120
+accountingstart
+day|week|month [day] HH:MM
+Specify how long accounting periods last. If month is given, each accounting period runs from the time HH:MM on the dayth day of one month to the same day and time of the next. (The day must be between 1 and 28.) If week is given, each accounting period runs from the time HH:MM of the dayth day of one week to the same day and time of the next week, with Monday as day 1 and Sunday as day 7. If day is given, each accounting period runs from the time HH:MM each day to the same time on the next day. All times are local, and given in 24-hour time. (Defaults to "month 1 0:00".)
+--------------------------------------------------------------------------------
+Relay
+index: 104
+address
+address
+The IP address or fully qualified domain name of this server (e.g. moria.mit.edu). You can leave this unset, and Tor will guess your IP address.
+--------------------------------------------------------------------------------
+Client
+index: 89
+allowdotexit
+0|1
+If enabled, we convert "www.google.com.foo.exit" addresses on the SocksPort/TransPort/NatdPort into "www.google.com" addresses that exit from the node "foo". Disabled by default since attacking websites and exit relays can use it to manipulate your path selection. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 51
+allowinvalidnodes
+entry|exit|middle|introduction|rendezvous|...
+If some Tor servers are obviously not working right, the directory authorities can manually mark them as invalid, meaning that it's not recommended you use them for entry or exit positions in your circuits. You can opt to use them in some circuit positions, though. The default is "middle,rendezvous", and other choices are not advised.
+--------------------------------------------------------------------------------
+Client
+index: 88
+allownonrfc953hostnames
+0|1
+When this option is disabled, Tor blocks hostnames containing illegal characters (like @ and :) rather than sending them to an exit node to be resolved. This helps trap accidental attempts to resolve URLs and so on. (Default: 0)
+--------------------------------------------------------------------------------
+Relay
+index: 105
+allowsinglehopexits
+0|1
+This option controls whether clients can use this server as a single hop proxy. If set to 1, clients can use this server as an exit even if it is the only hop in the circuit. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 20
+alternatebridgeauthority
+[nickname] [flags] address:port  fingerprint
+As DirServer, but replaces less of the default directory authorities. Using AlternateDirAuthority replaces the default Tor directory authorities, but leaves the hidden service authorities and bridge authorities in place. Similarly, Using AlternateHSAuthority replaces the default hidden service authorities, but not the directory or bridge authorities.
+--------------------------------------------------------------------------------
+General
+index: 18
+alternatedirauthority
+[nickname] [flags] address:port fingerprint
+
+--------------------------------------------------------------------------------
+General
+index: 19
+alternatehsauthority
+[nickname] [flags] address:port fingerprint
+
+--------------------------------------------------------------------------------
+Relay
+index: 106
+assumereachable
+0|1
+This option is used when bootstrapping a new Tor network. If set to 1, don't do self-reachability testing; just upload your server descriptor immediately. If AuthoritativeDirectory is also set, this option instructs the dirserver to bypass remote reachability testing too and list all connected servers as running.
+--------------------------------------------------------------------------------
+Authority
+index: 154
+authdirbaddir
+AddressPattern...
+Authoritative directories only. A set of address patterns for servers that will be listed as bad directories in any network status document this authority publishes, if AuthDirListBadDirs is set.
+--------------------------------------------------------------------------------
+Authority
+index: 155
+authdirbadexit
+AddressPattern...
+Authoritative directories only. A set of address patterns for servers that will be listed as bad exits in any network status document this authority publishes, if AuthDirListBadExits is set.
+--------------------------------------------------------------------------------
+Authority
+index: 156
+authdirinvalid
+AddressPattern...
+Authoritative directories only. A set of address patterns for servers that will never be listed as "valid" in any network status document that this authority publishes.
+--------------------------------------------------------------------------------
+Authority
+index: 158
+authdirlistbaddirs
+0|1
+Authoritative directories only. If set to 1, this directory has some opinion about which nodes are unsuitable as directory caches. (Do not set this to 1 unless you plan to list non-functioning directories as bad; otherwise, you are effectively voting in favor of every declared directory.)
+--------------------------------------------------------------------------------
+Authority
+index: 159
+authdirlistbadexits
+0|1
+Authoritative directories only. If set to 1, this directory has some opinion about which nodes are unsuitable as exit nodes. (Do not set this to 1 unless you plan to list non-functioning exits as bad; otherwise, you are effectively voting in favor of every declared exit as an exit.)
+--------------------------------------------------------------------------------
+Authority
+index: 161
+authdirmaxserversperaddr
+NUM
+Authoritative directories only. The maximum number of servers that we will list as acceptable on a single IP address. Set this to "0" for "no limit". (Default: 2)
+--------------------------------------------------------------------------------
+Authority
+index: 162
+authdirmaxserversperauthaddr
+NUM
+Authoritative directories only. Like AuthDirMaxServersPerAddr, but applies to addresses shared with directory authorities. (Default: 5)
+--------------------------------------------------------------------------------
+Authority
+index: 157
+authdirreject
+AddressPattern...
+Authoritative directories only. A set of address patterns for servers that will never be listed at all in any network status document that this authority publishes, or accepted as an OR address in any descriptor submitted for publication by this authority.
+--------------------------------------------------------------------------------
+Authority
+index: 160
+authdirrejectunlisted
+0|1
+Authoritative directories only. If set to 1, the directory server rejects all uploaded server descriptors that aren't explicitly listed in the fingerprints file. This acts as a "panic button" if we get hit with a Sybil attack. (Default: 0)
+--------------------------------------------------------------------------------
+Directory
+index: 135
+authoritativedirectory
+0|1
+When this option is set to 1, Tor operates as an authoritative directory server. Instead of caching the directory, it generates its own list of good servers, signs it, and sends that to the clients. Unless the clients already have you listed as a trusted directory, you probably do not want to set this option. Please coordinate with the other admins at tor-ops at torproject.org if you think you should be a directory.
+--------------------------------------------------------------------------------
+Client
+index: 95
+automaphostsonresolve
+0|1
+When this option is enabled, and we get a request to resolve an address that ends with one of the suffixes in AutomapHostsSuffixes, we map an unused virtual address to that address, and return the new virtual address. This is handy for making ".onion" addresses work with applications that resolve an address and then connect to it. (Default: 0).
+--------------------------------------------------------------------------------
+Client
+index: 96
+automaphostssuffixes
+SUFFIX,SUFFIX,...
+A comma-separated list of suffixes to use with AutomapHostsOnResolve. The "." suffix is equivalent to "all addresses." (Default: .exit,.onion).
+--------------------------------------------------------------------------------
+General
+index: 47
+avoiddiskwrites
+0|1
+If non-zero, try to write to disk less frequently than we would otherwise. This is useful when running on flash memory or other media that support only a limited number of writes. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 1
+bandwidthburst
+N bytes|KB|MB|GB
+Limit the maximum token bucket size (also known as the burst) to the given number of bytes in each direction. (Default: 10 MB)
+--------------------------------------------------------------------------------
+General
+index: 0
+bandwidthrate
+N bytes|KB|MB|GB
+A token bucket limits the average incoming bandwidth usage on this node to the specified number of bytes per second, and the average outgoing bandwidth usage to that same value. (Default: 5 MB)
+--------------------------------------------------------------------------------
+Client
+index: 53
+bridge
+IP:ORPort [fingerprint]
+When set along with UseBridges, instructs Tor to use the relay at "IP:ORPort" as a "bridge" relaying into the Tor network. If "fingerprint" is provided (using the same format as for DirServer), we will verify that the relay running at that location has the right fingerprint. We also use fingerprint to look up the bridge descriptor at the bridge authority, if it's provided and if UpdateBridgesFromAuthority is set too.
+--------------------------------------------------------------------------------
+Directory
+index: 144
+bridgeauthoritativedir
+0|1
+When this option is set in addition to AuthoritativeDirectory, Tor accepts and serves router descriptors, but it caches and serves the main networkstatus documents rather than generating its own. (Default: 0)
+--------------------------------------------------------------------------------
+Relay
+index: 127
+bridgerecordusagebycountry
+0|1
+When this option is enabled and BridgeRelay is also enabled, and we have GeoIP data, Tor keeps a keep a per-country count of how many client addresses have contacted it so that it can help the bridge authority guess which countries have blocked access to it. (Default: 1)
+--------------------------------------------------------------------------------
+Relay
+index: 107
+bridgerelay
+0|1
+Sets the relay to act as a "bridge" with respect to relaying connections from bridge users to the Tor network. Mainly it influences how the relay will cache and serve directory information. Usually used in combination with PublishServerDescriptor.
+--------------------------------------------------------------------------------
+Relay
+index: 130
+cellstatistics
+0|1
+When this option is enabled, Tor writes statistics on the mean time that cells spend in circuit queues to disk every 24 hours. Cannot be changed while Tor is running. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 54
+circuitbuildtimeout
+NUM
+Try for at most NUM seconds when building circuits. If the circuit isn't open in that time, give up on it. (Default: 1 minute.)
+--------------------------------------------------------------------------------
+Client
+index: 55
+circuitidletimeout
+NUM
+If we have kept a clean (never used) circuit around for NUM seconds, then close it. This way when the Tor client is entirely idle, it can expire all of its circuits, and then expire its TLS connections. Also, if we end up making a circuit that is not useful for exiting any of the requests we're receiving, it won't forever take up a slot in the circuit list. (Default: 1 hour.)
+--------------------------------------------------------------------------------
+General
+index: 50
+circuitpriorityhalflife
+NUM1
+If this value is set, we override the default algorithm for choosing which circuit's cell to deliver or relay next. When the value is 0, we round-robin between the active circuits on a connection, delivering one cell from each in turn. When the value is positive, we prefer delivering cells from whichever connection has the lowest weighted cell count, where cells are weighted exponentially according to the supplied CircuitPriorityHalflife value (in seconds). If this option is not set at all, we use the behavior recommended in the current consensus networkstatus. This is an advanced option; you generally shouldn't have to mess with it. (Default: not set.)
+--------------------------------------------------------------------------------
+Client
+index: 56
+circuitstreamtimeout
+NUM
+If non-zero, this option overrides our internal timeout schedule for how many seconds until we detach a stream from a circuit and try a new circuit. If your network is particularly slow, you might want to set this to a number like 60. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 99
+clientdnsrejectinternaladdresses
+0|1
+If true, Tor does not believe any anonymously retrieved DNS answer that tells it that an address resolves to an internal address (like 127.0.0.1 or 192.168.0.1). This option prevents certain browser-based attacks; don't turn it off unless you know what you're doing. (Default: 1).
+--------------------------------------------------------------------------------
+Client
+index: 57
+clientonly
+0|1
+If set to 1, Tor will under no circumstances run as a server or serve directory requests. The default is to run as a client unless ORPort is configured. (Usually, you don't need to set this; Tor is pretty smart at figuring out whether you are reliable and high-bandwidth enough to be a useful server.) (Default: 0)
+--------------------------------------------------------------------------------
+Authority
+index: 152
+consensusparams
+STRING
+STRING is a space-separated list of key=value pairs that Tor will include in the "params" line of its networkstatus vote.
+--------------------------------------------------------------------------------
+General
+index: 7
+constrainedsockets
+0|1
+If set, Tor will tell the kernel to attempt to shrink the buffers for all sockets to the size specified in ConstrainedSockSize. This is useful for virtual servers and other environments where system level TCP buffers may be limited. If you're on a virtual server, and you encounter the "Error creating network socket: No buffer space available" message, you are likely experiencing this problem. 
+
+The preferred solution is to have the admin increase the buffer pool for the host itself via /proc/sys/net/ipv4/tcp_mem or equivalent facility; this configuration option is a second-resort. 
+
+The DirPort option should also not be used if TCP buffers are scarce. The cached directory requests consume additional sockets which exacerbates the problem. 
+
+You should not enable this feature unless you encounter the "no buffer space available" issue. Reducing the TCP buffers affects window size for the TCP stream and will reduce throughput in proportion to round trip time on long paths. (Default: 0.)
+--------------------------------------------------------------------------------
+General
+index: 8
+constrainedsocksize
+N bytes|KB
+When ConstrainedSockets is enabled the receive and transmit buffers for all sockets will be set to this limit. Must be a value between 2048 and 262144, in 1024 byte increments. Default of 8192 is recommended.
+--------------------------------------------------------------------------------
+Relay
+index: 108
+contactinfo
+email_address
+Administrative contact information for server. This line might get picked up by spam harvesters, so you may want to obscure the fact that it's an email address.
+--------------------------------------------------------------------------------
+General
+index: 10
+controllistenaddress
+IP[:PORT]
+Bind the controller listener to this address. If you specify a port, bind to this port rather than the one specified in ControlPort. We strongly recommend that you leave this alone unless you know what you're doing, since giving attackers access to your control listener is really dangerous. (Default: 127.0.0.1) This directive can be specified multiple times to bind to multiple addresses/ports.
+--------------------------------------------------------------------------------
+General
+index: 9
+controlport
+Port
+If set, Tor will accept connections on this port and allow those connections to control the Tor process using the Tor Control Protocol (described in control-spec.txt). Note: unless you also specify one of HashedControlPassword or CookieAuthentication, setting this option will cause Tor to allow any process on the local host to control it. This option is required for many Tor controllers; most use the value of 9051.
+--------------------------------------------------------------------------------
+General
+index: 11
+controlsocket
+Path
+Like ControlPort, but listens on a Unix domain socket, rather than a TCP socket. (Unix and Unix-like systems only.)
+--------------------------------------------------------------------------------
+General
+index: 13
+cookieauthentication
+0|1
+If this option is set to 1, don't allow any connections on the control port except when the connecting process knows the contents of a file named "control_auth_cookie", which Tor will create in its data directory. This authentication method should only be used on systems with good filesystem security. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 14
+cookieauthfile
+Path
+If set, this option overrides the default location and file name for Tor's cookie file. (See CookieAuthentication above.)
+--------------------------------------------------------------------------------
+General
+index: 15
+cookieauthfilegroupreadable
+0|1|Groupname
+If this option is set to 0, don't allow the filesystem group to read the cookie file. If the option is set to 1, make the cookie file readable by the default GID. [Making the file readable by other groups is not yet implemented; let us know if you need this for some reason.] (Default: 0).
+--------------------------------------------------------------------------------
+General
+index: 16
+datadirectory
+DIR
+Store working data in DIR (Default: /usr/local/var/lib/tor)
+--------------------------------------------------------------------------------
+Authority
+index: 153
+dirallowprivateaddresses
+0|1
+If set to 1, Tor will accept router descriptors with arbitrary "Address" elements. Otherwise, if the address is not an IP address or is a private IP address, it will reject the router descriptor. Defaults to 0.
+--------------------------------------------------------------------------------
+Directory
+index: 147
+dirlistenaddress
+IP[:PORT]
+Bind the directory service to this address. If you specify a port, bind to this port rather than the one specified in DirPort. (Default: 0.0.0.0) This directive can be specified multiple times to bind to multiple addresses/ports.
+--------------------------------------------------------------------------------
+Directory
+index: 148
+dirpolicy
+policy,policy,...
+Set an entrance policy for this server, to limit who can connect to the directory ports. The policies have the same form as exit policies above.
+--------------------------------------------------------------------------------
+Directory
+index: 146
+dirport
+PORT
+Advertise the directory service on this port.
+--------------------------------------------------------------------------------
+Directory
+index: 136
+dirportfrontpage
+FILENAME
+When this option is set, it takes an HTML file and publishes it as "/" on the DirPort. Now relay operators can provide a disclaimer without needing to set up a separate webserver. There's a sample disclaimer in contrib/tor-exit-notice.html.
+--------------------------------------------------------------------------------
+Relay
+index: 131
+dirreqstatistics
+0|1
+When this option is enabled, Tor writes statistics on the number and response time of network status requests to disk every 24 hours. Cannot be changed while Tor is running. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 17
+dirserver
+[nickname] [flags] address:port fingerprint
+Use a nonstandard authoritative directory server at the provided address and port, with the specified key fingerprint. This option can be repeated many times, for multiple authoritative directory servers. Flags are separated by spaces, and determine what kind of an authority this directory is. By default, every authority is authoritative for current ("v2")-style directories, unless the "no-v2" flag is given. If the "v1" flags is provided, Tor will use this server as an authority for old-style (v1) directories as well. (Only directory mirrors care about this.) Tor will use this server as an authority for hidden service information if the "hs" flag is set, or if the "v1" flag is set and the "no-hs" flag is not set. Tor will use this authority as a bridge authoritative directory if the "bridge" flag is set. If a flag "orport=port" is given, Tor will use the given port when opening encrypted tunnels to the dirserver. Lastly, if a flag "v3ident=fp" is given, the dirserver is a v3 directo
 ry authority whose v3 long-term signing key has the fingerprint fp. 
+
+If no dirserver line is given, Tor will use the default directory servers. NOTE: this option is intended for setting up a private Tor network with its own directory authorities. If you use it, you will be distinguishable from other users, because you won't believe the same authorities they do.
+--------------------------------------------------------------------------------
+General
+index: 21
+disableallswap
+0|1
+If set to 1, Tor will attempt to lock all current and future memory pages, so that memory cannot be paged out. Windows, OS X and Solaris are currently not supported. We believe that this feature works on modern Gnu/Linux distributions, and that it should work on *BSD systems (untested). This option requires that you start your Tor as root, and you should use the User option to properly reduce Tor's privileges. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 98
+dnslistenaddress
+IP[:PORT]
+Bind to this address to listen for DNS connections. (Default: 127.0.0.1).
+--------------------------------------------------------------------------------
+Client
+index: 97
+dnsport
+PORT
+If non-zero, Tor listens for UDP DNS requests on this port and resolves them anonymously. (Default: 0).
+--------------------------------------------------------------------------------
+Client
+index: 100
+downloadextrainfo
+0|1
+If true, Tor downloads and caches "extra-info" documents. These documents contain information about servers other than the information in their regular router descriptors. Tor does not use this information for anything itself; to save bandwidth, leave this option turned off. (Default: 0).
+--------------------------------------------------------------------------------
+Client
+index: 74
+enforcedistinctsubnets
+0|1
+If 1, Tor will not put two servers whose IP addresses are "too close" on the same circuit. Currently, two addresses are "too close" if they lie in the same /16 range. (Default: 1)
+--------------------------------------------------------------------------------
+Client
+index: 60
+entrynodes
+node,node,...
+A list of identity fingerprints, nicknames, country codes and address patterns of nodes to use for the first hop in normal circuits. These are treated only as preferences unless StrictNodes (see below) is also set.
+--------------------------------------------------------------------------------
+Relay
+index: 132
+entrystatistics
+0|1
+When this option is enabled, Tor writes statistics on the number of directly connecting clients to disk every 24 hours. Cannot be changed while Tor is running. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 59
+excludeexitnodes
+node,node,...
+A list of identity fingerprints, nicknames, country codes and address patterns of nodes to never use when picking an exit node. Note that any node listed in ExcludeNodes is automatically considered to be part of this list.
+--------------------------------------------------------------------------------
+Client
+index: 58
+excludenodes
+node,node,...
+A list of identity fingerprints, nicknames, country codes and address patterns of nodes to never use when building a circuit. (Example: ExcludeNodes SlowServer, $ EFFFFFFFFFFFFFFF, {cc}, 255.254.0.0/8)
+--------------------------------------------------------------------------------
+Client
+index: 52
+excludesinglehoprelays
+0|1
+This option controls whether circuits built by Tor will include relays with the AllowSingleHopExits flag set to true. If ExcludeSingleHopRelays is set to 0, these relays will be included. Note that these relays might be at higher risk of being seized or observed, so they are not normally included. (Default: 1)
+--------------------------------------------------------------------------------
+Client
+index: 61
+exitnodes
+node,node,...
+A list of identity fingerprints, nicknames, country codes and address patterns of nodes to use for the last hop in normal exit circuits. These are treated only as preferences unless StrictNodes (see below) is also set.
+--------------------------------------------------------------------------------
+Relay
+index: 109
+exitpolicy
+policy,policy,...
+Set an exit policy for this server. Each policy is of the form "accept|reject ADDR[/MASK][:PORT]". If /MASK is omitted then this policy just applies to the host given. Instead of giving a host or network you can also use "*" to denote the universe (0.0.0.0/0).  PORT can be a single port number, an interval of ports "FROM_PORT-TO_PORT", or "*". If PORT is omitted, that means "*". 
+
+For example, "accept 18.7.22.69:*,reject 18.0.0.0/8:*,accept *:*" would reject any traffic destined for MIT except for web.mit.edu, and accept anything else. 
+
+To specify all internal and link-local networks (including 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8, 192.168.0.0/16, 10.0.0.0/8, and 172.16.0.0/12), you can use the "private" alias instead of an address. These addresses are rejected by default (at the beginning of your exit policy), along with your public IP address, unless you set the ExitPolicyRejectPrivate config option to 0. For example, once you've done that, you could allow HTTP to 127.0.0.1 and block all other connections to internal networks with "accept 127.0.0.1:80,reject private:*", though that may also allow connections to your own computer that are addressed to its public (external) IP address. See RFC 1918 and RFC 3330 for more details about internal and reserved IP address space. 
+
+This directive can be specified multiple times so you don't have to put it all on one line. 
+
+Policies are considered first to last, and the first match wins. If you want to _replace_ the default exit policy, end your exit policy with either a reject *:* or an accept *:*. Otherwise, you're _augmenting_ (prepending to) the default exit policy. The default exit policy is: 
+
+    reject *:25
+    reject *:119
+    reject *:135-139
+    reject *:445
+    reject *:563
+    reject *:1214
+    reject *:4661-4666
+    reject *:6346-6429
+    reject *:6699
+    reject *:6881-6999
+    accept *:*
+--------------------------------------------------------------------------------
+Relay
+index: 110
+exitpolicyrejectprivate
+0|1
+Reject all private (local) networks, along with your own public IP address, at the beginning of your exit policy. See above entry on ExitPolicy. (Default: 1)
+--------------------------------------------------------------------------------
+Relay
+index: 133
+exitportstatistics
+0|1
+When this option is enabled, Tor writes statistics on the number of relayed bytes and opened stream per exit port to disk every 24 hours. Cannot be changed while Tor is running. (Default: 0)
+--------------------------------------------------------------------------------
+Relay
+index: 134
+extrainfostatistics
+0|1
+When this option is enabled, Tor includes previously gathered statistics in its extra-info documents that it uploads to the directory authorities. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 101
+fallbacknetworkstatusfile
+FILENAME
+If Tor doesn't have a cached networkstatus file, it starts out using this one instead. Even if this file is out of date, Tor can still use it to learn about directory mirrors, so it doesn't need to put load on the authorities. (Default: None).
+--------------------------------------------------------------------------------
+Client
+index: 63
+fascistfirewall
+0|1
+If 1, Tor will only create outgoing connections to ORs running on ports that your firewall allows (defaults to 80 and 443; see FirewallPorts). This will allow you to run Tor as a client behind a firewall with restrictive policies, but will not allow you to run as a server behind such a firewall. If you prefer more fine-grained control, use ReachableAddresses instead.
+--------------------------------------------------------------------------------
+Client
+index: 90
+fastfirsthoppk
+0|1
+When this option is disabled, Tor uses the public key step for the first hop of creating circuits. Skipping it is generally safe since we have already used TLS to authenticate the relay and to establish forward-secure keys. Turning this option off makes circuit building slower. 
+
+Note that Tor will always use the public key step for the first hop if it's operating as a relay, and it will never use the public key step if it doesn't yet know the onion key of the first hop. (Default: 1)
+--------------------------------------------------------------------------------
+General
+index: 22
+fetchdirinfoearly
+0|1
+If set to 1, Tor will always fetch directory information like other directory caches, even if you don't meet the normal criteria for fetching early. Normal users should leave it off. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 23
+fetchdirinfoextraearly
+0|1
+If set to 1, Tor will fetch directory information before other directory caches. It will attempt to download directory information closer to the start of the consensus period. Normal users should leave it off. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 24
+fetchhidservdescriptors
+0|1
+If set to 0, Tor will never fetch any hidden service descriptors from the rendezvous directories. This option is only useful if you're using a Tor controller that handles hidden service fetches for you. (Default: 1)
+--------------------------------------------------------------------------------
+General
+index: 25
+fetchserverdescriptors
+0|1
+If set to 0, Tor will never fetch any network status summaries or server descriptors from the directory servers. This option is only useful if you're using a Tor controller that handles directory fetches for you. (Default: 1)
+--------------------------------------------------------------------------------
+General
+index: 26
+fetchuselessdescriptors
+0|1
+If set to 1, Tor will fetch every non-obsolete descriptor from the authorities that it hears about. Otherwise, it will avoid fetching useless descriptors, for example for routers that are not running. This option is useful if you're using the contributed "exitlist" script to enumerate Tor nodes that exit to certain addresses. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 64
+firewallports
+PORTS
+A list of ports that your firewall allows you to connect to. Only used when FascistFirewall is set. This option is deprecated; use ReachableAddresses instead. (Default: 80, 443)
+--------------------------------------------------------------------------------
+Relay
+index: 129
+geoipfile
+filename
+A filename containing GeoIP data, for use with BridgeRecordUsageByCountry.
+--------------------------------------------------------------------------------
+General
+index: 44
+hardwareaccel
+0|1
+If non-zero, try to use built-in (static) crypto hardware acceleration when available. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 12
+hashedcontrolpassword
+hashed_password
+Don't allow any connections on the control port except when the other process knows the password whose one-way hash is hashed_password. You can compute the hash of a password by running "tor --hash-password password". You can provide several acceptable passwords by using more than one HashedControlPassword line.
+--------------------------------------------------------------------------------
+Hidden Service
+index: 171
+hiddenserviceauthorizeclient
+auth-type client-name,client-name,...
+If configured, the hidden service is accessible for authorized clients only. The auth-type can either be 'basic' for a general-purpose authorization protocol or 'stealth' for a less scalable protocol that also hides service activity from unauthorized clients. Only clients that are listed here are authorized to access the hidden service. Valid client names are 1 to 19 characters long and only use characters in A-Za-z0-9+-_ (no spaces). If this option is set, the hidden service is not accessible for clients without authorization any more. Generated authorization data can be found in the hostname file. Clients need to put this authorization data in their configuration file using HidServAuth.
+--------------------------------------------------------------------------------
+Hidden Service
+index: 167
+hiddenservicedir
+DIRECTORY
+Store data files for a hidden service in DIRECTORY. Every hidden service must have a separate directory. You may use this option multiple times to specify multiple services.
+--------------------------------------------------------------------------------
+Hidden Service
+index: 168
+hiddenserviceport
+VIRTPORT [TARGET]
+Configure a virtual port VIRTPORT for a hidden service. You may use this option multiple times; each time applies to the service using the most recent hiddenservicedir. By default, this option maps the virtual port to the same port on 127.0.0.1. You may override the target port, address, or both by specifying a target of addr, port, or addr:port. You may also have multiple lines with the same VIRTPORT: when a user connects to that VIRTPORT, one of the TARGETs from those lines will be chosen at random.
+--------------------------------------------------------------------------------
+Hidden Service
+index: 170
+hiddenserviceversion
+version,version,...
+A list of rendezvous service descriptor versions to publish for the hidden service. Currently, only version 2 is supported. (Default: 2)
+--------------------------------------------------------------------------------
+Client
+index: 65
+hidservauth
+onion-address auth-cookie [service-name]
+Client authorization for a hidden service. Valid onion addresses contain 16 characters in a-z2-7 plus ".onion", and valid auth cookies contain 22 characters in A-Za-z0-9+/. The service name is only used for internal purposes, e.g., for Tor controllers. This option may be used multiple times for different hidden services. If a hidden service uses authorization and this option is not set, the hidden service is not accessible. Hidden services can be configured to require authorization using the HiddenServiceAuthorizeClient option.
+--------------------------------------------------------------------------------
+Directory
+index: 143
+hidservdirectoryv2
+0|1
+When this option is set, Tor accepts and serves v2 hidden service descriptors. Setting DirPort is not required for this, because clients connect via the ORPort by default. (Default: 1)
+--------------------------------------------------------------------------------
+Directory
+index: 142
+hsauthoritativedir
+0|1
+When this option is set in addition to AuthoritativeDirectory, Tor also accepts and serves hidden service descriptors. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 27
+httpproxy
+host[:port]
+Tor will make all its directory requests through this host:port (or host:80 if port is not specified), rather than connecting directly to any directory servers.
+--------------------------------------------------------------------------------
+General
+index: 28
+httpproxyauthenticator
+username:password
+If defined, Tor will use this username:password for Basic HTTP proxy authentication, as in RFC 2617. This is currently the only form of HTTP proxy authentication that Tor supports; feel free to submit a patch if you want it to support others.
+--------------------------------------------------------------------------------
+General
+index: 29
+httpsproxy
+host[:port]
+Tor will make all its OR (SSL) connections through this host:port (or host:443 if port is not specified), via HTTP CONNECT rather than connecting directly to servers. You may want to set FascistFirewall to restrict the set of ports you might try to connect to, if your HTTPS proxy only allows connecting to certain ports.
+--------------------------------------------------------------------------------
+General
+index: 30
+httpsproxyauthenticator
+username:password
+If defined, Tor will use this username:password for Basic HTTPS proxy authentication, as in RFC 2617. This is currently the only form of HTTPS proxy authentication that Tor supports; feel free to submit a patch if you want it to support others.
+--------------------------------------------------------------------------------
+General
+index: 35
+keepaliveperiod
+NUM
+To keep firewalls from expiring connections, send a padding keepalive cell every NUM seconds on open connections that are in use. If the connection has no open circuits, it will instead be closed after NUM seconds of idleness. (Default: 5 minutes)
+--------------------------------------------------------------------------------
+General
+index: 37
+log
+minSeverity[-maxSeverity] file FILENAME
+As above, but send log messages to the listed filename. The "Log" option may appear more than once in a configuration file. Messages are sent to all the logs that match their severity level.
+--------------------------------------------------------------------------------
+Client
+index: 69
+longlivedports
+PORTS
+A list of ports for services that tend to have long-running connections (e.g. chat and interactive shells). Circuits for streams that use these ports will contain only high-uptime nodes, to reduce the chance that a node will go down before the stream is finished. (Default: 21, 22, 706, 1863, 5050, 5190, 5222, 5223, 6667, 6697, 8300)
+--------------------------------------------------------------------------------
+Client
+index: 70
+mapaddress
+address newaddress
+When a request for address arrives to Tor, it will rewrite it to newaddress before processing it. For example, if you always want connections to www.indymedia.org to exit via torserver (where torserver is the nickname of the server), use "MapAddress www.indymedia.org www.indymedia.org.torserver.exit".
+--------------------------------------------------------------------------------
+General
+index: 2
+maxadvertisedbandwidth
+N bytes|KB|MB|GB
+If set, we will not advertise more than this amount of bandwidth for our BandwidthRate. Server operators who want to reduce the number of clients who ask to build circuits through them (since this is proportional to advertised bandwidth rate) can thus reduce the CPU demands on their server without impacting network performance.
+--------------------------------------------------------------------------------
+Client
+index: 72
+maxcircuitdirtiness
+NUM
+Feel free to reuse a circuit that was first used at most NUM seconds ago, but never attach a new stream to a circuit that is too old. (Default: 10 minutes)
+--------------------------------------------------------------------------------
+Relay
+index: 111
+maxonionspending
+NUM
+If you have more than this number of onionskins queued for decrypt, reject new ones. (Default: 100)
+--------------------------------------------------------------------------------
+Directory
+index: 145
+minuptimehidservdirectoryv2
+N seconds|minutes|hours|days|weeks
+Minimum uptime of a v2 hidden service directory to be accepted as such by authoritative directories. (Default: 24 hours)
+--------------------------------------------------------------------------------
+Relay
+index: 112
+myfamily
+node,node,...
+Declare that this Tor server is controlled or administered by a group or organization identical or similar to that of the other servers, defined by their identity fingerprints or nicknames. When two servers both declare that they are in the same 'family', Tor clients will not use them in the same circuit. (Each server only needs to list the other servers in its family; it doesn't need to list itself, but it won't hurt.)
+--------------------------------------------------------------------------------
+Directory
+index: 141
+namingauthoritativedirectory
+0|1
+When this option is set to 1, then the server advertises that it has opinions about nickname-to-fingerprint bindings. It will include these opinions in its published network-status pages, by listing servers with the flag "Named" if a correct binding between that nickname and fingerprint has been registered with the dirserver. Naming dirservers will refuse to accept or publish descriptors that contradict a registered binding. See approved-routers in the FILES section below.
+--------------------------------------------------------------------------------
+Client
+index: 94
+natdlistenaddress
+IP[:PORT]
+Bind to this address to listen for NATD connections. (Default: 127.0.0.1).
+--------------------------------------------------------------------------------
+Client
+index: 93
+natdport
+PORT
+Allow old versions of ipfw (as included in old versions of FreeBSD, etc.) to send connections through Tor using the NATD protocol. This option is only for people who cannot use TransPort.
+--------------------------------------------------------------------------------
+Client
+index: 71
+newcircuitperiod
+NUM
+Every NUM seconds consider whether to build a new circuit. (Default: 30 seconds)
+--------------------------------------------------------------------------------
+Relay
+index: 113
+nickname
+name
+Set the server's nickname to 'name'. Nicknames must be between 1 and 19 characters inclusive, and must contain only the characters [a-zA-Z0-9].
+--------------------------------------------------------------------------------
+Client
+index: 73
+nodefamily
+node,node,...
+The Tor servers, defined by their identity fingerprints or nicknames, constitute a "family" of similar or co-administered servers, so never use any two of them in the same circuit. Defining a NodeFamily is only needed when a server doesn't list the family itself (with MyFamily). This option can be used multiple times.
+--------------------------------------------------------------------------------
+Relay
+index: 114
+numcpus
+num
+How many processes to use at once for decrypting onionskins. (Default: 1)
+--------------------------------------------------------------------------------
+Client
+index: 84
+numentryguards
+NUM
+If UseEntryGuards is set to 1, we will try to pick a total of NUM routers as long-term entries for our circuits. (Defaults to 3.)
+--------------------------------------------------------------------------------
+Relay
+index: 116
+orlistenaddress
+IP[:PORT]
+Bind to this IP address to listen for connections from Tor clients and servers. If you specify a port, bind to this port rather than the one specified in ORPort. (Default: 0.0.0.0) This directive can be specified multiple times to bind to multiple addresses/ports.
+--------------------------------------------------------------------------------
+Relay
+index: 115
+orport
+PORT
+Advertise this port to listen for connections from Tor clients and servers.
+--------------------------------------------------------------------------------
+General
+index: 38
+outboundbindaddress
+IP
+Make all outbound connections originate from the IP address specified. This is only useful when you have multiple network interfaces, and you want all of Tor's outgoing connections to use a single one. This setting will be ignored for connections to the loopback addresses (127.0.0.0/8 and ::1).
+--------------------------------------------------------------------------------
+General
+index: 6
+perconnbwburst
+N bytes|KB|MB|GB
+If set, do separate rate limiting for each connection from a non-relay. You should never need to change this value, since a network-wide value is published in the consensus and your relay will use that value. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 5
+perconnbwrate
+N bytes|KB|MB|GB
+If set, do separate rate limiting for each connection from a non-relay. You should never need to change this value, since a network-wide value is published in the consensus and your relay will use that value. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 39
+pidfile
+FILE
+On startup, write our PID to FILE. On clean shutdown, remove FILE.
+--------------------------------------------------------------------------------
+General
+index: 49
+prefertunneleddirconns
+0|1
+If non-zero, we will avoid directory servers that don't support tunneled directory connections, when possible. (Default: 1)
+--------------------------------------------------------------------------------
+General
+index: 40
+protocolwarnings
+0|1
+If 1, Tor will log with severity 'warn' various cases of other parties not following the Tor specification. Otherwise, they are logged with severity 'info'. (Default: 0)
+--------------------------------------------------------------------------------
+Hidden Service
+index: 169
+publishhidservdescriptors
+0|1
+If set to 0, Tor will run any hidden services you configure, but it won't advertise them to the rendezvous directory. This option is only useful if you're using a Tor controller that handles hidserv publishing for you. (Default: 1)
+--------------------------------------------------------------------------------
+Relay
+index: 117
+publishserverdescriptor
+0|1|v1|v2|v3|bridge|hidserv,...
+This option is only considered if you have an ORPort defined. You can choose multiple arguments, separated by commas. If set to 0, Tor will act as a server but it will not publish its descriptor to the directory authorities. (This is useful if you're testing out your server, or if you're using a Tor controller that handles directory publishing for you.) Otherwise, Tor will publish its descriptor to all directory authorities of the type(s) specified. The value "1" is the default, which means "publish to the appropriate authorities".
+--------------------------------------------------------------------------------
+Client
+index: 66
+reachableaddresses
+ADDR[/MASK][:PORT]...
+A comma-separated list of IP addresses and ports that your firewall allows you to connect to. The format is as for the addresses in ExitPolicy, except that "accept" is understood unless "reject" is explicitly provided. For example, 'ReachableAddresses 99.0.0.0/8, reject 18.0.0.0/8:80, accept *:80' means that your firewall allows connections to everything inside net 99, rejects port 80 connections to net 18, and accepts connections to port 80 otherwise. (Default: 'accept *:*'.)
+--------------------------------------------------------------------------------
+Client
+index: 67
+reachablediraddresses
+ADDR[/MASK][:PORT]...
+Like ReachableAddresses, a list of addresses and ports. Tor will obey these restrictions when fetching directory information, using standard HTTP GET requests. If not set explicitly then the value of ReachableAddresses is used. If HTTPProxy is set then these connections will go through that proxy.
+--------------------------------------------------------------------------------
+Client
+index: 68
+reachableoraddresses
+ADDR[/MASK][:PORT]...
+Like ReachableAddresses, a list of addresses and ports. Tor will obey these restrictions when connecting to Onion Routers, using TLS/SSL. If not set explicitly then the value of ReachableAddresses is used. If HTTPSProxy is set then these connections will go through that proxy. 
+
+The separation between ReachableORAddresses and ReachableDirAddresses is only interesting when you are connecting through proxies (see HTTPProxy and HTTPSProxy). Most proxies limit TLS connections (which Tor uses to connect to Onion Routers) to port 443, and some limit HTTP GET requests (which Tor uses for fetching directory information) to port 80.
+--------------------------------------------------------------------------------
+Authority
+index: 150
+recommendedclientversions
+STRING
+STRING is a comma-separated list of Tor versions currently believed to be safe for clients to use. This information is included in version 2 directories. If this is not set then the value of RecommendedVersions is used. When this is set then VersioningAuthoritativeDirectory should be set too.
+--------------------------------------------------------------------------------
+Authority
+index: 151
+recommendedserverversions
+STRING
+STRING is a comma-separated list of Tor versions currently believed to be safe for servers to use. This information is included in version 2 directories. If this is not set then the value of RecommendedVersions is used. When this is set then VersioningAuthoritativeDirectory should be set too.
+--------------------------------------------------------------------------------
+Authority
+index: 149
+recommendedversions
+STRING
+STRING is a comma-separated list of Tor versions currently believed to be safe. The list is included in each directory, and nodes which pull down the directory learn whether they need to upgrade. This option can appear multiple times: the values from multiple lines are spliced together. When this is set then VersioningAuthoritativeDirectory should be set too.
+--------------------------------------------------------------------------------
+Client
+index: 103
+rejectplaintextports
+port,port,...
+Like WarnPlaintextPorts, but instead of warning about risky port uses, Tor will instead refuse to make the connection. (Default: None).
+--------------------------------------------------------------------------------
+General
+index: 4
+relaybandwidthburst
+N bytes|KB|MB|GB
+Limit the maximum token bucket size (also known as the burst) for _relayed traffic_ to the given number of bytes in each direction. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 3
+relaybandwidthrate
+N bytes|KB|MB|GB
+If defined, a separate token bucket limits the average incoming bandwidth usage for _relayed traffic_ on this node to the specified number of bytes per second, and the average outgoing bandwidth usage to that same value. Relayed traffic currently is calculated to include answers to directory requests, but that may change in future versions. (Default: 0)
+--------------------------------------------------------------------------------
+Hidden Service
+index: 172
+rendpostperiod
+N seconds|minutes|hours|days|weeks
+Every time the specified period elapses, Tor uploads any rendezvous service descriptors to the directory servers. This information is also uploaded whenever it changes. (Default: 20 minutes)
+--------------------------------------------------------------------------------
+General
+index: 41
+runasdaemon
+0|1
+If 1, Tor forks and daemonizes to the background. This option has no effect on Windows; instead you should use the --service command-line option. (Default: 0)
+--------------------------------------------------------------------------------
+General
+index: 42
+safelogging
+0|1|relay
+Tor can scrub potentially sensitive strings from log messages (e.g. addresses) by replacing them with the string [scrubbed]. This way logs can still be useful, but they don't leave behind personally identifying information about what sites a user might have visited. 
+
+If this option is set to 0, Tor will not perform any scrubbing, if it is set to 1, all potentially sensitive strings are replaced. If it is set to relay, all log messages generated when acting as a relay are sanitized, but all messages generated when acting as a client are not. (Default: 1)
+--------------------------------------------------------------------------------
+Client
+index: 85
+safesocks
+0|1
+When this option is enabled, Tor will reject application connections that use unsafe variants of the socks protocol  ones that only provide an IP address, meaning the application is doing a DNS resolve first. Specifically, these are socks4 and socks5 when not doing remote DNS. (Defaults to 0.)
+--------------------------------------------------------------------------------
+Relay
+index: 122
+serverdnsallowbrokenconfig
+0|1
+If this option is false, Tor exits immediately if there are problems parsing the system DNS configuration or connecting to nameservers. Otherwise, Tor continues to periodically retry the system nameservers until it eventually succeeds. (Defaults to "1".)
+--------------------------------------------------------------------------------
+Relay
+index: 126
+serverdnsallownonrfc953hostnames
+0|1
+When this option is disabled, Tor does not try to resolve hostnames containing illegal characters (like @ and :) rather than sending them to an exit node to be resolved. This helps trap accidental attempts to resolve URLs and so on. This option only affects name lookups that your server does on behalf of clients. (Default: 0)
+--------------------------------------------------------------------------------
+Relay
+index: 124
+serverdnsdetecthijacking
+0|1
+When this option is set to 1, we will test periodically to determine whether our local nameservers have been configured to hijack failing DNS requests (usually to an advertising site). If they are, we will attempt to correct this. This option only affects name lookups that your server does on behalf of clients. (Defaults to "1".)
+--------------------------------------------------------------------------------
+Relay
+index: 128
+serverdnsrandomizecase
+0|1
+When this option is set, Tor sets the case of each character randomly in outgoing DNS requests, and makes sure that the case matches in DNS replies. This so-called "0x20 hack" helps resist some types of DNS poisoning attack. For more information, see "Increased DNS Forgery Resistance through 0x20-Bit Encoding". This option only affects name lookups that your server does on behalf of clients. (Default: 1)
+--------------------------------------------------------------------------------
+Relay
+index: 121
+serverdnsresolvconffile
+filename
+Overrides the default DNS configuration with the configuration in filename. The file format is the same as the standard Unix "resolv.conf" file (7). This option, like all other ServerDNS options, only affects name lookups that your server does on behalf of clients. (Defaults to use the system DNS configuration.)
+--------------------------------------------------------------------------------
+Relay
+index: 123
+serverdnssearchdomains
+0|1
+If set to 1, then we will search for addresses in the local search domain. For example, if this system is configured to believe it is in "example.com", and a client tries to connect to "www", the client will be connected to "www.example.com". This option only affects name lookups that your server does on behalf of clients. (Defaults to "0".)
+--------------------------------------------------------------------------------
+Relay
+index: 125
+serverdnstestaddresses
+address,address,...
+When we're detecting DNS hijacking, make sure that these valid addresses aren't getting redirected. If they are, then our DNS is completely useless, and we'll reset our exit policy to "reject :". This option only affects name lookups that your server does on behalf of clients. (Defaults to "www.google.com, www.mit.edu, www.yahoo.com, www.slashdot.org".)
+--------------------------------------------------------------------------------
+Relay
+index: 118
+shutdownwaitlength
+NUM
+When we get a SIGINT and we're a server, we begin shutting down: we close listeners and start refusing new circuits. After NUM seconds, we exit. If we get a second SIGINT, we exit immedi- ately. (Default: 30 seconds)
+--------------------------------------------------------------------------------
+General
+index: 31
+socks4proxy
+host[:port]
+Tor will make all OR connections through the SOCKS 4 proxy at host:port (or host:1080 if port is not specified).
+--------------------------------------------------------------------------------
+General
+index: 32
+socks5proxy
+host[:port]
+Tor will make all OR connections through the SOCKS 5 proxy at host:port (or host:1080 if port is not specified).
+--------------------------------------------------------------------------------
+General
+index: 34
+socks5proxypassword
+password
+If defined, authenticate to the SOCKS 5 server using username and password in accordance to RFC 1929. Both username and password must be between 1 and 255 characters.
+--------------------------------------------------------------------------------
+General
+index: 33
+socks5proxyusername
+username
+
+--------------------------------------------------------------------------------
+Client
+index: 76
+sockslistenaddress
+IP[:PORT]
+Bind to this address to listen for connections from Socks-speaking applications. (Default: 127.0.0.1) You can also specify a port (e.g. 192.168.0.1:9100). This directive can be specified multiple times to bind to multiple addresses/ports.
+--------------------------------------------------------------------------------
+Client
+index: 77
+sockspolicy
+policy,policy,...
+Set an entrance policy for this server, to limit who can connect to the SocksPort and DNSPort ports. The policies have the same form as exit policies below.
+--------------------------------------------------------------------------------
+Client
+index: 75
+socksport
+PORT
+Advertise this port to listen for connections from Socks-speaking applications. Set this to 0 if you don't want to allow application connections. (Default: 9050)
+--------------------------------------------------------------------------------
+Client
+index: 78
+sockstimeout
+NUM
+Let a socks connection wait NUM seconds handshaking, and NUM seconds unattached waiting for an appropriate circuit, before we fail it. (Default: 2 minutes.)
+--------------------------------------------------------------------------------
+Client
+index: 62
+strictnodes
+0|1
+If 1 and EntryNodes config option is set, Tor will never use any nodes besides those listed in EntryNodes for the first hop of a normal circuit. If 1 and ExitNodes config option is set, Tor will never use any nodes besides those listed in ExitNodes for the last hop of a normal exit circuit. Note that Tor might still use these nodes for non-exit circuits such as one-hop directory fetches or hidden service support circuits.
+--------------------------------------------------------------------------------
+Testing
+index: 177
+testingauthdirtimetolearnreachability
+N minutes|hours
+After starting as an authority, do not make claims about whether routers are Running until this much time has passed. Changing this requires that TestingTorNetwork is set. (Default: 30 minutes)
+--------------------------------------------------------------------------------
+Testing
+index: 178
+testingestimateddescriptorpropagationtime
+N minutes|hours
+Clients try downloading router descriptors from directory caches after this time. Changing this requires that TestingTorNetwork is set. (Default: 10 minutes) 
+
+SIGNALS
+--------------------------------------------------------------------------------
+Testing
+index: 173
+testingtornetwork
+0|1
+If set to 1, Tor adjusts default values of the configuration options below, so that it is easier to set up a testing Tor network. May only be set if non-default set of DirServers is set. Cannot be unset while Tor is running. (Default: 0) 
+
+    ServerDNSAllowBrokenConfig 1
+    DirAllowPrivateAddresses 1
+    EnforceDistinctSubnets 0
+    AssumeReachable 1
+    AuthDirMaxServersPerAddr 0
+    AuthDirMaxServersPerAuthAddr 0
+    ClientDNSRejectInternalAddresses 0
+    ExitPolicyRejectPrivate 0
+    V3AuthVotingInterval 5 minutes
+    V3AuthVoteDelay 20 seconds
+    V3AuthDistDelay 20 seconds
+    TestingV3AuthInitialVotingInterval 5 minutes
+    TestingV3AuthInitialVoteDelay 20 seconds
+    TestingV3AuthInitialDistDelay 20 seconds
+    TestingAuthDirTimeToLearnReachability 0 minutes
+    TestingEstimatedDescriptorPropagationTime 0 minutes
+--------------------------------------------------------------------------------
+Testing
+index: 176
+testingv3authinitialdistdelay
+N minutes|hours
+Like TestingV3AuthInitialDistDelay, but for initial voting interval before the first consensus has been created. Changing this requires that TestingTorNetwork is set. (Default: 5 minutes)
+--------------------------------------------------------------------------------
+Testing
+index: 175
+testingv3authinitialvotedelay
+N minutes|hours
+Like TestingV3AuthInitialVoteDelay, but for initial voting interval before the first consensus has been created. Changing this requires that TestingTorNetwork is set. (Default: 5 minutes)
+--------------------------------------------------------------------------------
+Testing
+index: 174
+testingv3authinitialvotinginterval
+N minutes|hours
+Like V3AuthVotingInterval, but for initial voting interval before the first consensus has been created. Changing this requires that TestingTorNetwork is set. (Default: 30 minutes)
+--------------------------------------------------------------------------------
+Client
+index: 86
+testsocks
+0|1
+When this option is enabled, Tor will make a notice-level log entry for each connection to the Socks port indicating whether the request used a safe socks protocol or an unsafe one (see above entry on SafeSocks). This helps to determine whether an application using Tor is possibly leaking DNS requests. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 79
+trackhostexits
+host,.domain,...
+For each value in the comma separated list, Tor will track recent connections to hosts that match this value and attempt to reuse the same exit node for each. If the value is prepended with a '.', it is treated as matching an entire domain. If one of the values is just a '.', it means match everything. This option is useful if you frequently connect to sites that will expire all your authentication cookies (i.e. log you out) if your IP address changes. Note that this option does have the disadvantage of making it more clear that a given history is associated with a single user. However, most people who would wish to observe this will observe it through cookies or other protocol-specific means anyhow.
+--------------------------------------------------------------------------------
+Client
+index: 80
+trackhostexitsexpire
+NUM
+Since exit servers go up and down, it is desirable to expire the association between host and exit server after NUM seconds. The default is 1800 seconds (30 minutes).
+--------------------------------------------------------------------------------
+Client
+index: 92
+translistenaddress
+IP[:PORT]
+Bind to this address to listen for transparent proxy connections. (Default: 127.0.0.1). This is useful for exporting a transparent proxy server to an entire network.
+--------------------------------------------------------------------------------
+Client
+index: 91
+transport
+PORT
+If non-zero, enables transparent proxy support on PORT (by convention, 9040). Requires OS support for transparent proxies, such as BSDs' pf or Linux's IPTables. If you're planning to use Tor as a transparent proxy for a network, you'll want to examine and change VirtualAddrNetwork from the default setting. You'll also want to set the TransListenAddress option for the network you'd like to proxy. (Default: 0).
+--------------------------------------------------------------------------------
+General
+index: 48
+tunneldirconns
+0|1
+If non-zero, when a directory server we contact supports it, we will build a one-hop circuit and make an encrypted connection via its ORPort. (Default: 1)
+--------------------------------------------------------------------------------
+Client
+index: 81
+updatebridgesfromauthority
+0|1
+When set (along with UseBridges), Tor will try to fetch bridge descriptors from the configured bridge authorities when feasible. It will fall back to a direct request if the authority responds with a 404. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 82
+usebridges
+0|1
+When set, Tor will fetch descriptors for each bridge listed in the "Bridge" config lines, and use these relays as both entry guards and directory guards. (Default: 0)
+--------------------------------------------------------------------------------
+Client
+index: 83
+useentryguards
+0|1
+If this option is set to 1, we pick a few long-term entry servers, and try to stick with them. This is desirable because constantly changing servers increases the odds that an adversary who owns some servers will observe a fraction of your paths. (Defaults to 1.)
+--------------------------------------------------------------------------------
+General
+index: 43
+user
+UID
+On startup, setuid to this user and setgid to their primary group.
+--------------------------------------------------------------------------------
+Directory
+index: 137
+v1authoritativedirectory
+0|1
+When this option is set in addition to AuthoritativeDirectory, Tor generates version 1 directory and running-routers documents (for legacy Tor clients up to 0.1.0.x).
+--------------------------------------------------------------------------------
+Directory
+index: 138
+v2authoritativedirectory
+0|1
+When this option is set in addition to AuthoritativeDirectory, Tor generates version 2 network statuses and serves descriptors, etc as described in doc/spec/dir-spec-v2.txt (for Tor clients and servers running 0.1.1.x and 0.1.2.x).
+--------------------------------------------------------------------------------
+Authority
+index: 165
+v3authdistdelay
+N minutes|hours
+V3 authoritative directories only. Configures the server's preferred delay between publishing its consensus and signature and assuming it has all the signatures from all the other authorities. Note that the actual time used is not the server's preferred time, but the consensus of all preferences. (Default: 5 minutes.)
+--------------------------------------------------------------------------------
+Authority
+index: 166
+v3authnintervalsvalid
+NUM
+V3 authoritative directories only. Configures the number of VotingIntervals for which each consensus should be valid for. Choosing high numbers increases network partitioning risks; choosing low numbers increases directory traffic. Note that the actual number of intervals used is not the server's preferred number, but the consensus of all preferences. Must be at least 2. (Default: 3.)
+--------------------------------------------------------------------------------
+Directory
+index: 139
+v3authoritativedirectory
+0|1
+When this option is set in addition to AuthoritativeDirectory, Tor generates version 3 network statuses and serves descriptors, etc as described in doc/spec/dir-spec.txt (for Tor clients and servers running at least 0.2.0.x).
+--------------------------------------------------------------------------------
+Authority
+index: 164
+v3authvotedelay
+N minutes|hours
+V3 authoritative directories only. Configures the server's preferred delay between publishing its vote and assuming it has all the votes from all the other authorities. Note that the actual time used is not the server's preferred time, but the consensus of all preferences. (Default: 5 minutes.)
+--------------------------------------------------------------------------------
+Authority
+index: 163
+v3authvotinginterval
+N minutes|hours
+V3 authoritative directories only. Configures the server's preferred voting interval. Note that voting will actually happen at an interval chosen by consensus from all the authorities' preferred intervals. This time SHOULD divide evenly into a day. (Default: 1 hour)
+--------------------------------------------------------------------------------
+Directory
+index: 140
+versioningauthoritativedirectory
+0|1
+When this option is set to 1, Tor adds information on which versions of Tor are still believed safe for use to the published directory. Each version 1 authority is automatically a versioning authority; version 2 authorities provide this service optionally. See RecommendedVersions, RecommendedClientVersions, and RecommendedServerVersions.
+--------------------------------------------------------------------------------
+Client
+index: 87
+virtualaddrnetwork
+Address/bits
+When a controller asks for a virtual (unused) address with the MAPADDRESS command, Tor picks an unassigned address from this range. (Default: 127.192.0.0/10) 
+
+When providing proxy server service to a network of computers using a tool like dns-proxy-tor, change this address to "10.192.0.0/10" or "172.16.0.0/12". The default VirtualAddrNetwork address range on a properly configured machine will route to the loopback interface. For local use, no change to the default VirtualAddrNetwork setting is needed.
+--------------------------------------------------------------------------------
+Client
+index: 102
+warnplaintextports
+port,port,...
+Tells Tor to issue a warnings whenever the user tries to make an anonymous connection to one of these ports. This option is designed to alert users to services that risk sending passwords in the clear. (Default: 23,109,110,143).
diff --git a/seth/starter.py b/seth/starter.py
new file mode 100644
index 0000000..2b98706
--- /dev/null
+++ b/seth/starter.py
@@ -0,0 +1,297 @@
+"""
+Command line application for monitoring Tor relays, providing real time status
+information. This starts the application, parsing arguments and getting a Tor
+connection.
+"""
+
+import curses
+import locale
+import logging
+import os
+import platform
+import sys
+import time
+import threading
+
+import seth
+import seth.arguments
+import seth.controller
+import seth.util.panel
+import seth.util.tor_config
+import seth.util.tracker
+import seth.util.ui_tools
+
+import stem
+import stem.util.log
+import stem.util.system
+
+from seth.util import log, BASE_DIR, init_controller, msg, uses_settings
+
+
+ at uses_settings
+def main(config):
+  config.set('start_time', str(int(time.time())))
+
+  try:
+    args = seth.arguments.parse(sys.argv[1:])
+    config.set('startup.events', args.logged_events)
+  except ValueError as exc:
+    print exc
+    sys.exit(1)
+
+  if args.print_help:
+    print seth.arguments.get_help()
+    sys.exit()
+  elif args.print_version:
+    print seth.arguments.get_version()
+    sys.exit()
+
+  if args.debug_path is not None:
+    try:
+      _setup_debug_logging(args)
+      print msg('debug.saving_to_path', path = args.debug_path)
+    except IOError as exc:
+      print msg('debug.unable_to_write_file', path = args.debug_path, error = exc.strerror)
+      sys.exit(1)
+
+  _load_user_sethrc(args.config)
+
+  control_port = (args.control_address, args.control_port)
+  control_socket = args.control_socket
+
+  # If the user explicitely specified an endpoint then just try to connect to
+  # that.
+
+  if args.user_provided_socket and not args.user_provided_port:
+    control_port = None
+  elif args.user_provided_port and not args.user_provided_socket:
+    control_socket = None
+
+  controller = init_controller(
+    control_port = control_port,
+    control_socket = control_socket,
+    password = config.get('tor.password', None),
+    password_prompt = True,
+    chroot_path = config.get('tor.chroot', ''),
+  )
+
+  if controller is None:
+    exit(1)
+
+  _warn_if_root(controller)
+  _warn_if_unable_to_get_pid(controller)
+  _setup_freebsd_chroot(controller)
+  _notify_of_unknown_events()
+  _clear_password()
+  _load_tor_config_descriptions()
+  _use_english_subcommands()
+  _use_unicode()
+  _set_process_name()
+
+  try:
+    curses.wrapper(seth.controller.start_seth)
+  except UnboundLocalError as exc:
+    if os.environ['TERM'] != 'xterm':
+      print msg('setup.unknown_term', term = os.environ['TERM'])
+    else:
+      raise exc
+  except KeyboardInterrupt:
+    pass  # skip printing a stack trace
+  finally:
+    seth.util.panel.HALT_ACTIVITY = True
+    _shutdown_daemons(controller)
+
+
+def _setup_debug_logging(args):
+  """
+  Configures us to log at stem's trace level to a debug log path. This starts
+  it off with some general diagnostic information.
+  """
+
+  debug_dir = os.path.dirname(args.debug_path)
+
+  if not os.path.exists(debug_dir):
+    os.makedirs(debug_dir)
+
+  debug_handler = logging.FileHandler(args.debug_path, mode = 'w')
+  debug_handler.setLevel(stem.util.log.logging_level(stem.util.log.TRACE))
+  debug_handler.setFormatter(logging.Formatter(
+    fmt = '%(asctime)s [%(levelname)s] %(message)s',
+    datefmt = '%m/%d/%Y %H:%M:%S'
+  ))
+
+  logger = stem.util.log.get_logger()
+  logger.addHandler(debug_handler)
+
+  sethrc_content = "[file doesn't exist]"
+
+  if os.path.exists(args.config):
+    try:
+      with open(args.config) as sethrc_file:
+        sethrc_content = sethrc_file.read()
+    except IOError as exc:
+      sethrc_content = "[unable to read file: %s]" % exc.strerror
+
+  log.trace(
+    'debug.header',
+    seth_version = seth.__version__,
+    stem_version = stem.__version__,
+    python_version = '.'.join(map(str, sys.version_info[:3])),
+    system = platform.system(),
+    platform = ' '.join(platform.dist()),
+    sethrc_path = args.config,
+    sethrc_content = sethrc_content,
+  )
+
+
+ at uses_settings
+def _load_user_sethrc(path, config):
+  """
+  Loads user's personal sethrc if it's available.
+  """
+
+  if os.path.exists(path):
+    try:
+      config.load(path)
+
+      # If the user provided us with a chroot then validate and normalize the
+      # path.
+
+      chroot = config.get('tor.chroot', '').strip().rstrip(os.path.sep)
+
+      if chroot and not os.path.exists(chroot):
+        log.notice('setup.chroot_doesnt_exist', path = chroot)
+        config.set('tor.chroot', '')
+      else:
+        config.set('tor.chroot', chroot)  # use the normalized path
+    except IOError as exc:
+      log.warn('config.unable_to_read_file', error = exc.strerror)
+  else:
+    log.notice('config.nothing_loaded', path = path)
+
+
+def _warn_if_root(controller):
+  """
+  Give a notice if tor or seth are running with root.
+  """
+
+  tor_user = controller.get_user(None)
+
+  if tor_user == 'root':
+    log.notice('setup.tor_is_running_as_root')
+  elif os.getuid() == 0:
+    tor_user = tor_user if tor_user else '<tor user>'
+    log.notice('setup.seth_is_running_as_root', tor_user = tor_user)
+
+
+def _warn_if_unable_to_get_pid(controller):
+  """
+  Provide a warning if we're unable to determine tor's pid. This in turn will
+  limit our ability to query information about the process later.
+  """
+
+  try:
+    controller.get_pid()
+  except ValueError:
+    log.warn('setup.unable_to_determine_pid')
+
+
+ at uses_settings
+def _setup_freebsd_chroot(controller, config):
+  """
+  If we're running under FreeBSD then check the system for a chroot path.
+  """
+
+  if not config.get('tor.chroot', None) and platform.system() == 'FreeBSD':
+    jail_chroot = stem.util.system.bsd_jail_path(controller.get_pid(0))
+
+    if jail_chroot and os.path.exists(jail_chroot):
+      log.info('setup.set_freebsd_chroot', path = jail_chroot)
+      config.set('tor.chroot', jail_chroot)
+
+
+def _notify_of_unknown_events():
+  """
+  Provides a notice about any event types tor supports but seth doesn't.
+  """
+
+  missing_events = seth.arguments.missing_event_types()
+
+  if missing_events:
+    log.info('setup.unknown_event_types', event_types = ', '.join(missing_events))
+
+
+ at uses_settings
+def _clear_password(config):
+  """
+  Removing the reference to our controller password so the memory can be freed.
+  Without direct memory access this is about the best we can do to clear it.
+  """
+
+  config.set('tor.password', '')
+
+
+def _load_tor_config_descriptions():
+  """
+  Attempt to determine descriptions for tor's configuration options.
+  """
+
+  seth.util.tor_config.load_configuration_descriptions(BASE_DIR)
+
+
+def _use_english_subcommands():
+  """
+  Make subcommands we run (ps, netstat, etc) provide us with English results.
+  This is important so we can parse the output.
+  """
+
+  os.putenv('LANG', 'C')
+
+
+ at uses_settings
+def _use_unicode(config):
+  """
+  If using our LANG variable for rendering multi-byte characters lets us
+  get unicode support then then use it. This needs to be done before
+  initializing curses.
+  """
+
+  if not config.get('features.printUnicode', True):
+    return
+
+  is_lang_unicode = 'utf-' in os.getenv('LANG', '').lower()
+
+  if is_lang_unicode and seth.util.ui_tools.is_wide_characters_supported():
+    locale.setlocale(locale.LC_ALL, '')
+
+
+def _set_process_name():
+  """
+  Attempts to rename our process from "python setup.py <input args>" to
+  "seth <input args>".
+  """
+
+  stem.util.system.set_process_name("seth\0%s" % "\0".join(sys.argv[1:]))
+
+
+def _shutdown_daemons(controller):
+  """
+  Stops and joins on worker threads.
+  """
+
+  close_controller = threading.Thread(target = controller.close)
+  close_controller.setDaemon(True)
+  close_controller.start()
+
+  halt_threads = [
+    seth.controller.stop_controller(),
+    seth.util.tracker.stop_trackers(),
+    close_controller,
+  ]
+
+  for thread in halt_threads:
+    thread.join()
+
+
+if __name__ == '__main__':
+  main()
diff --git a/seth/torrc_panel.py b/seth/torrc_panel.py
new file mode 100644
index 0000000..813148f
--- /dev/null
+++ b/seth/torrc_panel.py
@@ -0,0 +1,351 @@
+"""
+Panel displaying the torrc or sethrc with the validation done against it.
+"""
+
+import math
+import curses
+import threading
+
+import seth.popups
+
+from seth.util import panel, tor_config, tor_controller, ui_tools
+
+from stem.control import State
+from stem.util import conf, enum, str_tools
+
+
+def conf_handler(key, value):
+  if key == "features.config.file.max_lines_per_entry":
+    return max(1, value)
+
+
+CONFIG = conf.config_dict("seth", {
+  "features.config.file.showScrollbars": True,
+  "features.config.file.max_lines_per_entry": 8,
+}, conf_handler)
+
+# TODO: The sethrc use case is incomplete. There should be equivilant reloading
+# and validation capabilities to the torrc.
+Config = enum.Enum("TORRC", "ARMRC")  # configuration file types that can be displayed
+
+
+class TorrcPanel(panel.Panel):
+  """
+  Renders the current torrc or sethrc with syntax highlighting in a scrollable
+  area.
+  """
+
+  def __init__(self, stdscr, config_type):
+    panel.Panel.__init__(self, stdscr, "torrc", 0)
+
+    self.vals_lock = threading.RLock()
+    self.config_type = config_type
+    self.scroll = 0
+    self.show_line_num = True     # shows left aligned line numbers
+    self.strip_comments = False   # drops comments and extra whitespace
+
+    # height of the content when last rendered (the cached value is invalid if
+    # _last_content_height_args is None or differs from the current dimensions)
+
+    self._last_content_height = 1
+    self._last_content_height_args = None
+
+    # listens for tor reload (sighup) events
+
+    controller = tor_controller()
+    controller.add_status_listener(self.reset_listener)
+
+    if controller.is_alive():
+      self.reset_listener(None, State.INIT, None)
+
+  def reset_listener(self, controller, event_type, _):
+    """
+    Reloads and displays the torrc on tor reload (sighup) events.
+    """
+
+    if event_type == State.INIT:
+      # loads the torrc and provides warnings in case of validation errors
+
+      try:
+        loaded_torrc = tor_config.get_torrc()
+        loaded_torrc.load(True)
+        loaded_torrc.log_validation_issues()
+        self.redraw(True)
+      except:
+        pass
+    elif event_type == State.RESET:
+      try:
+        tor_config.get_torrc().load(True)
+        self.redraw(True)
+      except:
+        pass
+
+  def set_comments_visible(self, is_visible):
+    """
+    Sets if comments and blank lines are shown or stripped.
+
+    Arguments:
+      is_visible - displayed comments and blank lines if true, strips otherwise
+    """
+
+    self.strip_comments = not is_visible
+    self._last_content_height_args = None
+    self.redraw(True)
+
+  def set_line_number_visible(self, is_visible):
+    """
+    Sets if line numbers are shown or hidden.
+
+    Arguments:
+      is_visible - displays line numbers if true, hides otherwise
+    """
+
+    self.show_line_num = is_visible
+    self._last_content_height_args = None
+    self.redraw(True)
+
+  def reload_torrc(self):
+    """
+    Reloads the torrc, displaying an indicator of success or failure.
+    """
+
+    try:
+      tor_config.get_torrc().load()
+      self._last_content_height_args = None
+      self.redraw(True)
+      result_msg = "torrc reloaded"
+    except IOError:
+      result_msg = "failed to reload torrc"
+
+    self._last_content_height_args = None
+    self.redraw(True)
+    seth.popups.show_msg(result_msg, 1)
+
+  def handle_key(self, key):
+    with self.vals_lock:
+      if key.is_scroll():
+        page_height = self.get_preferred_size()[0] - 1
+        new_scroll = ui_tools.get_scroll_position(key, self.scroll, page_height, self._last_content_height)
+
+        if self.scroll != new_scroll:
+          self.scroll = new_scroll
+          self.redraw(True)
+      elif key.match('n'):
+        self.set_line_number_visible(not self.show_line_num)
+      elif key.match('s'):
+        self.set_comments_visible(self.strip_comments)
+      elif key.match('r'):
+        self.reload_torrc()
+      else:
+        return False
+
+      return True
+
+  def set_visible(self, is_visible):
+    if not is_visible:
+      self._last_content_height_args = None  # redraws when next displayed
+
+    panel.Panel.set_visible(self, is_visible)
+
+  def get_help(self):
+    return [
+      ('up arrow', 'scroll up a line', None),
+      ('down arrow', 'scroll down a line', None),
+      ('page up', 'scroll up a page', None),
+      ('page down', 'scroll down a page', None),
+      ('s', 'comment stripping', 'on' if self.strip_comments else 'off'),
+      ('n', 'line numbering', 'on' if self.show_line_num else 'off'),
+      ('r', 'reload torrc', None),
+      ('x', 'reset tor (issue sighup)', None),
+    ]
+
+  def draw(self, width, height):
+    self.vals_lock.acquire()
+
+    # If true, we assume that the cached value in self._last_content_height is
+    # still accurate, and stop drawing when there's nothing more to display.
+    # Otherwise the self._last_content_height is suspect, and we'll process all
+    # the content to check if it's right (and redraw again with the corrected
+    # height if not).
+
+    trust_last_content_height = self._last_content_height_args == (width, height)
+
+    # restricts scroll location to valid bounds
+
+    self.scroll = max(0, min(self.scroll, self._last_content_height - height + 1))
+
+    rendered_contents, corrections, conf_location = None, {}, None
+
+    if self.config_type == Config.TORRC:
+      loaded_torrc = tor_config.get_torrc()
+      loaded_torrc.get_lock().acquire()
+      conf_location = loaded_torrc.get_config_location()
+
+      if not loaded_torrc.is_loaded():
+        rendered_contents = ["### Unable to load the torrc ###"]
+      else:
+        rendered_contents = loaded_torrc.get_display_contents(self.strip_comments)
+
+        # constructs a mapping of line numbers to the issue on it
+
+        corrections = dict((line_number, (issue, msg)) for line_number, issue, msg in loaded_torrc.get_corrections())
+
+      loaded_torrc.get_lock().release()
+    else:
+      loaded_sethrc = conf.get_config("seth")
+      conf_location = loaded_sethrc._path
+      rendered_contents = list(loaded_sethrc._raw_contents)
+
+    # offset to make room for the line numbers
+
+    line_number_offset = 0
+
+    if self.show_line_num:
+      if len(rendered_contents) == 0:
+        line_number_offset = 2
+      else:
+        line_number_offset = int(math.log10(len(rendered_contents))) + 2
+
+    # draws left-hand scroll bar if content's longer than the height
+
+    scroll_offset = 0
+
+    if CONFIG["features.config.file.showScrollbars"] and self._last_content_height > height - 1:
+      scroll_offset = 3
+      self.add_scroll_bar(self.scroll, self.scroll + height - 1, self._last_content_height, 1)
+
+    display_line = -self.scroll + 1  # line we're drawing on
+
+    # draws the top label
+
+    if self.is_title_visible():
+      source_label = "Tor" if self.config_type == Config.TORRC else "Arm"
+      location_label = " (%s)" % conf_location if conf_location else ""
+      self.addstr(0, 0, "%s Configuration File%s:" % (source_label, location_label), curses.A_STANDOUT)
+
+    is_multiline = False  # true if we're in the middle of a multiline torrc entry
+
+    for line_number in range(0, len(rendered_contents)):
+      line_text = rendered_contents[line_number]
+      line_text = line_text.rstrip()  # remove ending whitespace
+
+      # blank lines are hidden when stripping comments
+
+      if self.strip_comments and not line_text:
+        continue
+
+      # splits the line into its component (msg, format) tuples
+
+      line_comp = {
+        'option': ['', (curses.A_BOLD, 'green')],
+        'argument': ['', (curses.A_BOLD, 'cyan')],
+        'correction': ['', (curses.A_BOLD, 'cyan')],
+        'comment': ['', ('white',)],
+      }
+
+      # parses the comment
+
+      comment_index = line_text.find("#")
+
+      if comment_index != -1:
+        line_comp['comment'][0] = line_text[comment_index:]
+        line_text = line_text[:comment_index]
+
+      # splits the option and argument, preserving any whitespace around them
+
+      stripped_line = line_text.strip()
+      option_index = stripped_line.find(" ")
+
+      if is_multiline:
+        # part of a multiline entry started on a previous line so everything
+        # is part of the argument
+        line_comp['argument'][0] = line_text
+      elif option_index == -1:
+        # no argument provided
+        line_comp['option'][0] = line_text
+      else:
+        option_text = stripped_line[:option_index]
+        option_end = line_text.find(option_text) + len(option_text)
+        line_comp['option'][0] = line_text[:option_end]
+        line_comp['argument'][0] = line_text[option_end:]
+
+      # flags following lines as belonging to this multiline entry if it ends
+      # with a slash
+
+      if stripped_line:
+        is_multiline = stripped_line.endswith("\\")
+
+      # gets the correction
+
+      if line_number in corrections:
+        line_issue, line_issue_msg = corrections[line_number]
+
+        if line_issue in (tor_config.ValidationError.DUPLICATE, tor_config.ValidationError.IS_DEFAULT):
+          line_comp['option'][1] = (curses.A_BOLD, 'blue')
+          line_comp['argument'][1] = (curses.A_BOLD, 'blue')
+        elif line_issue == tor_config.ValidationError.MISMATCH:
+          line_comp['argument'][1] = (curses.A_BOLD, 'red')
+          line_comp['correction'][0] = ' (%s)' % line_issue_msg
+        else:
+          # For some types of configs the correction field is simply used to
+          # provide extra data (for instance, the type for tor state fields).
+
+          line_comp['correction'][0] = ' (%s)' % line_issue_msg
+          line_comp['correction'][1] = (curses.A_BOLD, 'magenta')
+
+      # draws the line number
+
+      if self.show_line_num and display_line < height and display_line >= 1:
+        line_number_str = ("%%%ii" % (line_number_offset - 1)) % (line_number + 1)
+        self.addstr(display_line, scroll_offset, line_number_str, curses.A_BOLD, 'yellow')
+
+      # draws the rest of the components with line wrap
+
+      cursor_location, line_offset = line_number_offset + scroll_offset, 0
+      max_lines_per_entry = CONFIG["features.config.file.max_lines_per_entry"]
+      display_queue = [line_comp[entry] for entry in ('option', 'argument', 'correction', 'comment')]
+
+      while display_queue:
+        msg, format = display_queue.pop(0)
+
+        max_msg_size, include_break = width - cursor_location, False
+
+        if len(msg) >= max_msg_size:
+          # message is too long - break it up
+
+          if line_offset == max_lines_per_entry - 1:
+            msg = str_tools.crop(msg, max_msg_size)
+          else:
+            include_break = True
+            msg, remainder = str_tools.crop(msg, max_msg_size, 4, 4, str_tools.Ending.HYPHEN, True)
+            display_queue.insert(0, (remainder.strip(), format))
+
+        draw_line = display_line + line_offset
+
+        if msg and draw_line < height and draw_line >= 1:
+          self.addstr(draw_line, cursor_location, msg, *format)
+
+        # If we're done, and have added content to this line, then start
+        # further content on the next line.
+
+        cursor_location += len(msg)
+        include_break |= not display_queue and cursor_location != line_number_offset + scroll_offset
+
+        if include_break:
+          line_offset += 1
+          cursor_location = line_number_offset + scroll_offset
+
+      display_line += max(line_offset, 1)
+
+      if trust_last_content_height and display_line >= height:
+        break
+
+    if not trust_last_content_height:
+      self._last_content_height_args = (width, height)
+      new_content_height = display_line + self.scroll - 1
+
+      if self._last_content_height != new_content_height:
+        self._last_content_height = new_content_height
+        self.redraw(True)
+
+    self.vals_lock.release()
diff --git a/seth/uninstall b/seth/uninstall
new file mode 100755
index 0000000..3bf9c4a
--- /dev/null
+++ b/seth/uninstall
@@ -0,0 +1,16 @@
+#!/bin/sh
+files="/usr/bin/seth /usr/share/man/man1/seth.1.gz /usr/share/seth"
+
+for i in $files
+do
+  if [ -f $i -o -d $i ]; then
+    rm -rf $i
+    
+    if [ $? = 0 ]; then
+      echo "removed $i"
+    else
+      exit 1
+    fi
+  fi
+done
+
diff --git a/seth/util/__init__.py b/seth/util/__init__.py
new file mode 100644
index 0000000..76830cd
--- /dev/null
+++ b/seth/util/__init__.py
@@ -0,0 +1,203 @@
+"""
+General purpose utilities for a variety of tasks supporting seth features and
+safely working with curses (hiding some of the gory details).
+"""
+
+__all__ = [
+  'log',
+  'panel',
+  'text_input',
+  'tor_config',
+  'tracker',
+  'ui_tools',
+]
+
+import calendar
+import collections
+import os
+import sys
+import time
+
+import stem.connection
+import stem.util.conf
+import stem.util.system
+
+from seth.util import log
+
+TOR_CONTROLLER = None
+BASE_DIR = os.path.sep.join(__file__.split(os.path.sep)[:-2])
+
+StateBandwidth = collections.namedtuple('StateBandwidth', (
+  'read_entries',
+  'write_entries',
+  'last_read_time',
+  'last_write_time',
+))
+
+try:
+  uses_settings = stem.util.conf.uses_settings('seth', os.path.join(BASE_DIR, 'config'), lazy_load = False)
+except IOError as exc:
+  print "Unable to load seth's internal configurations: %s" % exc
+  sys.exit(1)
+
+
+def tor_controller():
+  """
+  Singleton for getting our tor controller connection.
+
+  :returns: :class:`~stem.control.Controller` seth is using
+  """
+
+  return TOR_CONTROLLER
+
+
+def init_controller(*args, **kwargs):
+  """
+  Sets the Controller used by seth. This is a passthrough for Stem's
+  :func:`~stem.connection.connect` function.
+
+  :returns: :class:`~stem.control.Controller` seth is using
+  """
+
+  global TOR_CONTROLLER
+  TOR_CONTROLLER = stem.connection.connect(*args, **kwargs)
+  return TOR_CONTROLLER
+
+
+def join(entries, joiner = ' ', size = None):
+  """
+  Joins a series of strings similar to str.join(), but only up to a given size.
+  This returns an empty string if none of the entries will fit. For example...
+
+    >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 18)
+    'This is a looooong'
+
+    >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 17)
+    'This is a'
+
+    >>> join(['This', 'is', 'a', 'looooong', 'message'], size = 2)
+    ''
+
+  :param list entries: strings to be joined
+  :param str joiner: strings to join the entries with
+  :param int size: maximum length the result can be, there's no length
+    limitation if **None**
+
+  :returns: **str** of the joined entries up to the given length
+  """
+
+  if size is None:
+    return joiner.join(entries)
+
+  result = ''
+
+  for entry in entries:
+    new_result = joiner.join((result, entry)) if result else entry
+
+    if len(new_result) > size:
+      break
+    else:
+      result = new_result
+
+  return result
+
+
+ at uses_settings
+def msg(message, config, **attr):
+  """
+  Provides the given message.
+
+  :param str message: message handle to log
+  :param dict attr: attributes to format the message with
+
+  :returns: **str** that was requested
+  """
+
+  try:
+    return config.get('msg.%s' % message).format(**attr)
+  except:
+    log.notice('BUG: We attempted to use an undefined string resource (%s)' % message)
+    return ''
+
+
+ at uses_settings
+def bandwidth_from_state(config):
+  """
+  Read Tor's state file to determine its recent bandwidth usage. These
+  samplings are at fifteen minute granularity, and can only provide results if
+  we've been running for at least a day. This provides a named tuple with the
+  following...
+
+    * read_entries and write_entries
+
+      List of the average bytes read or written during each fifteen minute
+      period, oldest to newest.
+
+    * last_read_time and last_write_time
+
+      Unix timestamp for when the last entry was recorded.
+
+  :returns: **namedtuple** with the state file's bandwidth informaiton
+
+  :raises: **ValueError** if unable to get the bandwidth information from our
+    state file
+  """
+
+  controller = tor_controller()
+
+  if not controller.is_localhost():
+    raise ValueError('we can only prepopulate bandwidth information for a local tor instance')
+
+  start_time = stem.util.system.start_time(controller.get_pid(None))
+  uptime = time.time() - start_time if start_time else None
+
+  # Only attempt to prepopulate information if we've been running for a day.
+  # Reason is that the state file stores a day's worth of data, and we don't
+  # want to prepopulate with information from a prior tor instance.
+
+  if not uptime:
+    raise ValueError("unable to determine tor's uptime")
+  elif uptime < (24 * 60 * 60):
+    raise ValueError("insufficient uptime, tor must've been running for at least a day")
+
+  # read the user's state file in their data directory (usually '~/.tor')
+
+  data_dir = controller.get_conf('DataDirectory', None)
+
+  if not data_dir:
+    raise ValueError("unable to determine tor's data directory")
+
+  state_path = os.path.join(config.get('tor.chroot', '') + data_dir, 'state')
+
+  try:
+    with open(state_path) as state_file:
+      state_content = state_file.readlines()
+  except IOError as exc:
+    raise ValueError('unable to read the state file at %s, %s' % (state_path, exc))
+
+  # We're interested in two types of entries from our state file...
+  #
+  # * BWHistory*Values - Comma separated list of bytes we read or wrote
+  #   during each fifteen minute period. The last value is an incremental
+  #   counter for our current period, so ignoring that.
+  #
+  # * BWHistory*Ends - When our last sampling was recorded, in UTC.
+
+  attr = {}
+
+  for line in state_content:
+    line = line.strip()
+
+    if line.startswith('BWHistoryReadValues '):
+      attr['read_entries'] = [int(entry) / 900 for entry in line[20:].split(',')[:-1]]
+    elif line.startswith('BWHistoryWriteValues '):
+      attr['write_entries'] = [int(entry) / 900 for entry in line[21:].split(',')[:-1]]
+    elif line.startswith('BWHistoryReadEnds '):
+      attr['last_read_time'] = calendar.timegm(time.strptime(line[18:], '%Y-%m-%d %H:%M:%S')) - 900
+    elif line.startswith('BWHistoryWriteEnds '):
+      attr['last_write_time'] = calendar.timegm(time.strptime(line[19:], '%Y-%m-%d %H:%M:%S')) - 900
+
+  if len(attr) != 4:
+    raise ValueError('bandwidth stats missing from state file')
+
+  return StateBandwidth(**attr)
diff --git a/seth/util/log.py b/seth/util/log.py
new file mode 100644
index 0000000..f2290b9
--- /dev/null
+++ b/seth/util/log.py
@@ -0,0 +1,44 @@
+"""
+Logging utilities, primiarily short aliases for logging a message at various
+runlevels.
+"""
+
+import stem.util.log
+
+import seth.util
+
+
+def trace(msg, **attr):
+  _log(stem.util.log.TRACE, msg, **attr)
+
+
+def debug(msg, **attr):
+  _log(stem.util.log.DEBUG, msg, **attr)
+
+
+def info(msg, **attr):
+  _log(stem.util.log.INFO, msg, **attr)
+
+
+def notice(msg, **attr):
+  _log(stem.util.log.NOTICE, msg, **attr)
+
+
+def warn(msg, **attr):
+  _log(stem.util.log.WARN, msg, **attr)
+
+
+def error(msg, **attr):
+  _log(stem.util.log.ERROR, msg, **attr)
+
+
+def _log(runlevel, message, **attr):
+  """
+  Logs the given message, formatted with optional attributes.
+
+  :param stem.util.log.Runlevel runlevel: runlevel at which to log the message
+  :param str message: message handle to log
+  :param dict attr: attributes to format the message with
+  """
+
+  stem.util.log.log(runlevel, seth.util.msg(message, **attr))
diff --git a/seth/util/panel.py b/seth/util/panel.py
new file mode 100644
index 0000000..b793906
--- /dev/null
+++ b/seth/util/panel.py
@@ -0,0 +1,864 @@
+"""
+Wrapper for safely working with curses subwindows.
+"""
+
+import copy
+import time
+import curses
+import curses.ascii
+import curses.textpad
+from threading import RLock
+
+from seth.util import text_input, ui_tools
+
+from stem.util import log
+
+# global ui lock governing all panel instances (curses isn't thread save and
+# concurrency bugs produce especially sinister glitches)
+
+CURSES_LOCK = RLock()
+
+SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END)
+
+SPECIAL_KEYS = {
+  'up': curses.KEY_UP,
+  'down': curses.KEY_DOWN,
+  'left': curses.KEY_LEFT,
+  'right': curses.KEY_RIGHT,
+  'home': curses.KEY_HOME,
+  'end': curses.KEY_END,
+  'page_up': curses.KEY_PPAGE,
+  'page_down': curses.KEY_NPAGE,
+  'esc': 27,
+}
+
+
+# tags used by addfstr - this maps to functor/argument combinations since the
+# actual values (in the case of color attributes) might not yet be initialized
+
+def _no_op(arg):
+  return arg
+
+
+FORMAT_TAGS = {
+  "<b>": (_no_op, curses.A_BOLD),
+  "<u>": (_no_op, curses.A_UNDERLINE),
+  "<h>": (_no_op, curses.A_STANDOUT),
+}
+
+for color_label in ui_tools.COLOR_LIST:
+  FORMAT_TAGS["<%s>" % color_label] = (ui_tools.get_color, color_label)
+
+# prevents curses redraws if set
+HALT_ACTIVITY = False
+
+
+class Panel(object):
+  """
+  Wrapper for curses subwindows. This hides most of the ugliness in common
+  curses operations including:
+    - locking when concurrently drawing to multiple windows
+    - gracefully handle terminal resizing
+    - clip text that falls outside the panel
+    - convenience methods for word wrap, in-line formatting, etc
+
+  This uses a design akin to Swing where panel instances provide their display
+  implementation by overwriting the draw() method, and are redrawn with
+  redraw().
+  """
+
+  def __init__(self, parent, name, top, left = 0, height = -1, width = -1):
+    """
+    Creates a durable wrapper for a curses subwindow in the given parent.
+
+    Arguments:
+      parent - parent curses window
+      name   - identifier for the panel
+      top    - positioning of top within parent
+      left   - positioning of the left edge within the parent
+      height - maximum height of panel (uses all available space if -1)
+      width  - maximum width of panel (uses all available space if -1)
+    """
+
+    # The not-so-pythonic getters for these parameters are because some
+    # implementations aren't entirely deterministic (for instance panels
+    # might chose their height based on its parent's current width).
+
+    self.panel_name = name
+    self.parent = parent
+    self.visible = False
+    self.title_visible = True
+
+    # Attributes for pausing. The pause_attr contains variables our get_attr
+    # method is tracking, and the pause buffer has copies of the values from
+    # when we were last unpaused (unused unless we're paused).
+
+    self.paused = False
+    self.pause_attr = []
+    self.pause_buffer = {}
+    self.pause_time = -1
+
+    self.top = top
+    self.left = left
+    self.height = height
+    self.width = width
+
+    # The panel's subwindow instance. This is made available to implementors
+    # via their draw method and shouldn't be accessed directly.
+    #
+    # This is None if either the subwindow failed to be created or needs to be
+    # remade before it's used. The later could be for a couple reasons:
+    # - The subwindow was never initialized.
+    # - Any of the parameters used for subwindow initialization have changed.
+
+    self.win = None
+
+    self.max_y, self.max_x = -1, -1  # subwindow dimensions when last redrawn
+
+  def get_name(self):
+    """
+    Provides panel's identifier.
+    """
+
+    return self.panel_name
+
+  def is_title_visible(self):
+    """
+    True if the title is configured to be visible, False otherwise.
+    """
+
+    return self.title_visible
+
+  def set_title_visible(self, is_visible):
+    """
+    Configures the panel's title to be visible or not when it's next redrawn.
+    This is not guarenteed to be respected (not all panels have a title).
+    """
+
+    self.title_visible = is_visible
+
+  def get_parent(self):
+    """
+    Provides the parent used to create subwindows.
+    """
+
+    return self.parent
+
+  def is_visible(self):
+    """
+    Provides if the panel's configured to be visible or not.
+    """
+
+    return self.visible
+
+  def set_visible(self, is_visible):
+    """
+    Toggles if the panel is visible or not.
+
+    Arguments:
+      is_visible - panel is redrawn when requested if true, skipped otherwise
+    """
+
+    self.visible = is_visible
+
+  def is_paused(self):
+    """
+    Provides if the panel's configured to be paused or not.
+    """
+
+    return self.paused
+
+  def set_pause_attr(self, attr):
+    """
+    Configures the panel to track the given attribute so that get_attr provides
+    the value when it was last unpaused (or its current value if we're
+    currently unpaused). For instance...
+
+    > self.set_pause_attr("myVar")
+    > self.myVar = 5
+    > self.myVar = 6  # self.get_attr("myVar") -> 6
+    > self.set_paused(True)
+    > self.myVar = 7  # self.get_attr("myVar") -> 6
+    > self.set_paused(False)
+    > self.myVar = 7  # self.get_attr("myVar") -> 7
+
+    Arguments:
+      attr - parameter to be tracked for get_attr
+    """
+
+    self.pause_attr.append(attr)
+    self.pause_buffer[attr] = self.copy_attr(attr)
+
+  def get_attr(self, attr):
+    """
+    Provides the value of the given attribute when we were last unpaused. If
+    we're currently unpaused then this is the current value. If untracked this
+    returns None.
+
+    Arguments:
+      attr - local variable to be returned
+    """
+
+    if attr not in self.pause_attr:
+      return None
+    elif self.paused:
+      return self.pause_buffer[attr]
+    else:
+      return self.__dict__.get(attr)
+
+  def copy_attr(self, attr):
+    """
+    Provides a duplicate of the given configuration value, suitable for the
+    pause buffer.
+
+    Arguments:
+      attr - parameter to be provided back
+    """
+
+    current_value = self.__dict__.get(attr)
+    return copy.copy(current_value)
+
+  def set_paused(self, is_pause, suppress_redraw = False):
+    """
+    Toggles if the panel is paused or not. This causes the panel to be redrawn
+    when toggling is pause state unless told to do otherwise. This is
+    important when pausing since otherwise the panel's display could change
+    when redrawn for other reasons.
+
+    This returns True if the panel's pause state was changed, False otherwise.
+
+    Arguments:
+      is_pause        - freezes the state of the pause attributes if true, makes
+                        them editable otherwise
+      suppress_redraw - if true then this will never redraw the panel
+    """
+
+    if is_pause != self.paused:
+      if is_pause:
+        self.pause_time = time.time()
+
+      self.paused = is_pause
+
+      if is_pause:
+        # copies tracked attributes so we know what they were before pausing
+
+        for attr in self.pause_attr:
+          self.pause_buffer[attr] = self.copy_attr(attr)
+
+      if not suppress_redraw:
+        self.redraw(True)
+
+      return True
+    else:
+      return False
+
+  def get_pause_time(self):
+    """
+    Provides the time that we were last paused, returning -1 if we've never
+    been paused.
+    """
+
+    return self.pause_time
+
+  def get_top(self):
+    """
+    Provides the position subwindows are placed at within its parent.
+    """
+
+    return self.top
+
+  def set_top(self, top):
+    """
+    Changes the position where subwindows are placed within its parent.
+
+    Arguments:
+      top - positioning of top within parent
+    """
+
+    if self.top != top:
+      self.top = top
+      self.win = None
+
+  def get_left(self):
+    """
+    Provides the left position where this subwindow is placed within its
+    parent.
+    """
+
+    return self.left
+
+  def set_left(self, left):
+    """
+    Changes the left position where this subwindow is placed within its parent.
+
+    Arguments:
+      left - positioning of top within parent
+    """
+
+    if self.left != left:
+      self.left = left
+      self.win = None
+
+  def get_height(self):
+    """
+    Provides the height used for subwindows (-1 if it isn't limited).
+    """
+
+    return self.height
+
+  def set_height(self, height):
+    """
+    Changes the height used for subwindows. This uses all available space if -1.
+
+    Arguments:
+      height - maximum height of panel (uses all available space if -1)
+    """
+
+    if self.height != height:
+      self.height = height
+      self.win = None
+
+  def get_width(self):
+    """
+    Provides the width used for subwindows (-1 if it isn't limited).
+    """
+
+    return self.width
+
+  def set_width(self, width):
+    """
+    Changes the width used for subwindows. This uses all available space if -1.
+
+    Arguments:
+      width - maximum width of panel (uses all available space if -1)
+    """
+
+    if self.width != width:
+      self.width = width
+      self.win = None
+
+  def get_preferred_size(self):
+    """
+    Provides the dimensions the subwindow would use when next redrawn, given
+    that none of the properties of the panel or parent change before then. This
+    returns a tuple of (height, width).
+    """
+
+    new_height, new_width = self.parent.getmaxyx()
+    set_height, set_width = self.get_height(), self.get_width()
+    new_height = max(0, new_height - self.top)
+    new_width = max(0, new_width - self.left)
+
+    if set_height != -1:
+      new_height = min(new_height, set_height)
+
+    if set_width != -1:
+      new_width = min(new_width, set_width)
+
+    return (new_height, new_width)
+
+  def handle_key(self, key):
+    """
+    Handler for user input. This returns true if the key press was consumed,
+    false otherwise.
+
+    Arguments:
+      key - keycode for the key pressed
+    """
+
+    return False
+
+  def get_help(self):
+    """
+    Provides help information for the controls this page provides. This is a
+    list of tuples of the form...
+    (control, description, status)
+    """
+
+    return []
+
+  def draw(self, width, height):
+    """
+    Draws display's content. This is meant to be overwritten by
+    implementations and not called directly (use redraw() instead). The
+    dimensions provided are the drawable dimensions, which in terms of width is
+    a column less than the actual space.
+
+    Arguments:
+      width  - horizontal space available for content
+      height - vertical space available for content
+    """
+
+    pass
+
+  def redraw(self, force_redraw=False, block=False):
+    """
+    Clears display and redraws its content. This can skip redrawing content if
+    able (ie, the subwindow's unchanged), instead just refreshing the display.
+
+    Arguments:
+      force_redraw - forces the content to be cleared and redrawn if true
+      block       - if drawing concurrently with other panels this determines
+                    if the request is willing to wait its turn or should be
+                    abandoned
+    """
+
+    # skipped if not currently visible or activity has been halted
+
+    if not self.is_visible() or HALT_ACTIVITY:
+      return
+
+    # if the panel's completely outside its parent then this is a no-op
+
+    new_height, new_width = self.get_preferred_size()
+
+    if new_height == 0 or new_width == 0:
+      self.win = None
+      return
+
+    # recreates the subwindow if necessary
+
+    is_new_window = self._reset_subwindow()
+
+    # The reset argument is disregarded in a couple of situations:
+    # - The subwindow's been recreated (obviously it then doesn't have the old
+    #   content to refresh).
+    # - The subwindow's dimensions have changed since last drawn (this will
+    #   likely change the content's layout)
+
+    subwin_max_y, subwin_max_x = self.win.getmaxyx()
+
+    if is_new_window or subwin_max_y != self.max_y or subwin_max_x != self.max_x:
+      force_redraw = True
+
+    self.max_y, self.max_x = subwin_max_y, subwin_max_x
+
+    if not CURSES_LOCK.acquire(block):
+      return
+
+    try:
+      if force_redraw:
+        self.win.erase()  # clears any old contents
+        self.draw(self.max_x, self.max_y)
+      self.win.refresh()
+    finally:
+      CURSES_LOCK.release()
+
+  def hline(self, y, x, length, attr=curses.A_NORMAL):
+    """
+    Draws a horizontal line. This should only be called from the context of a
+    panel's draw method.
+
+    Arguments:
+      y      - vertical location
+      x      - horizontal location
+      length - length the line spans
+      attr   - text attributes
+    """
+
+    if self.win and self.max_x > x and self.max_y > y:
+      try:
+        draw_length = min(length, self.max_x - x)
+        self.win.hline(y, x, curses.ACS_HLINE | attr, draw_length)
+      except:
+        # in edge cases drawing could cause a _curses.error
+        pass
+
+  def vline(self, y, x, length, attr=curses.A_NORMAL):
+    """
+    Draws a vertical line. This should only be called from the context of a
+    panel's draw method.
+
+    Arguments:
+      y      - vertical location
+      x      - horizontal location
+      length - length the line spans
+      attr   - text attributes
+    """
+
+    if self.win and self.max_x > x and self.max_y > y:
+      try:
+        draw_length = min(length, self.max_y - y)
+        self.win.vline(y, x, curses.ACS_VLINE | attr, draw_length)
+      except:
+        # in edge cases drawing could cause a _curses.error
+        pass
+
+  def addch(self, y, x, char, attr=curses.A_NORMAL):
+    """
+    Draws a single character. This should only be called from the context of a
+    panel's draw method.
+
+    Arguments:
+      y    - vertical location
+      x    - horizontal location
+      char - character to be drawn
+      attr - text attributes
+    """
+
+    if self.win and self.max_x > x and self.max_y > y:
+      try:
+        self.win.addch(y, x, char, attr)
+      except:
+        # in edge cases drawing could cause a _curses.error
+        pass
+
+  def addstr(self, y, x, msg, *attributes):
+    """
+    Writes string to subwindow if able. This takes into account screen bounds
+    to avoid making curses upset. This should only be called from the context
+    of a panel's draw method.
+
+    Arguments:
+      y    - vertical location
+      x    - horizontal location
+      msg  - text to be added
+      attr - text attributes
+    """
+
+    format_attr = curses.A_NORMAL
+
+    for attr in attributes:
+      if isinstance(attr, str):
+        format_attr |= ui_tools.get_color(attr)
+      else:
+        format_attr |= attr
+
+    # subwindows need a single character buffer (either in the x or y
+    # direction) from actual content to prevent crash when shrank
+
+    if self.win and self.max_x > x and self.max_y > y:
+      try:
+        drawn_msg = msg[:self.max_x - x]
+        self.win.addstr(y, x, drawn_msg, format_attr)
+        return x + len(drawn_msg)
+      except:
+        # this might produce a _curses.error during edge cases, for instance
+        # when resizing with visible popups
+
+        pass
+
+    return x
+
+  def addfstr(self, y, x, msg):
+    """
+    Writes string to subwindow. The message can contain xhtml-style tags for
+    formatting, including:
+    <b>text</b>               bold
+    <u>text</u>               underline
+    <h>text</h>               highlight
+    <[color]>text</[color]>   use color (see ui_tools.get_color() for constants)
+
+    Tag nesting is supported and tag closing is strictly enforced (raising an
+    exception for invalid formatting). Unrecognized tags are treated as normal
+    text. This should only be called from the context of a panel's draw method.
+
+    Text in multiple color tags (for instance "<blue><red>hello</red></blue>")
+    uses the bitwise OR of those flags (hint: that's probably not what you
+    want).
+
+    Arguments:
+      y    - vertical location
+      x    - horizontal location
+      msg  - formatted text to be added
+    """
+
+    if self.win and self.max_y > y:
+      formatting = [curses.A_NORMAL]
+      expected_close_tags = []
+      unused_msg = msg
+
+      while self.max_x > x and len(unused_msg) > 0:
+        # finds next consumeable tag (left as None if there aren't any left)
+
+        next_tag, tag_start, tag_end = None, -1, -1
+
+        tmp_checked = 0  # portion of the message cleared for having any valid tags
+        expected_tags = FORMAT_TAGS.keys() + expected_close_tags
+
+        while next_tag is None:
+          tag_start = unused_msg.find("<", tmp_checked)
+          tag_end = unused_msg.find(">", tag_start) + 1 if tag_start != -1 else -1
+
+          if tag_start == -1 or tag_end == -1:
+            break  # no more tags to consume
+          else:
+            # check if the tag we've found matches anything being expected
+            if unused_msg[tag_start:tag_end] in expected_tags:
+              next_tag = unused_msg[tag_start:tag_end]
+              break  # found a tag to use
+            else:
+              # not a valid tag - narrow search to everything after it
+              tmp_checked = tag_end
+
+        # splits into text before and after tag
+
+        if next_tag:
+          msg_segment = unused_msg[:tag_start]
+          unused_msg = unused_msg[tag_end:]
+        else:
+          msg_segment = unused_msg
+          unused_msg = ""
+
+        # adds text before tag with current formatting
+
+        attr = 0
+
+        for text_format in formatting:
+          attr |= text_format
+
+        self.win.addstr(y, x, msg_segment[:self.max_x - x - 1], attr)
+        x += len(msg_segment)
+
+        # applies tag attributes for future text
+
+        if next_tag:
+          format_tag = "<" + next_tag[2:] if next_tag.startswith("</") else next_tag
+          format_match = FORMAT_TAGS[format_tag][0](FORMAT_TAGS[format_tag][1])
+
+          if not next_tag.startswith("</"):
+            # open tag - add formatting
+            expected_close_tags.append("</" + next_tag[1:])
+            formatting.append(format_match)
+          else:
+            # close tag - remove formatting
+            expected_close_tags.remove(next_tag)
+            formatting.remove(format_match)
+
+      # only check for unclosed tags if we processed the whole message (if we
+      # stopped processing prematurely it might still be valid)
+
+      if expected_close_tags and not unused_msg:
+        # if we're done then raise an exception for any unclosed tags (tisk, tisk)
+        base_msg = "Unclosed formatting tag%s:" % ("s" if len(expected_close_tags) > 1 else "")
+        raise ValueError("%s: '%s'\n  \"%s\"" % (base_msg, "', '".join(expected_close_tags), msg))
+
+  def getstr(self, y, x, initial_text = "", text_format = None, max_width = None, validator = None):
+    """
+    Provides a text field where the user can input a string, blocking until
+    they've done so and returning the result. If the user presses escape then
+    this terminates and provides back None. This should only be called from
+    the context of a panel's draw method.
+
+    This blanks any content within the space that the input field is rendered
+    (otherwise stray characters would be interpreted as part of the initial
+    input).
+
+    Arguments:
+      y            - vertical location
+      x            - horizontal location
+      initial_text - starting text in this field
+      text_format  - format used for the text
+      max_width    - maximum width for the text field
+      validator    - custom TextInputValidator for handling keybindings
+    """
+
+    if not text_format:
+      text_format = curses.A_NORMAL
+
+    # makes cursor visible
+
+    try:
+      previous_cursor_state = curses.curs_set(1)
+    except curses.error:
+      previous_cursor_state = 0
+
+    # temporary subwindow for user input
+
+    display_width = self.get_preferred_size()[1]
+
+    if max_width:
+      display_width = min(display_width, max_width + x)
+
+    input_subwindow = self.parent.subwin(1, display_width - x, self.top + y, self.left + x)
+
+    # blanks the field's area, filling it with the font in case it's hilighting
+
+    input_subwindow.clear()
+    input_subwindow.bkgd(' ', text_format)
+
+    # prepopulates the initial text
+
+    if initial_text:
+      input_subwindow.addstr(0, 0, initial_text[:display_width - x - 1], text_format)
+
+    # Displays the text field, blocking until the user's done. This closes the
+    # text panel and returns user_input to the initial text if the user presses
+    # escape.
+
+    textbox = curses.textpad.Textbox(input_subwindow)
+
+    if not validator:
+      validator = text_input.BasicValidator()
+
+    textbox.win.attron(text_format)
+    user_input = textbox.edit(lambda key: validator.validate(key, textbox)).strip()
+    textbox.win.attroff(text_format)
+
+    if textbox.lastcmd == curses.ascii.BEL:
+      user_input = None
+
+    # reverts visability settings
+
+    try:
+      curses.curs_set(previous_cursor_state)
+    except curses.error:
+      pass
+
+    return user_input
+
+  def add_scroll_bar(self, top, bottom, size, draw_top = 0, draw_bottom = -1, draw_left = 0):
+    """
+    Draws a left justified scroll bar reflecting position within a vertical
+    listing. This is shorted if necessary, and left undrawn if no space is
+    available. The bottom is squared off, having a layout like:
+     |
+    *|
+    *|
+    *|
+     |
+    -+
+
+    This should only be called from the context of a panel's draw method.
+
+    Arguments:
+      top        - list index for the top-most visible element
+      bottom     - list index for the bottom-most visible element
+      size       - size of the list in which the listed elements are contained
+      draw_top    - starting row where the scroll bar should be drawn
+      draw_bottom - ending row where the scroll bar should end, -1 if it should
+                   span to the bottom of the panel
+      draw_left   - left offset at which to draw the scroll bar
+    """
+
+    if (self.max_y - draw_top) < 2:
+      return  # not enough room
+
+    # sets draw_bottom to be the actual row on which the scrollbar should end
+
+    if draw_bottom == -1:
+      draw_bottom = self.max_y - 1
+    else:
+      draw_bottom = min(draw_bottom, self.max_y - 1)
+
+    # determines scrollbar dimensions
+
+    scrollbar_height = draw_bottom - draw_top
+    slider_top = scrollbar_height * top / size
+    slider_size = scrollbar_height * (bottom - top) / size
+
+    # ensures slider isn't at top or bottom unless really at those extreme bounds
+
+    if top > 0:
+      slider_top = max(slider_top, 1)
+
+    if bottom != size:
+      slider_top = min(slider_top, scrollbar_height - slider_size - 2)
+
+    # avoids a rounding error that causes the scrollbar to be too low when at
+    # the bottom
+
+    if bottom == size:
+      slider_top = scrollbar_height - slider_size - 1
+
+    # draws scrollbar slider
+
+    for i in range(scrollbar_height):
+      if i >= slider_top and i <= slider_top + slider_size:
+        self.addstr(i + draw_top, draw_left, " ", curses.A_STANDOUT)
+      else:
+        self.addstr(i + draw_top, draw_left, " ")
+
+    # draws box around the scroll bar
+
+    self.vline(draw_top, draw_left + 1, draw_bottom - 1)
+    self.addch(draw_bottom, draw_left + 1, curses.ACS_LRCORNER)
+    self.addch(draw_bottom, draw_left, curses.ACS_HLINE)
+
+  def _reset_subwindow(self):
+    """
+    Create a new subwindow instance for the panel if:
+    - Panel currently doesn't have a subwindow (was uninitialized or
+      invalidated).
+    - There's room for the panel to grow vertically (curses automatically
+      lets subwindows regrow horizontally, but not vertically).
+    - The subwindow has been displaced. This is a curses display bug that
+      manifests if the terminal's shrank then re-expanded. Displaced
+      subwindows are never restored to their proper position, resulting in
+      graphical glitches if we draw to them.
+    - The preferred size is smaller than the actual size (should shrink).
+
+    This returns True if a new subwindow instance was created, False otherwise.
+    """
+
+    new_height, new_width = self.get_preferred_size()
+
+    if new_height == 0:
+      return False  # subwindow would be outside its parent
+
+    # determines if a new subwindow should be recreated
+
+    recreate = self.win is None
+
+    if self.win:
+      subwin_max_y, subwin_max_x = self.win.getmaxyx()
+      recreate |= subwin_max_y < new_height           # check for vertical growth
+      recreate |= self.top > self.win.getparyx()[0]   # check for displacement
+      recreate |= subwin_max_x > new_width or subwin_max_y > new_height  # shrinking
+
+    # I'm not sure if recreating subwindows is some sort of memory leak but the
+    # Python curses bindings seem to lack all of the following:
+    # - subwindow deletion (to tell curses to free the memory)
+    # - subwindow moving/resizing (to restore the displaced windows)
+    # so this is the only option (besides removing subwindows entirely which
+    # would mean far more complicated code and no more selective refreshing)
+
+    if recreate:
+      self.win = self.parent.subwin(new_height, new_width, self.top, self.left)
+
+      # note: doing this log before setting win produces an infinite loop
+      log.debug("recreating panel '%s' with the dimensions of %i/%i" % (self.get_name(), new_height, new_width))
+
+    return recreate
+
+
+class KeyInput(object):
+  """
+  Keyboard input by the user.
+  """
+
+  def __init__(self, key):
+    self._key = key  # pressed key as an integer
+
+  def match(self, *keys):
+    """
+    Checks if we have a case insensitive match with the given key. Beside
+    characters, this also recognizes: up, down, left, right, home, end,
+    page_up, page_down, and esc.
+    """
+
+    for key in keys:
+      if key in SPECIAL_KEYS:
+        if self._key == SPECIAL_KEYS[key]:
+          return True
+      elif len(key) == 1:
+        if self._key in (ord(key.lower()), ord(key.upper())):
+          return True
+      else:
+        raise ValueError("%s wasn't among our recognized key codes" % key)
+
+    return False
+
+  def is_scroll(self):
+    """
+    True if the key is used for scrolling, false otherwise.
+    """
+
+    return self._key in SCROLL_KEYS
+
+  def is_selection(self):
+    """
+    True if the key matches the enter or space keys.
+    """
+
+    return self._key in (curses.KEY_ENTER, 10, ord(' '))
diff --git a/seth/util/text_input.py b/seth/util/text_input.py
new file mode 100644
index 0000000..4faec1e
--- /dev/null
+++ b/seth/util/text_input.py
@@ -0,0 +1,213 @@
+"""
+Provides input validators that provide text input with various capabilities.
+These can be chained together with the first matching validator taking
+precidence.
+"""
+
+import os
+import curses
+
+PASS = -1
+
+
+class TextInputValidator:
+  """
+  Basic interface for validators. Implementations should override the handle_key
+  method.
+  """
+
+  def __init__(self, next_validator = None):
+    self.next_validator = next_validator
+
+  def validate(self, key, textbox):
+    """
+    Processes the given key input for the textbox. This may modify the
+    textbox's content, cursor position, etc depending on the functionality
+    of the validator. This returns the key that the textbox should interpret,
+    PASS if this validator doesn't want to take any action.
+
+    Arguments:
+      key     - key code input from the user
+      textbox - curses Textbox instance the input came from
+    """
+
+    result = self.handle_key(key, textbox)
+
+    if result != PASS:
+      return result
+    elif self.next_validator:
+      return self.next_validator.validate(key, textbox)
+    else:
+      return key
+
+  def handle_key(self, key, textbox):
+    """
+    Process the given keycode with this validator, returning the keycode for
+    the textbox to process, and PASS if this doesn't want to modify it.
+
+    Arguments:
+      key     - key code input from the user
+      textbox - curses Textbox instance the input came from
+    """
+
+    return PASS
+
+
+class BasicValidator(TextInputValidator):
+  """
+  Interceptor for keystrokes given to a textbox, doing the following:
+  - quits by setting the input to curses.ascii.BEL when escape is pressed
+  - stops the cursor at the end of the box's content when pressing the right
+    arrow
+  - home and end keys move to the start/end of the line
+  """
+
+  def handle_key(self, key, textbox):
+    y, x = textbox.win.getyx()
+
+    if curses.ascii.isprint(key) and x < textbox.maxx:
+      # Shifts the existing text forward so input is an insert method rather
+      # than replacement. The curses.textpad accepts an insert mode flag but
+      # this has a couple issues...
+      # - The flag is only available for Python 2.6+, before that the
+      #   constructor only accepted a subwindow argument as per:
+      #   https://trac.torproject.org/projects/tor/ticket/2354
+      # - The textpad doesn't shift text that has text attributes. This is
+      #   because keycodes read by textbox.win.inch() includes formatting,
+      #   causing the curses.ascii.isprint() check it does to fail.
+
+      current_input = textbox.gather()
+      textbox.win.addstr(y, x + 1, current_input[x:textbox.maxx - 1])
+      textbox.win.move(y, x)  # reverts cursor movement during gather call
+    elif key == 27:
+      # curses.ascii.BEL is a character codes that causes textpad to terminate
+
+      return curses.ascii.BEL
+    elif key == curses.KEY_HOME:
+      textbox.win.move(y, 0)
+      return None
+    elif key in (curses.KEY_END, curses.KEY_RIGHT):
+      msg_length = len(textbox.gather())
+      textbox.win.move(y, x)  # reverts cursor movement during gather call
+
+      if key == curses.KEY_END and msg_length > 0 and x < msg_length - 1:
+        # if we're in the content then move to the end
+
+        textbox.win.move(y, msg_length - 1)
+        return None
+      elif key == curses.KEY_RIGHT and x >= msg_length - 1:
+        # don't move the cursor if there's no content after it
+
+        return None
+    elif key == 410:
+      # if we're resizing the display during text entry then cancel it
+      # (otherwise the input field is filled with nonprintable characters)
+
+      return curses.ascii.BEL
+
+    return PASS
+
+
+class HistoryValidator(TextInputValidator):
+  """
+  This intercepts the up and down arrow keys to scroll through a backlog of
+  previous commands.
+  """
+
+  def __init__(self, command_backlog = [], next_validator = None):
+    TextInputValidator.__init__(self, next_validator)
+
+    # contents that can be scrolled back through, newest to oldest
+
+    self.command_backlog = command_backlog
+
+    # selected item from the backlog, -1 if we're not on a backlog item
+
+    self.selection_index = -1
+
+    # the fields input prior to selecting a backlog item
+
+    self.custom_input = ""
+
+  def handle_key(self, key, textbox):
+    if key in (curses.KEY_UP, curses.KEY_DOWN):
+      offset = 1 if key == curses.KEY_UP else -1
+      new_selection = self.selection_index + offset
+
+      # constrains the new selection to valid bounds
+
+      new_selection = max(-1, new_selection)
+      new_selection = min(len(self.command_backlog) - 1, new_selection)
+
+      # skips if this is a no-op
+
+      if self.selection_index == new_selection:
+        return None
+
+      # saves the previous input if we weren't on the backlog
+
+      if self.selection_index == -1:
+        self.custom_input = textbox.gather().strip()
+
+      if new_selection == -1:
+        new_input = self.custom_input
+      else:
+        new_input = self.command_backlog[new_selection]
+
+      y, _ = textbox.win.getyx()
+      _, max_x = textbox.win.getmaxyx()
+      textbox.win.clear()
+      textbox.win.addstr(y, 0, new_input[:max_x - 1])
+      textbox.win.move(y, min(len(new_input), max_x - 1))
+
+      self.selection_index = new_selection
+      return None
+
+    return PASS
+
+
+class TabCompleter(TextInputValidator):
+  """
+  Provides tab completion based on the current input, finishing if there's only
+  a single match. This expects a functor that accepts the current input and
+  provides matches.
+  """
+
+  def __init__(self, completer, next_validator = None):
+    TextInputValidator.__init__(self, next_validator)
+
+    # functor that accepts a string and gives a list of matches
+
+    self.completer = completer
+
+  def handle_key(self, key, textbox):
+    # Matches against the tab key. The ord('\t') is nine, though strangely none
+    # of the curses.KEY_*TAB constants match this...
+
+    if key == 9:
+      current_contents = textbox.gather().strip()
+      matches = self.completer(current_contents)
+      new_input = None
+
+      if len(matches) == 1:
+        # only a single match, fill it in
+        new_input = matches[0]
+      elif len(matches) > 1:
+        # looks for a common prefix we can complete
+        common_prefix = os.path.commonprefix(matches)  # weird that this comes from path...
+
+        if common_prefix != current_contents:
+          new_input = common_prefix
+
+        # TODO: somehow display matches... this is not gonna be fun
+
+      if new_input:
+        y, _ = textbox.win.getyx()
+        _, max_x = textbox.win.getmaxyx()
+        textbox.win.clear()
+        textbox.win.addstr(y, 0, new_input[:max_x - 1])
+        textbox.win.move(y, min(len(new_input), max_x - 1))
+
+      return None
+
+    return PASS
diff --git a/seth/util/tor_config.py b/seth/util/tor_config.py
new file mode 100644
index 0000000..b67445a
--- /dev/null
+++ b/seth/util/tor_config.py
@@ -0,0 +1,1116 @@
+"""
+Helper functions for working with tor's configuration file.
+"""
+
+import os
+import time
+import socket
+import threading
+
+import stem.version
+
+from seth.util import tor_controller, ui_tools
+
+from stem.util import conf, enum, log, str_tools, system
+
+# filename used for cached tor config descriptions
+
+CONFIG_DESC_FILENAME = "torConfigDesc.txt"
+
+# messages related to loading the tor configuration descriptions
+
+DESC_LOAD_SUCCESS_MSG = "Loaded configuration descriptions from '%s' (runtime: %0.3f)"
+DESC_LOAD_FAILED_MSG = "Unable to load configuration descriptions (%s)"
+DESC_INTERNAL_LOAD_SUCCESS_MSG = "Falling back to descriptions for Tor %s"
+DESC_INTERNAL_LOAD_FAILED_MSG = "Unable to load fallback descriptions. Categories and help for Tor's configuration options won't be available. (%s)"
+DESC_READ_MAN_SUCCESS_MSG = "Read descriptions for tor's configuration options from its man page (runtime %0.3f)"
+DESC_READ_MAN_FAILED_MSG = "Unable to get the descriptions of Tor's configuration options from its man page (%s)"
+DESC_SAVE_SUCCESS_MSG = "Saved configuration descriptions to '%s' (runtime: %0.3f)"
+DESC_SAVE_FAILED_MSG = "Unable to save configuration descriptions (%s)"
+
+
+def conf_handler(key, value):
+  if key == "torrc.important":
+    # stores lowercase entries to drop case sensitivity
+    return [entry.lower() for entry in value]
+
+
+CONFIG = conf.config_dict("seth", {
+  "features.torrc.validate": True,
+  "torrc.important": [],
+  "torrc.alias": {},
+  "torrc.units.size.b": [],
+  "torrc.units.size.kb": [],
+  "torrc.units.size.mb": [],
+  "torrc.units.size.gb": [],
+  "torrc.units.size.tb": [],
+  "torrc.units.time.sec": [],
+  "torrc.units.time.min": [],
+  "torrc.units.time.hour": [],
+  "torrc.units.time.day": [],
+  "torrc.units.time.week": [],
+  "startup.data_directory": "~/.seth",
+  "features.config.descriptions.enabled": True,
+  "features.config.descriptions.persist": True,
+  "tor.chroot": '',
+}, conf_handler)
+
+
+def general_conf_handler(config, key):
+  value = config.get(key)
+
+  if key.startswith("torrc.summary."):
+    # we'll look for summary keys with a lowercase config name
+    CONFIG[key.lower()] = value
+  elif key.startswith("torrc.units.") and value:
+    # all the torrc.units.* values are comma separated lists
+    return [entry.strip() for entry in value[0].split(",")]
+
+
+conf.get_config("seth").add_listener(general_conf_handler, backfill = True)
+
+# enums and values for numeric torrc entries
+
+ValueType = enum.Enum("UNRECOGNIZED", "SIZE", "TIME")
+SIZE_MULT = {"b": 1, "kb": 1024, "mb": 1048576, "gb": 1073741824, "tb": 1099511627776}
+TIME_MULT = {"sec": 1, "min": 60, "hour": 3600, "day": 86400, "week": 604800}
+
+# enums for issues found during torrc validation:
+# DUPLICATE  - entry is ignored due to being a duplicate
+# MISMATCH   - the value doesn't match tor's current state
+# MISSING    - value differs from its default but is missing from the torrc
+# IS_DEFAULT - the configuration option matches tor's default
+
+ValidationError = enum.Enum("DUPLICATE", "MISMATCH", "MISSING", "IS_DEFAULT")
+
+# descriptions of tor's configuration options fetched from its man page
+
+CONFIG_DESCRIPTIONS_LOCK = threading.RLock()
+CONFIG_DESCRIPTIONS = {}
+
+# categories for tor configuration options
+
+Category = enum.Enum("GENERAL", "CLIENT", "RELAY", "DIRECTORY", "AUTHORITY", "HIDDEN_SERVICE", "TESTING", "UNKNOWN")
+
+TORRC = None  # singleton torrc instance
+MAN_OPT_INDENT = 7  # indentation before options in the man page
+MAN_EX_INDENT = 15  # indentation used for man page examples
+PERSIST_ENTRY_DIVIDER = "-" * 80 + "\n"  # splits config entries when saving to a file
+MULTILINE_PARAM = None  # cached multiline parameters (lazily loaded)
+
+# torrc options that bind to ports
+
+PORT_OPT = ("SocksPort", "ORPort", "DirPort", "ControlPort", "TransPort")
+
+
+class ManPageEntry:
+  """
+  Information provided about a tor configuration option in its man page entry.
+  """
+
+  def __init__(self, option, index, category, arg_usage, description):
+    self.option = option
+    self.index = index
+    self.category = category
+    self.arg_usage = arg_usage
+    self.description = description
+
+
+def get_torrc():
+  """
+  Singleton constructor for a Controller. Be aware that this starts as being
+  unloaded, needing the torrc contents to be loaded before being functional.
+  """
+
+  global TORRC
+
+  if TORRC is None:
+    TORRC = Torrc()
+
+  return TORRC
+
+
+def load_option_descriptions(load_path = None, check_version = True):
+  """
+  Fetches and parses descriptions for tor's configuration options from its man
+  page. This can be a somewhat lengthy call, and raises an IOError if issues
+  occure. When successful loading from a file this returns the version for the
+  contents loaded.
+
+  If available, this can load the configuration descriptions from a file where
+  they were previously persisted to cut down on the load time (latency for this
+  is around 200ms).
+
+  Arguments:
+    load_path     - if set, this attempts to fetch the configuration
+                   descriptions from the given path instead of the man page
+    check_version - discards the results if true and tor's version doens't
+                   match the cached descriptors, otherwise accepts anyway
+  """
+
+  CONFIG_DESCRIPTIONS_LOCK.acquire()
+  CONFIG_DESCRIPTIONS.clear()
+
+  raised_exc = None
+  loaded_version = ""
+
+  try:
+    if load_path:
+      # Input file is expected to be of the form:
+      # <option>
+      # <arg description>
+      # <description, possibly multiple lines>
+      # <PERSIST_ENTRY_DIVIDER>
+      input_file = open(load_path, "r")
+      input_file_contents = input_file.readlines()
+      input_file.close()
+
+      try:
+        version_line = input_file_contents.pop(0).rstrip()
+
+        if version_line.startswith("Tor Version "):
+          file_version = version_line[12:]
+          loaded_version = file_version
+          tor_version = tor_controller().get_info("version", "")
+
+          if check_version and file_version != tor_version:
+            msg = "wrong version, tor is %s but the file's from %s" % (tor_version, file_version)
+            raise IOError(msg)
+        else:
+          raise IOError("unable to parse version")
+
+        while input_file_contents:
+          # gets category enum, failing if it doesn't exist
+          category = input_file_contents.pop(0).rstrip()
+
+          if category not in Category:
+            base_msg = "invalid category in input file: '%s'"
+            raise IOError(base_msg % category)
+
+          # gets the position in the man page
+          index_arg, index_str = -1, input_file_contents.pop(0).rstrip()
+
+          if index_str.startswith("index: "):
+            index_str = index_str[7:]
+
+            if index_str.isdigit():
+              index_arg = int(index_str)
+            else:
+              raise IOError("non-numeric index value: %s" % index_str)
+          else:
+            raise IOError("malformed index argument: %s" % index_str)
+
+          option = input_file_contents.pop(0).rstrip()
+          argument = input_file_contents.pop(0).rstrip()
+
+          description, loaded_line = "", input_file_contents.pop(0)
+
+          while loaded_line != PERSIST_ENTRY_DIVIDER:
+            description += loaded_line
+
+            if input_file_contents:
+              loaded_line = input_file_contents.pop(0)
+            else:
+              break
+
+          CONFIG_DESCRIPTIONS[option.lower()] = ManPageEntry(option, index_arg, category, argument, description.rstrip())
+      except IndexError:
+        CONFIG_DESCRIPTIONS.clear()
+        raise IOError("input file format is invalid")
+    else:
+      man_call_results = system.call("man tor", None)
+
+      if not man_call_results:
+        raise IOError("man page not found")
+
+      # Fetches all options available with this tor instance. This isn't
+      # vital, and the valid_options are left empty if the call fails.
+
+      controller, valid_options = tor_controller(), []
+      config_option_query = controller.get_info("config/names", None)
+
+      if config_option_query:
+        for line in config_option_query.strip().split("\n"):
+          valid_options.append(line[:line.find(" ")].lower())
+
+      option_count, last_option, last_arg = 0, None, None
+      last_category, last_description = Category.GENERAL, ""
+
+      for line in man_call_results:
+        line = ui_tools.get_printable(line)
+        stripped_line = line.strip()
+
+        # we have content, but an indent less than an option (ignore line)
+        # if stripped_line and not line.startswith(" " * MAN_OPT_INDENT): continue
+
+        # line starts with an indent equivilant to a new config option
+
+        is_opt_indent = line.startswith(" " * MAN_OPT_INDENT) and line[MAN_OPT_INDENT] != " "
+
+        is_category_line = not line.startswith(" ") and "OPTIONS" in line
+
+        # if this is a category header or a new option, add an entry using the
+        # buffered results
+
+        if is_opt_indent or is_category_line:
+          # Filters the line based on if the option is recognized by tor or
+          # not. This isn't necessary for seth, so if unable to make the check
+          # then we skip filtering (no loss, the map will just have some extra
+          # noise).
+
+          stripped_description = last_description.strip()
+
+          if last_option and (not valid_options or last_option.lower() in valid_options):
+            CONFIG_DESCRIPTIONS[last_option.lower()] = ManPageEntry(last_option, option_count, last_category, last_arg, stripped_description)
+            option_count += 1
+
+          last_description = ""
+
+          # parses the option and argument
+
+          line = line.strip()
+          div_index = line.find(" ")
+
+          if div_index != -1:
+            last_option, last_arg = line[:div_index], line[div_index + 1:]
+
+          # if this is a category header then switch it
+
+          if is_category_line:
+            if line.startswith("OPTIONS"):
+              last_category = Category.GENERAL
+            elif line.startswith("CLIENT"):
+              last_category = Category.CLIENT
+            elif line.startswith("SERVER"):
+              last_category = Category.RELAY
+            elif line.startswith("DIRECTORY SERVER"):
+              last_category = Category.DIRECTORY
+            elif line.startswith("DIRECTORY AUTHORITY SERVER"):
+              last_category = Category.AUTHORITY
+            elif line.startswith("HIDDEN SERVICE"):
+              last_category = Category.HIDDEN_SERVICE
+            elif line.startswith("TESTING NETWORK"):
+              last_category = Category.TESTING
+            else:
+              log.notice("Unrecognized category in the man page: %s" % line.strip())
+        else:
+          # Appends the text to the running description. Empty lines and lines
+          # starting with a specific indentation are used for formatting, for
+          # instance the ExitPolicy and TestingTorNetwork entries.
+
+          if last_description and last_description[-1] != "\n":
+            last_description += " "
+
+          if not stripped_line:
+            last_description += "\n\n"
+          elif line.startswith(" " * MAN_EX_INDENT):
+            last_description += "    %s\n" % stripped_line
+          else:
+            last_description += stripped_line
+  except IOError as exc:
+    raised_exc = exc
+
+  CONFIG_DESCRIPTIONS_LOCK.release()
+
+  if raised_exc:
+    raise raised_exc
+  else:
+    return loaded_version
+
+
+def save_option_descriptions(path):
+  """
+  Preserves the current configuration descriptors to the given path. This
+  raises an IOError or OSError if unable to do so.
+
+  Arguments:
+    path - location to persist configuration descriptors
+  """
+
+  # make dir if the path doesn't already exist
+
+  base_dir = os.path.dirname(path)
+
+  if not os.path.exists(base_dir):
+    os.makedirs(base_dir)
+
+  output_file = open(path, "w")
+
+  CONFIG_DESCRIPTIONS_LOCK.acquire()
+  sorted_options = CONFIG_DESCRIPTIONS.keys()
+  sorted_options.sort()
+
+  tor_version = tor_controller().get_info("version", "")
+  output_file.write("Tor Version %s\n" % tor_version)
+
+  for i in range(len(sorted_options)):
+    man_entry = get_config_description(sorted_options[i])
+    output_file.write("%s\nindex: %i\n%s\n%s\n%s\n" % (man_entry.category, man_entry.index, man_entry.option, man_entry.arg_usage, man_entry.description))
+
+    if i != len(sorted_options) - 1:
+      output_file.write(PERSIST_ENTRY_DIVIDER)
+
+  output_file.close()
+  CONFIG_DESCRIPTIONS_LOCK.release()
+
+
+def get_config_summary(option):
+  """
+  Provides a short summary description of the configuration option. If none is
+  known then this proivdes None.
+
+  Arguments:
+    option - tor config option
+  """
+
+  return CONFIG.get("torrc.summary.%s" % option.lower())
+
+
+def is_important(option):
+  """
+  Provides True if the option has the 'important' flag in the configuration,
+  False otherwise.
+
+  Arguments:
+    option - tor config option
+  """
+
+  return option.lower() in CONFIG["torrc.important"]
+
+
+def get_config_description(option):
+  """
+  Provides ManPageEntry instances populated with information fetched from the
+  tor man page. This provides None if no such option has been loaded. If the
+  man page is in the process of being loaded then this call blocks until it
+  finishes.
+
+  Arguments:
+    option - tor config option
+  """
+
+  CONFIG_DESCRIPTIONS_LOCK.acquire()
+
+  if option.lower() in CONFIG_DESCRIPTIONS:
+    return_val = CONFIG_DESCRIPTIONS[option.lower()]
+  else:
+    return_val = None
+
+  CONFIG_DESCRIPTIONS_LOCK.release()
+  return return_val
+
+
+def get_config_options():
+  """
+  Provides the configuration options from the loaded man page. This is an empty
+  list if no man page has been loaded.
+  """
+
+  CONFIG_DESCRIPTIONS_LOCK.acquire()
+
+  return_val = [CONFIG_DESCRIPTIONS[opt].option for opt in CONFIG_DESCRIPTIONS]
+
+  CONFIG_DESCRIPTIONS_LOCK.release()
+  return return_val
+
+
+def get_config_location():
+  """
+  Provides the location of the torrc, raising an IOError with the reason if the
+  path can't be determined.
+  """
+
+  controller = tor_controller()
+  config_location = controller.get_info("config-file", None)
+  tor_pid, tor_prefix = controller.controller.get_pid(None), CONFIG['tor.chroot']
+
+  if not config_location:
+    raise IOError("unable to query the torrc location")
+
+  try:
+    tor_cwd = system.cwd(tor_pid)
+    return tor_prefix + system.expand_path(config_location, tor_cwd)
+  except IOError as exc:
+    raise IOError("querying tor's pwd failed because %s" % exc)
+
+
+def get_multiline_parameters():
+  """
+  Provides parameters that can be defined multiple times in the torrc without
+  overwriting the value.
+  """
+
+  # fetches config options with the LINELIST (aka 'LineList'), LINELIST_S (aka
+  # 'Dependent'), and LINELIST_V (aka 'Virtual') types
+
+  global MULTILINE_PARAM
+
+  if MULTILINE_PARAM is None:
+    controller, multiline_entries = tor_controller(), []
+
+    config_option_query = controller.get_info("config/names", None)
+
+    if config_option_query:
+      for line in config_option_query.strip().split("\n"):
+        conf_option, conf_type = line.strip().split(" ", 1)
+
+        if conf_type in ("LineList", "Dependant", "Virtual"):
+          multiline_entries.append(conf_option)
+    else:
+      # unable to query tor connection, so not caching results
+      return ()
+
+    MULTILINE_PARAM = multiline_entries
+
+  return tuple(MULTILINE_PARAM)
+
+
+def get_custom_options(include_value = False):
+  """
+  Provides the torrc parameters that differ from their defaults.
+
+  Arguments:
+    include_value - provides the current value with results if true, otherwise
+                   this just contains the options
+  """
+
+  config_text = tor_controller().get_info("config-text", "").strip()
+  config_lines = config_text.split("\n")
+
+  # removes any duplicates
+
+  config_lines = list(set(config_lines))
+
+  # The "GETINFO config-text" query only provides options that differ
+  # from Tor's defaults with the exception of its Log and Nickname entries
+  # which, even if undefined, returns "Log notice stdout" as per:
+  # https://trac.torproject.org/projects/tor/ticket/2362
+  #
+  # If this is from the deb then it will be "Log notice file /var/log/tor/log"
+  # due to special patching applied to it, as per:
+  # https://trac.torproject.org/projects/tor/ticket/4602
+
+  try:
+    config_lines.remove("Log notice stdout")
+  except ValueError:
+    pass
+
+  try:
+    config_lines.remove("Log notice file /var/log/tor/log")
+  except ValueError:
+    pass
+
+  try:
+    config_lines.remove("Nickname %s" % socket.gethostname())
+  except ValueError:
+    pass
+
+  if include_value:
+    return config_lines
+  else:
+    return [line[:line.find(" ")] for line in config_lines]
+
+
+def save_conf(destination = None, contents = None):
+  """
+  Saves the configuration to the given path. If this is equivilant to
+  issuing a SAVECONF (the contents and destination match what tor's using)
+  then that's done. Otherwise, this writes the contents directly. This raises
+  an IOError if unsuccessful.
+
+  Arguments:
+    destination - path to be saved to, the current config location if None
+    contents    - configuration to be saved, the current config if None
+  """
+
+  if destination:
+    destination = os.path.abspath(destination)
+
+  # fills default config values, and sets is_saveconf to false if they differ
+  # from the arguments
+
+  is_saveconf, start_time = True, time.time()
+
+  current_config = get_custom_options(True)
+
+  if not contents:
+    contents = current_config
+  else:
+    is_saveconf &= contents == current_config
+
+  # The "GETINFO config-text" option was introduced in Tor version 0.2.2.7. If
+  # we're writing custom contents then this is fine, but if we're trying to
+  # save the current configuration then we need to fail if it's unavailable.
+  # Otherwise we'd write a blank torrc as per...
+  # https://trac.torproject.org/projects/tor/ticket/3614
+
+  if contents == ['']:
+    # double check that "GETINFO config-text" is unavailable rather than just
+    # giving an empty result
+
+    if tor_controller().get_info("config-text", None) is None:
+      raise IOError("determining the torrc requires Tor version 0.2.2.7")
+
+  current_location = None
+
+  try:
+    current_location = get_config_location()
+
+    if not destination:
+      destination = current_location
+    else:
+      is_saveconf &= destination == current_location
+  except IOError:
+    pass
+
+  if not destination:
+    raise IOError("unable to determine the torrc's path")
+
+  log_msg = "Saved config by %%s to %s (runtime: %%0.4f)" % destination
+
+  # attempts SAVECONF if we're updating our torrc with the current state
+
+  if is_saveconf:
+    try:
+      tor_controller().save_conf()
+
+      try:
+        get_torrc().load()
+      except IOError:
+        pass
+
+      log.debug(log_msg % ("SAVECONF", time.time() - start_time))
+      return  # if successful then we're done
+    except:
+      pass
+
+  # if the SAVECONF fails or this is a custom save then write contents directly
+
+  try:
+    # make dir if the path doesn't already exist
+
+    base_dir = os.path.dirname(destination)
+
+    if not os.path.exists(base_dir):
+      os.makedirs(base_dir)
+
+    # saves the configuration to the file
+
+    config_file = open(destination, "w")
+    config_file.write("\n".join(contents))
+    config_file.close()
+  except (IOError, OSError) as exc:
+    raise IOError(exc)
+
+  # reloads the cached torrc if overwriting it
+
+  if destination == current_location:
+    try:
+      get_torrc().load()
+    except IOError:
+      pass
+
+  log.debug(log_msg % ("directly writing", time.time() - start_time))
+
+
+def validate(contents = None):
+  """
+  Performs validation on the given torrc contents, providing back a listing of
+  (line number, issue, msg) tuples for issues found. If the issue occures on a
+  multiline torrc entry then the line number is for the last line of the entry.
+
+  Arguments:
+    contents - torrc contents
+  """
+
+  controller = tor_controller()
+  custom_options = get_custom_options()
+  issues_found, seen_options = [], []
+
+  # Strips comments and collapses multiline multi-line entries, for more
+  # information see:
+  # https://trac.torproject.org/projects/tor/ticket/1929
+
+  stripped_contents, multiline_buffer = [], ""
+
+  for line in _strip_comments(contents):
+    if not line:
+      stripped_contents.append("")
+    else:
+      line = multiline_buffer + line
+      multiline_buffer = ""
+
+      if line.endswith("\\"):
+        multiline_buffer = line[:-1]
+        stripped_contents.append("")
+      else:
+        stripped_contents.append(line.strip())
+
+  for line_number in range(len(stripped_contents) - 1, -1, -1):
+    line_text = stripped_contents[line_number]
+
+    if not line_text:
+      continue
+
+    line_comp = line_text.split(None, 1)
+
+    if len(line_comp) == 2:
+      option, value = line_comp
+    else:
+      option, value = line_text, ""
+
+    # Tor is case insensetive when parsing its torrc. This poses a bit of an
+    # issue for us because we want all of our checks to be case insensetive
+    # too but also want messages to match the normal camel-case conventions.
+    #
+    # Using the custom_options to account for this. It contains the tor reported
+    # options (camel case) and is either a matching set or the following defaut
+    # value check will fail. Hence using that hash to correct the case.
+    #
+    # TODO: when refactoring for stem make this less confusing...
+
+    for custom_opt in custom_options:
+      if custom_opt.lower() == option.lower():
+        option = custom_opt
+        break
+
+    # if an aliased option then use its real name
+
+    if option in CONFIG["torrc.alias"]:
+      option = CONFIG["torrc.alias"][option]
+
+    # most parameters are overwritten if defined multiple times
+
+    if option in seen_options and option not in get_multiline_parameters():
+      issues_found.append((line_number, ValidationError.DUPLICATE, option))
+      continue
+    else:
+      seen_options.append(option)
+
+    # checks if the value isn't necessary due to matching the defaults
+
+    if option not in custom_options:
+      issues_found.append((line_number, ValidationError.IS_DEFAULT, option))
+
+    # replace aliases with their recognized representation
+
+    if option in CONFIG["torrc.alias"]:
+      option = CONFIG["torrc.alias"][option]
+
+    # tor appears to replace tabs with a space, for instance:
+    # "accept\t*:563" is read back as "accept *:563"
+
+    value = value.replace("\t", " ")
+
+    # parse value if it's a size or time, expanding the units
+
+    value, value_type = _parse_conf_value(value)
+
+    # issues GETCONF to get the values tor's currently configured to use
+
+    tor_values = controller.get_conf(option, [], True)
+
+    # multiline entries can be comma separated values (for both tor and conf)
+
+    value_list = [value]
+
+    if option in get_multiline_parameters():
+      value_list = [val.strip() for val in value.split(",")]
+
+      fetched_values, tor_values = tor_values, []
+      for fetched_value in fetched_values:
+        for fetched_entry in fetched_value.split(","):
+          fetched_entry = fetched_entry.strip()
+
+          if fetched_entry not in tor_values:
+            tor_values.append(fetched_entry)
+
+    for val in value_list:
+      # checks if both the argument and tor's value are empty
+
+      is_blank_match = not val and not tor_values
+
+      if not is_blank_match and val not in tor_values:
+        # converts corrections to reader friedly size values
+
+        display_values = tor_values
+
+        if value_type == ValueType.SIZE:
+          display_values = [str_tools.size_label(int(val)) for val in tor_values]
+        elif value_type == ValueType.TIME:
+          display_values = [str_tools.time_label(int(val)) for val in tor_values]
+
+        issues_found.append((line_number, ValidationError.MISMATCH, ", ".join(display_values)))
+
+  # checks if any custom options are missing from the torrc
+
+  for option in custom_options:
+    # In new versions the 'DirReqStatistics' option is true by default and
+    # disabled on startup if geoip lookups are unavailable. If this option is
+    # missing then that's most likely the reason.
+    #
+    # https://trac.torproject.org/projects/tor/ticket/4237
+
+    if option == "DirReqStatistics":
+      continue
+
+    if option not in seen_options:
+      issues_found.append((None, ValidationError.MISSING, option))
+
+  return issues_found
+
+
+def _parse_conf_value(conf_arg):
+  """
+  Converts size or time values to their lowest units (bytes or seconds) which
+  is what GETCONF calls provide. The returned is a tuple of the value and unit
+  type.
+
+  Arguments:
+    conf_arg - torrc argument
+  """
+
+  if conf_arg.count(" ") == 1:
+    val, unit = conf_arg.lower().split(" ", 1)
+
+    if not val.isdigit():
+      return conf_arg, ValueType.UNRECOGNIZED
+
+    mult, mult_type = _get_unit_type(unit)
+
+    if mult is not None:
+      return str(int(val) * mult), mult_type
+
+  return conf_arg, ValueType.UNRECOGNIZED
+
+
+def _get_unit_type(unit):
+  """
+  Provides the type and multiplier for an argument's unit. The multiplier is
+  None if the unit isn't recognized.
+
+  Arguments:
+    unit - string representation of a unit
+  """
+
+  for label in SIZE_MULT:
+    if unit in CONFIG["torrc.units.size." + label]:
+      return SIZE_MULT[label], ValueType.SIZE
+
+  for label in TIME_MULT:
+    if unit in CONFIG["torrc.units.time." + label]:
+      return TIME_MULT[label], ValueType.TIME
+
+  return None, ValueType.UNRECOGNIZED
+
+
+def _strip_comments(contents):
+  """
+  Removes comments and extra whitespace from the given torrc contents.
+
+  Arguments:
+    contents - torrc contents
+  """
+
+  stripped_contents = []
+
+  for line in contents:
+    if line and "#" in line:
+      line = line[:line.find("#")]
+
+    stripped_contents.append(line.strip())
+
+  return stripped_contents
+
+
+class Torrc():
+  """
+  Wrapper for the torrc. All getters provide None if the contents are unloaded.
+  """
+
+  def __init__(self):
+    self.contents = None
+    self.config_location = None
+    self.vals_lock = threading.RLock()
+
+    # cached results for the current contents
+    self.displayable_contents = None
+    self.stripped_contents = None
+    self.corrections = None
+
+    # flag to indicate if we've given a load failure warning before
+    self.is_foad_fail_warned = False
+
+  def load(self, log_failure = False):
+    """
+    Loads or reloads the torrc contents, raising an IOError if there's a
+    problem.
+
+    Arguments:
+      log_failure - if the torrc fails to load and we've never provided a
+                   warning for this before then logs a warning
+    """
+
+    self.vals_lock.acquire()
+
+    # clears contents and caches
+    self.contents, self.config_location = None, None
+    self.displayable_contents = None
+    self.stripped_contents = None
+    self.corrections = None
+
+    try:
+      self.config_location = get_config_location()
+      config_file = open(self.config_location, "r")
+      self.contents = config_file.readlines()
+      config_file.close()
+    except IOError as exc:
+      if log_failure and not self.is_foad_fail_warned:
+        log.warn("Unable to load torrc (%s)" % exc.strerror)
+        self.is_foad_fail_warned = True
+
+      self.vals_lock.release()
+      raise exc
+
+    self.vals_lock.release()
+
+  def is_loaded(self):
+    """
+    Provides true if there's loaded contents, false otherwise.
+    """
+
+    return self.contents is not None
+
+  def get_config_location(self):
+    """
+    Provides the location of the loaded configuration contents. This may be
+    available, even if the torrc failed to be loaded.
+    """
+
+    return self.config_location
+
+  def get_contents(self):
+    """
+    Provides the contents of the configuration file.
+    """
+
+    self.vals_lock.acquire()
+    return_val = list(self.contents) if self.contents else None
+    self.vals_lock.release()
+    return return_val
+
+  def get_display_contents(self, strip = False):
+    """
+    Provides the contents of the configuration file, formatted in a rendering
+    frindly fashion:
+    - Tabs print as three spaces. Keeping them as tabs is problematic for
+      layouts since it's counted as a single character, but occupies several
+      cells.
+    - Strips control and unprintable characters.
+
+    Arguments:
+      strip - removes comments and extra whitespace if true
+    """
+
+    self.vals_lock.acquire()
+
+    if not self.is_loaded():
+      return_val = None
+    else:
+      if self.displayable_contents is None:
+        # restricts contents to displayable characters
+        self.displayable_contents = []
+
+        for line_number in range(len(self.contents)):
+          line_text = self.contents[line_number]
+          line_text = line_text.replace("\t", "   ")
+          line_text = ui_tools.get_printable(line_text)
+          self.displayable_contents.append(line_text)
+
+      if strip:
+        if self.stripped_contents is None:
+          self.stripped_contents = _strip_comments(self.displayable_contents)
+
+        return_val = list(self.stripped_contents)
+      else:
+        return_val = list(self.displayable_contents)
+
+    self.vals_lock.release()
+    return return_val
+
+  def get_corrections(self):
+    """
+    Performs validation on the loaded contents and provides back the
+    corrections. If validation is disabled then this won't provide any
+    results.
+    """
+
+    self.vals_lock.acquire()
+
+    if not self.is_loaded():
+      return_val = None
+    else:
+      tor_version = tor_controller().get_version(None)
+      skip_validation = not CONFIG["features.torrc.validate"]
+      skip_validation |= (tor_version is None or not tor_version >= stem.version.Requirement.GETINFO_CONFIG_TEXT)
+
+      if skip_validation:
+        log.info("Skipping torrc validation (requires tor 0.2.2.7-alpha)")
+        return_val = {}
+      else:
+        if self.corrections is None:
+          self.corrections = validate(self.contents)
+
+        return_val = list(self.corrections)
+
+    self.vals_lock.release()
+    return return_val
+
+  def get_lock(self):
+    """
+    Provides the lock governing concurrent access to the contents.
+    """
+
+    return self.vals_lock
+
+  def log_validation_issues(self):
+    """
+    Performs validation on the loaded contents, and logs warnings for issues
+    that are found.
+    """
+
+    corrections = self.get_corrections()
+
+    if corrections:
+      duplicate_options, default_options, mismatch_lines, missing_options = [], [], [], []
+
+      for line_number, issue, msg in corrections:
+        if issue == ValidationError.DUPLICATE:
+          duplicate_options.append("%s (line %i)" % (msg, line_number + 1))
+        elif issue == ValidationError.IS_DEFAULT:
+          default_options.append("%s (line %i)" % (msg, line_number + 1))
+        elif issue == ValidationError.MISMATCH:
+          mismatch_lines.append(line_number + 1)
+        elif issue == ValidationError.MISSING:
+          missing_options.append(msg)
+
+      if duplicate_options or default_options:
+        msg = "Unneeded torrc entries found. They've been highlighted in blue on the torrc page."
+
+        if duplicate_options:
+          if len(duplicate_options) > 1:
+            msg += "\n- entries ignored due to having duplicates: "
+          else:
+            msg += "\n- entry ignored due to having a duplicate: "
+
+          duplicate_options.sort()
+          msg += ", ".join(duplicate_options)
+
+        if default_options:
+          if len(default_options) > 1:
+            msg += "\n- entries match their default values: "
+          else:
+            msg += "\n- entry matches its default value: "
+
+          default_options.sort()
+          msg += ", ".join(default_options)
+
+        log.notice(msg)
+
+      if mismatch_lines or missing_options:
+        msg = "The torrc differs from what tor's using. You can issue a sighup to reload the torrc values by pressing x."
+
+        if mismatch_lines:
+          if len(mismatch_lines) > 1:
+            msg += "\n- torrc values differ on lines: "
+          else:
+            msg += "\n- torrc value differs on line: "
+
+          mismatch_lines.sort()
+          msg += ", ".join([str(val + 1) for val in mismatch_lines])
+
+        if missing_options:
+          if len(missing_options) > 1:
+            msg += "\n- configuration values are missing from the torrc: "
+          else:
+            msg += "\n- configuration value is missing from the torrc: "
+
+          missing_options.sort()
+          msg += ", ".join(missing_options)
+
+        log.warn(msg)
+
+
+def load_configuration_descriptions(path_prefix):
+  """
+  Attempts to load descriptions for tor's configuration options, fetching them
+  from the man page and persisting them to a file to speed future startups.
+  """
+
+  # It is important that this is loaded before entering the curses context,
+  # otherwise the man call pegs the cpu for around a minute (I'm not sure
+  # why... curses must mess the terminal in a way that's important to man).
+
+  if CONFIG["features.config.descriptions.enabled"]:
+    is_config_descriptions_loaded = False
+
+    # determines the path where cached descriptions should be persisted (left
+    # undefined if caching is disabled)
+
+    descriptor_path = None
+
+    if CONFIG["features.config.descriptions.persist"]:
+      data_dir = CONFIG["startup.data_directory"]
+
+      if not data_dir.endswith("/"):
+        data_dir += "/"
+
+      descriptor_path = os.path.expanduser(data_dir + "cache/") + CONFIG_DESC_FILENAME
+
+    # attempts to load configuration descriptions cached in the data directory
+
+    if descriptor_path:
+      try:
+        load_start_time = time.time()
+        load_option_descriptions(descriptor_path)
+        is_config_descriptions_loaded = True
+
+        log.info(DESC_LOAD_SUCCESS_MSG % (descriptor_path, time.time() - load_start_time))
+      except IOError as exc:
+        log.info(DESC_LOAD_FAILED_MSG % exc.strerror)
+
+    # fetches configuration options from the man page
+
+    if not is_config_descriptions_loaded:
+      try:
+        load_start_time = time.time()
+        load_option_descriptions()
+        is_config_descriptions_loaded = True
+
+        log.info(DESC_READ_MAN_SUCCESS_MSG % (time.time() - load_start_time))
+      except IOError as exc:
+        log.notice(DESC_READ_MAN_FAILED_MSG % exc.strerror)
+
+      # persists configuration descriptions
+
+      if is_config_descriptions_loaded and descriptor_path:
+        try:
+          load_start_time = time.time()
+          save_option_descriptions(descriptor_path)
+          log.info(DESC_SAVE_SUCCESS_MSG % (descriptor_path, time.time() - load_start_time))
+        except IOError as exc:
+          log.notice(DESC_SAVE_FAILED_MSG % exc.strerror)
+        except OSError as exc:
+          log.notice(DESC_SAVE_FAILED_MSG % exc)
+
+    # finally fall back to the cached descriptors provided with seth (this is
+    # often the case for tbb and manual builds)
+
+    if not is_config_descriptions_loaded:
+      try:
+        load_start_time = time.time()
+        loaded_version = load_option_descriptions("%sresources/%s" % (path_prefix, CONFIG_DESC_FILENAME), False)
+        is_config_descriptions_loaded = True
+        log.notice(DESC_INTERNAL_LOAD_SUCCESS_MSG % loaded_version)
+      except IOError as exc:
+        log.error(DESC_INTERNAL_LOAD_FAILED_MSG % exc.strerror)
diff --git a/seth/util/tracker.py b/seth/util/tracker.py
new file mode 100644
index 0000000..edf00fa
--- /dev/null
+++ b/seth/util/tracker.py
@@ -0,0 +1,666 @@
+"""
+Background tasks for gathering information about the tor process.
+
+::
+
+  get_connection_tracker - provides a ConnectionTracker for our tor process
+  get_resource_tracker - provides a ResourceTracker for our tor process
+  get_port_usage_tracker - provides a PortUsageTracker for our system
+
+  stop_trackers - halts any active trackers
+
+  Daemon - common parent for resolvers
+    |- ConnectionTracker - periodically checks the connections established by tor
+    |  |- get_custom_resolver - provide the custom conntion resolver we're using
+    |  |- set_custom_resolver - overwrites automatic resolver selecion with a custom resolver
+    |  +- get_value - provides our latest connection results
+    |
+    |- ResourceTracker - periodically checks the resource usage of tor
+    |  +- get_value - provides our latest resource usage results
+    |
+    |- PortUsageTracker - provides information about port usage on the local system
+    |  +- get_processes_using_ports - mapping of ports to the processes using it
+    |
+    |- run_counter - number of successful runs
+    |- get_rate - provides the rate at which we run
+    |- set_rate - sets the rate at which we run
+    |- set_paused - pauses or continues work
+    +- stop - stops further work by the daemon
+
+.. data:: Resources
+
+  Resource usage information retrieved about the tor process.
+
+  :var float cpu_sample: average cpu usage since we last checked
+  :var float cpu_average: average cpu usage since we first started tracking the process
+  :var float cpu_total: total cpu time the process has used since starting
+  :var int memory_bytes: memory usage of the process in bytes
+  :var float memory_percent: percentage of our memory used by this process
+  :var float timestamp: unix timestamp for when this information was fetched
+"""
+
+import collections
+import time
+import threading
+
+from stem.control import State
+from stem.util import conf, connection, proc, str_tools, system
+
+from seth.util import log, tor_controller
+
+CONFIG = conf.config_dict('seth', {
+  'queries.connections.rate': 5,
+  'queries.resources.rate': 5,
+  'queries.port_usage.rate': 5,
+})
+
+CONNECTION_TRACKER = None
+RESOURCE_TRACKER = None
+PORT_USAGE_TRACKER = None
+
+Resources = collections.namedtuple('Resources', [
+  'cpu_sample',
+  'cpu_average',
+  'cpu_total',
+  'memory_bytes',
+  'memory_percent',
+  'timestamp',
+])
+
+
+def get_connection_tracker():
+  """
+  Singleton for tracking the connections established by tor.
+  """
+
+  global CONNECTION_TRACKER
+
+  if CONNECTION_TRACKER is None:
+    CONNECTION_TRACKER = ConnectionTracker(CONFIG['queries.connections.rate'])
+
+  return CONNECTION_TRACKER
+
+
+def get_resource_tracker():
+  """
+  Singleton for tracking the resource usage of our tor process.
+  """
+
+  global RESOURCE_TRACKER
+
+  if RESOURCE_TRACKER is None:
+    RESOURCE_TRACKER = ResourceTracker(CONFIG['queries.resources.rate'])
+
+  return RESOURCE_TRACKER
+
+
+def get_port_usage_tracker():
+  """
+  Singleton for tracking the process using a set of ports.
+  """
+
+  global PORT_USAGE_TRACKER
+
+  if PORT_USAGE_TRACKER is None:
+    PORT_USAGE_TRACKER = PortUsageTracker(CONFIG['queries.port_usage.rate'])
+
+  return PORT_USAGE_TRACKER
+
+
+def stop_trackers():
+  """
+  Halts active trackers, providing back the thread shutting them down.
+
+  :returns: **threading.Thread** shutting down the daemons
+  """
+
+  def halt_trackers():
+    trackers = filter(lambda t: t.is_alive(), [
+      get_resource_tracker(),
+      get_connection_tracker(),
+    ])
+
+    for tracker in trackers:
+      tracker.stop()
+
+    for tracker in trackers:
+      tracker.join()
+
+  halt_thread = threading.Thread(target = halt_trackers)
+  halt_thread.setDaemon(True)
+  halt_thread.start()
+  return halt_thread
+
+
+def _resources_via_ps(pid):
+  """
+  Fetches resource usage information about a given process via ps. This returns
+  a tuple of the form...
+
+    (total_cpu_time, uptime, memory_in_bytes, memory_in_percent)
+
+  :param int pid: process to be queried
+
+  :returns: **tuple** with the resource usage information
+
+  :raises: **IOError** if unsuccessful
+  """
+
+  # ps results are of the form...
+  #
+  #     TIME     ELAPSED   RSS %MEM
+  # 3-08:06:32 21-00:00:12 121844 23.5
+  #
+  # ... or if Tor has only recently been started...
+  #
+  #     TIME      ELAPSED    RSS %MEM
+  #  0:04.40        37:57  18772  0.9
+
+  ps_call = system.call("ps -p {pid} -o cputime,etime,rss,%mem".format(pid = pid))
+
+  if ps_call and len(ps_call) >= 2:
+    stats = ps_call[1].strip().split()
+
+    if len(stats) == 4:
+      try:
+        total_cpu_time = str_tools.parse_short_time_label(stats[0])
+        uptime = str_tools.parse_short_time_label(stats[1])
+        memory_bytes = int(stats[2]) * 1024  # ps size is in kb
+        memory_percent = float(stats[3]) / 100.0
+
+        return (total_cpu_time, uptime, memory_bytes, memory_percent)
+      except ValueError:
+        pass
+
+  raise IOError("unrecognized output from ps: %s" % ps_call)
+
+
+def _resources_via_proc(pid):
+  """
+  Fetches resource usage information about a given process via proc. This
+  returns a tuple of the form...
+
+    (total_cpu_time, uptime, memory_in_bytes, memory_in_percent)
+
+  :param int pid: process to be queried
+
+  :returns: **tuple** with the resource usage information
+
+  :raises: **IOError** if unsuccessful
+  """
+
+  utime, stime, start_time = proc.stats(
+    pid,
+    proc.Stat.CPU_UTIME,
+    proc.Stat.CPU_STIME,
+    proc.Stat.START_TIME,
+  )
+
+  total_cpu_time = float(utime) + float(stime)
+  memory_in_bytes = proc.memory_usage(pid)[0]
+  total_memory = proc.physical_memory()
+
+  uptime = time.time() - float(start_time)
+  memory_in_percent = float(memory_in_bytes) / total_memory
+
+  return (total_cpu_time, uptime, memory_in_bytes, memory_in_percent)
+
+
+def _process_for_ports(local_ports, remote_ports):
+  """
+  Provides the name of the process using the given ports.
+
+  :param list local_ports: local port numbers to look up
+  :param list remote_ports: remote port numbers to look up
+
+  :returns: **dict** mapping the ports to the associated process names
+
+  :raises: **IOError** if unsuccessful
+  """
+
+  def _parse_lsof_line(line):
+    line_comp = line.split()
+
+    if not line:
+      return None, None, None  # blank line
+    elif len(line_comp) != 10:
+      raise ValueError('lines are expected to have ten fields')
+    elif line_comp[9] != '(ESTABLISHED)':
+      return None, None, None  # connection isn't established
+
+    cmd = line_comp[0]
+    port_map = line_comp[8]
+
+    if '->' not in port_map:
+      raise ValueError("'%s' is expected to be a '->' separated mapping" % port_map)
+
+    local, remote = port_map.split('->', 1)
+
+    if ':' not in local or ':' not in remote:
+      raise ValueError("'%s' is expected to be 'address:port' entries" % port_map)
+
+    local_port = local.split(':', 1)[1]
+    remote_port = remote.split(':', 1)[1]
+
+    if not connection.is_valid_port(local_port):
+      raise ValueError("'%s' isn't a valid port" % local_port)
+    elif not connection.is_valid_port(remote_port):
+      raise ValueError("'%s' isn't a valid port" % remote_port)
+
+    return int(local_port), int(remote_port), cmd
+
+  # atagar at fenrir:~/Desktop/seth$ lsof -i tcp:51849 -i tcp:37277
+  # COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
+  # tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9051->localhost:37277 (ESTABLISHED)
+  # tor     2001 atagar   15u  IPv4  22024      0t0  TCP localhost:9051->localhost:51849 (ESTABLISHED)
+  # python  2462 atagar    3u  IPv4  14047      0t0  TCP localhost:37277->localhost:9051 (ESTABLISHED)
+  # python  3444 atagar    3u  IPv4  22023      0t0  TCP localhost:51849->localhost:9051 (ESTABLISHED)
+
+  lsof_cmd = 'lsof -nP ' + ' '.join(['-i tcp:%s' % port for port in (local_ports + remote_ports)])
+  lsof_call = system.call(lsof_cmd)
+
+  if lsof_call:
+    results = {}
+
+    if lsof_call[0].startswith('COMMAND  '):
+      lsof_call = lsof_call[1:]  # strip the title line
+
+    for line in lsof_call:
+      try:
+        local_port, remote_port, cmd = _parse_lsof_line(line)
+
+        if local_port in local_ports:
+          results[local_port] = cmd
+        elif remote_port in remote_ports:
+          results[remote_port] = cmd
+      except ValueError as exc:
+        raise IOError("unrecognized output from lsof (%s): %s" % (exc, line))
+
+    return results
+
+  raise IOError("no results from lsof")
+
+
+class Daemon(threading.Thread):
+  """
+  Daemon that can perform a given action at a set rate. Subclasses are expected
+  to implement our _task() method with the work to be done.
+  """
+
+  def __init__(self, rate):
+    super(Daemon, self).__init__()
+    self.setDaemon(True)
+
+    self._process_lock = threading.RLock()
+    self._process_pid = None
+    self._process_name = None
+
+    self._rate = rate
+    self._last_ran = -1  # time when we last ran
+    self._run_counter = 0  # counter for the number of successful runs
+
+    self._is_paused = False
+    self._pause_condition = threading.Condition()
+    self._halt = False  # terminates thread if true
+
+    controller = tor_controller()
+    controller.add_status_listener(self._tor_status_listener)
+    self._tor_status_listener(controller, State.INIT, None)
+
+  def run(self):
+    while not self._halt:
+      time_since_last_ran = time.time() - self._last_ran
+
+      if self._is_paused or time_since_last_ran < self._rate:
+        sleep_duration = max(0.02, self._rate - time_since_last_ran)
+
+        with self._pause_condition:
+          if not self._halt:
+            self._pause_condition.wait(sleep_duration)
+
+        continue  # done waiting, try again
+
+      with self._process_lock:
+        if self._process_pid is not None:
+          is_successful = self._task(self._process_pid, self._process_name)
+        else:
+          is_successful = False
+
+        if is_successful:
+          self._run_counter += 1
+
+      self._last_ran = time.time()
+
+  def _task(self, process_pid, process_name):
+    """
+    Task the resolver is meant to perform. This should be implemented by
+    subclasses.
+
+    :param int process_pid: pid of the process we're tracking
+    :param str process_name: name of the process we're tracking
+
+    :returns: **bool** indicating if our run was successful or not
+    """
+
+    return True
+
+  def run_counter(self):
+    """
+    Provides the number of times we've successful runs so far. This can be used
+    by callers to determine if our results have been seen by them before or
+    not.
+
+    :returns: **int** for the run count we're on
+    """
+
+    return self._run_counter
+
+  def get_rate(self):
+    """
+    Provides the rate at which we perform our task.
+
+    :returns: **float** for the rate in seconds at which we perform our task
+    """
+
+    return self._rate
+
+  def set_rate(self, rate):
+    """
+    Sets the rate at which we perform our task in seconds.
+
+    :param float rate: rate at which to perform work in seconds
+    """
+
+    self._rate = rate
+
+  def set_paused(self, pause):
+    """
+    Either resumes or holds off on doing further work.
+
+    :param bool pause: halts work if **True**, resumes otherwise
+    """
+
+    self._is_paused = pause
+
+  def stop(self):
+    """
+    Halts further work and terminates the thread.
+    """
+
+    with self._pause_condition:
+      self._halt = True
+      self._pause_condition.notifyAll()
+
+  def _tor_status_listener(self, controller, event_type, _):
+    with self._process_lock:
+      if not self._halt and event_type in (State.INIT, State.RESET):
+        tor_pid = controller.get_pid(None)
+        tor_cmd = system.name_by_pid(tor_pid) if tor_pid else None
+
+        self._process_pid = tor_pid
+        self._process_name = tor_cmd if tor_cmd else 'tor'
+
+  def __enter__(self):
+    self.start()
+    return self
+
+  def __exit__(self, exit_type, value, traceback):
+    self.stop()
+    self.join()
+
+
+class ConnectionTracker(Daemon):
+  """
+  Periodically retrieves the connections established by tor.
+  """
+
+  def __init__(self, rate):
+    super(ConnectionTracker, self).__init__(rate)
+
+    self._connections = []
+    self._resolvers = connection.system_resolvers()
+    self._custom_resolver = None
+
+    # Number of times in a row we've either failed with our current resolver or
+    # concluded that our rate is too low.
+
+    self._failure_count = 0
+    self._rate_too_low_count = 0
+
+  def _task(self, process_pid, process_name):
+    if self._custom_resolver:
+      resolver = self._custom_resolver
+      is_default_resolver = False
+    elif self._resolvers:
+      resolver = self._resolvers[0]
+      is_default_resolver = True
+    else:
+      return False  # nothing to resolve with
+
+    try:
+      start_time = time.time()
+
+      self._connections = connection.get_connections(
+        resolver,
+        process_pid = process_pid,
+        process_name = process_name,
+      )
+
+      runtime = time.time() - start_time
+
+      if is_default_resolver:
+        self._failure_count = 0
+
+      # Reduce our rate if connection resolution is taking a long time. This is
+      # most often an issue for extremely busy relays.
+
+      min_rate = 100 * runtime
+
+      if self.get_rate() < min_rate:
+        self._rate_too_low_count += 1
+
+        if self._rate_too_low_count >= 3:
+          min_rate += 1  # little extra padding so we don't frequently update this
+          self.set_rate(min_rate)
+          self._rate_too_low_count = 0
+          log.debug('tracker.lookup_rate_increased', seconds = "%0.1f" % min_rate)
+      else:
+        self._rate_too_low_count = 0
+
+      return True
+    except IOError as exc:
+      log.info('wrap', text = exc)
+
+      # Fail over to another resolver if we've repeatedly been unable to use
+      # this one.
+
+      if is_default_resolver:
+        self._failure_count += 1
+
+        if self._failure_count >= 3:
+          self._resolvers.pop(0)
+          self._failure_count = 0
+
+          if self._resolvers:
+            log.notice(
+              'tracker.unable_to_use_resolver',
+              old_resolver = resolver,
+              new_resolver = self._resolvers[0],
+            )
+          else:
+            log.notice('tracker.unable_to_use_all_resolvers')
+
+      return False
+
+  def get_custom_resolver(self):
+    """
+    Provides the custom resolver the user has selected. This is **None** if
+    we're picking resolvers dynamically.
+
+    :returns: :data:`~stem.util.connection.Resolver` we're overwritten to use
+    """
+
+    return self._custom_resolver
+
+  def set_custom_resolver(self, resolver):
+    """
+    Sets the resolver used for connection resolution. If **None** then this is
+    automatically determined based on what is available.
+
+    :param stem.util.connection.Resolver resolver: resolver to use
+    """
+
+    self._custom_resolver = resolver
+
+  def get_value(self):
+    """
+    Provides a listing of tor's latest connections.
+
+    :returns: **list** of :class:`~stem.util.connection.Connection` we last
+      retrieved, an empty list if our tracker's been stopped
+    """
+
+    if self._halt:
+      return []
+    else:
+      return list(self._connections)
+
+
+class ResourceTracker(Daemon):
+  """
+  Periodically retrieves the resource usage of tor.
+  """
+
+  def __init__(self, rate):
+    super(ResourceTracker, self).__init__(rate)
+
+    self._resources = None
+    self._use_proc = proc.is_available()  # determines if we use proc or ps for lookups
+    self._failure_count = 0  # number of times in a row we've failed to get results
+
+  def get_value(self):
+    """
+    Provides tor's latest resource usage.
+
+    :returns: latest :data:`~seth.util.tracker.Resources` we've polled
+    """
+
+    result = self._resources
+    return result if result else Resources(0.0, 0.0, 0.0, 0, 0.0, 0.0)
+
+  def _task(self, process_pid, process_name):
+    try:
+      resolver = _resources_via_proc if self._use_proc else _resources_via_ps
+      total_cpu_time, uptime, memory_in_bytes, memory_in_percent = resolver(process_pid)
+
+      if self._resources:
+        cpu_sample = (total_cpu_time - self._resources.cpu_total) / self._resources.cpu_total
+      else:
+        cpu_sample = 0.0  # we need a prior datapoint to give a sampling
+
+      self._resources = Resources(
+        cpu_sample = cpu_sample,
+        cpu_average = total_cpu_time / uptime,
+        cpu_total = total_cpu_time,
+        memory_bytes = memory_in_bytes,
+        memory_percent = memory_in_percent,
+        timestamp = time.time(),
+      )
+
+      self._failure_count = 0
+      return True
+    except IOError as exc:
+      self._failure_count += 1
+
+      if self._use_proc:
+        if self._failure_count >= 3:
+          # We've failed three times resolving via proc. Warn, and fall back
+          # to ps resolutions.
+
+          self._use_proc = False
+          self._failure_count = 0
+
+          log.info(
+            'tracker.abort_getting_resources',
+            resolver = 'proc',
+            response = 'falling back to ps',
+            exc = exc,
+          )
+        else:
+          log.debug('tracker.unable_to_get_resources', resolver = 'proc', exc = exc)
+      else:
+        if self._failure_count >= 3:
+          # Give up on further attempts.
+
+          log.info(
+            'tracker.abort_getting_resources',
+            resolver = 'ps',
+            response = 'giving up on getting resource usage information',
+            exc = exc,
+          )
+
+          self.stop()
+        else:
+          log.debug('tracker.unable_to_get_resources', resolver = 'ps', exc = exc)
+
+      return False
+
+
+class PortUsageTracker(Daemon):
+  """
+  Periodically retrieves the processes using a set of ports.
+  """
+
+  def __init__(self, rate):
+    super(PortUsageTracker, self).__init__(rate)
+
+    self._last_requested_ports = []
+    self._processes_for_ports = {}
+    self._failure_count = 0  # number of times in a row we've failed to get results
+
+  def get_processes_using_ports(self, ports):
+    """
+    Registers a given set of ports for further lookups, and returns the last
+    set of 'port => process' mappings we retrieved. Note that this means that
+    we will not return the requested ports unless they're requested again after
+    a successful lookup has been performed.
+
+    :param list ports: port numbers to look up
+
+    :returns: **dict** mapping port numbers to the process using it
+    """
+
+    self._last_requested_ports = ports
+    return self._processes_for_ports
+
+  def _task(self, process_pid, process_name):
+    ports = self._last_requested_ports
+
+    if not ports:
+      return True
+
+    result = {}
+
+    # Use cached results from our last lookup if available.
+
+    for port, process in self._processes_for_ports.items():
+      if port in ports:
+        result[port] = process
+        ports.remove(port)
+
+    try:
+      result.update(_process_for_ports(ports, ports))
+
+      self._processes_for_ports = result
+      self._failure_count = 0
+      return True
+    except IOError as exc:
+      self._failure_count += 1
+
+      if self._failure_count >= 3:
+        log.info('tracker.abort_getting_port_usage', exc = exc)
+        self.stop()
+      else:
+        log.debug('tracker.unable_to_get_port_usages', exc = exc)
+
+      return False
diff --git a/seth/util/ui_tools.py b/seth/util/ui_tools.py
new file mode 100644
index 0000000..1ff8c1b
--- /dev/null
+++ b/seth/util/ui_tools.py
@@ -0,0 +1,400 @@
+"""
+Toolkit for working with curses.
+"""
+
+import curses
+
+from curses.ascii import isprint
+
+from seth.util import log, msg
+
+from stem.util import conf, system
+
+COLOR_LIST = {
+  'red': curses.COLOR_RED,
+  'green': curses.COLOR_GREEN,
+  'yellow': curses.COLOR_YELLOW,
+  'blue': curses.COLOR_BLUE,
+  'cyan': curses.COLOR_CYAN,
+  'magenta': curses.COLOR_MAGENTA,
+  'black': curses.COLOR_BLACK,
+  'white': curses.COLOR_WHITE,
+}
+
+DEFAULT_COLOR_ATTR = dict([(color, 0) for color in COLOR_LIST])
+COLOR_ATTR = None
+
+
+def conf_handler(key, value):
+  if key == 'features.color_override':
+    if value not in COLOR_LIST.keys() and value != 'none':
+      raise ValueError(msg('usage.unable_to_set_color_override', color = value))
+
+
+CONFIG = conf.config_dict('seth', {
+  'features.color_override': 'none',
+  'features.colorInterface': True,
+}, conf_handler)
+
+
+def is_color_supported():
+  """
+  Checks if curses presently supports rendering colors.
+
+  :returns: **True** if colors can be rendered, **False** otherwise
+  """
+
+  return _color_attr() != DEFAULT_COLOR_ATTR
+
+
+def get_color(color):
+  """
+  Provides attribute corresponding to a given text color. Supported colors
+  include:
+
+    * red
+    * green
+    * yellow
+    * blue
+    * cyan
+    * magenta
+    * black
+    * white
+
+  If color support isn't available or colors can't be initialized then this uses the
+  terminal's default coloring scheme.
+
+  :param str color: color attributes to be provided
+
+  :returns: **tuple** color pair used by curses to render the color
+  """
+
+  color_override = get_color_override()
+
+  if color_override:
+    color = color_override
+
+  return _color_attr()[color]
+
+
+def set_color_override(color = None):
+  """
+  Overwrites all requests for color with the given color instead.
+
+  :param str color: color to override all requests with, **None** if color
+    requests shouldn't be overwritten
+
+  :raises: **ValueError** if the color name is invalid
+  """
+
+  seth_config = conf.get_config('seth')
+
+  if color is None:
+    seth_config.set('features.color_override', 'none')
+  elif color in COLOR_LIST.keys():
+    seth_config.set('features.color_override', color)
+  else:
+    raise ValueError(msg('usage.unable_to_set_color_override', color = color))
+
+
+def get_color_override():
+  """
+  Provides the override color used by the interface.
+
+  :returns: **str** for the color requrests will be overwritten with, **None**
+    if no override is set
+  """
+
+  color_override = CONFIG.get('features.color_override', 'none')
+
+  if color_override == 'none':
+    return None
+  else:
+    return color_override
+
+
+def _color_attr():
+  """
+  Initializes color mappings usable by curses. This can only be done after
+  calling curses.initscr().
+  """
+
+  global COLOR_ATTR
+
+  if COLOR_ATTR is None:
+    if not CONFIG['features.colorInterface']:
+      COLOR_ATTR = DEFAULT_COLOR_ATTR
+    elif curses.has_colors():
+      color_attr = dict(DEFAULT_COLOR_ATTR)
+
+      for color_pair, color_name in enumerate(COLOR_LIST):
+        foreground_color = COLOR_LIST[color_name]
+        background_color = -1  # allows for default (possibly transparent) background
+        curses.init_pair(color_pair + 1, foreground_color, background_color)
+        color_attr[color_name] = curses.color_pair(color_pair + 1)
+
+      log.info('setup.color_support_available')
+      COLOR_ATTR = color_attr
+    else:
+      log.info('setup.color_support_unavailable')
+      COLOR_ATTR = DEFAULT_COLOR_ATTR
+
+  return COLOR_ATTR
+
+
+def disable_acs():
+  """
+  Replaces the curses ACS characters. This can be preferable if curses is
+  unable to render them...
+
+  http://www.atagar.com/seth/images/acs_display_failure.png
+  """
+
+  for item in curses.__dict__:
+    if item.startswith('ACS_'):
+      curses.__dict__[item] = ord('+')
+
+  # replace a few common border pipes that are better rendered as '|' or
+  # '-' instead
+
+  curses.ACS_SBSB = ord('|')
+  curses.ACS_VLINE = ord('|')
+  curses.ACS_BSBS = ord('-')
+  curses.ACS_HLINE = ord('-')
+
+
+def get_printable(line, keep_newlines = True):
+  """
+  Provides the line back with non-printable characters stripped.
+
+  :param str line: string to be processed
+  :param str keep_newlines: retains newlines if **True**, stripped otherwise
+
+  :returns: **str** of the line with only printable content
+  """
+
+  line = line.replace('\xc2', "'")
+  line = filter(lambda char: isprint(char) or (keep_newlines and char == '\n'), line)
+
+  return line
+
+
+def draw_box(panel, top, left, width, height, attr=curses.A_NORMAL):
+  """
+  Draws a box in the panel with the given bounds.
+
+  Arguments:
+    panel  - panel in which to draw
+    top    - vertical position of the box's top
+    left   - horizontal position of the box's left side
+    width  - width of the drawn box
+    height - height of the drawn box
+    attr   - text attributes
+  """
+
+  # draws the top and bottom
+
+  panel.hline(top, left + 1, width - 2, attr)
+  panel.hline(top + height - 1, left + 1, width - 2, attr)
+
+  # draws the left and right sides
+
+  panel.vline(top + 1, left, height - 2, attr)
+  panel.vline(top + 1, left + width - 1, height - 2, attr)
+
+  # draws the corners
+
+  panel.addch(top, left, curses.ACS_ULCORNER, attr)
+  panel.addch(top, left + width - 1, curses.ACS_URCORNER, attr)
+  panel.addch(top + height - 1, left, curses.ACS_LLCORNER, attr)
+
+
+def get_scroll_position(key, position, page_height, content_height, is_cursor = False):
+  """
+  Parses navigation keys, providing the new scroll possition the panel should
+  use. Position is always between zero and (content_height - page_height). This
+  handles the following keys:
+  Up / Down - scrolls a position up or down
+  Page Up / Page Down - scrolls by the page_height
+  Home - top of the content
+  End - bottom of the content
+
+  This provides the input position if the key doesn't correspond to the above.
+
+  Arguments:
+    key           - keycode for the user's input
+    position      - starting position
+    page_height    - size of a single screen's worth of content
+    content_height - total lines of content that can be scrolled
+    is_cursor      - tracks a cursor position rather than scroll if true
+  """
+
+  if key.is_scroll():
+    shift = 0
+
+    if key.match('up'):
+      shift = -1
+    elif key.match('down'):
+      shift = 1
+    elif key.match('page_up'):
+      shift = -page_height + 1 if is_cursor else -page_height
+    elif key.match('page_down'):
+      shift = page_height - 1 if is_cursor else page_height
+    elif key.match('home'):
+      shift = -content_height
+    elif key.match('end'):
+      shift = content_height
+
+    # returns the shift, restricted to valid bounds
+
+    max_location = content_height - 1 if is_cursor else content_height - page_height
+    return max(0, min(position + shift, max_location))
+  else:
+    return position
+
+
+class Scroller:
+  """
+  Tracks the scrolling position when there might be a visible cursor. This
+  expects that there is a single line displayed per an entry in the contents.
+  """
+
+  def __init__(self, is_cursor_enabled):
+    self.scroll_location, self.cursor_location = 0, 0
+    self.cursor_selection = None
+    self.is_cursor_enabled = is_cursor_enabled
+
+  def get_scroll_location(self, content, page_height):
+    """
+    Provides the scrolling location, taking into account its cursor's location
+    content size, and page height.
+
+    Arguments:
+      content    - displayed content
+      page_height - height of the display area for the content
+    """
+
+    if content and page_height:
+      self.scroll_location = max(0, min(self.scroll_location, len(content) - page_height + 1))
+
+      if self.is_cursor_enabled:
+        self.get_cursor_selection(content)  # resets the cursor location
+
+        # makes sure the cursor is visible
+
+        if self.cursor_location < self.scroll_location:
+          self.scroll_location = self.cursor_location
+        elif self.cursor_location > self.scroll_location + page_height - 1:
+          self.scroll_location = self.cursor_location - page_height + 1
+
+      # checks if the bottom would run off the content (this could be the
+      # case when the content's size is dynamic and entries are removed)
+
+      if len(content) > page_height:
+        self.scroll_location = min(self.scroll_location, len(content) - page_height)
+
+    return self.scroll_location
+
+  def get_cursor_selection(self, content):
+    """
+    Provides the selected item in the content. This is the same entry until
+    the cursor moves or it's no longer available (in which case it moves on to
+    the next entry).
+
+    Arguments:
+      content - displayed content
+    """
+
+    # TODO: needs to handle duplicate entries when using this for the
+    # connection panel
+
+    if not self.is_cursor_enabled:
+      return None
+    elif not content:
+      self.cursor_location, self.cursor_selection = 0, None
+      return None
+
+    self.cursor_location = min(self.cursor_location, len(content) - 1)
+
+    if self.cursor_selection is not None and self.cursor_selection in content:
+      # moves cursor location to track the selection
+      self.cursor_location = content.index(self.cursor_selection)
+    else:
+      # select the next closest entry
+      self.cursor_selection = content[self.cursor_location]
+
+    return self.cursor_selection
+
+  def handle_key(self, key, content, page_height):
+    """
+    Moves either the scroll or cursor according to the given input.
+
+    Arguments:
+      key        - key code of user input
+      content    - displayed content
+      page_height - height of the display area for the content
+    """
+
+    if self.is_cursor_enabled:
+      self.get_cursor_selection(content)  # resets the cursor location
+      start_location = self.cursor_location
+    else:
+      start_location = self.scroll_location
+
+    new_location = get_scroll_position(key, start_location, page_height, len(content), self.is_cursor_enabled)
+
+    if start_location != new_location:
+      if self.is_cursor_enabled:
+        self.cursor_selection = content[new_location]
+      else:
+        self.scroll_location = new_location
+
+      return True
+    else:
+      return False
+
+
+def is_wide_characters_supported():
+  """
+  Checks if our version of curses has wide character support. This is required
+  to print unicode.
+
+  :returns: **bool** that's **True** if curses supports wide characters, and
+    **False** if it either can't or this can't be determined
+  """
+
+  try:
+    # Gets the dynamic library used by the interpretor for curses. This uses
+    # 'ldd' on Linux or 'otool -L' on OSX.
+    #
+    # atagar at fenrir:~/Desktop$ ldd /usr/lib/python2.6/lib-dynload/_curses.so
+    #   linux-gate.so.1 =>  (0x00a51000)
+    #   libncursesw.so.5 => /lib/libncursesw.so.5 (0x00faa000)
+    #   libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0x002f1000)
+    #   libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00158000)
+    #   libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0x00398000)
+    #   /lib/ld-linux.so.2 (0x00ca8000)
+    #
+    # atagar$ otool -L /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so
+    # /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so:
+    #   /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
+    #   /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
+    #   /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.6)
+
+    import _curses
+
+    lib_dependency_lines = None
+
+    if system.is_available("ldd"):
+      lib_dependency_lines = system.call("ldd %s" % _curses.__file__)
+    elif system.is_available("otool"):
+      lib_dependency_lines = system.call("otool -L %s" % _curses.__file__)
+
+    if lib_dependency_lines:
+      for line in lib_dependency_lines:
+        if "libncursesw" in line:
+          return True
+  except:
+    pass
+
+  return False
diff --git a/sethrc.sample b/sethrc.sample
new file mode 100644
index 0000000..6f120b4
--- /dev/null
+++ b/sethrc.sample
@@ -0,0 +1,244 @@
+# Startup options
+tor.password
+startup.events N3
+startup.dataDirectory ~/.seth
+
+# Seconds between querying information
+
+queries.connections.rate 5
+queries.resources.rate 5
+queries.port_usage.rate 5
+
+queries.refreshRate.rate 5
+
+# allows individual panels to be included/excluded
+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
+
+# Renders the interface with color if set and the terminal supports it
+features.colorInterface true
+
+# Uses ACS (alternate character support) to display nice borders. This may not
+# work on all terminals.
+features.acsSupport true
+
+# Replaces all colored content (ie, anything that isn't white) with this
+# color. Valid options are:
+# none, red, green, yellow, blue, cyan, magenta, black, white
+features.colorOverride none
+
+# Includes unicode characters in the interface.
+features.printUnicode true
+
+# Checks the torrc for issues, warning and hilighting problems if true
+features.torrc.validate true
+
+# Set this if you're running in a chroot jail or other environment where tor's
+# resources (log, state, etc) should have a prefix in their paths.
+
+tor.chroot
+
+# If set, seth appends any log messages it reports while running to the given
+# log file. This does not take filters into account or include prepopulated
+# events.
+features.logFile 
+
+# Seconds to wait on user input before refreshing content
+features.redrawRate 5
+
+# Rate (seconds) to periodically redraw the screen, disabled if zero. This
+# shouldn't be necessary, but can correct issues if the terminal gets into a
+# funky state.
+features.refreshRate 5
+
+# Confirms promt to confirm when quiting if true
+features.confirmQuit true
+
+# Paremters for the log panel
+# ---------------------------
+# showDateDividers
+#   show borders with dates for entries from previous days
+# showDuplicateEntries
+#   shows all log entries if true, otherwise collapses similar entries with an
+#   indicator for how much is being hidden
+# entryDuration
+#   number of days log entries are kept before being dropped (if zero then
+#   they're kept until cropped due to caching limits)
+# maxLinesPerEntry
+#   max number of lines to display for a single log entry
+# prepopulate
+#   attempts to read past events from the log file if true
+# prepopulateReadLimit
+#   maximum entries read from the log file, used to prevent huge log files from
+#   causing a slow startup time.
+# maxRefreshRate
+#   rate limiting (in milliseconds) for drawing the log if updates are made
+#   rapidly (for instance, when at the DEBUG runlevel)
+# regex
+#   preconfigured regular expression pattern, up to five will be loaded
+
+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 My First Regex Pattern
+#features.log.regex ^My Second Regex Pattern$
+
+# Paremters for the config panel
+# ---------------------------
+# order
+#   three comma separated configuration attributes, options including:
+#
+#     * 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
+# features.config.prepopulateEditValues
+#   when editing config values the current value is prepopulated if true, and
+#   left blank otherwise
+# state.colWidth.*
+#   column content width
+# state.showPrivateOptions
+#   tor provides config options of the form "__<option>" that can be dangerous
+#   to set, if true seth provides these on the config panel
+# state.showVirtualOptions
+#   virtual options are placeholders for other option groups, never having
+#   values or being setable themselves
+# file.showScrollbars
+#   displays scrollbars when the torrc content is longer than the display
+# file.maxLinesPerEntry
+#   max number of lines to display for a single entry in the torrc
+
+features.config.order MAN_ENTRY, OPTION, IS_DEFAULT
+features.config.selectionDetails.height 6
+features.config.prepopulateEditValues true
+features.config.state.colWidth.option 25
+features.config.state.colWidth.value 15
+features.config.state.showPrivateOptions false
+features.config.state.showVirtualOptions false
+features.config.file.showScrollbars true
+features.config.file.maxLinesPerEntry 8
+
+# Descriptions for tor's configuration options can be loaded from its man page
+# to give usage information on the settings page. They can also be persisted to
+# a file to speed future lookups.
+# ---------------------------
+# enabled
+#   allows the descriptions to be fetched from the man page if true
+# persist
+#   caches the descriptions (substantially saving on future startup times)
+
+features.config.descriptions.enabled true
+features.config.descriptions.persist true
+
+# General graph parameters
+# ------------------------
+# height
+#   height of graphed stats
+# maxWidth
+#   maximum number of graphed entries
+# interval
+#   each second,   5 seconds,     30 seconds,  minutely,      
+#   15 minute,     30 minute,     hourly,      daily
+# bound
+#   global_max - global maximum (highest value ever seen)
+#   local_max - local maximum (highest value currently on the graph)
+#   tight - local maximum and minimum
+# type
+#   none, bandwidth, connections, resources
+
+features.graph.height 7
+features.graph.maxWidth 150
+features.graph.interval each second
+features.graph.bound local_max
+features.graph.type bandwidth
+
+# Parameters for graphing bandwidth stats
+# ---------------------------------------
+# prepopulate
+#   attempts to use tor's state file to prepopulate the bandwidth graph at the
+#   15-minute interval (this requires the minimum of a day's worth of uptime)
+# transferInBytes
+#   shows rate measurments in bytes if true, bits otherwise
+# accounting.show
+#   provides accounting stats if AccountingMax was set
+
+features.graph.bw.prepopulate true
+features.graph.bw.transferInBytes false
+features.graph.bw.accounting.show true
+
+# Parameters for connection display
+# ---------------------------------
+# listingType
+#   the primary category of information shown by default, options including:
+#
+#   * IP_ADDRESS
+#   * HOSTNAME
+#   * FINGERPRINT
+#   * NICKNAME
+#
+# order
+#   three comma separated configuration attributes, options including:
+#
+#   * 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
+#   appear more frequently - just increase the rate at which the uptime field
+#   is updated)
+# resolveApps
+#   issues lsof queries to determining the applications involved in local
+#   SOCKS and CONTROL connections
+# markInitialConnections
+#   if true, the uptime of the initial connections when we start are marked
+#   with a '+' (these uptimes are estimates since seth can only track a
+#   connection's duration while it runs)
+# showIps
+#   shows ip addresses for other tor relays, dropping this information if
+#   false
+# showExitPort
+#   shows port related information of exit connections we relay if true
+# showColumn.*
+#   toggles the visability of the connection table columns
+
+features.connection.listingType IP_ADDRESS
+features.connection.order CATEGORY, LISTING, UPTIME
+features.connection.refreshRate 5
+features.connection.resolveApps true
+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
+
+# Caching parameters
+cache.logPanel.size 1000
+cache.sethLog.size 1000
+cache.sethLog.trimSize 200
+
diff --git a/setup.py b/setup.py
index 616222c..3d16aad 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ import os
 import sys
 import gzip
 import tempfile
-from arm.version import VERSION
+from seth.version import VERSION
 from distutils.core import setup
 
 def getResources(dst, sourceDir):
@@ -11,29 +11,29 @@ def getResources(dst, sourceDir):
   Provides a list of tuples of the form...
   [(destination, (file1, file2...)), ...]
 
-  for the given contents of the arm directory (that's right, distutils isn't
+  for the given contents of the seth directory (that's right, distutils isn't
   smart enough to know how to copy directories).
   """
 
   results = []
 
-  for root, _, files in os.walk(os.path.join("arm", sourceDir)):
+  for root, _, files in os.walk(os.path.join("seth", sourceDir)):
     if files:
       fileListing = tuple([os.path.join(root, file) for file in files])
       results.append((os.path.join(dst, root[4:]), fileListing))
 
   return results
 
-# Use 'tor-arm' instead of 'arm' in the path for the sample armrc if we're
+# Use 'tor-seth' instead of 'seth' in the path for the sample sethrc if we're
 # building for debian.
 
 isDebInstall = False
 for arg in sys.argv:
-  if "tor-arm" in arg or "release_deb" in arg:
+  if "tor-seth" in arg or "release_deb" in arg:
     isDebInstall = True
     break
 
-docPath = "/usr/share/doc/%s" % ("tor-arm" if isDebInstall else "arm")
+docPath = "/usr/share/doc/%s" % ("tor-seth" if isDebInstall else "seth")
 
 # Allow the docPath to be overridden via a '--docPath' argument. This is to
 # support custom documentation locations on Gentoo, as discussed in:
@@ -59,7 +59,7 @@ except ValueError: pass # --docPath flag not found
 #   install-purelib=/usr/share
 # which would mean a bit more unnecessary clutter.
 
-manFilename = "arm/resoureces/arm.1"
+manFilename = "seth/resoureces/seth.1"
 if "install" in sys.argv:
   sys.argv += ["--install-purelib", "/usr/share"]
 
@@ -68,13 +68,13 @@ if "install" in sys.argv:
   # page instead.
 
   try:
-    manInputFile = open('arm/resources/arm.1', 'r')
+    manInputFile = open('seth/resources/seth.1', 'r')
     manContents = manInputFile.read()
     manInputFile.close()
 
     # temporary destination for the man page guarenteed to be unoccupied (to
     # avoid conflicting with files that are already there)
-    tmpFilename = tempfile.mktemp("/arm.1.gz")
+    tmpFilename = tempfile.mktemp("/seth.1.gz")
 
     # make dir if the path doesn't already exist
     baseDir = os.path.dirname(tmpFilename)
@@ -90,34 +90,34 @@ if "install" in sys.argv:
   except IOError, exc:
     print "Unable to compress man page: %s" % exc
 
-installPackages = ['arm', 'arm.cli', 'arm.cli.graphing', 'arm.cli.connections', 'arm.cli.menu', 'arm.util', 'arm.stem']
+installPackages = ['seth', 'seth.cli', 'seth.cli.graphing', 'seth.cli.connections', 'seth.cli.menu', 'seth.util', 'seth.stem']
 
-setup(name='arm',
+setup(name='seth',
       version=VERSION,
       description='Terminal tor status monitor',
       license='GPL v3',
       author='Damian Johnson',
       author_email='atagar at torproject.org',
-      url='http://www.atagar.com/arm/',
+      url='http://www.atagar.com/seth/',
       packages=installPackages,
-      package_dir={'arm': 'arm'},
-      data_files=[("/usr/bin", ["run_arm"]),
+      package_dir={'seth': 'seth'},
+      data_files=[("/usr/bin", ["run_seth"]),
                   ("/usr/share/man/man1", [manFilename]),
-                  (docPath, ["armrc.sample"]),
-                  ("/usr/share/arm/gui", ["arm/gui/arm.xml"]),
-                  ("/usr/share/arm", ["arm/settings.cfg", "arm/uninstall"])] +
-                  getResources("/usr/share/arm", "resources"),
+                  (docPath, ["sethrc.sample"]),
+                  ("/usr/share/seth/gui", ["seth/gui/seth.xml"]),
+                  ("/usr/share/seth", ["seth/settings.cfg", "seth/uninstall"])] +
+                  getResources("/usr/share/seth", "resources"),
      )
 
 # Cleans up the temporary compressed man page.
-if manFilename != 'arm/resoureces/arm.1' and os.path.isfile(manFilename):
+if manFilename != 'seth/resoureces/seth.1' and os.path.isfile(manFilename):
   if "-q" not in sys.argv: print "Removing %s" % manFilename
   os.remove(manFilename)
 
 # Removes the egg_info file. Apparently it is not optional during setup
 # (hardcoded in distutils/command/install.py), nor are there any arguments to
 # bypass its creation. The deb build removes this as part of its rules script.
-eggPath = '/usr/share/arm-%s.egg-info' % VERSION
+eggPath = '/usr/share/seth-%s.egg-info' % VERSION
 
 if not isDebInstall and os.path.isfile(eggPath):
   if "-q" not in sys.argv: print "Removing %s" % eggPath
diff --git a/test/__init__.py b/test/__init__.py
index 20963bc..31a70bb 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,5 +1,5 @@
 """
-Unit tests for arm.
+Unit tests for seth.
 """
 
 __all__ = [
diff --git a/test/arguments.py b/test/arguments.py
index dd4f45b..3b3024a 100644
--- a/test/arguments.py
+++ b/test/arguments.py
@@ -2,7 +2,7 @@ import unittest
 
 from mock import Mock, patch
 
-from arm.arguments import DEFAULT_ARGS, parse, expand_events, missing_event_types
+from seth.arguments import DEFAULT_ARGS, parse, expand_events, missing_event_types
 
 
 class TestArgumentParsing(unittest.TestCase):
@@ -106,7 +106,7 @@ class TestExpandEvents(unittest.TestCase):
 
 
 class TestMissingEventTypes(unittest.TestCase):
-  @patch('arm.arguments.tor_controller')
+  @patch('seth.arguments.tor_controller')
   def test_with_a_failed_query(self, controller_mock):
     controller = Mock()
     controller.get_info.return_value = None
@@ -114,7 +114,7 @@ class TestMissingEventTypes(unittest.TestCase):
 
     self.assertEqual([], missing_event_types())
 
-  @patch('arm.arguments.tor_controller')
+  @patch('seth.arguments.tor_controller')
   def test_without_unrecognized_events(self, controller_mock):
     controller = Mock()
     controller.get_info.return_value = 'DEBUG INFO NOTICE WARN ERR'
@@ -122,7 +122,7 @@ class TestMissingEventTypes(unittest.TestCase):
 
     self.assertEqual([], missing_event_types())
 
-  @patch('arm.arguments.tor_controller')
+  @patch('seth.arguments.tor_controller')
   def test_with_unrecognized_events(self, controller_mock):
     controller = Mock()
     controller.get_info.return_value = 'EVENT1 DEBUG INFO NOTICE WARN EVENT2 ERR EVENT3'
diff --git a/test/settings.cfg b/test/settings.cfg
index f055974..fd40878 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -9,8 +9,8 @@ pep8.ignore E127
 # False positives from pyflakes. These are mappings between the path and the
 # issue.
 
-pyflakes.ignore arm/prereq.py => 'stem' imported but unused
-pyflakes.ignore arm/prereq.py => 'curses' imported but unused
+pyflakes.ignore seth/prereq.py => 'stem' imported but unused
+pyflakes.ignore seth/prereq.py => 'curses' imported but unused
 pyflakes.ignore run_tests.py => 'pyflakes' imported but unused
 pyflakes.ignore run_tests.py => 'pep8' imported but unused
 
diff --git a/test/util/__init__.py b/test/util/__init__.py
index f338157..0ec5c02 100644
--- a/test/util/__init__.py
+++ b/test/util/__init__.py
@@ -1,5 +1,5 @@
 """
-Unit tests for arm's utilities.
+Unit tests for seth's utilities.
 """
 
 __all__ = ['bandwidth_from_state']
diff --git a/test/util/bandwidth_from_state.py b/test/util/bandwidth_from_state.py
index 6877371..87c0a40 100644
--- a/test/util/bandwidth_from_state.py
+++ b/test/util/bandwidth_from_state.py
@@ -5,7 +5,7 @@ import unittest
 
 from mock import Mock, patch
 
-from arm.util import bandwidth_from_state
+from seth.util import bandwidth_from_state
 
 STATE_FILE = """\
 # Tor state file last generated on 2014-07-20 13:05:10 local time
@@ -32,7 +32,7 @@ BWHistoryWriteEnds %s
 
 
 class TestBandwidthFromState(unittest.TestCase):
-  @patch('arm.util.tor_controller')
+  @patch('seth.util.tor_controller')
   def test_when_not_localhost(self, tor_controller_mock):
     tor_controller_mock().is_localhost.return_value = False
 
@@ -42,7 +42,7 @@ class TestBandwidthFromState(unittest.TestCase):
     except ValueError as exc:
       self.assertEqual('we can only prepopulate bandwidth information for a local tor instance', str(exc))
 
-  @patch('arm.util.tor_controller')
+  @patch('seth.util.tor_controller')
   def test_unknown_pid(self, tor_controller_mock):
     tor_controller_mock().is_localhost.return_value = True
     tor_controller_mock().get_pid.return_value = None
@@ -53,7 +53,7 @@ class TestBandwidthFromState(unittest.TestCase):
     except ValueError as exc:
       self.assertEqual("unable to determine tor's uptime", str(exc))
 
-  @patch('arm.util.tor_controller')
+  @patch('seth.util.tor_controller')
   @patch('stem.util.system.start_time')
   def test_insufficient_uptime(self, start_time_mock, tor_controller_mock):
     tor_controller_mock().is_localhost.return_value = True
@@ -65,7 +65,7 @@ class TestBandwidthFromState(unittest.TestCase):
     except ValueError as exc:
       self.assertEqual("insufficient uptime, tor must've been running for at least a day", str(exc))
 
-  @patch('arm.util.tor_controller')
+  @patch('seth.util.tor_controller')
   @patch('stem.util.system.start_time', Mock(return_value = 50))
   def test_no_data_dir(self, tor_controller_mock):
     tor_controller_mock().is_localhost.return_value = True
@@ -77,8 +77,8 @@ class TestBandwidthFromState(unittest.TestCase):
     except ValueError as exc:
       self.assertEqual("unable to determine tor's data directory", str(exc))
 
-  @patch('arm.util.tor_controller')
-  @patch('arm.util.open', create = True)
+  @patch('seth.util.tor_controller')
+  @patch('seth.util.open', create = True)
   @patch('stem.util.system.start_time', Mock(return_value = 50))
   def test_no_bandwidth_entries(self, open_mock, tor_controller_mock):
     tor_controller_mock().is_localhost.return_value = True
@@ -93,8 +93,8 @@ class TestBandwidthFromState(unittest.TestCase):
 
     open_mock.assert_called_once_with('/home/atagar/.tor/state')
 
-  @patch('arm.util.tor_controller')
-  @patch('arm.util.open', create = True)
+  @patch('seth.util.tor_controller')
+  @patch('seth.util.open', create = True)
   @patch('stem.util.system.start_time', Mock(return_value = 50))
   def test_when_successful(self, open_mock, tor_controller_mock):
     tor_controller_mock().is_localhost.return_value = True
diff --git a/test/util/tracker/__init__.py b/test/util/tracker/__init__.py
index 1d7d5de..ddf5bf9 100644
--- a/test/util/tracker/__init__.py
+++ b/test/util/tracker/__init__.py
@@ -1,5 +1,5 @@
 """
-Unit tests for arm's tracker utilities.
+Unit tests for seth's tracker utilities.
 """
 
 __all__ = [
diff --git a/test/util/tracker/connection_tracker.py b/test/util/tracker/connection_tracker.py
index e6120bf..639b06c 100644
--- a/test/util/tracker/connection_tracker.py
+++ b/test/util/tracker/connection_tracker.py
@@ -1,7 +1,7 @@
 import time
 import unittest
 
-from arm.util.tracker import ConnectionTracker
+from seth.util.tracker import ConnectionTracker
 
 from stem.util import connection
 
@@ -13,10 +13,10 @@ CONNECTION_3 = connection.Connection('127.0.0.1', 1059, '74.125.28.106', 80, 'tc
 
 
 class TestConnectionTracker(unittest.TestCase):
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker.connection.get_connections')
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
-  @patch('arm.util.tracker.connection.system_resolvers', Mock(return_value = [connection.Resolver.NETSTAT]))
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker.connection.get_connections')
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.connection.system_resolvers', Mock(return_value = [connection.Resolver.NETSTAT]))
   def test_fetching_connections(self, get_value_mock, tor_controller_mock):
     tor_controller_mock().get_pid.return_value = 12345
     get_value_mock.return_value = [CONNECTION_1, CONNECTION_2, CONNECTION_3]
@@ -36,10 +36,10 @@ class TestConnectionTracker(unittest.TestCase):
       self.assertEqual(2, daemon.run_counter())
       self.assertEqual([], connections)
 
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker.connection.get_connections')
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
-  @patch('arm.util.tracker.connection.system_resolvers', Mock(return_value = [connection.Resolver.NETSTAT, connection.Resolver.LSOF]))
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker.connection.get_connections')
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.connection.system_resolvers', Mock(return_value = [connection.Resolver.NETSTAT, connection.Resolver.LSOF]))
   def test_resolver_failover(self, get_value_mock, tor_controller_mock):
     tor_controller_mock().get_pid.return_value = 12345
     get_value_mock.side_effect = IOError()
diff --git a/test/util/tracker/daemon.py b/test/util/tracker/daemon.py
index 5bf75fd..4e786a0 100644
--- a/test/util/tracker/daemon.py
+++ b/test/util/tracker/daemon.py
@@ -1,14 +1,14 @@
 import time
 import unittest
 
-from arm.util.tracker import Daemon
+from seth.util.tracker import Daemon
 
 from mock import Mock, patch
 
 
 class TestDaemon(unittest.TestCase):
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker.system')
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker.system')
   def test_init(self, system_mock, tor_controller_mock):
     # Check that we register ourselves to listen for status changes, and
     # properly retrieve the process' pid and name.
@@ -25,8 +25,8 @@ class TestDaemon(unittest.TestCase):
     tor_controller_mock().add_status_listener.assert_called_with(daemon._tor_status_listener)
     system_mock.name_by_pid.assert_called_with(12345)
 
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker.system')
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker.system')
   def test_init_without_name(self, system_mock, tor_controller_mock):
     # Check when we default to 'tor' if unable to determine the process' name.
 
@@ -36,8 +36,8 @@ class TestDaemon(unittest.TestCase):
     daemon = Daemon(0.05)
     self.assertEqual('tor', daemon._process_name)
 
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker.system')
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker.system')
   def test_init_without_pid(self, system_mock, tor_controller_mock):
     # Check when we can't determine tor's pid.
 
@@ -48,8 +48,8 @@ class TestDaemon(unittest.TestCase):
     self.assertEqual('tor', daemon._process_name)
     self.assertEqual(0, system_mock.call_count)
 
-  @patch('arm.util.tracker.tor_controller', Mock(return_value = Mock()))
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.tor_controller', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
   def test_daemon_calls_task(self):
     # Check that our Daemon calls the task method at the given rate.
 
@@ -57,8 +57,8 @@ class TestDaemon(unittest.TestCase):
       time.sleep(0.05)
       self.assertTrue(2 < daemon.run_counter())
 
-  @patch('arm.util.tracker.tor_controller', Mock(return_value = Mock()))
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.tor_controller', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
   def test_pausing_daemon(self):
     # Check that we can pause and unpause daemon.
 
diff --git a/test/util/tracker/port_usage_tracker.py b/test/util/tracker/port_usage_tracker.py
index 2f3856b..de32158 100644
--- a/test/util/tracker/port_usage_tracker.py
+++ b/test/util/tracker/port_usage_tracker.py
@@ -1,7 +1,7 @@
 import time
 import unittest
 
-from arm.util.tracker import PortUsageTracker, _process_for_ports
+from seth.util.tracker import PortUsageTracker, _process_for_ports
 
 from mock import Mock, patch
 
@@ -44,7 +44,7 @@ tor     2001 atagar   14u  IPv4  14048      0t0  TCP localhost:9037351->localhos
 
 
 class TestPortUsageTracker(unittest.TestCase):
-  @patch('arm.util.tracker.system.call', Mock(return_value = LSOF_OUTPUT.split('\n')))
+  @patch('seth.util.tracker.system.call', Mock(return_value = LSOF_OUTPUT.split('\n')))
   def test_process_for_ports(self):
     self.assertEqual({}, _process_for_ports([], []))
     self.assertEqual({}, _process_for_ports([80, 443], []))
@@ -52,7 +52,7 @@ class TestPortUsageTracker(unittest.TestCase):
 
     self.assertEqual({37277: 'python', 51849: 'tor'}, _process_for_ports([37277], [51849]))
 
-  @patch('arm.util.tracker.system.call')
+  @patch('seth.util.tracker.system.call')
   def test_process_for_ports_malformed(self, call_mock):
     # Issues that are valid, but should result in us not having any content.
 
@@ -81,9 +81,9 @@ class TestPortUsageTracker(unittest.TestCase):
       call_mock.return_value = test_input.split('\n')
       self.assertRaises(IOError, _process_for_ports, [80], [443])
 
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker._process_for_ports')
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker._process_for_ports')
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
   def test_fetching_samplings(self, process_for_ports_mock, tor_controller_mock):
     tor_controller_mock().get_pid.return_value = 12345
     process_for_ports_mock.return_value = {37277: 'python', 51849: 'tor'}
@@ -96,9 +96,9 @@ class TestPortUsageTracker(unittest.TestCase):
 
       self.assertEqual({37277: 'python', 51849: 'tor'}, daemon.get_processes_using_ports([37277, 51849]))
 
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker._process_for_ports')
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker._process_for_ports')
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
   def test_resolver_failover(self, process_for_ports_mock, tor_controller_mock):
     tor_controller_mock().get_pid.return_value = 12345
     process_for_ports_mock.side_effect = IOError()
diff --git a/test/util/tracker/resource_tracker.py b/test/util/tracker/resource_tracker.py
index 64883d9..74341f4 100644
--- a/test/util/tracker/resource_tracker.py
+++ b/test/util/tracker/resource_tracker.py
@@ -1,7 +1,7 @@
 import time
 import unittest
 
-from arm.util.tracker import ResourceTracker, _resources_via_ps, _resources_via_proc
+from seth.util.tracker import ResourceTracker, _resources_via_ps, _resources_via_proc
 
 from mock import Mock, patch
 
@@ -12,10 +12,10 @@ PS_OUTPUT = """\
 
 
 class TestResourceTracker(unittest.TestCase):
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker._resources_via_proc')
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
-  @patch('arm.util.tracker.proc.is_available', Mock(return_value = True))
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker._resources_via_proc')
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.proc.is_available', Mock(return_value = True))
   def test_fetching_samplings(self, resources_via_proc_mock, tor_controller_mock):
     tor_controller_mock().get_pid.return_value = 12345
     resources_via_proc_mock.return_value = (105.3, 2.4, 8072, 0.3)
@@ -47,11 +47,11 @@ class TestResourceTracker(unittest.TestCase):
 
     resources_via_proc_mock.assert_called_with(12345)
 
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker.proc.is_available')
-  @patch('arm.util.tracker._resources_via_ps', Mock(return_value = (105.3, 2.4, 8072, 0.3)))
-  @patch('arm.util.tracker._resources_via_proc', Mock(return_value = (340.3, 3.2, 6020, 0.26)))
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker.proc.is_available')
+  @patch('seth.util.tracker._resources_via_ps', Mock(return_value = (105.3, 2.4, 8072, 0.3)))
+  @patch('seth.util.tracker._resources_via_proc', Mock(return_value = (340.3, 3.2, 6020, 0.26)))
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
   def test_picking_proc_or_ps(self, is_proc_available_mock, tor_controller_mock):
     tor_controller_mock().get_pid.return_value = 12345
 
@@ -85,11 +85,11 @@ class TestResourceTracker(unittest.TestCase):
       self.assertEqual(0.3, resources.memory_percent)
       self.assertTrue((time.time() - resources.timestamp) < 0.5)
 
-  @patch('arm.util.tracker.tor_controller')
-  @patch('arm.util.tracker._resources_via_ps', Mock(return_value = (105.3, 2.4, 8072, 0.3)))
-  @patch('arm.util.tracker._resources_via_proc', Mock(side_effect = IOError()))
-  @patch('arm.util.tracker.system', Mock(return_value = Mock()))
-  @patch('arm.util.tracker.proc.is_available', Mock(return_value = True))
+  @patch('seth.util.tracker.tor_controller')
+  @patch('seth.util.tracker._resources_via_ps', Mock(return_value = (105.3, 2.4, 8072, 0.3)))
+  @patch('seth.util.tracker._resources_via_proc', Mock(side_effect = IOError()))
+  @patch('seth.util.tracker.system', Mock(return_value = Mock()))
+  @patch('seth.util.tracker.proc.is_available', Mock(return_value = True))
   def test_failing_over_to_ps(self, tor_controller_mock):
     tor_controller_mock().get_pid.return_value = 12345
 
@@ -121,7 +121,7 @@ class TestResourceTracker(unittest.TestCase):
       self.assertEqual(0.3, resources.memory_percent)
       self.assertTrue((time.time() - resources.timestamp) < 0.5)
 
-  @patch('arm.util.tracker.system.call', Mock(return_value = PS_OUTPUT.split('\n')))
+  @patch('seth.util.tracker.system.call', Mock(return_value = PS_OUTPUT.split('\n')))
   def test_resources_via_ps(self):
     total_cpu_time, uptime, memory_in_bytes, memory_in_percent = _resources_via_ps(12345)
 
@@ -131,9 +131,9 @@ class TestResourceTracker(unittest.TestCase):
     self.assertEqual(0.004, memory_in_percent)
 
   @patch('time.time', Mock(return_value = 1388967218.973117))
-  @patch('arm.util.tracker.proc.stats', Mock(return_value = (1.5, 0.5, 1388967200.9)))
-  @patch('arm.util.tracker.proc.memory_usage', Mock(return_value = (19300352, 6432)))
-  @patch('arm.util.tracker.proc.physical_memory', Mock(return_value = 4825088000))
+  @patch('seth.util.tracker.proc.stats', Mock(return_value = (1.5, 0.5, 1388967200.9)))
+  @patch('seth.util.tracker.proc.memory_usage', Mock(return_value = (19300352, 6432)))
+  @patch('seth.util.tracker.proc.physical_memory', Mock(return_value = 4825088000))
   def test_resources_via_proc(self):
     total_cpu_time, uptime, memory_in_bytes, memory_in_percent = _resources_via_proc(12345)
 





More information about the tor-commits mailing list