[tor-commits] [nyx/master] Consolidate remaining nyx/connections/* module

atagar at torproject.org atagar at torproject.org
Tue Sep 22 17:08:41 UTC 2015


commit 02327e0bed85ad2e87ce3c0ecb3bb62bcce117dd
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Sep 5 15:00:57 2015 -0700

    Consolidate remaining nyx/connections/* module
    
    Ok, trimmed the remaining bits enough that we can consolidate in
    connection_panel.py. Still a mess, but now a manageable mess.
---
 nyx/connection_panel.py             |  594 ++++++++++++++++++++++++++++++++++-
 nyx/connections/__init__.py         |   10 -
 nyx/connections/circ_entry.py       |  177 -----------
 nyx/connections/conn_entry.py       |  453 --------------------------
 nyx/connections/descriptor_popup.py |  174 ----------
 nyx/popups.py                       |  167 +++++++++-
 setup.py                            |    2 +-
 7 files changed, 752 insertions(+), 825 deletions(-)

diff --git a/nyx/connection_panel.py b/nyx/connection_panel.py
index c583a00..9fcacae 100644
--- a/nyx/connection_panel.py
+++ b/nyx/connection_panel.py
@@ -6,17 +6,18 @@ import re
 import time
 import collections
 import curses
+import datetime
 import itertools
 import threading
 
 import nyx.popups
 import nyx.util.tracker
+import nyx.util.ui_tools
 
-from nyx.connections import descriptor_popup
 from nyx.util import panel, tor_controller, ui_tools
 
 from stem.control import Listener, State
-from stem.util import conf, connection, enum
+from stem.util import conf, connection, enum, str_tools
 
 try:
   # added in python 3.2
@@ -71,6 +72,12 @@ SORT_COLORS = {
   SortAttr.COUNTRY: 'blue',
 }
 
+# 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
+
 
 def conf_handler(key, value):
   if key == 'features.connection.listing_type':
@@ -90,6 +97,10 @@ CONFIG = conf.config_dict('nyx', {
 }, conf_handler)
 
 
+def to_unix_time(dt):
+  return (dt - datetime.datetime(1970, 1, 1)).total_seconds()
+
+
 class Entry(object):
   @staticmethod
   @lru_cache()
@@ -137,8 +148,7 @@ class ConnectionEntry(Entry):
 
   @lru_cache()
   def get_lines(self):
-    import nyx.connections.conn_entry
-    return [nyx.connections.conn_entry.ConnectionLine(self, self._connection)]
+    return [ConnectionLine(self, self._connection)]
 
   @lru_cache()
   def get_type(self):
@@ -201,7 +211,6 @@ class CircuitEntry(Entry):
 
   @lru_cache()
   def get_lines(self):
-    from nyx.connections.circ_entry import CircHeaderLine, CircLine
     return [CircHeaderLine(self, self._circuit)] + [CircLine(self, self._circuit, fp) for fp, _ in self._circuit.path]
 
   def get_type(self):
@@ -211,6 +220,575 @@ class CircuitEntry(Entry):
     return False
 
 
+class ConnectionLine(object):
+  """
+  Display component of the ConnectionEntry.
+  """
+
+  def __init__(self, entry, conn, include_port = True):
+    self._entry = entry
+    self.connection = conn
+
+    # includes the port or expanded ip address field when displaying listing
+    # information if true
+
+    self.include_port = include_port
+
+  def get_listing_prefix(self):
+    """
+    Provides a list of characters to be appended before the listing entry.
+    """
+
+    return ()
+
+  def get_locale(self, default = None):
+    """
+    Provides the two letter country code for the remote endpoint.
+    """
+
+    return tor_controller().get_info('ip-to-country/%s' % self.connection.remote_address, default)
+
+  def get_fingerprint(self, default = None):
+    """
+    Provides the fingerprint of this relay.
+    """
+
+    if self._entry.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT):
+      my_fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprint(self.connection.remote_address, self.connection.remote_port)
+      return my_fingerprint if my_fingerprint else default
+    else:
+      return default  # inbound connections don't have an ORPort we can resolve
+
+  def get_nickname(self, default = None):
+    """
+    Provides the nickname of this relay.
+    """
+
+    nickname = nyx.util.tracker.get_consensus_tracker().get_relay_nickname(self.get_fingerprint())
+    return nickname if nickname else default
+
+  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>)
+
+    Listing.IP_ADDRESS:
+      src - <internal addr:port> --> <external addr:port>
+      dst - <destination addr:port>
+      etc - <fingerprint> <nickname>
+
+    Listing.FINGERPRINT:
+      src - localhost
+      dst - <destination fingerprint>
+      etc - <nickname> <destination addr:port>
+
+    Listing.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 = self._get_listing_entry(width, listing_type)
+
+    # fill in the current uptime and return the results
+
+    time_prefix = '+' if self.connection.is_legacy else ' '
+
+    time_label = time_prefix + '%5s' % str_tools.time_label(current_time - self.connection.start_time, 1)
+    my_listing[2] = (time_label, my_listing[2][1])
+
+    return my_listing
+
+  @lru_cache()
+  def _get_listing_entry(self, width, listing_type):
+    entry_type = self._entry.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 = nyx.util.ui_tools.get_color(CATEGORY_COLOR[entry_type])
+
+    draw_entry = [(' ', line_format),
+                  (self._get_listing_content(width - 19, listing_type), line_format),
+                  ('      ', line_format),
+                  (' (', line_format),
+                  (entry_type.upper(), line_format | curses.A_BOLD),
+                  (')' + ' ' * (9 - len(entry_type)), line_format)]
+
+    return draw_entry
+
+  @lru_cache()
+  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._entry.get_type()])
+    return [(line, detail_format) for line in self._get_detail_content(width)]
+
+  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._entry.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
+      port = self.connection.local_port if self._entry.get_type() == Category.HIDDEN else self.connection.remote_port
+
+      try:
+        process = nyx.util.tracker.get_port_usage_tracker().fetch(port)
+        display_label = '%s (%s)' % (process.name, process.pid) if process.pid else process.name
+      except nyx.util.tracker.UnresolvedResult:
+        display_label = 'resolving...'
+      except nyx.util.tracker.UnknownApplication:
+        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 == Listing.IP_ADDRESS:
+      if width > used_space + 42:
+        # show fingerprint (column width: 42 characters)
+
+        etc += '%-40s  ' % self.get_fingerprint('UNKNOWN')
+        used_space += 42
+
+      if width > used_space + 10:
+        # show nickname (column width: remainder)
+
+        nickname_space = width - used_space
+        nickname_label = str_tools.crop(self.get_nickname('UNKNOWN'), nickname_space, 0)
+        etc += ('%%-%is  ' % nickname_space) % nickname_label
+        used_space += nickname_space + 2
+    elif listing_type == Listing.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
+
+        if is_locale_included:
+          nickname_space -= 28
+
+        nickname_label = str_tools.crop(self.get_nickname('UNKNOWN'), 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:
+        # show fingerprint (column width: 42 characters)
+        etc += '%-40s  ' % self.get_fingerprint('UNKNOWN')
+        used_space += 42
+
+      if width > used_space + 28:
+        # 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._entry.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.connection.local_port if self.include_port else ''
+
+    src, dst, etc = '', '', ''
+
+    if listing_type == Listing.IP_ADDRESS:
+      my_external_address = controller.get_info('address', self.connection.local_address)
+
+      # Show our external address if it's going through tor.
+
+      if my_type not in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
+        src_address = my_external_address + local_port
+      else:
+        src_address = self.connection.local_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 (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
+
+      etc = self.get_etc_content(width - used_space, listing_type)
+      used_space += len(etc)
+    elif listing_type == Listing.FINGERPRINT:
+      src = 'localhost'
+      dst = '%-40s' % ('localhost' if my_type == Category.CONTROL else self.get_fingerprint('UNKNOWN'))
+
+      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 = controller.get_conf('nickname', 'UNKNOWN')
+      dst = controller.get_conf('nickname', 'UNKNOWN') if my_type == Category.CONTROL else self.get_nickname('UNKNOWN')
+
+      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._entry.is_private() else self.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.get_fingerprint()
+    controller = tor_controller()
+
+    if fingerprint:
+      lines[1] = '%-13sfingerprint: %s' % (lines[1], fingerprint)  # append fingerprint to second line
+
+      router_status_entry = controller.get_network_status(fingerprint, None)
+      server_descriptor = controller.get_server_descriptor(fingerprint, None)
+
+      if router_status_entry:
+        dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else ''
+        lines[2] = 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label)
+        lines[3] = 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y")
+        lines[4] = 'flags: %s' % ', '.join(router_status_entry.flags)
+
+      if server_descriptor:
+        policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown'
+        lines[5] = 'exit policy: %s' % policy_label
+        lines[3] = '%-35s os: %-14s version: %s' % (lines[3], server_descriptor.operating_system, server_descriptor.tor_version)
+
+        if server_descriptor.contact:
+          lines[6] = 'contact: %s' % server_descriptor.contact
+    else:
+      all_matches = nyx.util.tracker.get_consensus_tracker().get_all_relay_fingerprints(self.connection.remote_address)
+
+      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 + 1, 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):
+    """
+    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, 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
+    """
+
+    # destination of the connection
+
+    address_label = '<scrubbed>' if self._entry.is_private() else self.connection.remote_address
+    port_label = ':%s' % self.connection.remote_port
+    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._entry.get_type() == Category.EXIT:
+        purpose = connection.port_usage(self.connection.remote_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.connection.remote_address):
+        extra_info = []
+
+        if include_locale and not tor_controller().is_geoip_unavailable():
+          foreign_locale = self.get_locale('??')
+          extra_info.append(foreign_locale)
+          space_available -= len(foreign_locale) + 2
+
+        if extra_info:
+          destination_address += ' (%s)' % ', '.join(extra_info)
+
+    return destination_address[:max_length]
+
+
+class CircHeaderLine(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, entry, circ):
+    if circ.status == 'BUILT':
+      self._remote_fingerprint = circ.path[-1][0]
+      exit_address, exit_port = nyx.util.tracker.get_consensus_tracker().get_relay_address(self._remote_fingerprint, ('192.168.0.1', 0))
+      self.is_built = True
+    else:
+      exit_address, exit_port = '0.0.0.0', 0
+      self.is_built = False
+      self._remote_fingerprint = None
+
+    ConnectionLine.__init__(self, entry, nyx.util.tracker.Connection(to_unix_time(circ.created), False, '127.0.0.1', 0, exit_address, exit_port, 'tcp'), include_port = False)
+    self.circuit = circ
+
+  def get_fingerprint(self, default = None):
+    return self._remote_fingerprint if self._remote_fingerprint else ConnectionLine.get_fingerprint(self, default)
+
+  def get_destination_label(self, max_length, include_locale = False):
+    if not self.is_built:
+      return 'Building...'
+
+    return ConnectionLine.get_destination_label(self, max_length, include_locale)
+
+  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.circuit.purpose.capitalize(), '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 ''
+
+  @lru_cache()
+  def get_details(self, width):
+    if not self.is_built:
+      detail_format = (curses.A_BOLD, CATEGORY_COLOR[self._entry.get_type()])
+      return [('Building Circuit...', detail_format)]
+    else:
+      return ConnectionLine.get_details(self, width)
+
+
+class CircLine(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, entry, circ, fingerprint):
+    relay_ip, relay_port = nyx.util.tracker.get_consensus_tracker().get_relay_address(fingerprint, ('192.168.0.1', 0))
+    ConnectionLine.__init__(self, entry, nyx.util.tracker.Connection(to_unix_time(circ.created), False, '127.0.0.1', 0, relay_ip, relay_port, 'tcp'), include_port = False)
+    self._fingerprint = fingerprint
+    self._is_last = False
+
+    circ_path = [path_entry[0] for path_entry in circ.path]
+    circ_index = circ_path.index(fingerprint)
+
+    if circ_index == len(circ_path) - 1:
+      placement_type = 'Exit' if circ.status == 'BUILT' else 'Extending'
+      self._is_last = True
+    elif circ_index == 0:
+      placement_type = 'Guard'
+    else:
+      placement_type = 'Middle'
+
+    self.placement_label = '%i / %s' % (circ_index + 1, placement_type)
+
+  def get_fingerprint(self, default = None):
+    self._fingerprint
+
+  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 self._get_listing_entry(width, listing_type)
+
+  @lru_cache()
+  def _get_listing_entry(self, width, listing_type):
+    line_format = nyx.util.ui_tools.get_color(CATEGORY_COLOR[self._entry.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 == Listing.IP_ADDRESS:
+      # 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.get_nickname('UNKNOWN'), 25, 0))
+
+      etc = self.get_etc_content(width - baseline_space - len(dst), listing_type)
+    elif listing_type == Listing.FINGERPRINT:
+      # dst width is derived as:
+      # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
+
+      dst = '%-55s' % self.get_fingerprint('UNKNOWN')
+      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.get_nickname('UNKNOWN')
+
+    return ((dst + etc, line_format),
+            (' ' * (width - baseline_space - len(dst) - len(etc) + 5), line_format),
+            ('%-14s' % self.placement_label, line_format))
+
+
 class ConnectionPanel(panel.Panel, threading.Thread):
   """
   Listing of connections tor is making, with information correlated against
@@ -280,10 +858,8 @@ class ConnectionPanel(panel.Panel, threading.Thread):
 
     # mark the initially exitsing connection uptimes as being estimates
 
-    from nyx.connections import conn_entry
-
     for entry in self._entries:
-      if isinstance(entry, conn_entry.ConnectionEntry):
+      if isinstance(entry, ConnectionEntry):
         entry.get_lines()[0].is_initial_connection = True
 
     # listens for when tor stops so we know to stop reflecting changes
@@ -471,7 +1047,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
           color = CATEGORY_COLOR[selection.get_type()]
           fingerprint = selection.get_fingerprint()
           is_close_key = lambda key: key.is_selection() or key.match('d') or key.match('left') or key.match('right')
-          key = descriptor_popup.show_descriptor_popup(fingerprint, color, self.max_x, is_close_key)
+          key = nyx.popups.show_descriptor_popup(fingerprint, color, self.max_x, is_close_key)
 
           if not key or key.is_selection() or key.match('d'):
             break  # closes popup
diff --git a/nyx/connections/__init__.py b/nyx/connections/__init__.py
deleted file mode 100644
index 577823a..0000000
--- a/nyx/connections/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""
-Resources related to our connection panel.
-"""
-
-__all__ = [
-  'circ_entry',
-  'conn_entry',
-  'descriptor_popup',
-  'entries',
-]
diff --git a/nyx/connections/circ_entry.py b/nyx/connections/circ_entry.py
deleted file mode 100644
index 920d599..0000000
--- a/nyx/connections/circ_entry.py
+++ /dev/null
@@ -1,177 +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
-import datetime
-
-import nyx.util.tracker
-import nyx.util.ui_tools
-import nyx.connection_panel
-
-from nyx.connections import conn_entry
-
-from stem.util import str_tools
-
-try:
-  # added in python 3.2
-  from functools import lru_cache
-except ImportError:
-  from stem.util.lru_cache import lru_cache
-
-
-def to_unix_time(dt):
-  return (dt - datetime.datetime(1970, 1, 1)).total_seconds()
-
-
-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, entry, circ):
-    if circ.status == 'BUILT':
-      self._remote_fingerprint = circ.path[-1][0]
-      exit_address, exit_port = nyx.util.tracker.get_consensus_tracker().get_relay_address(self._remote_fingerprint, ('192.168.0.1', 0))
-      self.is_built = True
-    else:
-      exit_address, exit_port = '0.0.0.0', 0
-      self.is_built = False
-      self._remote_fingerprint = None
-
-    conn_entry.ConnectionLine.__init__(self, entry, nyx.util.tracker.Connection(to_unix_time(circ.created), False, '127.0.0.1', 0, exit_address, exit_port, 'tcp'), include_port = False)
-    self.circuit = circ
-
-  def get_fingerprint(self, default = None):
-    return self._remote_fingerprint if self._remote_fingerprint else conn_entry.ConnectionLine.get_fingerprint(self, default)
-
-  def get_destination_label(self, max_length, include_locale = False):
-    if not self.is_built:
-      return 'Building...'
-
-    return conn_entry.ConnectionLine.get_destination_label(self, max_length, include_locale)
-
-  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.circuit.purpose.capitalize(), '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 ''
-
-  @lru_cache()
-  def get_details(self, width):
-    if not self.is_built:
-      detail_format = (curses.A_BOLD, nyx.connection_panel.CATEGORY_COLOR[self._entry.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, entry, circ, fingerprint):
-    relay_ip, relay_port = nyx.util.tracker.get_consensus_tracker().get_relay_address(fingerprint, ('192.168.0.1', 0))
-    conn_entry.ConnectionLine.__init__(self, entry, nyx.util.tracker.Connection(to_unix_time(circ.created), False, '127.0.0.1', 0, relay_ip, relay_port, 'tcp'), include_port = False)
-    self._fingerprint = fingerprint
-    self._is_last = False
-
-    circ_path = [path_entry[0] for path_entry in circ.path]
-    circ_index = circ_path.index(fingerprint)
-
-    if circ_index == len(circ_path) - 1:
-      placement_type = 'Exit' if circ.status == 'BUILT' else 'Extending'
-      self._is_last = True
-    elif circ_index == 0:
-      placement_type = 'Guard'
-    else:
-      placement_type = 'Middle'
-
-    self.placement_label = '%i / %s' % (circ_index + 1, placement_type)
-
-  def get_fingerprint(self, default = None):
-    self._fingerprint
-
-  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 self._get_listing_entry(width, listing_type)
-
-  @lru_cache()
-  def _get_listing_entry(self, width, listing_type):
-    line_format = nyx.util.ui_tools.get_color(nyx.connection_panel.CATEGORY_COLOR[self._entry.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 == nyx.connection_panel.Listing.IP_ADDRESS:
-      # 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.get_nickname('UNKNOWN'), 25, 0))
-
-      etc = self.get_etc_content(width - baseline_space - len(dst), listing_type)
-    elif listing_type == nyx.connection_panel.Listing.FINGERPRINT:
-      # dst width is derived as:
-      # src (9) + dst (40) + divider (7) + right gap (2) - bracket (3) = 55 char
-
-      dst = '%-55s' % self.get_fingerprint('UNKNOWN')
-      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.get_nickname('UNKNOWN')
-
-    return ((dst + etc, line_format),
-            (' ' * (width - baseline_space - len(dst) - len(etc) + 5), line_format),
-            ('%-14s' % self.placement_label, line_format))
diff --git a/nyx/connections/conn_entry.py b/nyx/connections/conn_entry.py
deleted file mode 100644
index df36dd8..0000000
--- a/nyx/connections/conn_entry.py
+++ /dev/null
@@ -1,453 +0,0 @@
-"""
-Connection panel entries related to actual connections to or from the system
-(ie, results seen by netstat, lsof, etc).
-"""
-
-import curses
-
-import nyx.connection_panel
-import nyx.util.tracker
-import nyx.util.ui_tools
-
-from nyx.util import tor_controller
-from nyx.connection_panel import Category
-
-from stem.util import conf, connection, str_tools
-
-try:
-  # added in python 3.2
-  from functools import lru_cache
-except ImportError:
-  from stem.util.lru_cache import lru_cache
-
-# 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
-
-CONFIG = conf.config_dict('nyx', {
-  'features.connection.showIps': True,
-})
-
-
-class ConnectionLine(object):
-  """
-  Display component of the ConnectionEntry.
-  """
-
-  def __init__(self, entry, conn, include_port = True):
-    self._entry = entry
-    self.connection = conn
-
-    # includes the port or expanded ip address field when displaying listing
-    # information if true
-
-    self.include_port = include_port
-
-  def get_listing_prefix(self):
-    """
-    Provides a list of characters to be appended before the listing entry.
-    """
-
-    return ()
-
-  def get_locale(self, default = None):
-    """
-    Provides the two letter country code for the remote endpoint.
-    """
-
-    return tor_controller().get_info('ip-to-country/%s' % self.connection.remote_address, default)
-
-  def get_fingerprint(self, default = None):
-    """
-    Provides the fingerprint of this relay.
-    """
-
-    if self._entry.get_type() in (Category.OUTBOUND, Category.CIRCUIT, Category.DIRECTORY, Category.EXIT):
-      my_fingerprint = nyx.util.tracker.get_consensus_tracker().get_relay_fingerprint(self.connection.remote_address, self.connection.remote_port)
-      return my_fingerprint if my_fingerprint else default
-    else:
-      return default  # inbound connections don't have an ORPort we can resolve
-
-  def get_nickname(self, default = None):
-    """
-    Provides the nickname of this relay.
-    """
-
-    nickname = nyx.util.tracker.get_consensus_tracker().get_relay_nickname(self.get_fingerprint())
-    return nickname if nickname else default
-
-  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>)
-
-    Listing.IP_ADDRESS:
-      src - <internal addr:port> --> <external addr:port>
-      dst - <destination addr:port>
-      etc - <fingerprint> <nickname>
-
-    Listing.FINGERPRINT:
-      src - localhost
-      dst - <destination fingerprint>
-      etc - <nickname> <destination addr:port>
-
-    Listing.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 = self._get_listing_entry(width, listing_type)
-
-    # fill in the current uptime and return the results
-
-    time_prefix = '+' if self.connection.is_legacy else ' '
-
-    time_label = time_prefix + '%5s' % str_tools.time_label(current_time - self.connection.start_time, 1)
-    my_listing[2] = (time_label, my_listing[2][1])
-
-    return my_listing
-
-  @lru_cache()
-  def _get_listing_entry(self, width, listing_type):
-    entry_type = self._entry.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 = nyx.util.ui_tools.get_color(nyx.connection_panel.CATEGORY_COLOR[entry_type])
-
-    draw_entry = [(' ', line_format),
-                  (self._get_listing_content(width - 19, listing_type), line_format),
-                  ('      ', line_format),
-                  (' (', line_format),
-                  (entry_type.upper(), line_format | curses.A_BOLD),
-                  (')' + ' ' * (9 - len(entry_type)), line_format)]
-
-    return draw_entry
-
-  @lru_cache()
-  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, nyx.connection_panel.CATEGORY_COLOR[self._entry.get_type()])
-    return [(line, detail_format) for line in self._get_detail_content(width)]
-
-  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._entry.get_type() in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
-      port = self.connection.local_port if self._entry.get_type() == Category.HIDDEN else self.connection.remote_port
-
-      try:
-        process = nyx.util.tracker.get_port_usage_tracker().fetch(port)
-        display_label = '%s (%s)' % (process.name, process.pid) if process.pid else process.name
-      except nyx.util.tracker.UnresolvedResult:
-        display_label = 'resolving...'
-      except nyx.util.tracker.UnknownApplication:
-        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 == nyx.connection_panel.Listing.IP_ADDRESS:
-      if width > used_space + 42:
-        # show fingerprint (column width: 42 characters)
-
-        etc += '%-40s  ' % self.get_fingerprint('UNKNOWN')
-        used_space += 42
-
-      if width > used_space + 10:
-        # show nickname (column width: remainder)
-
-        nickname_space = width - used_space
-        nickname_label = str_tools.crop(self.get_nickname('UNKNOWN'), nickname_space, 0)
-        etc += ('%%-%is  ' % nickname_space) % nickname_label
-        used_space += nickname_space + 2
-    elif listing_type == nyx.connection_panel.Listing.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
-
-        if is_locale_included:
-          nickname_space -= 28
-
-        nickname_label = str_tools.crop(self.get_nickname('UNKNOWN'), 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:
-        # show fingerprint (column width: 42 characters)
-        etc += '%-40s  ' % self.get_fingerprint('UNKNOWN')
-        used_space += 42
-
-      if width > used_space + 28:
-        # 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._entry.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.connection.local_port if self.include_port else ''
-
-    src, dst, etc = '', '', ''
-
-    if listing_type == nyx.connection_panel.Listing.IP_ADDRESS:
-      my_external_address = controller.get_info('address', self.connection.local_address)
-
-      # Show our external address if it's going through tor.
-
-      if my_type not in (Category.SOCKS, Category.HIDDEN, Category.CONTROL):
-        src_address = my_external_address + local_port
-      else:
-        src_address = self.connection.local_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 (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
-
-      etc = self.get_etc_content(width - used_space, listing_type)
-      used_space += len(etc)
-    elif listing_type == nyx.connection_panel.Listing.FINGERPRINT:
-      src = 'localhost'
-      dst = '%-40s' % ('localhost' if my_type == Category.CONTROL else self.get_fingerprint('UNKNOWN'))
-
-      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 = controller.get_conf('nickname', 'UNKNOWN')
-      dst = controller.get_conf('nickname', 'UNKNOWN') if my_type == Category.CONTROL else self.get_nickname('UNKNOWN')
-
-      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._entry.is_private() else self.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.get_fingerprint()
-    controller = tor_controller()
-
-    if fingerprint:
-      lines[1] = '%-13sfingerprint: %s' % (lines[1], fingerprint)  # append fingerprint to second line
-
-      router_status_entry = controller.get_network_status(fingerprint, None)
-      server_descriptor = controller.get_server_descriptor(fingerprint, None)
-
-      if router_status_entry:
-        dir_port_label = 'dirport: %s' % router_status_entry.dir_port if router_status_entry.dir_port else ''
-        lines[2] = 'nickname: %-25s orport: %-10s %s' % (router_status_entry.nickname, router_status_entry.or_port, dir_port_label)
-        lines[3] = 'published: %s' % router_status_entry.published.strftime("%H:%M %m/%d/%Y")
-        lines[4] = 'flags: %s' % ', '.join(router_status_entry.flags)
-
-      if server_descriptor:
-        policy_label = server_descriptor.exit_policy.summary() if server_descriptor.exit_policy else 'unknown'
-        lines[5] = 'exit policy: %s' % policy_label
-        lines[3] = '%-35s os: %-14s version: %s' % (lines[3], server_descriptor.operating_system, server_descriptor.tor_version)
-
-        if server_descriptor.contact:
-          lines[6] = 'contact: %s' % server_descriptor.contact
-    else:
-      all_matches = nyx.util.tracker.get_consensus_tracker().get_all_relay_fingerprints(self.connection.remote_address)
-
-      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 + 1, 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):
-    """
-    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, 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
-    """
-
-    # destination of the connection
-
-    address_label = '<scrubbed>' if self._entry.is_private() else self.connection.remote_address
-    port_label = ':%s' % self.connection.remote_port
-    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._entry.get_type() == Category.EXIT:
-        purpose = connection.port_usage(self.connection.remote_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.connection.remote_address):
-        extra_info = []
-
-        if include_locale and not tor_controller().is_geoip_unavailable():
-          foreign_locale = self.get_locale('??')
-          extra_info.append(foreign_locale)
-          space_available -= len(foreign_locale) + 2
-
-        if extra_info:
-          destination_address += ' (%s)' % ', '.join(extra_info)
-
-    return destination_address[:max_length]
diff --git a/nyx/connections/descriptor_popup.py b/nyx/connections/descriptor_popup.py
deleted file mode 100644
index 358784c..0000000
--- a/nyx/connections/descriptor_popup.py
+++ /dev/null
@@ -1,174 +0,0 @@
-"""
-Popup providing the raw descriptor and consensus information for a relay.
-"""
-
-import math
-import curses
-
-import nyx.popups
-
-from nyx.util import tor_controller, ui_tools
-
-from stem.util import str_tools
-
-HEADERS = ['Consensus:', 'Microdescriptor:', 'Server Descriptor:']
-HEADER_COLOR = 'cyan'
-LINE_NUMBER_COLOR = 'yellow'
-
-BLOCK_START, BLOCK_END = '-----BEGIN ', '-----END '
-
-UNRESOLVED_MSG = 'No consensus data available'
-ERROR_MSG = 'Unable to retrieve data'
-
-
-def show_descriptor_popup(fingerprint, color, max_width, is_close_key):
-  """
-  Provides a dialog showing the descriptors for a given relay.
-
-  :param str fingerprint: fingerprint of the relay to be shown
-  :param str color: text color of the dialog
-  :param int max_width: maximum width of the dialog
-  :param function is_close_key: method to indicate if a key should close the
-    dialog or not
-
-  :returns: :class:`~nyx.util.panel.KeyInput` for the keyboard input that
-    closed the dialog
-  """
-
-  if fingerprint:
-    title = 'Consensus Descriptor:'
-    lines = _display_text(fingerprint)
-    show_line_numbers = True
-  else:
-    title = 'Consensus Descriptor (%s):' % fingerprint
-    lines = [UNRESOLVED_MSG]
-    show_line_numbers = False
-
-  popup_height, popup_width = _preferred_size(lines, max_width, show_line_numbers)
-
-  with nyx.popups.popup_window(popup_height, popup_width) as (popup, _, height):
-    if not popup:
-      return None
-
-    scroll, redraw = 0, True
-
-    while True:
-      if redraw:
-        _draw(popup, title, lines, color, scroll, show_line_numbers)
-        redraw = False
-
-      key = nyx.controller.get_controller().key_input()
-
-      if key.is_scroll():
-        new_scroll = ui_tools.get_scroll_position(key, scroll, height - 2, len(lines))
-
-        if scroll != new_scroll:
-          scroll, redraw = new_scroll, True
-      elif is_close_key(key):
-        return key
-
-
-def _display_text(fingerprint):
-  """
-  Provides the descriptors for a relay.
-
-  :param str fingerprint: relay fingerprint to be looked up
-
-  :returns: **list** with the lines that should be displayed in the dialog
-  """
-
-  controller = tor_controller()
-  router_status_entry = controller.get_network_status(fingerprint, None)
-  microdescriptor = controller.get_microdescriptor(fingerprint, None)
-  server_descriptor = controller.get_server_descriptor(fingerprint, None)
-
-  description = 'Consensus:\n\n%s' % (router_status_entry if router_status_entry else ERROR_MSG)
-
-  if server_descriptor:
-    description += '\n\nServer Descriptor:\n\n%s' % server_descriptor
-
-  if microdescriptor:
-    description += '\n\nMicrodescriptor:\n\n%s' % microdescriptor
-
-  return description.split('\n')
-
-
-def _preferred_size(text, max_width, show_line_numbers):
-  """
-  Provides the preferred dimensions of our dialog.
-
-  :param list text: lines of text to be shown
-  :param int max_width: maximum width the dialog can be
-  :param bool show_line_numbers: if we should leave room for line numbers
-
-  :returns: **tuple** of the preferred (height, width)
-  """
-
-  width, height = 0, len(text) + 2
-  line_number_width = int(math.log10(len(text))) + 2 if show_line_numbers else 0
-  max_content_width = max_width - line_number_width - 4
-
-  for line in text:
-    width = min(max_width, max(width, len(line) + line_number_width + 4))
-    height += len(line) / max_content_width  # extra lines due to text wrap
-
-  return (height, width)
-
-
-def _draw(popup, title, lines, entry_color, scroll, show_line_numbers):
-  def draw_msg(popup, min_x, x, y, width, msg, *attr):
-    while msg:
-      draw_msg, msg = str_tools.crop(msg, width - x, None, ending = None, get_remainder = True)
-
-      if not draw_msg:
-        draw_msg, msg = str_tools.crop(msg, width - x), ''  # first word is longer than the line
-
-      x = popup.addstr(y, x, draw_msg, *attr)
-
-      if msg:
-        x, y = min_x, y + 1
-
-    return x, y
-
-  popup.win.erase()
-
-  line_number_width = int(math.log10(len(lines))) + 1
-  in_block = False   # flag indicating if we're currently in crypto content
-  width = popup.max_x - 2  # leave space on the right for the border and an empty line
-  height = popup.max_y - 2  # height of the dialog without the top and bottom border
-  offset = line_number_width + 3 if show_line_numbers else 2
-
-  y = 1
-
-  for i, line in enumerate(lines):
-    keyword, value = line, ''
-    color = entry_color
-
-    if line in HEADERS:
-      color = HEADER_COLOR
-    elif line.startswith(BLOCK_START):
-      in_block = True
-    elif line.startswith(BLOCK_END):
-      in_block = False
-    elif in_block:
-      keyword, value = '', line
-    elif ' ' in line and line != UNRESOLVED_MSG and line != ERROR_MSG:
-      keyword, value = line.split(' ', 1)
-
-    if i < scroll:
-      continue
-
-    if show_line_numbers:
-      popup.addstr(y, 2, str(i + 1).rjust(line_number_width), curses.A_BOLD, LINE_NUMBER_COLOR)
-
-    x, y = draw_msg(popup, offset, offset, y, width, keyword, color, curses.A_BOLD)
-    x, y = draw_msg(popup, offset, x + 1, y, width, value, color)
-
-    y += 1
-
-    if y > height:
-      break
-
-  popup.win.box()
-  popup.addstr(0, 0, title, curses.A_STANDOUT)
-  popup.win.refresh()
diff --git a/nyx/popups.py b/nyx/popups.py
index 351467f..d2d5ded 100644
--- a/nyx/popups.py
+++ b/nyx/popups.py
@@ -2,16 +2,28 @@
 Functions for displaying popups in the interface.
 """
 
+import math
 import curses
 import operator
 
 import nyx.controller
 
 from nyx import __version__, __release_date__
-from nyx.util import panel, ui_tools
+from nyx.util import tor_controller, panel, ui_tools
+
+from stem.util import str_tools
 
 NO_STATS_MSG = "Usage stats aren't available yet, press any key..."
 
+HEADERS = ['Consensus:', 'Microdescriptor:', 'Server Descriptor:']
+HEADER_COLOR = 'cyan'
+LINE_NUMBER_COLOR = 'yellow'
+
+BLOCK_START, BLOCK_END = '-----BEGIN ', '-----END '
+
+UNRESOLVED_MSG = 'No consensus data available'
+ERROR_MSG = 'Unable to retrieve data'
+
 
 def popup_window(height = -1, width = -1, top = 0, left = 0, below_static = True):
   """
@@ -418,3 +430,156 @@ def show_menu(title, options, old_selection):
 
   top_panel.set_title_visible(True)
   return selection
+
+
+def show_descriptor_popup(fingerprint, color, max_width, is_close_key):
+  """
+  Provides a dialog showing the descriptors for a given relay.
+
+  :param str fingerprint: fingerprint of the relay to be shown
+  :param str color: text color of the dialog
+  :param int max_width: maximum width of the dialog
+  :param function is_close_key: method to indicate if a key should close the
+    dialog or not
+
+  :returns: :class:`~nyx.util.panel.KeyInput` for the keyboard input that
+    closed the dialog
+  """
+
+  if fingerprint:
+    title = 'Consensus Descriptor:'
+    lines = _display_text(fingerprint)
+    show_line_numbers = True
+  else:
+    title = 'Consensus Descriptor (%s):' % fingerprint
+    lines = [UNRESOLVED_MSG]
+    show_line_numbers = False
+
+  popup_height, popup_width = _preferred_size(lines, max_width, show_line_numbers)
+
+  with popup_window(popup_height, popup_width) as (popup, _, height):
+    if not popup:
+      return None
+
+    scroll, redraw = 0, True
+
+    while True:
+      if redraw:
+        _draw(popup, title, lines, color, scroll, show_line_numbers)
+        redraw = False
+
+      key = nyx.controller.get_controller().key_input()
+
+      if key.is_scroll():
+        new_scroll = ui_tools.get_scroll_position(key, scroll, height - 2, len(lines))
+
+        if scroll != new_scroll:
+          scroll, redraw = new_scroll, True
+      elif is_close_key(key):
+        return key
+
+
+def _display_text(fingerprint):
+  """
+  Provides the descriptors for a relay.
+
+  :param str fingerprint: relay fingerprint to be looked up
+
+  :returns: **list** with the lines that should be displayed in the dialog
+  """
+
+  controller = tor_controller()
+  router_status_entry = controller.get_network_status(fingerprint, None)
+  microdescriptor = controller.get_microdescriptor(fingerprint, None)
+  server_descriptor = controller.get_server_descriptor(fingerprint, None)
+
+  description = 'Consensus:\n\n%s' % (router_status_entry if router_status_entry else ERROR_MSG)
+
+  if server_descriptor:
+    description += '\n\nServer Descriptor:\n\n%s' % server_descriptor
+
+  if microdescriptor:
+    description += '\n\nMicrodescriptor:\n\n%s' % microdescriptor
+
+  return description.split('\n')
+
+
+def _preferred_size(text, max_width, show_line_numbers):
+  """
+  Provides the preferred dimensions of our dialog.
+
+  :param list text: lines of text to be shown
+  :param int max_width: maximum width the dialog can be
+  :param bool show_line_numbers: if we should leave room for line numbers
+
+  :returns: **tuple** of the preferred (height, width)
+  """
+
+  width, height = 0, len(text) + 2
+  line_number_width = int(math.log10(len(text))) + 2 if show_line_numbers else 0
+  max_content_width = max_width - line_number_width - 4
+
+  for line in text:
+    width = min(max_width, max(width, len(line) + line_number_width + 4))
+    height += len(line) / max_content_width  # extra lines due to text wrap
+
+  return (height, width)
+
+
+def _draw(popup, title, lines, entry_color, scroll, show_line_numbers):
+  def draw_msg(popup, min_x, x, y, width, msg, *attr):
+    while msg:
+      draw_msg, msg = str_tools.crop(msg, width - x, None, ending = None, get_remainder = True)
+
+      if not draw_msg:
+        draw_msg, msg = str_tools.crop(msg, width - x), ''  # first word is longer than the line
+
+      x = popup.addstr(y, x, draw_msg, *attr)
+
+      if msg:
+        x, y = min_x, y + 1
+
+    return x, y
+
+  popup.win.erase()
+
+  line_number_width = int(math.log10(len(lines))) + 1
+  in_block = False   # flag indicating if we're currently in crypto content
+  width = popup.max_x - 2  # leave space on the right for the border and an empty line
+  height = popup.max_y - 2  # height of the dialog without the top and bottom border
+  offset = line_number_width + 3 if show_line_numbers else 2
+
+  y = 1
+
+  for i, line in enumerate(lines):
+    keyword, value = line, ''
+    color = entry_color
+
+    if line in HEADERS:
+      color = HEADER_COLOR
+    elif line.startswith(BLOCK_START):
+      in_block = True
+    elif line.startswith(BLOCK_END):
+      in_block = False
+    elif in_block:
+      keyword, value = '', line
+    elif ' ' in line and line != UNRESOLVED_MSG and line != ERROR_MSG:
+      keyword, value = line.split(' ', 1)
+
+    if i < scroll:
+      continue
+
+    if show_line_numbers:
+      popup.addstr(y, 2, str(i + 1).rjust(line_number_width), curses.A_BOLD, LINE_NUMBER_COLOR)
+
+    x, y = draw_msg(popup, offset, offset, y, width, keyword, color, curses.A_BOLD)
+    x, y = draw_msg(popup, offset, x + 1, y, width, value, color)
+
+    y += 1
+
+    if y > height:
+      break
+
+  popup.win.box()
+  popup.addstr(0, 0, title, curses.A_STANDOUT)
+  popup.win.refresh()
diff --git a/setup.py b/setup.py
index f5399ef..f5b1c05 100644
--- a/setup.py
+++ b/setup.py
@@ -99,7 +99,7 @@ setup(
   author = nyx.__author__,
   author_email = nyx.__contact__,
   url = nyx.__url__,
-  packages = ['nyx', 'nyx.connections', 'nyx.menu', 'nyx.util'],
+  packages = ['nyx', 'nyx.menu', 'nyx.util'],
   keywords = 'tor onion controller',
   install_requires = ['stem>=1.4.1'],
   package_data = {'nyx': ['config/*', 'resources/*']},





More information about the tor-commits mailing list