[tor-commits] [arm/master] Rewrite of the descriptor popup dialog

atagar at torproject.org atagar at torproject.org
Mon Aug 15 17:13:46 UTC 2011


commit 9b9ea8e1d247ef5290ab730cf7a88d76d94d8f02
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Aug 15 10:11:00 2011 -0700

    Rewrite of the descriptor popup dialog
    
    For a time I'd been planning to drop the descriptor popup from the codebase.
    However, it's fairly useful so cleaning up the code and keeping it. This is
    functionally equivilant except that it's a bit simpler and has minor
    performance improvements.
---
 README                                 |   10 +-
 src/cli/__init__.py                    |    2 +-
 src/cli/connections/__init__.py        |    2 +-
 src/cli/connections/connPanel.py       |   14 ++-
 src/cli/connections/descriptorPopup.py |  229 ++++++++++++++++++++++++++++++++
 src/cli/descriptorPopup.py             |  216 ------------------------------
 src/util/panel.py                      |    2 +
 7 files changed, 248 insertions(+), 227 deletions(-)

diff --git a/README b/README
index 2e0bd6e..119d24e 100644
--- a/README
+++ b/README
@@ -171,10 +171,11 @@ Layout:
       
       connections/
         __init__.py
-        connPanel.py      - (page 2) lists the active tor connections
-        circEntry.py      - circuit entries in the connection panel
-        connEntry.py      - individual connections to or from the system
-        entries.py        - common parent for connPanel display entries
+        connPanel.py       - (page 2) lists the active tor connections
+        circEntry.py       - circuit entries in the connection panel
+        connEntry.py       - individual connections to or from the system
+        descriptorPopup.py - displays raw descriptor and consensus entries
+        entries.py         - common parent for connPanel display entries
       
       menu/
         __init__.py
@@ -185,7 +186,6 @@ Layout:
       __init__.py
       controller.py          - main display loop, handling input and layout
       headerPanel.py         - top of all pages, providing general information
-      descriptorPopup.py     - displays connection descriptor data
       popups.py              - toolkit providing display popups
       wizard.py              - provides the relay setup wizard
       
diff --git a/src/cli/__init__.py b/src/cli/__init__.py
index 435883d..b0d6bd8 100644
--- a/src/cli/__init__.py
+++ b/src/cli/__init__.py
@@ -2,5 +2,5 @@
 Panels, popups, and handlers comprising the arm user interface.
 """
 
-__all__ = ["configPanel", "controller", "descriptorPopup", "headerPanel", "logPanel", "popups", "torrcPanel", "wizard"]
+__all__ = ["configPanel", "controller", "headerPanel", "logPanel", "popups", "torrcPanel", "wizard"]
 
diff --git a/src/cli/connections/__init__.py b/src/cli/connections/__init__.py
index 5babdde..0f29d23 100644
--- a/src/cli/connections/__init__.py
+++ b/src/cli/connections/__init__.py
@@ -2,5 +2,5 @@
 Connection panel related resources.
 """
 
-__all__ = ["circEntry", "connEntry", "connPanel", "entries"]
+__all__ = ["circEntry", "connEntry", "connPanel", "descriptorPopup", "entries"]
 
diff --git a/src/cli/connections/connPanel.py b/src/cli/connections/connPanel.py
index 705fd77..923902c 100644
--- a/src/cli/connections/connPanel.py
+++ b/src/cli/connections/connPanel.py
@@ -6,10 +6,9 @@ import time
 import curses
 import threading
 
-import cli.descriptorPopup
 import cli.popups
 
-from cli.connections import entries, connEntry, circEntry
+from cli.connections import descriptorPopup, entries, connEntry, circEntry
 from util import connections, enum, panel, torTools, uiTools
 
 DEFAULT_CONFIG = {"features.connection.resolveApps": True,
@@ -214,7 +213,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
       if selection != -1: self.setListingType(options[selection])
     elif key == ord('d') or key == ord('D'):
       # presents popup for raw consensus data
-      cli.descriptorPopup.showDescriptorPopup(self)
+      descriptorPopup.showDescriptorPopup(self)
     else: isKeystrokeConsumed = False
     
     self.valsLock.release()
@@ -269,6 +268,13 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     options.append(("u", "resolving utility", resolverUtil))
     return options
   
+  def getSelection(self):
+    """
+    Provides the currently selected connection entry.
+    """
+    
+    return self._scroller.getCursorSelection(self._entryLines)
+  
   def draw(self, width, height):
     self.valsLock.acquire()
     
@@ -280,7 +286,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
     isScrollbarVisible = len(self._entryLines) > height - detailPanelOffset - 1
     
     scrollLoc = self._scroller.getScrollLoc(self._entryLines, height - detailPanelOffset - 1)
-    cursorSelection = self._scroller.getCursorSelection(self._entryLines)
+    cursorSelection = self.getSelection()
     
     # draws the detail panel if currently displaying it
     if self._showDetails and cursorSelection:
diff --git a/src/cli/connections/descriptorPopup.py b/src/cli/connections/descriptorPopup.py
new file mode 100644
index 0000000..eed213f
--- /dev/null
+++ b/src/cli/connections/descriptorPopup.py
@@ -0,0 +1,229 @@
+"""
+Popup providing the raw descriptor and consensus information for a relay.
+"""
+
+import math
+import curses
+
+import cli.popups
+import cli.connections.connEntry
+
+from util import panel, torTools, uiTools
+
+# field keywords used to identify areas for coloring
+LINE_NUM_COLOR = "yellow"
+HEADER_COLOR = "cyan"
+HEADER_PREFIX = ["ns/id/", "desc/id/"]
+
+SIG_COLOR = "red"
+SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
+SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
+
+UNRESOLVED_MSG = "No consensus data available"
+ERROR_MSG = "Unable to retrieve data"
+
+def showDescriptorPopup(connPanel):
+  """
+  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:
+    connPanel - connection panel providing the dialog
+  """
+  
+  # hides the title of the connection panel
+  connPanel.setTitleVisible(False)
+  connPanel.redraw(True)
+  
+  control = cli.controller.getController()
+  panel.CURSES_LOCK.acquire()
+  isDone = False
+  
+  try:
+    while not isDone:
+      selection = connPanel.getSelection()
+      if not selection: break
+      
+      fingerprint = selection.foreign.getFingerprint()
+      if fingerprint == "UNKNOWN": fingerprint = None
+      
+      displayText = getDisplayText(fingerprint)
+      displayColor = cli.connections.connEntry.CATEGORY_COLOR[selection.getType()]
+      showLineNumber = fingerprint != None
+      
+      # determines the maximum popup size the displayText can fill
+      pHeight, pWidth = getPreferredSize(displayText, connPanel.maxX, showLineNumber)
+      
+      popup, _, height = cli.popups.init(pHeight, pWidth)
+      if not popup: break
+      scroll, isChanged = 0, True
+      
+      try:
+        while not isDone:
+          if isChanged:
+            draw(popup, fingerprint, displayText, displayColor, scroll, showLineNumber)
+            isChanged = False
+          
+          key = control.getScreen().getch()
+          
+          if uiTools.isScrollKey(key):
+            # TODO: This is a bit buggy in that scrolling is by displayText
+            # 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...
+            
+            newScroll = uiTools.getScrollPosition(key, scroll, height - 2, len(displayText))
+            
+            if scroll != newScroll:
+              scroll, isChanged = newScroll, True
+          elif uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')):
+            isDone = True # closes popup
+          elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):
+            # navigation - pass on to connPanel and recreate popup
+            connPanel.handleKey(curses.KEY_UP if key == curses.KEY_LEFT else curses.KEY_DOWN)
+            break
+      finally: cli.popups.finalize()
+  finally:
+    connPanel.setTitleVisible(True)
+    connPanel.redraw(True)
+    panel.CURSES_LOCK.release()
+
+def getDisplayText(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]
+  conn, description = torTools.getConn(), []
+  
+  description.append("ns/id/%s" % fingerprint)
+  consensusEntry = conn.getConsensusEntry(fingerprint)
+  
+  if consensusEntry: description += consensusEntry.split("\n")
+  else: description += [ERROR_MSG, ""]
+  
+  description.append("desc/id/%s" % fingerprint)
+  descriptorEntry = conn.getDescriptorEntry(fingerprint)
+  
+  if descriptorEntry: description += descriptorEntry.split("\n")
+  else: description += [ERROR_MSG]
+  
+  return description
+
+def getPreferredSize(text, maxWidth, showLineNumber):
+  """
+  Provides the (height, width) tuple for the preferred size of the given text.
+  """
+  
+  width, height = 0, len(text) + 2
+  lineNumWidth = int(math.log10(len(text))) + 1
+  for line in text:
+    # width includes content, line number field, and border
+    lineWidth = len(line) + 5
+    if showLineNumber: lineWidth += lineNumWidth
+    width = max(width, lineWidth)
+    
+    # tracks number of extra lines that will be taken due to text wrap
+    height += (lineWidth - 2) / maxWidth
+  
+  return (height, width)
+
+def draw(popup, fingerprint, displayText, displayColor, scroll, showLineNumber):
+  popup.win.erase()
+  popup.win.box()
+  xOffset = 2
+  
+  if fingerprint: title = "Consensus Descriptor (%s):" % fingerprint
+  else: title = "Consensus Descriptor:"
+  popup.addstr(0, 0, title, curses.A_STANDOUT)
+  
+  lineNumWidth = int(math.log10(len(displayText))) + 1
+  isEncryptionBlock = 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):
+    lineText = displayText[i].strip()
+    if lineText in SIG_START_KEYS: isEncryptionBlock = True
+    elif lineText in SIG_END_KEYS: isEncryptionBlock = False
+  
+  drawLine, pageHeight = 1, popup.maxY - 2
+  for i in range(scroll, scroll + pageHeight):
+    lineText = displayText[i].strip()
+    xOffset = 2
+    
+    if showLineNumber:
+      lineNumLabel = ("%%%ii" % lineNumWidth) % (i + 1)
+      lineNumFormat = curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR)
+      
+      popup.addstr(drawLine, xOffset, lineNumLabel, lineNumFormat)
+      xOffset += lineNumWidth + 1
+    
+    # Most consensus and descriptor lines are keyword/value pairs. Both are
+    # shown with the same color, but the keyword is bolded.
+    
+    keyword, value = lineText, ""
+    drawFormat = uiTools.getColor(displayColor)
+    
+    if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]):
+      keyword, value = lineText, ""
+      drawFormat = uiTools.getColor(HEADER_COLOR)
+    elif lineText == UNRESOLVED_MSG or lineText == ERROR_MSG:
+      keyword, value = lineText, ""
+    elif lineText in SIG_START_KEYS:
+      keyword, value = lineText, ""
+      isEncryptionBlock = True
+      drawFormat = uiTools.getColor(SIG_COLOR)
+    elif lineText in SIG_END_KEYS:
+      keyword, value = lineText, ""
+      isEncryptionBlock = False
+      drawFormat = uiTools.getColor(SIG_COLOR)
+    elif isEncryptionBlock:
+      keyword, value = "", lineText
+      drawFormat = uiTools.getColor(SIG_COLOR)
+    elif " " in lineText:
+      divIndex = lineText.find(" ")
+      keyword, value = lineText[:divIndex], lineText[divIndex:]
+    
+    displayQueue = [(keyword, drawFormat | curses.A_BOLD), (value, drawFormat)]
+    cursorLoc = xOffset
+    
+    while displayQueue:
+      msg, format = displayQueue.pop(0)
+      if not msg: continue
+      
+      maxMsgSize = popup.maxX - 1 - cursorLoc
+      if len(msg) >= maxMsgSize:
+        # needs to split up the line
+        msg, remainder = uiTools.cropStr(msg, maxMsgSize, None, endType = None, getRemainder = True)
+        
+        if xOffset == cursorLoc and msg == "":
+          # first word is longer than the line
+          msg = uiTools.cropStr(remainder, maxMsgSize)
+          
+          if " " in remainder:
+            remainder = remainder.split(" ", 1)[1]
+          else: remainder = ""
+        
+        popup.addstr(drawLine, cursorLoc, msg, format)
+        cursorLoc = xOffset
+        
+        if remainder:
+          displayQueue.insert(0, (remainder.strip(), format))
+          drawLine += 1
+      else:
+        popup.addstr(drawLine, cursorLoc, msg, format)
+        cursorLoc += len(msg)
+      
+      if drawLine > pageHeight: break
+    
+    drawLine += 1
+    if drawLine > pageHeight: break
+  
+  popup.win.refresh()
+
diff --git a/src/cli/descriptorPopup.py b/src/cli/descriptorPopup.py
deleted file mode 100644
index 5b9f646..0000000
--- a/src/cli/descriptorPopup.py
+++ /dev/null
@@ -1,216 +0,0 @@
-#!/usr/bin/env python
-# descriptorPopup.py -- popup panel used to show raw consensus data
-# Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)
-
-import math
-import curses
-
-import controller
-import connections.connEntry
-import popups
-from util import panel, torTools, uiTools
-
-# field keywords used to identify areas for coloring
-LINE_NUM_COLOR = "yellow"
-HEADER_COLOR = "cyan"
-HEADER_PREFIX = ["ns/id/", "desc/id/"]
-
-SIG_COLOR = "red"
-SIG_START_KEYS = ["-----BEGIN RSA PUBLIC KEY-----", "-----BEGIN SIGNATURE-----"]
-SIG_END_KEYS = ["-----END RSA PUBLIC KEY-----", "-----END SIGNATURE-----"]
-
-UNRESOLVED_MSG = "No consensus data available"
-ERROR_MSG = "Unable to retrieve data"
-
-def addstr_wrap(panel, y, x, text, formatting, startX = 0, endX = -1, maxY = -1):
-  """
-  Writes text with word wrapping, returning the ending y/x coordinate.
-  y: starting write line
-  x: column offset from startX
-  text / formatting: content to be written
-  startX / endX: column bounds in which text may be written
-  """
-  
-  # moved out of panel (trying not to polute new code!)
-  # TODO: unpleaseantly complex usage - replace with something else when
-  # rewriting confPanel and descriptorPopup (the only places this is used)
-  if not text: return (y, x)          # nothing to write
-  if endX == -1: endX = panel.maxX     # defaults to writing to end of panel
-  if maxY == -1: maxY = panel.maxY + 1 # defaults to writing to bottom of panel
-  lineWidth = endX - startX           # room for text
-  while True:
-    if len(text) > lineWidth - x - 1:
-      chunkSize = text.rfind(" ", 0, lineWidth - x)
-      writeText = text[:chunkSize]
-      text = text[chunkSize:].strip()
-      
-      panel.addstr(y, x + startX, writeText, formatting)
-      y, x = y + 1, 0
-      if y >= maxY: return (y, x)
-    else:
-      panel.addstr(y, x + startX, text, formatting)
-      return (y, x + len(text))
-
-class PopupProperties:
-  """
-  State attributes of popup window for consensus descriptions.
-  """
-  
-  def __init__(self):
-    self.fingerprint = ""
-    self.entryColor = "white"
-    self.text = []
-    self.scroll = 0
-    self.showLineNum = True
-  
-  def reset(self, fingerprint, entryColor):
-    self.fingerprint = fingerprint
-    self.entryColor = entryColor
-    self.text = []
-    self.scroll = 0
-    
-    if fingerprint == "UNKNOWN":
-      self.fingerprint = None
-      self.showLineNum = False
-      self.text.append(UNRESOLVED_MSG)
-    else:
-      conn = torTools.getConn()
-      self.showLineNum = True
-      
-      self.text.append("ns/id/%s" % fingerprint)
-      consensusEntry = conn.getConsensusEntry(fingerprint)
-      
-      if consensusEntry: self.text += consensusEntry.split("\n")
-      else: self.text = self.text + [ERROR_MSG, ""]
-      
-      self.text.append("desc/id/%s" % fingerprint)
-      descriptorEntry = conn.getDescriptorEntry(fingerprint)
-      
-      if descriptorEntry: self.text += descriptorEntry.split("\n")
-      else: self.text = self.text + [ERROR_MSG]
-  
-  def handleKey(self, key, height):
-    if key == curses.KEY_UP: self.scroll = max(self.scroll - 1, 0)
-    elif key == curses.KEY_DOWN: self.scroll = max(0, min(self.scroll + 1, len(self.text) - height))
-    elif key == curses.KEY_PPAGE: self.scroll = max(self.scroll - height, 0)
-    elif key == curses.KEY_NPAGE: self.scroll = max(0, min(self.scroll + height, len(self.text) - height))
-
-def showDescriptorPopup(connectionPanel):
-  """
-  Presents consensus descriptor in popup window with the following controls:
-  Up, Down, Page Up, Page Down - scroll descriptor
-  Right, Left - next / previous connection
-  Enter, Space, d, D - close popup
-  """
-  
-  # hides the title of the first panel on the page
-  control = controller.getController()
-  topPanel = control.getDisplayPanels(includeSticky = False)[0]
-  topPanel.setTitleVisible(False)
-  topPanel.redraw(True)
-  
-  properties = PopupProperties()
-  isVisible = True
-  
-  panel.CURSES_LOCK.acquire()
-  
-  try:
-    while isVisible:
-      selection = connectionPanel._scroller.getCursorSelection(connectionPanel._entryLines)
-      if not selection: break
-      fingerprint = selection.foreign.getFingerprint()
-      entryColor = connections.connEntry.CATEGORY_COLOR[selection.getType()]
-      properties.reset(fingerprint, entryColor)
-      
-      # constrains popup size to match text
-      width, height = 0, 0
-      for line in properties.text:
-        # width includes content, line number field, and border
-        lineWidth = len(line) + 5
-        if properties.showLineNum: lineWidth += int(math.log10(len(properties.text))) + 1
-        width = max(width, lineWidth)
-        
-        # tracks number of extra lines that will be taken due to text wrap
-        height += (lineWidth - 2) / connectionPanel.maxX
-      
-      while isVisible:
-        popupHeight = min(len(properties.text) + height + 2, connectionPanel.maxY)
-        popup, _, _ = popups.init(popupHeight, width)
-        if not popup: break
-        
-        try:
-          draw(popup, properties)
-          key = control.getScreen().getch()
-          
-          if uiTools.isSelectionKey(key) or key in (ord('d'), ord('D')):
-            # closes popup
-            isVisible = False
-          elif key in (curses.KEY_LEFT, curses.KEY_RIGHT):
-            # navigation - pass on to connPanel and recreate popup
-            connectionPanel.handleKey(curses.KEY_UP if key == curses.KEY_LEFT else curses.KEY_DOWN)
-            break
-          else: properties.handleKey(key, popup.height - 2)
-        finally: popups.finalize()
-  finally: panel.CURSES_LOCK.release()
-  
-  topPanel.setTitleVisible(True)
-
-def draw(popup, properties):
-  popup.win.erase()
-  popup.win.box()
-  xOffset = 2
-  
-  if properties.text:
-    if properties.fingerprint: popup.addstr(0, 0, "Consensus Descriptor (%s):" % properties.fingerprint, curses.A_STANDOUT)
-    else: popup.addstr(0, 0, "Consensus Descriptor:", curses.A_STANDOUT)
-    
-    isEncryption = False          # true if line is part of an encryption block
-    
-    # checks if first line is in an encryption block
-    for i in range(0, properties.scroll):
-      lineText = properties.text[i].strip()
-      if lineText in SIG_START_KEYS: isEncryption = True
-      elif lineText in SIG_END_KEYS: isEncryption = False
-    
-    pageHeight = popup.maxY - 2
-    numFieldWidth = int(math.log10(len(properties.text))) + 1
-    lineNum = 1
-    for i in range(properties.scroll, min(len(properties.text), properties.scroll + pageHeight)):
-      lineText = properties.text[i].strip()
-      
-      numOffset = 0     # offset for line numbering
-      if properties.showLineNum:
-        popup.addstr(lineNum, xOffset, ("%%%ii" % numFieldWidth) % (i + 1), curses.A_BOLD | uiTools.getColor(LINE_NUM_COLOR))
-        numOffset = numFieldWidth + 1
-      
-      if lineText:
-        keyword = lineText.split()[0]   # first word of line
-        remainder = lineText[len(keyword):]
-        keywordFormat = curses.A_BOLD | uiTools.getColor(properties.entryColor)
-        remainderFormat = uiTools.getColor(properties.entryColor)
-        
-        if lineText.startswith(HEADER_PREFIX[0]) or lineText.startswith(HEADER_PREFIX[1]):
-          keyword, remainder = lineText, ""
-          keywordFormat = curses.A_BOLD | uiTools.getColor(HEADER_COLOR)
-        if lineText == UNRESOLVED_MSG or lineText == ERROR_MSG:
-          keyword, remainder = lineText, ""
-        if lineText in SIG_START_KEYS:
-          keyword, remainder = lineText, ""
-          isEncryption = True
-          keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
-        elif lineText in SIG_END_KEYS:
-          keyword, remainder = lineText, ""
-          isEncryption = False
-          keywordFormat = curses.A_BOLD | uiTools.getColor(SIG_COLOR)
-        elif isEncryption:
-          keyword, remainder = lineText, ""
-          keywordFormat = uiTools.getColor(SIG_COLOR)
-        
-        lineNum, xLoc = addstr_wrap(popup, lineNum, 0, keyword, keywordFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
-        lineNum, xLoc = addstr_wrap(popup, lineNum, xLoc, remainder, remainderFormat, xOffset + numOffset, popup.maxX - 1, popup.maxY - 1)
-      
-      lineNum += 1
-      if lineNum > pageHeight: break
-      
-  popup.win.refresh()
-
diff --git a/src/util/panel.py b/src/util/panel.py
index 8a95479..0e84005 100644
--- a/src/util/panel.py
+++ b/src/util/panel.py
@@ -677,6 +677,8 @@ class Panel():
     for i in range(scrollbarHeight):
       if i >= sliderTop and i <= sliderTop + sliderSize:
         self.addstr(i + drawTop, drawLeft, " ", curses.A_STANDOUT)
+      else:
+        self.addstr(i + drawTop, drawLeft, " ")
     
     # draws box around the scroll bar
     self.vline(drawTop, drawLeft + 1, drawBottom - 1)



More information about the tor-commits mailing list