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

[or-cvs] r19615: {arm} Introduced wrapper to hide curses ugliness which greatly sim (in arm/trunk: . interface)



Author: atagar
Date: 2009-06-02 04:37:49 -0400 (Tue, 02 Jun 2009)
New Revision: 19615

Added:
   arm/trunk/interface/bandwidthPanel.py
   arm/trunk/interface/logPanel.py
   arm/trunk/interface/staticPanel.py
   arm/trunk/interface/util.py
Removed:
   arm/trunk/interface/bandwidthMonitor.py
   arm/trunk/interface/eventLog.py
Modified:
   arm/trunk/arm.py
   arm/trunk/interface/controller.py
Log:
Introduced wrapper to hide curses ugliness which greatly simplified interface code. Also fixed all problems with resizing - it's now rock solid.



Modified: arm/trunk/arm.py
===================================================================
--- arm/trunk/arm.py	2009-06-02 08:33:48 UTC (rev 19614)
+++ arm/trunk/arm.py	2009-06-02 08:37:49 UTC (rev 19615)
@@ -22,7 +22,7 @@
   sys.exit()
 
 from interface import controller
-from interface import eventLog
+from interface import logPanel
 
 DEFAULT_CONTROL_ADDR = "127.0.0.1"
 DEFAULT_CONTROL_PORT = 9051
@@ -47,7 +47,7 @@
 arm -c                  authenticate using the default cookie
 arm -i 1643 -p          prompt for password using control port 1643
 arm -e=we -p=nemesis    use password 'nemesis' with 'WARN'/'ERR' events
-""" % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_AUTH_COOKIE, DEFAULT_LOGGED_EVENTS, eventLog.EVENT_LISTING)
+""" % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_AUTH_COOKIE, DEFAULT_LOGGED_EVENTS, logPanel.EVENT_LISTING)
 
 class Input:
   "Collection of the user's command line input"
@@ -174,13 +174,13 @@
   
   # validates and expands logged event flags
   try:
-    expandedEvents = eventLog.expandEvents(input.loggedEvents)
+    expandedEvents = logPanel.expandEvents(input.loggedEvents)
   except ValueError, exc:
     for flag in str(exc):
       print "Unrecognized event flag: %s" % flag
     sys.exit()
   
-  # disables TorCtl from logging events (can possibly interrupt curses)
+  # disables TorCtl from logging events (noisy and can possibly interrupt curses)
   TorUtil.loglevel = "NONE"
   
   # attempts to open a socket to the tor server

Deleted: arm/trunk/interface/bandwidthMonitor.py
===================================================================
--- arm/trunk/interface/bandwidthMonitor.py	2009-06-02 08:33:48 UTC (rev 19614)
+++ arm/trunk/interface/bandwidthMonitor.py	2009-06-02 08:37:49 UTC (rev 19615)
@@ -1,143 +0,0 @@
-#!/usr/bin/env python
-# bandwidthMonitor.py -- Resources related to monitoring Tor bandwidth usage.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import curses
-import controller
-from TorCtl import TorCtl
-
-BANDWIDTH_GRAPH_SAMPLES = 5         # seconds of data used for a bar in the graph
-BANDWIDTH_GRAPH_COL = 30            # columns of data in graph
-BANDWIDTH_GRAPH_COLOR_DL = "green"  # download section color
-BANDWIDTH_GRAPH_COLOR_UL = "cyan"   # upload section color
-
-def drawBandwidthLabel(screen, staticInfo, maxX):
-  """ Draws bandwidth label text (drops stats if not enough room). """
-  rateLabel = controller.getSizeLabel(int(staticInfo["BandwidthRate"]))
-  burstLabel = controller.getSizeLabel(int(staticInfo["BandwidthBurst"]))
-  labelContents = "Bandwidth (cap: %s, burst: %s):" % (rateLabel, burstLabel)
-  if maxX < len(labelContents):
-    labelContents = "%s):" % labelContents[:labelContents.find(",")]  # removes burst measure
-    if x < len(labelContents): labelContents = "Bandwidth:"           # removes both
-  
-  screen.erase()
-  screen.addstr(0, 0, labelContents[:maxX - 1], controller.LABEL_ATTR)
-  screen.refresh()
-
-class BandwidthMonitor(TorCtl.PostEventListener):
-  """
-  Tor event listener, taking bandwidth sampling and drawing bar graph. This is
-  updated every second by the BW events and graph samples are spaced at
-  BANDWIDTH_GRAPH_SAMPLES second intervals.
-  """
-  
-  def __init__(self, bandwidthScreen, cursesLock):
-    TorCtl.PostEventListener.__init__(self)
-    self.tick = 0                           # number of updates performed
-    self.bandwidthScreen = bandwidthScreen  # curses window where bandwidth's displayed
-    self.lastDownloadRate = 0               # most recently sampled rates
-    self.lastUploadRate = 0
-    self.maxDownloadRate = 1                # max rates seen, used to determine graph bounds
-    self.maxUploadRate = 1
-    self.isPaused = False
-    self.pauseBuffer = None                 # mirror instance used to track updates when paused
-    self.cursesLock = cursesLock            # lock to safely use bandwidthScreen
-    
-    # graphed download (read) and upload (write) rates - first index accumulator
-    self.downloadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
-    self.uploadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
-    
-  def bandwidth_event(self, event):
-    if self.isPaused:
-      self.pauseBuffer.bandwidth_event(event)
-    else:
-      self.lastDownloadRate = event.read
-      self.lastUploadRate = event.written
-      
-      self.downloadRates[0] += event.read
-      self.uploadRates[0] += event.written
-      
-      self.tick += 1
-      if self.tick % BANDWIDTH_GRAPH_SAMPLES == 0:
-        self.maxDownloadRate = max(self.maxDownloadRate, self.downloadRates[0])
-        self.downloadRates.insert(0, 0)
-        del self.downloadRates[BANDWIDTH_GRAPH_COL + 1:]
-        
-        self.maxUploadRate = max(self.maxUploadRate, self.uploadRates[0])
-        self.uploadRates.insert(0, 0)
-        del self.uploadRates[BANDWIDTH_GRAPH_COL + 1:]
-      
-      self.refreshDisplay()
-  
-  def refreshDisplay(self):
-    """ Redraws bandwidth panel. """
-    
-    # doesn't draw if headless (indicating that the instance is for a pause buffer)
-    if self.bandwidthScreen:
-      if not self.cursesLock.acquire(False): return
-      try:
-        self.bandwidthScreen.erase()
-        y, x = self.bandwidthScreen.getmaxyx()
-        dlColor = controller.COLOR_ATTR[BANDWIDTH_GRAPH_COLOR_DL]
-        ulColor = controller.COLOR_ATTR[BANDWIDTH_GRAPH_COLOR_UL]
-        
-        # current numeric measures
-        self.bandwidthScreen.addstr(0, 0, ("Downloaded (%s/sec):" % controller.getSizeLabel(self.lastDownloadRate))[:x - 1], curses.A_BOLD | dlColor)
-        if x > 35: self.bandwidthScreen.addstr(0, 35, ("Uploaded (%s/sec):" % controller.getSizeLabel(self.lastUploadRate))[:x - 36], curses.A_BOLD | ulColor)
-        
-        # graph bounds in KB (uses highest recorded value as max)
-        if y > 1:self.bandwidthScreen.addstr(1, 0, ("%4s" % str(self.maxDownloadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES))[:x - 1], dlColor)
-        if y > 6: self.bandwidthScreen.addstr(6, 0, "   0"[:x - 1], dlColor)
-        
-        if x > 35:
-          if y > 1: self.bandwidthScreen.addstr(1, 35, ("%4s" % str(self.maxUploadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES))[:x - 36], ulColor)
-          if y > 6: self.bandwidthScreen.addstr(6, 35, "   0"[:x - 36], ulColor)
-        
-        # creates bar graph of bandwidth usage over time
-        for col in range(BANDWIDTH_GRAPH_COL):
-          if col > x - 8: break
-          bytesDownloaded = self.downloadRates[col + 1]
-          colHeight = min(5, 5 * bytesDownloaded / self.maxDownloadRate)
-          for row in range(colHeight):
-            if y > (6 - row): self.bandwidthScreen.addstr(6 - row, col + 5, " ", curses.A_STANDOUT | dlColor)
-        
-        for col in range(BANDWIDTH_GRAPH_COL):
-          if col > x - 42: break
-          bytesUploaded = self.uploadRates[col + 1]
-          colHeight = min(5, 5 * bytesUploaded / self.maxUploadRate)
-          for row in range(colHeight):
-            if y > (6 - row): self.bandwidthScreen.addstr(6 - row, col + 40, " ", curses.A_STANDOUT | ulColor)
-          
-        self.bandwidthScreen.refresh()
-      finally:
-        self.cursesLock.release()
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents bandwidth updates from being presented.
-    """
-    
-    if isPause == self.isPaused: return
-    
-    self.isPaused = isPause
-    if self.isPaused:
-      if self.pauseBuffer == None:
-        self.pauseBuffer = BandwidthMonitor(None, None)
-      
-      self.pauseBuffer.tick = self.tick
-      self.pauseBuffer.lastDownloadRate = self.lastDownloadRate
-      self.pauseBuffer.lastuploadRate = self.lastUploadRate
-      self.pauseBuffer.maxDownloadRate = self.maxDownloadRate
-      self.pauseBuffer.maxUploadRate = self.maxUploadRate
-      self.pauseBuffer.downloadRates = list(self.downloadRates)
-      self.pauseBuffer.uploadRates = list(self.uploadRates)
-    else:
-      self.tick = self.pauseBuffer.tick
-      self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
-      self.lastUploadRate = self.pauseBuffer.lastuploadRate
-      self.maxDownloadRate = self.pauseBuffer.maxDownloadRate
-      self.maxUploadRate = self.pauseBuffer.maxUploadRate
-      self.downloadRates = self.pauseBuffer.downloadRates
-      self.uploadRates = self.pauseBuffer.uploadRates
-      self.refreshDisplay()
-

Copied: arm/trunk/interface/bandwidthPanel.py (from rev 19594, arm/trunk/interface/bandwidthMonitor.py)
===================================================================
--- arm/trunk/interface/bandwidthPanel.py	                        (rev 0)
+++ arm/trunk/interface/bandwidthPanel.py	2009-06-02 08:37:49 UTC (rev 19615)
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+# bandwidthPanel.py -- Resources related to monitoring Tor bandwidth usage.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import curses
+from TorCtl import TorCtl
+
+import util
+
+BANDWIDTH_GRAPH_SAMPLES = 5         # seconds of data used for a bar in the graph
+BANDWIDTH_GRAPH_COL = 30            # columns of data in graph
+BANDWIDTH_GRAPH_COLOR_DL = "green"  # download section color
+BANDWIDTH_GRAPH_COLOR_UL = "cyan"   # upload section color
+
+def drawBandwidthLabel(scr, staticInfo):
+  """ Draws bandwidth label text (drops stats if not enough room). """
+  scr.clear()
+  maxX = scr.maxX
+  
+  rateLabel = util.getSizeLabel(int(staticInfo["BandwidthRate"]))
+  burstLabel = util.getSizeLabel(int(staticInfo["BandwidthBurst"]))
+  labelContents = "Bandwidth (cap: %s, burst: %s):" % (rateLabel, burstLabel)
+  if maxX < len(labelContents):
+    labelContents = "%s):" % labelContents[:labelContents.find(",")]  # removes burst measure
+    if maxX < len(labelContents): labelContents = "Bandwidth:"           # removes both
+  
+  scr.addstr(0, 0, labelContents, util.LABEL_ATTR)
+  scr.refresh()
+
+class BandwidthMonitor(TorCtl.PostEventListener):
+  """
+  Tor event listener, taking bandwidth sampling and drawing bar graph. This is
+  updated every second by the BW events and graph samples are spaced at
+  BANDWIDTH_GRAPH_SAMPLES second intervals.
+  """
+  
+  def __init__(self, scr):
+    TorCtl.PostEventListener.__init__(self)
+    self.scr = scr                # associated subwindow
+    self.tick = 0                 # number of updates performed
+    self.lastDownloadRate = 0     # most recently sampled rates
+    self.lastUploadRate = 0
+    self.maxDownloadRate = 1      # max rates seen, used to determine graph bounds
+    self.maxUploadRate = 1
+    self.isPaused = False
+    self.pauseBuffer = None       # mirror instance used to track updates when paused
+    
+    # graphed download (read) and upload (write) rates - first index accumulator
+    self.downloadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
+    self.uploadRates = [0] * (BANDWIDTH_GRAPH_COL + 1)
+  
+  def bandwidth_event(self, event):
+    if self.isPaused: self.pauseBuffer.bandwidth_event(event)
+    else:
+      self.lastDownloadRate = event.read
+      self.lastUploadRate = event.written
+      
+      self.downloadRates[0] += event.read
+      self.uploadRates[0] += event.written
+      
+      self.tick += 1
+      if self.tick % BANDWIDTH_GRAPH_SAMPLES == 0:
+        self.maxDownloadRate = max(self.maxDownloadRate, self.downloadRates[0])
+        self.downloadRates.insert(0, 0)
+        del self.downloadRates[BANDWIDTH_GRAPH_COL + 1:]
+        
+        self.maxUploadRate = max(self.maxUploadRate, self.uploadRates[0])
+        self.uploadRates.insert(0, 0)
+        del self.uploadRates[BANDWIDTH_GRAPH_COL + 1:]
+      
+      self.refreshDisplay()
+  
+  def refreshDisplay(self):
+    """ Redraws bandwidth panel. """
+    # doesn't draw if headless (indicating that the instance is for a pause buffer)
+    if self.scr:
+      if not self.scr.lock.acquire(False): return
+      try:
+        self.scr.clear()
+        dlColor = util.getColor(BANDWIDTH_GRAPH_COLOR_DL)
+        ulColor = util.getColor(BANDWIDTH_GRAPH_COLOR_UL)
+        
+        # current numeric measures
+        self.scr.addstr(0, 0, "Downloaded (%s/sec):" % util.getSizeLabel(self.lastDownloadRate), curses.A_BOLD | dlColor)
+        self.scr.addstr(0, 35, "Uploaded (%s/sec):" % util.getSizeLabel(self.lastUploadRate), curses.A_BOLD | ulColor)
+        
+        # graph bounds in KB (uses highest recorded value as max)
+        self.scr.addstr(1, 0, "%4s" % str(self.maxDownloadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES), dlColor)
+        self.scr.addstr(6, 0, "   0", dlColor)
+        
+        self.scr.addstr(1, 35, "%4s" % str(self.maxUploadRate / 1024 / BANDWIDTH_GRAPH_SAMPLES), ulColor)
+        self.scr.addstr(6, 35, "   0", ulColor)
+        
+        # creates bar graph of bandwidth usage over time
+        for col in range(BANDWIDTH_GRAPH_COL):
+          bytesDownloaded = self.downloadRates[col + 1]
+          colHeight = min(5, 5 * bytesDownloaded / self.maxDownloadRate)
+          for row in range(colHeight):
+            self.scr.addstr(6 - row, col + 5, " ", curses.A_STANDOUT | dlColor)
+        
+        for col in range(BANDWIDTH_GRAPH_COL):
+          bytesUploaded = self.uploadRates[col + 1]
+          colHeight = min(5, 5 * bytesUploaded / self.maxUploadRate)
+          for row in range(colHeight):
+            self.scr.addstr(6 - row, col + 40, " ", curses.A_STANDOUT | ulColor)
+        
+        self.scr.refresh()
+      finally:
+        self.scr.lock.release()
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents bandwidth updates from being presented.
+    """
+    
+    if isPause == self.isPaused: return
+    
+    self.isPaused = isPause
+    if self.isPaused:
+      if self.pauseBuffer == None: self.pauseBuffer = BandwidthMonitor(None)
+      
+      self.pauseBuffer.tick = self.tick
+      self.pauseBuffer.lastDownloadRate = self.lastDownloadRate
+      self.pauseBuffer.lastuploadRate = self.lastUploadRate
+      self.pauseBuffer.maxDownloadRate = self.maxDownloadRate
+      self.pauseBuffer.maxUploadRate = self.maxUploadRate
+      self.pauseBuffer.downloadRates = list(self.downloadRates)
+      self.pauseBuffer.uploadRates = list(self.uploadRates)
+    else:
+      self.tick = self.pauseBuffer.tick
+      self.lastDownloadRate = self.pauseBuffer.lastDownloadRate
+      self.lastUploadRate = self.pauseBuffer.lastuploadRate
+      self.maxDownloadRate = self.pauseBuffer.maxDownloadRate
+      self.maxUploadRate = self.pauseBuffer.maxUploadRate
+      self.downloadRates = self.pauseBuffer.downloadRates
+      self.uploadRates = self.pauseBuffer.uploadRates
+      self.refreshDisplay()
+

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2009-06-02 08:33:48 UTC (rev 19614)
+++ arm/trunk/interface/controller.py	2009-06-02 08:37:49 UTC (rev 19615)
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
 # controller.py -- arm interface (curses monitor for relay status).
 # Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
 
@@ -5,156 +6,44 @@
 Curses (terminal) interface for the arm relay status monitor.
 """
 
-import os
-import sys
 import time
 import curses
-
-import eventLog
-import bandwidthMonitor
-
 from threading import RLock
 from TorCtl import TorCtl
 
-REFRESH_RATE = 5                    # seconds between redrawing screen
-cursesLock = RLock()                # curses isn't thread safe and concurrency
-                                    # bugs produce especially sinister glitches
+import util
+import staticPanel
+import bandwidthPanel
+import logPanel
 
-# default formatting constants
-LABEL_ATTR = curses.A_STANDOUT
-SUMMARY_ATTR = curses.A_NORMAL
+REFRESH_RATE = 5        # seconds between redrawing screen
+cursesLock = RLock()    # global curses lock (curses isn't thread safe and
+                        # concurrency bugs produce especially sinister glitches
 
-# colors curses can handle
-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))
+CTL_HELP, CTL_PAUSED, CTL_EVENT_INPUT, CTL_EVENT_ERR = range(4) # enums for message in control label
 
-# TODO: change COLOR_ATTR into something that can be imported via 'from'
-# foreground color mappings (starts uninitialized - all colors associated with default white fg / black bg)
-COLOR_ATTR_INITIALIZED = False
-COLOR_ATTR = dict([(color[0], 0) for color in COLOR_LIST])
+# mapping of panels to (height, start y), -1 if unlimited
+PANEL_INFO = {
+  "summary": (6, 0),          # top static content
+  "control": (1, 6),          # line for user input
+  "bandwidthLabel": (1, 7),   # bandwidth section label
+  "bandwidth": (8, 8),        # bandwidth measurements / graph
+  "logLabel": (1, 16),        # message log label
+  "log": (-1, 17)}            # uses all remaining space for message log
 
-def getSizeLabel(bytes):
-  """
-  Converts byte count into label in its most significant units, for instance
-  7500 bytes would return "7 KB".
-  """
+def drawControlLabel(scr, msgType, arg=""):
+  """ Draws single line label for interface controls. """
+  scr.clear()
   
-  if bytes >= 1073741824: return "%i GB" % (bytes / 1073741824)
-  elif bytes >= 1048576: return "%i MB" % (bytes / 1048576)
-  elif bytes >= 1024: return "%i KB" % (bytes / 1024)
-  else: return "%i bytes" % bytes
-
-def getStaticInfo(conn):
-  """
-  Provides mapping of static Tor settings and system information to their
-  corresponding string values. Keys include:
-  info - version, config-file, address, fingerprint
-  sys - sys-name, sys-os, sys-version
-  config - Nickname, ORPort, DirPort, ControlPort, ExitPolicy, BandwidthRate, BandwidthBurst
-  config booleans - IsPasswordAuthSet, IsCookieAuthSet
-  """
+  if msgType == CTL_HELP: scr.addstr(0, 0, "q: quit, e: change events, p: pause")
+  elif msgType == CTL_PAUSED: scr.addstr(0, 0, "Paused", curses.A_STANDOUT)
+  elif msgType == CTL_EVENT_INPUT: scr.addstr(0, 0, "Events to log: ")
+  elif msgType == CTL_EVENT_ERR: scr.addstr(0, 0, arg, curses.A_STANDOUT)
+  else:
+    assert False, "Unrecognized event type for control label: " + str(msgType)
   
-  vals = conn.get_info(["version", "config-file"])
-  
-  # gets parameters that throw errors if unavailable
-  for param in ["address", "fingerprint"]:
-    try:
-      vals.update(conn.get_info(param))
-    except TorCtl.ErrorReply:
-      vals[param] = "Unknown"
-  
-  # populates with some basic system information
-  unameVals = os.uname()
-  vals["sys-name"] = unameVals[1]
-  vals["sys-os"] = unameVals[0]
-  vals["sys-version"] = unameVals[2]
-  
-  # parameters from the user's torrc
-  configFields = ["Nickname", "ORPort", "DirPort", "ControlPort", "ExitPolicy", "BandwidthRate", "BandwidthBurst"]
-  vals.update(dict([(key, conn.get_option(key)[0][1]) for key in configFields]))
-  
-  # simply keeps booleans for if authentication info is set
-  vals["IsPasswordAuthSet"] = not conn.get_option("HashedControlPassword")[0][1] == None
-  vals["IsCookieAuthSet"] = conn.get_option("CookieAuthentication")[0][1] == "1"
-  
-  return vals
+  scr.refresh()
 
-def drawSummary(screen, vals, maxX, maxY):
-  """
-  Draws top area containing static information.
-  
-  arm - <System Name> (<OS> <Version>)     Tor <Tor Version>
-  <Relay Nickname> - <IP Addr>:<ORPort>, [Dir Port: <DirPort>, ]Control Port (<open, password, cookie>): <ControlPort>
-  Fingerprint: <Fingerprint>
-  Config: <Config>
-  Exit Policy: <ExitPolicy>
-  
-  Example:
-  arm - odin (Linux 2.6.24-24-generic)     Tor 0.2.0.34 (r18423)
-  odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
-  Fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
-  Config: /home/atagar/.vidalia/torrc
-  Exit Policy: reject *:*
-  """
-  
-  # extra erase/refresh is needed to avoid internal caching screwing up and
-  # refusing to redisplay content in the case of graphical glitches - probably
-  # an obscure curses bug...
-  screen.erase()
-  screen.refresh()
-  
-  screen.erase()
-  
-  # Line 1
-  if maxY >= 1:
-    screen.addstr(0, 0, ("arm - %s (%s %s)" % (vals["sys-name"], vals["sys-os"], vals["sys-version"]))[:maxX - 1], SUMMARY_ATTR)
-    if 45 < maxX: screen.addstr(0, 45, ("Tor %s" % vals["version"])[:maxX - 46], SUMMARY_ATTR)
-  
-  # Line 2 (authentication label red if open, green if credentials required)
-  if maxY >= 2:
-    dirPortLabel = "Dir Port: %s, " % vals["DirPort"] if not vals["DirPort"] == None else ""
-    
-    # TODO: if both cookie and password are set then which takes priority?
-    if vals["IsPasswordAuthSet"]: controlPortAuthLabel = "password"
-    elif vals["IsCookieAuthSet"]: controlPortAuthLabel = "cookie"
-    else: controlPortAuthLabel = "open"
-    controlPortAuthColor = "red" if controlPortAuthLabel == "open" else "green"
-    
-    labelStart = "%s - %s:%s, %sControl Port (" % (vals["Nickname"], vals["address"], vals["ORPort"], dirPortLabel)
-    screen.addstr(1, 0, labelStart[:maxX - 1], SUMMARY_ATTR)
-    xLoc = len(labelStart)
-    if xLoc < maxX: screen.addstr(1, xLoc, controlPortAuthLabel[:maxX - xLoc - 1], COLOR_ATTR[controlPortAuthColor] | SUMMARY_ATTR)
-    xLoc += len(controlPortAuthLabel)
-    if xLoc < maxX: screen.addstr(1, xLoc, ("): %s" % vals["ControlPort"])[:maxX - xLoc - 1], SUMMARY_ATTR)
-    
-  # Lines 3-5
-  if maxY >= 3: screen.addstr(2, 0, ("Fingerprint: %s" % vals["fingerprint"])[:maxX - 1], SUMMARY_ATTR)
-  if maxY >= 4: screen.addstr(3, 0, ("Config: %s" % vals["config-file"])[:maxX - 1], SUMMARY_ATTR)
-  if maxY >= 5:
-    exitPolicy = vals["ExitPolicy"]
-    
-    # adds note when default exit policy is appended
-    if exitPolicy == None: exitPolicy = "<default>"
-    elif not exitPolicy.endswith("accept *:*") and not exitPolicy.endswith("reject *:*"):
-      exitPolicy += ", <default>"
-    screen.addstr(4, 0, ("Exit Policy: %s" % exitPolicy)[:maxX - 1], SUMMARY_ATTR)
-  
-  screen.refresh()
-
-def drawPauseLabel(screen, isPaused, maxX):
-  """ Draws single line label for interface controls. """
-  # TODO: possibly include 'h: help' if the project grows much
-  screen.erase()
-  if isPaused: screen.addstr(0, 0, "Paused"[:maxX - 1], LABEL_ATTR)
-  else: screen.addstr(0, 0, "q: quit, e: change events, p: pause"[:maxX - 1])
-  screen.refresh()
-
 def setEventListening(loggedEvents, conn, logListener):
   """
   Tries to set events being listened for, displaying error for any event
@@ -185,10 +74,50 @@
         logListener.registerEvent("ARM-ERR", "Unsupported event type: %s" % eventType, "red")
       else:
         raise exc
+  
   loggedEvents = list(loggedEvents)
   loggedEvents.sort() # alphabetizes
   return loggedEvents
 
+def refreshSubwindows(stdscr, panels={}):
+  """
+  Creates constituent parts of the display. Any subwindows that have been
+  displaced are recreated to take advantage of the maximum bounds.
+  """
+  
+  y, x = stdscr.getmaxyx()
+  
+  if panels == {}:
+    # initializes subwindows - upper left must be a valid coordinate
+    for panelName, (maxHeight, startY) in PANEL_INFO.items():
+      if maxHeight == -1: height = max(1, y - startY)
+      else: height = max(1, min(maxHeight, y - startY))
+      startY = min(startY, y - 1)
+      panels[panelName] = util.TermSubwindow(stdscr.subwin(height, x, startY, 0), cursesLock, startY)
+  else:
+    # 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 entirly which 
+    # would mean more complicated code and no more selective refreshing)
+    
+    for panelName, (maxHeight, startY) in PANEL_INFO.items():
+      if startY > y: continue # out of bounds - ignore
+      panelSrc = panels[panelName]
+      currentY, currentX = panelSrc.win.getparyx()
+      currentHeight, currentWidth = panelSrc.win.getmaxyx()
+      
+      # figure out panel can grow - if so recreate
+      if maxHeight == -1: height = max(1, y - startY)
+      else: height = max(1, min(maxHeight, y - startY))
+      startY = min(startY, y - 1)
+      
+      if currentY < startY or currentHeight < height:
+        panels[panelName].win = stdscr.subwin(height, x, startY, 0)
+  
+  return panels
+
 def drawTorMonitor(stdscr, conn, loggedEvents):
   """
   Starts arm interface reflecting information on provided control port.
@@ -199,110 +128,59 @@
     otherwise unrecognized events)
   """
   
-  global COLOR_ATTR_INITIALIZED
-  
-  # use terminal defaults to allow things like semi-transparent backgrounds
-  curses.use_default_colors()
-  
-  # initializes color mappings if able
-  if curses.has_colors() and not COLOR_ATTR_INITIALIZED:
-    COLOR_ATTR_INITIALIZED = True
-    colorpair = 0
-    
-    for name, fgColor in COLOR_LIST:
-      colorpair += 1
-      curses.init_pair(colorpair, fgColor, -1) # -1 allows for default (possibly transparent) background
-      COLOR_ATTR[name] = curses.color_pair(colorpair)
-  
+  curses.use_default_colors()           # allows things like semi-transparent backgrounds
+  util.initColors()                     # initalizes color pairs for colored text
   curses.halfdelay(REFRESH_RATE * 10)   # uses getch call as timer for REFRESH_RATE seconds
-  staticInfo = getStaticInfo(conn)
-  y, x = stdscr.getmaxyx()
-  oldX, oldY = -1, -1
   
   # attempts to make the cursor invisible (not supported in all terminals)
   try: curses.curs_set(0)
   except curses.error: pass
   
-  # note: subwindows need a character buffer (either in the x or y direction)
-  # from actual content to prevent crash when shrank
-  # max/min calls are to allow the program to initialize if the screens too small
-  summaryScreen = stdscr.subwin(min(y, 6), x, 0, 0)                   # top static content
-  pauseLabel = stdscr.subwin(1, x, min(y - 1, 6), 0)                  # line concerned with user interface
-  bandwidthLabel = stdscr.subwin(1, x, min(y - 1, 7), 0)              # bandwidth section label
-  bandwidthScreen = stdscr.subwin(min(y - 8, 8), x, min(y - 2, 8), 0) # bandwidth measurements / graph
-  logLabel = stdscr.subwin(1, x, min(y - 1, 16), 0)                   # message log label
-  logScreen = stdscr.subwin(max(1, y - 17), x, min(y - 1, 17), 0)     # uses all remaining space for message log
+  panels = refreshSubwindows(stdscr)
+  staticInfo = staticPanel.getStaticInfo(conn)
   
-  # listeners that update bandwidthScreen and logScreen with Tor statuses
-  logListener = eventLog.LogMonitor(logScreen, "BW" in loggedEvents, "UNKNOWN" in loggedEvents, cursesLock)
+  # listeners that update bandwidth and log panels with Tor status
+  logListener = logPanel.LogMonitor(panels["log"], "BW" in loggedEvents, "UNKNOWN" in loggedEvents)
   conn.add_event_listener(logListener)
   
-  bandwidthListener = bandwidthMonitor.BandwidthMonitor(bandwidthScreen, cursesLock)
+  bandwidthListener = bandwidthPanel.BandwidthMonitor(panels["bandwidth"])
   conn.add_event_listener(bandwidthListener)
   
   loggedEvents = setEventListening(loggedEvents, conn, logListener)
   eventsListing = ", ".join(loggedEvents)
-  
-  bandwidthScreen.refresh()
-  logScreen.refresh()
+  oldY, oldX = -1, -1
   isPaused = False
-  tick = -1 # loop iteration
   
   while True:
-    tick += 1
-    y, x = stdscr.getmaxyx()
+    # tried only refreshing when the screen was resized but it caused a
+    # noticeable lag when resizing and didn't have an appreciable effect
+    # on system usage
     
-    if x != oldX or y != oldY or tick % 5 == 0:
-      # resized - redraws content
-      # occasionally refreshes anyway to help make resilient against an
-      # occasional graphical glitch - currently only known cause of this is 
-      # displaced subwindows overwritting static content when resized to be 
-      # very small
+    cursesLock.acquire()
+    try:
+      y, x = stdscr.getmaxyx()
+      if y > oldY: panels = refreshSubwindows(stdscr, panels)
       
-      # note: Having this refresh only occure after this resize is detected 
-      # (getmaxyx changes) introduces a noticeable lag in screen updates. If 
-      # it's done every pass through the loop resize repaint responsiveness is 
-      # perfect, but this is much more demanding in the common case (no resizing).
-      cursesLock.acquire()
+      staticPanel.drawSummary(panels["summary"], staticInfo)
       
-      try:
-        if y > oldY:
-          # screen height increased - recreates subwindows that are able to grow
-          # I'm not sure if this 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 entirly which 
-          # would mean more complicated code and no more selective refreshing)
-          
-          # TODO: BUG - this fails if doing maximize operation (relies on gradual growth/shrink - 
-          # fix would be to eliminate elif and consult new y)
-          if oldY < 6: summaryScreen = stdscr.subwin(y, x, 0, 0)
-          elif oldY < 7: pauseLabel = stdscr.subwin(1, x, 6, 0)
-          elif oldY < 8: bandwidthLabel = stdscr.subwin(1, x, 7, 0)
-          elif oldY < 16:
-            bandwidthScreen = stdscr.subwin(y - 8, x, 8, 0)
-            bandwidthListener.bandwidthScreen = bandwidthScreen
-          elif oldY < 17: logLabel = stdscr.subwin(1, x, 16, 0)
-          else:
-            logScreen = stdscr.subwin(y - 17, x, 17, 0)
-            logListener.logScreen = logScreen
-        
-        drawSummary(summaryScreen, staticInfo, x, y)
-        drawPauseLabel(pauseLabel, isPaused, x)
-        bandwidthMonitor.drawBandwidthLabel(bandwidthLabel, staticInfo, x)
-        bandwidthListener.refreshDisplay()
-        eventLog.drawEventLogLabel(logLabel, eventsListing, x)
-        logListener.refreshDisplay()
-        oldX, oldY = x, y
-        stdscr.refresh()
-      finally:
-        cursesLock.release()
+      msgType = CTL_PAUSED if isPaused else CTL_HELP
+      drawControlLabel(panels["control"], msgType)
+      
+      bandwidthPanel.drawBandwidthLabel(panels["bandwidthLabel"], staticInfo)
+      bandwidthListener.refreshDisplay()
+      
+      logPanel.drawEventLogLabel(panels["logLabel"], eventsListing)
+      logListener.refreshDisplay()
+      
+      oldY, oldX = y, x
+      stdscr.refresh()
+    finally:
+      cursesLock.release()
     
     key = stdscr.getch()
-    if key == ord('q') or key == ord('Q'): break # quits
+    if key == 27 or key == ord('q') or key == ord('Q'): break # quits (also on esc)
     elif key == ord('e') or key == ord('E'):
-      # allow user to enter new types of events to log - blank leaves unchanged
+      # allow user to enter new types of events to log - unchanged if left blank
       
       cursesLock.acquire()
       try:
@@ -314,9 +192,7 @@
         logListener.setPaused(True)
         
         # provides prompt
-        pauseLabel.erase()
-        pauseLabel.addstr(0, 0, "Events to log: "[:x - 1])
-        pauseLabel.refresh()
+        drawControlLabel(panels["control"], CTL_EVENT_INPUT)
         
         # makes cursor and typing visible
         try: curses.curs_set(1)
@@ -324,22 +200,20 @@
         curses.echo()
         
         # switches bandwidth area to list event types
-        bandwidthLabel.erase()
-        bandwidthLabel.addstr(0, 0, "Event Types:"[:x - 1], LABEL_ATTR)
-        bandwidthLabel.refresh()
+        panels["bandwidthLabel"].clear()
+        panels["bandwidthLabel"].addstr(0, 0, "Event Types:", util.LABEL_ATTR)
+        panels["bandwidthLabel"].refresh()
         
-        bandwidthScreen.erase()
-        bandwidthScreenMaxY, tmp = bandwidthScreen.getmaxyx()
+        panels["bandwidth"].clear()
         lineNum = 0
-        for line in eventLog.EVENT_LISTING.split("\n"):
+        for line in logPanel.EVENT_LISTING.split("\n"):
           line = line.strip()
-          if bandwidthScreenMaxY <= lineNum: break
-          bandwidthScreen.addstr(lineNum, 0, line[:x - 1])
+          panels["bandwidth"].addstr(lineNum, 0, line[:x - 1])
           lineNum += 1
-        bandwidthScreen.refresh()
+        panels["bandwidth"].refresh()
         
         # gets user input (this blocks monitor updates)
-        eventsInput = pauseLabel.getstr(0, 15)
+        eventsInput = panels["control"].win.getstr(0, 15)
         
         # strips spaces
         eventsInput = eventsInput.replace(' ', '')
@@ -352,27 +226,21 @@
         # TODO: it would be nice to quit on esc, but looks like this might not be possible...
         if eventsInput != "":
           try:
-            expandedEvents = eventLog.expandEvents(eventsInput)
+            expandedEvents = logPanel.expandEvents(eventsInput)
             logListener.includeBW = "BW" in expandedEvents
             logListener.includeUnknown = "UNKNOWN" in expandedEvents
             
             loggedEvents = setEventListening(expandedEvents, conn, logListener)
             eventsListing = ", ".join(loggedEvents)
           except ValueError, exc:
-            pauseLabel.erase()
-            pauseLabel.addstr(0, 0, ("Invalid flags: %s" % str(exc))[:x - 1], curses.A_STANDOUT)
-            pauseLabel.refresh()
+            drawControlLabel(panels["control"], CTL_EVENT_ERR, "Invalid flags: %s" % str(exc))
             time.sleep(2)
         
         # returns listeners to previous pause status
         bandwidthListener.setPaused(isBwPaused)
         logListener.setPaused(isLogPaused)
-        
-        oldX = -1 # forces refresh (by spoofing a resize)
       finally:
         cursesLock.release()
-      
-      
     elif key == ord('p') or key == ord('P'):
       # toggles update freezing
       cursesLock.acquire()
@@ -380,7 +248,8 @@
         isPaused = not isPaused
         logListener.setPaused(isPaused)
         bandwidthListener.setPaused(isPaused)
-        drawPauseLabel(pauseLabel, isPaused, x)
+        msgType = CTL_PAUSED if isPaused else CTL_HELP
+        drawControlLabel(panels["control"], msgType)
       finally:
         cursesLock.release()
 

Deleted: arm/trunk/interface/eventLog.py
===================================================================
--- arm/trunk/interface/eventLog.py	2009-06-02 08:33:48 UTC (rev 19614)
+++ arm/trunk/interface/eventLog.py	2009-06-02 08:37:49 UTC (rev 19615)
@@ -1,222 +0,0 @@
-#!/usr/bin/env python
-# eventLog.py -- Resources related to Tor event monitoring.
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import time
-import curses
-import controller
-from curses.ascii import isprint
-from TorCtl import TorCtl
-
-MAX_LOG_ENTRIES = 80                # size of log buffer (max number of entries)
-RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
-
-EVENT_TYPES = {
-  "d": "DEBUG",   "a": "ADDRMAP",     "l": "NEWDESC",   "u": "AUTHDIR_NEWDESCS",
-  "i": "INFO",    "b": "BW",          "m": "NS",        "v": "CLIENTS_SEEN",
-  "n": "NOTICE",  "c": "CIRC",        "o": "ORCONN",    "x": "STATUS_GENERAL",
-  "w": "WARN",    "f": "DESCCHANGED", "s": "STREAM",    "y": "STATUS_CLIENT",
-  "e": "ERR",     "g": "GUARD",       "t": "STREAM_BW", "z": "STATUS_SERVER"}
-  
-EVENT_LISTING = """        d DEBUG     a ADDRMAP       l NEWDESC         u AUTHDIR_NEWDESCS
-        i INFO      b BW            m NS              v CLIENTS_SEEN
-        n NOTICE    c CIRC          o ORCONN          x STATUS_GENERAL
-        w WARN      f DESCCHANGED   s STREAM          y STATUS_CLIENT
-        e ERR       g GUARD         t STREAM_BW       z STATUS_SERVER
-        Aliases:    A All Events    U Unknown Events  R Runlevels (dinwe)"""
-
-def expandEvents(eventAbbr):
-  """
-  Expands event abbreviations to their full names. Beside mappings privided in
-  EVENT_TYPES this recognizes:
-  A - alias for all events
-  U - "UNKNOWN" events
-  R - alias for runtime events (DEBUG, INFO, NOTICE, WARN, ERR)
-  Raises ValueError with invalid input if any part isn't recognized.
-  """
-  
-  expandedEvents = set()
-  invalidFlags = ""
-  for flag in eventAbbr:
-    if flag == "A":
-      expandedEvents = set(EVENT_TYPES.values())
-      expandedEvents.add("UNKNOWN")
-      break
-    elif flag == "U":
-      expandedEvents.add("UNKNOWN")
-    elif flag == "R":
-      expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
-    elif flag in EVENT_TYPES:
-      expandedEvents.add(EVENT_TYPES[flag])
-    else:
-      invalidFlags += flag
-  
-  if invalidFlags: raise ValueError(invalidFlags)
-  else: return expandedEvents
-
-def drawEventLogLabel(screen, eventsListing, maxX):
-  """
-  Draws single line label for event log. Uses ellipsis if too long, for instance:
-  Events (DEBUG, INFO, NOTICE, WARN...):
-  """
-  
-  eventsLabel = "Events"
-  
-  firstLabelLen = eventsListing.find(", ")
-  if firstLabelLen == -1: firstLabelLen = len(eventsListing)
-  else: firstLabelLen += 3
-  
-  if maxX > 10 + firstLabelLen:
-    eventsLabel += " ("
-    if len(eventsListing) > maxX - 11:
-      labelBreak = eventsListing[:maxX - 12].rfind(", ")
-      eventsLabel += "%s..." % eventsListing[:labelBreak]
-    else: eventsLabel += eventsListing
-    eventsLabel += ")"
-  eventsLabel += ":"
-  
-  screen.erase()
-  # not sure why but when stressed this call sometimes fails
-  try: screen.addstr(0, 0, eventsLabel[:maxX - 1], controller.LABEL_ATTR)
-  except curses.error: pass
-  screen.refresh()
-
-class LogMonitor(TorCtl.PostEventListener):
-  """
-  Tor event listener, noting messages, the time, and their type in a curses
-  subwindow.
-  """
-  
-  def __init__(self, logScreen, includeBW, includeUnknown, cursesLock):
-    TorCtl.PostEventListener.__init__(self)
-    self.msgLog = []                      # tuples of (logText, color)
-    self.logScreen = logScreen            # curses window where log's displayed
-    self.isPaused = False
-    self.pauseBuffer = []                 # location where messages are buffered if paused
-    self.includeBW = includeBW            # true if we're supposed to listen for BW events
-    self.includeUnknown = includeUnknown  # true if registering unrecognized events
-    self.cursesLock = cursesLock          # lock to safely use logScreen
-  
-  # Listens for all event types and redirects to registerEvent
-  # TODO: not sure how to stimulate all event types - should be tried before
-  # implemented to see what's the best formatting, what information is
-  # important, and to make sure of variable's types so we don't get exceptions.
-  
-  def circ_status_event(self, event):
-    self.registerEvent("CIRC", "<STUB>", "white") # TODO: implement - variables: event.circ_id, event.status, event.path, event.purpose, event.reason, event.remote_reason
-  
-  def stream_status_event(self, event):
-    self.registerEvent("STREAM", "<STUB>", "white") # TODO: implement - variables: event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose
-  
-  def or_conn_status_event(self, event):
-    self.registerEvent("ORCONN", "<STUB>", "white") # TODO: implement - variables: event.status, event.endpoint, event.age, event.read_bytes, event.wrote_bytes, event.reason, event.ncircs
-  
-  def stream_bw_event(self, event):
-    self.registerEvent("STREAM_BW", "<STUB>", "white") # TODO: implement - variables: event.strm_id, event.bytes_read, event.bytes_written
-  
-  def bandwidth_event(self, event):
-    if self.includeBW: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
-  
-  def msg_event(self, event):
-    self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
-  
-  def new_desc_event(self, event):
-    self.registerEvent("NEWDESC", "<STUB>", "white") # TODO: implement - variables: event.idlist
-  
-  def address_mapped_event(self, event):
-    self.registerEvent("ADDRMAP", "<STUB>", "white") # TODO: implement - variables: event.from_addr, event.to_addr, event.when
-  
-  def ns_event(self, event):
-    self.registerEvent("NS", "<STUB>", "white") # TODO: implement - variables: event.nslist
-  
-  def new_consensus_event(self, event):
-    self.registerEvent("NEWCONSENSUS", "<STUB>", "white") # TODO: implement - variables: event.nslist
-  
-  def unknown_event(self, event):
-    if self.includeUnknown: self.registerEvent("UNKNOWN", event.event_string, "red")
-  
-  def registerEvent(self, type, msg, color):
-    """
-    Notes event and redraws log. If paused it's held in a temporary buffer.
-    """
-    
-    # strips control characters to avoid screwing up the terminal
-    msg = "".join([char for char in msg if isprint(char)])
-    
-    eventTime = time.localtime()
-    msgLine = "%02i:%02i:%02i [%s] %s" % (eventTime[3], eventTime[4], eventTime[5], type, msg)
-    
-    if self.isPaused:
-      self.pauseBuffer.insert(0, (msgLine, color))
-      if len(self.pauseBuffer) > MAX_LOG_ENTRIES: del self.pauseBuffer[MAX_LOG_ENTRIES:]
-    else:
-      self.msgLog.insert(0, (msgLine, color))
-      if len(self.msgLog) > MAX_LOG_ENTRIES: del self.msgLog[MAX_LOG_ENTRIES:]
-      self.refreshDisplay()
-  
-  def refreshDisplay(self):
-    """
-    Redraws message log. Entries stretch to use available space and may
-    contain up to two lines. Starts with newest entries.
-    """
-    
-    if self.logScreen:
-      if not self.cursesLock.acquire(False): return
-      try:
-        self.logScreen.erase()
-        y, x = self.logScreen.getmaxyx()
-        lineCount = 0
-        
-        for (line, color) in self.msgLog:
-          # splits over too lines if too long
-          if len(line) < x:
-            self.logScreen.addstr(lineCount, 0, line[:x - 1], controller.COLOR_ATTR[color])
-            lineCount += 1
-          else:
-            if lineCount >= y - 1: break
-            (line1, line2) = self._splitLine(line, x)
-            self.logScreen.addstr(lineCount, 0, line1, controller.COLOR_ATTR[color])
-            self.logScreen.addstr(lineCount + 1, 0, line2[:x - 1], controller.COLOR_ATTR[color])
-            lineCount += 2
-          
-          if lineCount >= y: break
-        self.logScreen.refresh()
-      finally:
-        self.cursesLock.release()
-  
-  def setPaused(self, isPause):
-    """
-    If true, prevents message log from being updated with new events.
-    """
-    
-    if isPause == self.isPaused: return
-    
-    self.isPaused = isPause
-    if self.isPaused: self.pauseBuffer = []
-    else:
-      self.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
-      self.refreshDisplay()
-  
-  # divides long message to cover two lines
-  def _splitLine(self, message, x):
-    # divides message into two lines, attempting to do it on a wordbreak
-    lastWordbreak = message[:x].rfind(" ")
-    if x - lastWordbreak < 10:
-      line1 = message[:lastWordbreak]
-      line2 = "  %s" % message[lastWordbreak:].strip()
-    else:
-      # over ten characters until the last word - dividing
-      line1 = "%s-" % message[:x - 2]
-      line2 = "  %s" % message[x - 2:].strip()
-    
-    # ends line with ellipsis if too long
-    if len(line2) > x:
-      lastWordbreak = line2[:x - 4].rfind(" ")
-      
-      # doesn't use wordbreak if it's a long word or the whole line is one 
-      # word (picking up on two space indent to have index 1)
-      if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
-      line2 = "%s..." % line2[:lastWordbreak]
-    
-    return (line1, line2)
-

Copied: arm/trunk/interface/logPanel.py (from rev 19594, arm/trunk/interface/eventLog.py)
===================================================================
--- arm/trunk/interface/logPanel.py	                        (rev 0)
+++ arm/trunk/interface/logPanel.py	2009-06-02 08:37:49 UTC (rev 19615)
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+# logPanel.py -- Resources related to Tor event monitoring.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import time
+import curses
+from curses.ascii import isprint
+from TorCtl import TorCtl
+
+import util
+
+MAX_LOG_ENTRIES = 80                # size of log buffer (max number of entries)
+RUNLEVEL_EVENT_COLOR = {"DEBUG": "magenta", "INFO": "blue", "NOTICE": "green", "WARN": "yellow", "ERR": "red"}
+
+EVENT_TYPES = {
+  "d": "DEBUG",   "a": "ADDRMAP",     "l": "NEWDESC",   "u": "AUTHDIR_NEWDESCS",
+  "i": "INFO",    "b": "BW",          "m": "NS",        "v": "CLIENTS_SEEN",
+  "n": "NOTICE",  "c": "CIRC",        "o": "ORCONN",    "x": "STATUS_GENERAL",
+  "w": "WARN",    "f": "DESCCHANGED", "s": "STREAM",    "y": "STATUS_CLIENT",
+  "e": "ERR",     "g": "GUARD",       "t": "STREAM_BW", "z": "STATUS_SERVER"}
+  
+EVENT_LISTING = """        d DEBUG     a ADDRMAP       l NEWDESC         u AUTHDIR_NEWDESCS
+        i INFO      b BW            m NS              v CLIENTS_SEEN
+        n NOTICE    c CIRC          o ORCONN          x STATUS_GENERAL
+        w WARN      f DESCCHANGED   s STREAM          y STATUS_CLIENT
+        e ERR       g GUARD         t STREAM_BW       z STATUS_SERVER
+        Aliases:    A All Events    U Unknown Events  R Runlevels (dinwe)"""
+
+def expandEvents(eventAbbr):
+  """
+  Expands event abbreviations to their full names. Beside mappings privided in
+  EVENT_TYPES this recognizes:
+  A - alias for all events
+  U - "UNKNOWN" events
+  R - alias for runtime events (DEBUG, INFO, NOTICE, WARN, ERR)
+  Raises ValueError with invalid input if any part isn't recognized.
+  
+  Example:
+  "inUt" -> ["INFO", "NOTICE", "UNKNOWN", "STREAM_BW"]
+  """
+  
+  expandedEvents = set()
+  invalidFlags = ""
+  for flag in eventAbbr:
+    if flag == "A":
+      expandedEvents = set(EVENT_TYPES.values())
+      expandedEvents.add("UNKNOWN")
+      break
+    elif flag == "U":
+      expandedEvents.add("UNKNOWN")
+    elif flag == "R":
+      expandedEvents = expandedEvents.union(set(["DEBUG", "INFO", "NOTICE", "WARN", "ERR"]))
+    elif flag in EVENT_TYPES:
+      expandedEvents.add(EVENT_TYPES[flag])
+    else:
+      invalidFlags += flag
+  
+  if invalidFlags: raise ValueError(invalidFlags)
+  else: return expandedEvents
+
+def drawEventLogLabel(scr, eventsListing):
+  """
+  Draws single line label for event log. Uses ellipsis if too long, for instance:
+  Events (DEBUG, INFO, NOTICE, WARN...):
+  """
+  scr.clear()
+  maxX = scr.maxX
+  
+  eventsLabel = "Events"
+  
+  firstLabelLen = eventsListing.find(", ")
+  if firstLabelLen == -1: firstLabelLen = len(eventsListing)
+  else: firstLabelLen += 3
+  
+  if maxX > 10 + firstLabelLen:
+    eventsLabel += " ("
+    if len(eventsListing) > maxX - 11:
+      labelBreak = eventsListing[:maxX - 12].rfind(", ")
+      eventsLabel += "%s..." % eventsListing[:labelBreak]
+    else: eventsLabel += eventsListing
+    eventsLabel += ")"
+  eventsLabel += ":"
+  
+  scr.addstr(0, 0, eventsLabel, util.LABEL_ATTR)
+  scr.refresh()
+
+class LogMonitor(TorCtl.PostEventListener):
+  """
+  Tor event listener, noting messages, the time, and their type in a curses
+  subwindow.
+  """
+  
+  def __init__(self, scr, includeBW, includeUnknown):
+    TorCtl.PostEventListener.__init__(self)
+    self.scr = scr                        # associated subwindow
+    self.msgLog = []                      # tuples of (logText, color)
+    self.isPaused = False
+    self.pauseBuffer = []                 # location where messages are buffered if paused
+    self.includeBW = includeBW            # true if we're supposed to listen for BW events
+    self.includeUnknown = includeUnknown  # true if registering unrecognized events
+  
+  # Listens for all event types and redirects to registerEvent
+  # TODO: not sure how to stimulate all event types - should be tried before
+  # implemented to see what's the best formatting, what information is
+  # important, and to make sure of variable's types so we don't get exceptions.
+  
+  def circ_status_event(self, event):
+    self.registerEvent("CIRC", "<STUB>", "white") # TODO: implement - variables: event.circ_id, event.status, event.path, event.purpose, event.reason, event.remote_reason
+  
+  def stream_status_event(self, event):
+    self.registerEvent("STREAM", "<STUB>", "white") # TODO: implement - variables: event.strm_id, event.status, event.circ_id, event.target_host, event.target_port, event.reason, event.remote_reason, event.source, event.source_addr, event.purpose
+  
+  def or_conn_status_event(self, event):
+    self.registerEvent("ORCONN", "<STUB>", "white") # TODO: implement - variables: event.status, event.endpoint, event.age, event.read_bytes, event.wrote_bytes, event.reason, event.ncircs
+  
+  def stream_bw_event(self, event):
+    self.registerEvent("STREAM_BW", "<STUB>", "white") # TODO: implement - variables: event.strm_id, event.bytes_read, event.bytes_written
+  
+  def bandwidth_event(self, event):
+    if self.includeBW: self.registerEvent("BW", "READ: %i, WRITTEN: %i" % (event.read, event.written), "cyan")
+  
+  def msg_event(self, event):
+    self.registerEvent(event.level, event.msg, RUNLEVEL_EVENT_COLOR[event.level])
+  
+  def new_desc_event(self, event):
+    self.registerEvent("NEWDESC", "<STUB>", "white") # TODO: implement - variables: event.idlist
+  
+  def address_mapped_event(self, event):
+    self.registerEvent("ADDRMAP", "<STUB>", "white") # TODO: implement - variables: event.from_addr, event.to_addr, event.when
+  
+  def ns_event(self, event):
+    self.registerEvent("NS", "<STUB>", "white") # TODO: implement - variables: event.nslist
+  
+  def new_consensus_event(self, event):
+    self.registerEvent("NEWCONSENSUS", "<STUB>", "white") # TODO: implement - variables: event.nslist
+  
+  def unknown_event(self, event):
+    if self.includeUnknown: self.registerEvent("UNKNOWN", event.event_string, "red")
+  
+  def registerEvent(self, type, msg, color):
+    """
+    Notes event and redraws log. If paused it's held in a temporary buffer.
+    """
+    
+    # strips control characters to avoid screwing up the terminal
+    msg = "".join([char for char in msg if isprint(char)])
+    
+    eventTime = time.localtime()
+    msgLine = "%02i:%02i:%02i [%s] %s" % (eventTime[3], eventTime[4], eventTime[5], type, msg)
+    
+    if self.isPaused:
+      self.pauseBuffer.insert(0, (msgLine, color))
+      if len(self.pauseBuffer) > MAX_LOG_ENTRIES: del self.pauseBuffer[MAX_LOG_ENTRIES:]
+    else:
+      self.msgLog.insert(0, (msgLine, color))
+      if len(self.msgLog) > MAX_LOG_ENTRIES: del self.msgLog[MAX_LOG_ENTRIES:]
+      self.refreshDisplay()
+  
+  def refreshDisplay(self):
+    """
+    Redraws message log. Entries stretch to use available space and may
+    contain up to two lines. Starts with newest entries.
+    """
+    
+    if self.scr:
+      if not self.scr.lock.acquire(False): return
+      try:
+        self.scr.clear()
+        x, y = self.scr.maxX, self.scr.maxY
+        lineCount = 0
+        
+        for (line, color) in self.msgLog:
+          # splits over too lines if too long
+          if len(line) < x:
+            self.scr.addstr(lineCount, 0, line, util.getColor(color))
+            lineCount += 1
+          else:
+            (line1, line2) = self._splitLine(line, x)
+            self.scr.addstr(lineCount, 0, line1, util.getColor(color))
+            self.scr.addstr(lineCount + 1, 0, line2, util.getColor(color))
+            lineCount += 2
+          
+          if lineCount >= y: break # further log messages wouldn't fit
+        self.scr.refresh()
+      finally:
+        self.scr.lock.release()
+  
+  def setPaused(self, isPause):
+    """
+    If true, prevents message log from being updated with new events.
+    """
+    
+    if isPause == self.isPaused: return
+    
+    self.isPaused = isPause
+    if self.isPaused: self.pauseBuffer = []
+    else:
+      self.msgLog = (self.pauseBuffer + self.msgLog)[:MAX_LOG_ENTRIES]
+      self.refreshDisplay()
+  
+  # divides long message to cover two lines
+  def _splitLine(self, message, x):
+    # divides message into two lines, attempting to do it on a wordbreak
+    lastWordbreak = message[:x].rfind(" ")
+    if x - lastWordbreak < 10:
+      line1 = message[:lastWordbreak]
+      line2 = "  %s" % message[lastWordbreak:].strip()
+    else:
+      # over ten characters until the last word - dividing
+      line1 = "%s-" % message[:x - 2]
+      line2 = "  %s" % message[x - 2:].strip()
+    
+    # ends line with ellipsis if too long
+    if len(line2) > x:
+      lastWordbreak = line2[:x - 4].rfind(" ")
+      
+      # doesn't use wordbreak if it's a long word or the whole line is one 
+      # word (picking up on two space indent to have index 1)
+      if x - lastWordbreak > 10 or lastWordbreak == 1: lastWordbreak = x - 4
+      line2 = "%s..." % line2[:lastWordbreak]
+    
+    return (line1, line2)
+

Added: arm/trunk/interface/staticPanel.py
===================================================================
--- arm/trunk/interface/staticPanel.py	                        (rev 0)
+++ arm/trunk/interface/staticPanel.py	2009-06-02 08:37:49 UTC (rev 19615)
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# summaryPanel.py -- Static system and Tor related information.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import os
+import curses
+from TorCtl import TorCtl
+
+import util
+
+def getStaticInfo(conn):
+  """
+  Provides mapping of static Tor settings and system information to their
+  corresponding string values. Keys include:
+  info - version, config-file, address, fingerprint
+  sys - sys-name, sys-os, sys-version
+  config - Nickname, ORPort, DirPort, ControlPort, ExitPolicy, BandwidthRate, BandwidthBurst
+  config booleans - IsPasswordAuthSet, IsCookieAuthSet
+  """
+  
+  vals = conn.get_info(["version", "config-file"])
+  
+  # gets parameters that throw errors if unavailable
+  for param in ["address", "fingerprint"]:
+    try:
+      vals.update(conn.get_info(param))
+    except TorCtl.ErrorReply:
+      vals[param] = "Unknown"
+  
+  # populates with some basic system information
+  unameVals = os.uname()
+  vals["sys-name"] = unameVals[1]
+  vals["sys-os"] = unameVals[0]
+  vals["sys-version"] = unameVals[2]
+  
+  # parameters from the user's torrc
+  configFields = ["Nickname", "ORPort", "DirPort", "ControlPort", "ExitPolicy", "BandwidthRate", "BandwidthBurst"]
+  vals.update(dict([(key, conn.get_option(key)[0][1]) for key in configFields]))
+  
+  # simply keeps booleans for if authentication info is set
+  vals["IsPasswordAuthSet"] = not conn.get_option("HashedControlPassword")[0][1] == None
+  vals["IsCookieAuthSet"] = conn.get_option("CookieAuthentication")[0][1] == "1"
+  
+  return vals
+
+def drawSummary(scr, vals):
+  """
+  Draws top area containing static information.
+  
+  arm - <System Name> (<OS> <Version>)     Tor <Tor Version>
+  <Relay Nickname> - <IP Addr>:<ORPort>, [Dir Port: <DirPort>, ]Control Port (<open, password, cookie>): <ControlPort>
+  Fingerprint: <Fingerprint>
+  Config: <Config>
+  Exit Policy: <ExitPolicy>
+  
+  Example:
+  arm - odin (Linux 2.6.24-24-generic)     Tor 0.2.0.34 (r18423)
+  odin - 76.104.132.98:9001, Dir Port: 9030, Control Port (cookie): 9051
+  Fingerprint: BDAD31F6F318E0413833E8EBDA956F76E4D66788
+  Config: /home/atagar/.vidalia/torrc
+  Exit Policy: reject *:*
+  """
+  
+  # extra erase/refresh is needed to avoid internal caching screwing up and
+  # refusing to redisplay content in the case of graphical glitches - probably
+  # an obscure curses bug...
+  scr.win.erase()
+  scr.win.refresh()
+  
+  scr.clear()
+  
+  # Line 1
+  scr.addstr(0, 0, "arm - %s (%s %s)" % (vals["sys-name"], vals["sys-os"], vals["sys-version"]))
+  scr.addstr(0, 45, "Tor %s" % vals["version"])
+  
+  # Line 2 (authentication label red if open, green if credentials required)
+  dirPortLabel = "Dir Port: %s, " % vals["DirPort"] if not vals["DirPort"] == None else ""
+  
+  # TODO: if both cookie and password are set then which takes priority?
+  if vals["IsPasswordAuthSet"]: controlPortAuthLabel = "password"
+  elif vals["IsCookieAuthSet"]: controlPortAuthLabel = "cookie"
+  else: controlPortAuthLabel = "open"
+  controlPortAuthColor = "red" if controlPortAuthLabel == "open" else "green"
+  
+  labelStart = "%s - %s:%s, %sControl Port (" % (vals["Nickname"], vals["address"], vals["ORPort"], dirPortLabel)
+  scr.addstr(1, 0, labelStart)
+  xLoc = len(labelStart)
+  scr.addstr(1, xLoc, controlPortAuthLabel, util.getColor(controlPortAuthColor))
+  xLoc += len(controlPortAuthLabel)
+  scr.addstr(1, xLoc, "): %s" % vals["ControlPort"])
+  
+  # Lines 3-5
+  scr.addstr(2, 0, "Fingerprint: %s" % vals["fingerprint"])
+  scr.addstr(3, 0, "Config: %s" % vals["config-file"])
+  exitPolicy = vals["ExitPolicy"]
+  
+  # adds note when default exit policy is appended
+  if exitPolicy == None: exitPolicy = "<default>"
+  elif not exitPolicy.endswith("accept *:*") and not exitPolicy.endswith("reject *:*"):
+    exitPolicy += ", <default>"
+  scr.addstr(4, 0, "Exit Policy: %s" % exitPolicy)
+  
+  scr.refresh()
+

Added: arm/trunk/interface/util.py
===================================================================
--- arm/trunk/interface/util.py	                        (rev 0)
+++ arm/trunk/interface/util.py	2009-06-02 08:37:49 UTC (rev 19615)
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+# util.py -- support functions common for arm user interface.
+# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
+
+import curses
+
+LABEL_ATTR = curses.A_STANDOUT          # default formatting constant
+
+# colors curses can handle
+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))
+
+# foreground color mappings (starts uninitialized - all colors associated with default white fg / black bg)
+COLOR_ATTR_INITIALIZED = False
+COLOR_ATTR = dict([(color[0], 0) for color in COLOR_LIST])
+
+def initColors():
+  """
+  Initializes color mappings for the current curses. This needs to be called
+  after curses.initscr().
+  """
+  
+  global COLOR_ATTR_INITIALIZED
+  if not COLOR_ATTR_INITIALIZED:
+    COLOR_ATTR_INITIALIZED = True
+    
+    # if color support is available initializes color mappings
+    if curses.has_colors():
+      colorpair = 0
+      
+      for name, fgColor in COLOR_LIST:
+        colorpair += 1
+        curses.init_pair(colorpair, fgColor, -1) # -1 allows for default (possibly transparent) background
+        COLOR_ATTR[name] = curses.color_pair(colorpair)
+
+def getColor(color):
+  """
+  Provides attribute corresponding to a given text color. Supported colors
+  include:
+  red, green, yellow, blue, cyan, magenta, black, and white
+  
+  If color support isn't available then this uses the default terminal coloring
+  scheme.
+  """
+  
+  return COLOR_ATTR[color]
+
+def getSizeLabel(bytes):
+  """
+  Converts byte count into label in its most significant units, for instance
+  7500 bytes would return "7 KB".
+  """
+  
+  if bytes >= 1073741824: return "%i GB" % (bytes / 1073741824)
+  elif bytes >= 1048576: return "%i MB" % (bytes / 1048576)
+  elif bytes >= 1024: return "%i KB" % (bytes / 1024)
+  else: return "%i bytes" % bytes
+
+class TermSubwindow():
+  """
+  Wrapper for curses subwindows. This provides safe proxies to common methods.
+  """
+  
+  def __init__(self, win, lock, startY):
+    self.win = win          # associated curses subwindow
+    self.lock = lock        # global curses lock
+    self.startY = startY    # y-coordinate where made
+    self.disable = False    # set if we detect being displaced
+    self._resetBounds()     # sets last known dimensions of win
+  
+  def clear(self):
+    """
+    Erases window and resets bounds used in writting to it.
+    """
+    
+    self.disable = self.startY > self.win.getparyx()[0]
+    if not self.disable: self.win.erase()
+    self._resetBounds()
+  
+  def refresh(self):
+    """
+    Proxy for window refresh.
+    """
+    
+    if not self.disable: self.win.refresh()
+  
+  def addstr(self, y, x, msg, attr=curses.A_NORMAL):
+    """
+    Writes string to subwindow if able. This takes into account screen bounds
+    to avoid making curses upset.
+    """
+    
+    # subwindows need a character buffer (either in the x or y direction) from
+    # actual content to prevent crash when shrank
+    if self.maxX > x and self.maxY > y:
+      if not self.disable: self.win.addstr(y, x, msg[:self.maxX - x - 1], attr)
+  
+  def _resetBounds(self):
+    self.maxY, self.maxX = self.win.getmaxyx()
+