[tor-commits] [nyx/master] Move conn_panel to be a top level module

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


commit 7a9c8b78b80e504a3b83fb92588b1bb2d36e3f39
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Aug 31 10:04:38 2015 -0700

    Move conn_panel to be a top level module
    
    Plan is to rewrite all of nyx/connections/* and hopefully shrink it to a point
    where it can all be in one module. We did this for the graphing module and it
    worked well - fingers crossed!
---
 nyx/connection_panel.py       |  583 +++++++++++++++++++++++++++++++++++++++++
 nyx/connections/__init__.py   |    1 -
 nyx/connections/circ_entry.py |   11 +-
 nyx/connections/conn_entry.py |   17 +-
 nyx/connections/conn_panel.py |  583 -----------------------------------------
 nyx/connections/entries.py    |    6 +-
 nyx/controller.py             |    4 +-
 7 files changed, 603 insertions(+), 602 deletions(-)

diff --git a/nyx/connection_panel.py b/nyx/connection_panel.py
new file mode 100644
index 0000000..f2d613e
--- /dev/null
+++ b/nyx/connection_panel.py
@@ -0,0 +1,583 @@
+"""
+Listing of the currently established connections tor has made.
+"""
+
+import re
+import time
+import collections
+import curses
+import itertools
+import threading
+
+import nyx.popups
+import nyx.util.tracker
+
+from nyx.connections import descriptor_popup, entries
+from nyx.util import panel, tor_controller, ui_tools
+
+from stem.control import State
+from stem.util import conf, connection, enum
+
+# height of the detail panel content, not counting top and bottom border
+
+DETAILS_HEIGHT = 7
+
+# listing types
+
+Listing = enum.Enum(('IP_ADDRESS', 'IP Address'), 'FINGERPRINT', 'NICKNAME')
+
+EXIT_USAGE_WIDTH = 15
+UPDATE_RATE = 5  # rate in seconds at which we refresh
+
+# Connection Categories:
+#   Inbound      Relay connection, coming to us.
+#   Outbound     Relay connection, leaving us.
+#   Exit         Outbound relay connection leaving the Tor network.
+#   Hidden       Connections to a hidden service we're providing.
+#   Socks        Socks connections for applications using Tor.
+#   Circuit      Circuits our tor client has created.
+#   Directory    Fetching tor consensus information.
+#   Control      Tor controller (nyx, vidalia, etc).
+
+Category = enum.Enum('INBOUND', 'OUTBOUND', 'EXIT', 'HIDDEN', 'SOCKS', 'CIRCUIT', 'DIRECTORY', 'CONTROL')
+
+CATEGORY_COLOR = {
+  Category.INBOUND: 'green',
+  Category.OUTBOUND: 'blue',
+  Category.EXIT: 'red',
+  Category.HIDDEN: 'magenta',
+  Category.SOCKS: 'yellow',
+  Category.CIRCUIT: 'cyan',
+  Category.DIRECTORY: 'magenta',
+  Category.CONTROL: 'red',
+}
+
+SortAttr = enum.Enum('CATEGORY', 'UPTIME', 'LISTING', 'IP_ADDRESS', 'PORT', 'FINGERPRINT', 'NICKNAME', 'COUNTRY')
+
+SORT_COLORS = {
+  SortAttr.CATEGORY: 'red',
+  SortAttr.UPTIME: 'yellow',
+  SortAttr.LISTING: 'green',
+  SortAttr.IP_ADDRESS: 'blue',
+  SortAttr.PORT: 'blue',
+  SortAttr.FINGERPRINT: 'cyan',
+  SortAttr.NICKNAME: 'cyan',
+  SortAttr.COUNTRY: 'blue',
+}
+
+
+def conf_handler(key, value):
+  if key == 'features.connection.listing_type':
+    return conf.parse_enum(key, value, Listing)
+  elif key == 'features.connection.order':
+    return conf.parse_enum_csv(key, value[0], SortAttr, 3)
+
+
+CONFIG = conf.config_dict('nyx', {
+  'features.connection.resolveApps': True,
+  'features.connection.listing_type': Listing.IP_ADDRESS,
+  'features.connection.order': [
+    SortAttr.CATEGORY,
+    SortAttr.LISTING,
+    SortAttr.UPTIME],
+  'features.connection.showIps': True,
+}, conf_handler)
+
+
+class ConnectionPanel(panel.Panel, threading.Thread):
+  """
+  Listing of connections tor is making, with information correlated against
+  the current consensus and other data sources.
+  """
+
+  def __init__(self, stdscr):
+    panel.Panel.__init__(self, stdscr, 'connections', 0)
+    threading.Thread.__init__(self)
+    self.setDaemon(True)
+
+    # defaults our listing selection to fingerprints if ip address
+    # displaying is disabled
+    #
+    # TODO: This is a little sucky in that it won't work if showIps changes
+    # while we're running (... but nyx doesn't allow for that atm)
+
+    if not CONFIG['features.connection.showIps'] and CONFIG['features.connection.listing_type'] == 0:
+      nyx_config = conf.get_config('nyx')
+      nyx_config.set('features.connection.listing_type', Listing.keys()[Listing.index_of(Listing.FINGERPRINT)])
+
+    self._scroller = ui_tools.Scroller(True)
+    self._entries = []            # last fetched display entries
+    self._entry_lines = []        # individual lines rendered from the entries listing
+    self._show_details = False    # presents the details panel if true
+
+    self._last_update = -1        # time the content was last revised
+    self._is_tor_running = True   # indicates if tor is currently running or not
+    self._halt_time = None        # time when tor was stopped
+    self._vals_lock = threading.RLock()
+
+    self._pause_condition = threading.Condition()
+    self._halt = False  # terminates thread if true
+
+    # Tracks exiting port and client country statistics
+
+    self._client_locale_usage = {}
+    self._exit_port_usage = {}
+
+    # If we're a bridge and been running over a day then prepopulates with the
+    # last day's clients.
+
+    controller = tor_controller()
+    bridge_clients = controller.get_info('status/clients-seen', None)
+
+    if bridge_clients:
+      # Response has a couple arguments...
+      # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8
+
+      country_summary = None
+
+      for line in bridge_clients.split():
+        if line.startswith('CountrySummary='):
+          country_summary = line[15:]
+          break
+
+      if country_summary:
+        for entry in country_summary.split(','):
+          if re.match('^..=[0-9]+$', entry):
+            locale, count = entry.split('=', 1)
+            self._client_locale_usage[locale] = int(count)
+
+    # Last sampling received from the ConnectionResolver, used to detect when
+    # it changes.
+
+    self._last_resource_fetch = -1
+
+    # 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):
+        entry.get_lines()[0].is_initial_connection = True
+
+    # listens for when tor stops so we know to stop reflecting changes
+
+    controller.add_status_listener(self.tor_state_listener)
+
+  def tor_state_listener(self, controller, event_type, _):
+    """
+    Freezes the connection contents when Tor stops.
+    """
+
+    self._is_tor_running = event_type in (State.INIT, State.RESET)
+    self._halt_time = None if self._is_tor_running else time.time()
+    self.redraw(True)
+
+  def get_pause_time(self):
+    """
+    Provides the time Tor stopped if it isn't running. Otherwise this is the
+    time we were last paused.
+    """
+
+    return self._halt_time if self._halt_time else panel.Panel.get_pause_time(self)
+
+  def set_sort_order(self, ordering = None):
+    """
+    Sets the connection attributes we're sorting by and resorts the contents.
+
+    Arguments:
+      ordering - new ordering, if undefined then this resorts with the last
+                 set ordering
+    """
+
+    with self._vals_lock:
+      if ordering:
+        nyx_config = conf.get_config('nyx')
+
+        ordering_keys = [SortAttr.keys()[SortAttr.index_of(v)] for v in ordering]
+        nyx_config.set('features.connection.order', ', '.join(ordering_keys))
+
+      def sort_value(entry, attr):
+        if attr == SortAttr.LISTING:
+          if self.get_listing_type() == Listing.IP_ADDRESS:
+            attr = SortAttr.IP_ADDRESS
+          elif self.get_listing_type() == Listing.FINGERPRINT:
+            attr = SortAttr.FINGERPRINT
+          elif self.get_listing_type() == Listing.NICKNAME:
+            attr = SortAttr.NICKNAME
+
+        connection_line = entry.get_lines()[0]
+
+        if attr == SortAttr.IP_ADDRESS:
+          if entry.is_private():
+            return 255 ** 4  # orders at the end
+
+          ip_value = 0
+
+          for octet in connection_line.connection.remote_address.split('.'):
+            ip_value = ip_value * 255 + int(octet)
+
+          return ip_value * 65536 + connection_line.connection.remote_port
+        elif attr == SortAttr.PORT:
+          return connection_line.connection.remote_port
+        elif attr == SortAttr.FINGERPRINT:
+          return connection_line.get_fingerprint('UNKNOWN')
+        elif attr == SortAttr.NICKNAME:
+          return connection_line.get_nickname('z' * 20)
+        elif attr == SortAttr.CATEGORY:
+          return Category.index_of(entry.get_type())
+        elif attr == SortAttr.UPTIME:
+          return connection_line.connection.start_time
+        elif attr == SortAttr.COUNTRY:
+          return '' if entry.is_private() else connection_line.get_locale('')
+        else:
+          return ''
+
+      self._entries.sort(key = lambda i: [sort_value(i, attr) for attr in CONFIG['features.connection.order']])
+      self._entry_lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in self._entries]))
+
+  def get_listing_type(self):
+    """
+    Provides the priority content we list connections by.
+    """
+
+    return CONFIG['features.connection.listing_type']
+
+  def set_listing_type(self, listing_type):
+    """
+    Sets the priority information presented by the panel.
+
+    Arguments:
+      listing_type - Listing instance for the primary information to be shown
+    """
+
+    if self.get_listing_type() == listing_type:
+      return
+
+    with self._vals_lock:
+      nyx_config = conf.get_config('nyx')
+      nyx_config.set('features.connection.listing_type', Listing.keys()[Listing.index_of(listing_type)])
+
+      # if we're sorting by the listing then we need to resort
+
+      if SortAttr.LISTING in CONFIG['features.connection.order']:
+        self.set_sort_order()
+
+  def show_sort_dialog(self):
+    """
+    Provides the sort dialog for our connections.
+    """
+
+    # set ordering for connection options
+
+    title_label = 'Connection Ordering:'
+    options = list(SortAttr)
+    old_selection = CONFIG['features.connection.order']
+    option_colors = dict([(attr, SORT_COLORS[attr]) for attr in options])
+    results = nyx.popups.show_sort_dialog(title_label, options, old_selection, option_colors)
+
+    if results:
+      self.set_sort_order(results)
+
+  def handle_key(self, key):
+    with self._vals_lock:
+      user_traffic_allowed = tor_controller().is_user_traffic_allowed()
+
+      if key.is_scroll():
+        page_height = self.get_preferred_size()[0] - 1
+
+        if self._show_details:
+          page_height -= (DETAILS_HEIGHT + 1)
+
+        is_changed = self._scroller.handle_key(key, self._entry_lines, page_height)
+
+        if is_changed:
+          self.redraw(True)
+      elif key.is_selection():
+        self._show_details = not self._show_details
+        self.redraw(True)
+      elif key.match('s'):
+        self.show_sort_dialog()
+      elif key.match('u'):
+        # provides a menu to pick the connection resolver
+
+        title = 'Resolver Util:'
+        options = ['auto'] + list(connection.Resolver)
+        conn_resolver = nyx.util.tracker.get_connection_tracker()
+
+        current_overwrite = conn_resolver.get_custom_resolver()
+
+        if current_overwrite is None:
+          old_selection = 0
+        else:
+          old_selection = options.index(current_overwrite)
+
+        selection = nyx.popups.show_menu(title, options, old_selection)
+
+        # applies new setting
+
+        if selection != -1:
+          selected_option = options[selection] if selection != 0 else None
+          conn_resolver.set_custom_resolver(selected_option)
+      elif key.match('l'):
+        # provides a menu to pick the primary information we list connections by
+
+        title = 'List By:'
+        options = list(Listing)
+
+        old_selection = options.index(self.get_listing_type())
+        selection = nyx.popups.show_menu(title, options, old_selection)
+
+        # applies new setting
+
+        if selection != -1:
+          self.set_listing_type(options[selection])
+      elif key.match('d'):
+        self.set_title_visible(False)
+        self.redraw(True)
+
+        while True:
+          selection = self.get_selection()
+
+          if not selection:
+            break
+
+          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)
+
+          if not key or key.is_selection() or key.match('d'):
+            break  # closes popup
+          elif key.match('left'):
+            self.handle_key(panel.KeyInput(curses.KEY_UP))
+          elif key.match('right'):
+            self.handle_key(panel.KeyInput(curses.KEY_DOWN))
+
+        self.set_title_visible(True)
+        self.redraw(True)
+      elif key.match('c') and user_traffic_allowed.inbound:
+        nyx.popups.show_count_dialog('Client Locales', self._client_locale_usage)
+      elif key.match('e') and user_traffic_allowed.outbound:
+        counts = {}
+        key_width = max(map(len, self._exit_port_usage.keys()))
+
+        for k, v in self._exit_port_usage.items():
+          usage = connection.port_usage(k)
+
+          if usage:
+            k = k.ljust(key_width + 3) + usage.ljust(EXIT_USAGE_WIDTH)
+
+          counts[k] = v
+
+        nyx.popups.show_count_dialog('Exiting Port Usage', counts)
+      else:
+        return False
+
+      return True
+
+  def run(self):
+    """
+    Keeps connections listing updated, checking for new entries at a set rate.
+    """
+
+    last_ran = -1
+
+    while not self._halt:
+      if self.is_paused() or not self._is_tor_running or (time.time() - last_ran) < UPDATE_RATE:
+        with self._pause_condition:
+          if not self._halt:
+            self._pause_condition.wait(0.2)
+
+        continue  # done waiting, try again
+
+      self._update()
+      self.redraw(True)
+
+      # If this is our first run then fill in our fingerprint tracker. This
+      # requires fetching all the router status entries which takes a few
+      # seconds, so best done when we're finished with the rest of the first
+      # iteration to hide the lag.
+
+      if last_ran == -1:
+        nyx.util.tracker.get_consensus_tracker().update(tor_controller().get_network_statuses([]))
+
+      last_ran = time.time()
+
+  def get_help(self):
+    resolver_util = nyx.util.tracker.get_connection_tracker().get_custom_resolver()
+    user_traffic_allowed = tor_controller().is_user_traffic_allowed()
+
+    options = [
+      ('up arrow', 'scroll up a line', None),
+      ('down arrow', 'scroll down a line', None),
+      ('page up', 'scroll up a page', None),
+      ('page down', 'scroll down a page', None),
+      ('enter', 'show connection details', None),
+      ('d', 'raw consensus descriptor', None),
+      ('l', 'listed identity', self.get_listing_type().lower()),
+      ('s', 'sort ordering', None),
+      ('u', 'resolving utility', 'auto' if resolver_util is None else resolver_util),
+    ]
+
+    if user_traffic_allowed.inbound:
+      options.append(('c', 'client locale usage summary', None))
+
+    if user_traffic_allowed.outbound:
+      options.append(('e', 'exit port usage summary', None))
+
+    return options
+
+  def get_selection(self):
+    """
+    Provides the currently selected connection entry.
+    """
+
+    return self._scroller.get_cursor_selection(self._entry_lines)
+
+  def draw(self, width, height):
+    with self._vals_lock:
+      # if we don't have any contents then refuse to show details
+
+      if not self._entries:
+        self._show_details = False
+
+      # extra line when showing the detail panel is for the bottom border
+
+      detail_panel_offset = DETAILS_HEIGHT + 1 if self._show_details else 0
+      is_scrollbar_visible = len(self._entry_lines) > height - detail_panel_offset - 1
+
+      scroll_location = self._scroller.get_scroll_location(self._entry_lines, height - detail_panel_offset - 1)
+      cursor_selection = self.get_selection()
+
+      # draws the detail panel if currently displaying it
+
+      if self._show_details and cursor_selection:
+        # This is a solid border unless the scrollbar is visible, in which case a
+        # 'T' pipe connects the border to the bar.
+
+        ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
+
+        if is_scrollbar_visible:
+          self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
+
+        draw_entries = cursor_selection.get_details(width)
+
+        for i in range(min(len(draw_entries), DETAILS_HEIGHT)):
+          self.addstr(1 + i, 2, draw_entries[i][0], *draw_entries[i][1])
+
+      # title label with connection counts
+
+      if self.is_title_visible():
+        self._draw_title(self._entries)
+
+      scroll_offset = 0
+
+      if is_scrollbar_visible:
+        scroll_offset = 2
+        self.add_scroll_bar(scroll_location, scroll_location + height - detail_panel_offset - 1, len(self._entry_lines), 1 + detail_panel_offset)
+
+      if self.is_paused() or not self._is_tor_running:
+        current_time = self.get_pause_time()
+      else:
+        current_time = time.time()
+
+      for line_number in range(scroll_location, len(self._entry_lines)):
+        entry_line = self._entry_lines[line_number]
+
+        # hilighting if this is the selected line
+
+        extra_format = curses.A_STANDOUT if entry_line == cursor_selection else curses.A_NORMAL
+
+        draw_line = line_number + detail_panel_offset + 1 - scroll_location
+
+        prefix = entry_line.get_listing_prefix()
+
+        for i in range(len(prefix)):
+          self.addch(draw_line, scroll_offset + i, prefix[i])
+
+        x_offset = scroll_offset + len(prefix)
+        draw_entry = entry_line.get_listing_entry(width - scroll_offset - len(prefix), current_time, self.get_listing_type())
+
+        for msg, attr in draw_entry:
+          attr |= extra_format
+          self.addstr(draw_line, x_offset, msg, attr)
+          x_offset += len(msg)
+
+        if draw_line >= height:
+          break
+
+  def _draw_title(self, entries):
+    if self._show_details:
+      title = 'Connection Details:'
+    elif not entries:
+      title = 'Connections:'
+    else:
+      counts = collections.Counter([entry.get_type() for entry in entries])
+      count_labels = ['%i %s' % (counts[category], category.lower()) for category in Category if counts[category]]
+      title = 'Connections (%s):' % ', '.join(count_labels)
+
+    self.addstr(0, 0, title, curses.A_STANDOUT)
+
+  def stop(self):
+    """
+    Halts further resolutions and terminates the thread.
+    """
+
+    with self._pause_condition:
+      self._halt = True
+      self._pause_condition.notifyAll()
+
+  def _update(self):
+    """
+    Fetches the newest resolved connections.
+    """
+
+    conn_resolver = nyx.util.tracker.get_connection_tracker()
+    current_resolution_count = conn_resolver.run_counter()
+
+    if not conn_resolver.is_alive():
+      return  # if we're not fetching connections then this is a no-op
+    elif current_resolution_count == self._last_resource_fetch:
+      return  # no new connections to process
+
+    new_entries = [entries.Entry.from_connection(conn) for conn in conn_resolver.get_value()]
+
+    for circ in tor_controller().get_circuits([]):
+      # Skips established single-hop circuits (these are for directory
+      # fetches, not client circuits)
+
+      if not (circ.status == 'BUILT' and len(circ.path) == 1):
+        new_entries.append(entries.Entry.from_circuit(circ))
+
+    with self._vals_lock:
+      # update stats for client and exit connections
+
+      for entry in new_entries:
+        entry_line = entry.get_lines()[0]
+
+        if entry.is_private() and entry.get_type() == Category.INBOUND:
+          client_locale = entry_line.get_locale(None)
+
+          if client_locale:
+            self._client_locale_usage[client_locale] = self._client_locale_usage.get(client_locale, 0) + 1
+        elif entry.get_type() == Category.EXIT:
+          exit_port = entry_line.connection.remote_port
+          self._exit_port_usage[exit_port] = self._exit_port_usage.get(exit_port, 0) + 1
+
+      self._entries, self._entry_lines = new_entries, list(itertools.chain.from_iterable([entry.get_lines() for entry in new_entries]))
+
+      self.set_sort_order()
+      self._last_resource_fetch = current_resolution_count
+
+    if CONFIG['features.connection.resolveApps']:
+      local_ports, remote_ports = [], []
+
+      for entry in new_entries:
+        line = entry.get_lines()[0]
+
+        if entry.get_type() in (Category.SOCKS, Category.CONTROL):
+          local_ports.append(line.connection.remote_port)
+        elif entry.get_type() == Category.HIDDEN:
+          remote_ports.append(line.connection.local_port)
+
+      nyx.util.tracker.get_port_usage_tracker().query(local_ports, remote_ports)
diff --git a/nyx/connections/__init__.py b/nyx/connections/__init__.py
index c7e90a9..577823a 100644
--- a/nyx/connections/__init__.py
+++ b/nyx/connections/__init__.py
@@ -5,7 +5,6 @@ Resources related to our connection panel.
 __all__ = [
   'circ_entry',
   'conn_entry',
-  'conn_panel',
   'descriptor_popup',
   'entries',
 ]
diff --git a/nyx/connections/circ_entry.py b/nyx/connections/circ_entry.py
index cd42fc8..d2b1ef6 100644
--- a/nyx/connections/circ_entry.py
+++ b/nyx/connections/circ_entry.py
@@ -13,8 +13,9 @@ import datetime
 
 import nyx.util.tracker
 import nyx.util.ui_tools
+import nyx.connection_panel
 
-from nyx.connections import conn_entry, conn_panel, entries
+from nyx.connections import conn_entry, entries
 
 from stem.util import str_tools
 
@@ -69,7 +70,7 @@ class CircHeaderLine(conn_entry.ConnectionLine):
 
   def get_details(self, width):
     if not self.is_built:
-      detail_format = (curses.A_BOLD, conn_panel.CATEGORY_COLOR[self._entry.get_type()])
+      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)
@@ -127,7 +128,7 @@ class CircLine(conn_entry.ConnectionLine):
     return entries.ConnectionPanelLine.get_listing_entry(self, width, current_time, listing_type)
 
   def _get_listing_entry(self, width, current_time, listing_type):
-    line_format = nyx.util.ui_tools.get_color(conn_panel.CATEGORY_COLOR[self._entry.get_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)
@@ -139,7 +140,7 @@ class CircLine(conn_entry.ConnectionLine):
 
     dst, etc = '', ''
 
-    if listing_type == conn_panel.Listing.IP_ADDRESS:
+    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
 
@@ -150,7 +151,7 @@ class CircLine(conn_entry.ConnectionLine):
       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 == conn_panel.Listing.FINGERPRINT:
+    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
 
diff --git a/nyx/connections/conn_entry.py b/nyx/connections/conn_entry.py
index fa85bc6..1f26f4c 100644
--- a/nyx/connections/conn_entry.py
+++ b/nyx/connections/conn_entry.py
@@ -5,12 +5,13 @@ Connection panel entries related to actual connections to or from the system
 
 import curses
 
+import nyx.connection_panel
 import nyx.util.tracker
 import nyx.util.ui_tools
 
 from nyx.util import tor_controller
-from nyx.connections import conn_panel, entries
-from nyx.connections.conn_panel import Category
+from nyx.connections import entries
+from nyx.connection_panel import Category
 
 from stem.util import conf, connection, str_tools
 
@@ -129,7 +130,7 @@ class ConnectionLine(entries.ConnectionPanelLine):
     # category - "<type>"
     # postType - ")   "
 
-    line_format = nyx.util.ui_tools.get_color(conn_panel.CATEGORY_COLOR[entry_type])
+    line_format = nyx.util.ui_tools.get_color(nyx.connection_panel.CATEGORY_COLOR[entry_type])
     time_width = 6 if CONFIG['features.connection.markInitialConnections'] else 5
 
     draw_entry = [(' ', line_format),
@@ -150,7 +151,7 @@ class ConnectionLine(entries.ConnectionPanelLine):
       width - available space to display in
     """
 
-    detail_format = (curses.A_BOLD, conn_panel.CATEGORY_COLOR[self._entry.get_type()])
+    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):
@@ -185,7 +186,7 @@ class ConnectionLine(entries.ConnectionPanelLine):
     destination_address = self.get_destination_label(26, include_locale = True)
     etc, used_space = '', 0
 
-    if listing_type == conn_panel.Listing.IP_ADDRESS:
+    if listing_type == nyx.connection_panel.Listing.IP_ADDRESS:
       if width > used_space + 42 and CONFIG['features.connection.showColumn.fingerprint']:
         # show fingerprint (column width: 42 characters)
 
@@ -199,7 +200,7 @@ class ConnectionLine(entries.ConnectionPanelLine):
         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 == conn_panel.Listing.FINGERPRINT:
+    elif listing_type == nyx.connection_panel.Listing.FINGERPRINT:
       if width > used_space + 17:
         # show nickname (column width: min 17 characters, consumes any remaining space)
 
@@ -258,7 +259,7 @@ class ConnectionLine(entries.ConnectionPanelLine):
 
     src, dst, etc = '', '', ''
 
-    if listing_type == conn_panel.Listing.IP_ADDRESS:
+    if listing_type == nyx.connection_panel.Listing.IP_ADDRESS:
       my_external_address = controller.get_info('address', self.connection.local_address)
       address_differ = my_external_address != self.connection.local_address
 
@@ -318,7 +319,7 @@ class ConnectionLine(entries.ConnectionPanelLine):
 
       etc = self.get_etc_content(width - used_space, listing_type)
       used_space += len(etc)
-    elif listing_type == conn_panel.Listing.FINGERPRINT:
+    elif listing_type == nyx.connection_panel.Listing.FINGERPRINT:
       src = 'localhost'
 
       if my_type == Category.CONTROL:
diff --git a/nyx/connections/conn_panel.py b/nyx/connections/conn_panel.py
deleted file mode 100644
index f2d613e..0000000
--- a/nyx/connections/conn_panel.py
+++ /dev/null
@@ -1,583 +0,0 @@
-"""
-Listing of the currently established connections tor has made.
-"""
-
-import re
-import time
-import collections
-import curses
-import itertools
-import threading
-
-import nyx.popups
-import nyx.util.tracker
-
-from nyx.connections import descriptor_popup, entries
-from nyx.util import panel, tor_controller, ui_tools
-
-from stem.control import State
-from stem.util import conf, connection, enum
-
-# height of the detail panel content, not counting top and bottom border
-
-DETAILS_HEIGHT = 7
-
-# listing types
-
-Listing = enum.Enum(('IP_ADDRESS', 'IP Address'), 'FINGERPRINT', 'NICKNAME')
-
-EXIT_USAGE_WIDTH = 15
-UPDATE_RATE = 5  # rate in seconds at which we refresh
-
-# Connection Categories:
-#   Inbound      Relay connection, coming to us.
-#   Outbound     Relay connection, leaving us.
-#   Exit         Outbound relay connection leaving the Tor network.
-#   Hidden       Connections to a hidden service we're providing.
-#   Socks        Socks connections for applications using Tor.
-#   Circuit      Circuits our tor client has created.
-#   Directory    Fetching tor consensus information.
-#   Control      Tor controller (nyx, vidalia, etc).
-
-Category = enum.Enum('INBOUND', 'OUTBOUND', 'EXIT', 'HIDDEN', 'SOCKS', 'CIRCUIT', 'DIRECTORY', 'CONTROL')
-
-CATEGORY_COLOR = {
-  Category.INBOUND: 'green',
-  Category.OUTBOUND: 'blue',
-  Category.EXIT: 'red',
-  Category.HIDDEN: 'magenta',
-  Category.SOCKS: 'yellow',
-  Category.CIRCUIT: 'cyan',
-  Category.DIRECTORY: 'magenta',
-  Category.CONTROL: 'red',
-}
-
-SortAttr = enum.Enum('CATEGORY', 'UPTIME', 'LISTING', 'IP_ADDRESS', 'PORT', 'FINGERPRINT', 'NICKNAME', 'COUNTRY')
-
-SORT_COLORS = {
-  SortAttr.CATEGORY: 'red',
-  SortAttr.UPTIME: 'yellow',
-  SortAttr.LISTING: 'green',
-  SortAttr.IP_ADDRESS: 'blue',
-  SortAttr.PORT: 'blue',
-  SortAttr.FINGERPRINT: 'cyan',
-  SortAttr.NICKNAME: 'cyan',
-  SortAttr.COUNTRY: 'blue',
-}
-
-
-def conf_handler(key, value):
-  if key == 'features.connection.listing_type':
-    return conf.parse_enum(key, value, Listing)
-  elif key == 'features.connection.order':
-    return conf.parse_enum_csv(key, value[0], SortAttr, 3)
-
-
-CONFIG = conf.config_dict('nyx', {
-  'features.connection.resolveApps': True,
-  'features.connection.listing_type': Listing.IP_ADDRESS,
-  'features.connection.order': [
-    SortAttr.CATEGORY,
-    SortAttr.LISTING,
-    SortAttr.UPTIME],
-  'features.connection.showIps': True,
-}, conf_handler)
-
-
-class ConnectionPanel(panel.Panel, threading.Thread):
-  """
-  Listing of connections tor is making, with information correlated against
-  the current consensus and other data sources.
-  """
-
-  def __init__(self, stdscr):
-    panel.Panel.__init__(self, stdscr, 'connections', 0)
-    threading.Thread.__init__(self)
-    self.setDaemon(True)
-
-    # defaults our listing selection to fingerprints if ip address
-    # displaying is disabled
-    #
-    # TODO: This is a little sucky in that it won't work if showIps changes
-    # while we're running (... but nyx doesn't allow for that atm)
-
-    if not CONFIG['features.connection.showIps'] and CONFIG['features.connection.listing_type'] == 0:
-      nyx_config = conf.get_config('nyx')
-      nyx_config.set('features.connection.listing_type', Listing.keys()[Listing.index_of(Listing.FINGERPRINT)])
-
-    self._scroller = ui_tools.Scroller(True)
-    self._entries = []            # last fetched display entries
-    self._entry_lines = []        # individual lines rendered from the entries listing
-    self._show_details = False    # presents the details panel if true
-
-    self._last_update = -1        # time the content was last revised
-    self._is_tor_running = True   # indicates if tor is currently running or not
-    self._halt_time = None        # time when tor was stopped
-    self._vals_lock = threading.RLock()
-
-    self._pause_condition = threading.Condition()
-    self._halt = False  # terminates thread if true
-
-    # Tracks exiting port and client country statistics
-
-    self._client_locale_usage = {}
-    self._exit_port_usage = {}
-
-    # If we're a bridge and been running over a day then prepopulates with the
-    # last day's clients.
-
-    controller = tor_controller()
-    bridge_clients = controller.get_info('status/clients-seen', None)
-
-    if bridge_clients:
-      # Response has a couple arguments...
-      # TimeStarted="2011-08-17 15:50:49" CountrySummary=us=16,de=8,uk=8
-
-      country_summary = None
-
-      for line in bridge_clients.split():
-        if line.startswith('CountrySummary='):
-          country_summary = line[15:]
-          break
-
-      if country_summary:
-        for entry in country_summary.split(','):
-          if re.match('^..=[0-9]+$', entry):
-            locale, count = entry.split('=', 1)
-            self._client_locale_usage[locale] = int(count)
-
-    # Last sampling received from the ConnectionResolver, used to detect when
-    # it changes.
-
-    self._last_resource_fetch = -1
-
-    # 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):
-        entry.get_lines()[0].is_initial_connection = True
-
-    # listens for when tor stops so we know to stop reflecting changes
-
-    controller.add_status_listener(self.tor_state_listener)
-
-  def tor_state_listener(self, controller, event_type, _):
-    """
-    Freezes the connection contents when Tor stops.
-    """
-
-    self._is_tor_running = event_type in (State.INIT, State.RESET)
-    self._halt_time = None if self._is_tor_running else time.time()
-    self.redraw(True)
-
-  def get_pause_time(self):
-    """
-    Provides the time Tor stopped if it isn't running. Otherwise this is the
-    time we were last paused.
-    """
-
-    return self._halt_time if self._halt_time else panel.Panel.get_pause_time(self)
-
-  def set_sort_order(self, ordering = None):
-    """
-    Sets the connection attributes we're sorting by and resorts the contents.
-
-    Arguments:
-      ordering - new ordering, if undefined then this resorts with the last
-                 set ordering
-    """
-
-    with self._vals_lock:
-      if ordering:
-        nyx_config = conf.get_config('nyx')
-
-        ordering_keys = [SortAttr.keys()[SortAttr.index_of(v)] for v in ordering]
-        nyx_config.set('features.connection.order', ', '.join(ordering_keys))
-
-      def sort_value(entry, attr):
-        if attr == SortAttr.LISTING:
-          if self.get_listing_type() == Listing.IP_ADDRESS:
-            attr = SortAttr.IP_ADDRESS
-          elif self.get_listing_type() == Listing.FINGERPRINT:
-            attr = SortAttr.FINGERPRINT
-          elif self.get_listing_type() == Listing.NICKNAME:
-            attr = SortAttr.NICKNAME
-
-        connection_line = entry.get_lines()[0]
-
-        if attr == SortAttr.IP_ADDRESS:
-          if entry.is_private():
-            return 255 ** 4  # orders at the end
-
-          ip_value = 0
-
-          for octet in connection_line.connection.remote_address.split('.'):
-            ip_value = ip_value * 255 + int(octet)
-
-          return ip_value * 65536 + connection_line.connection.remote_port
-        elif attr == SortAttr.PORT:
-          return connection_line.connection.remote_port
-        elif attr == SortAttr.FINGERPRINT:
-          return connection_line.get_fingerprint('UNKNOWN')
-        elif attr == SortAttr.NICKNAME:
-          return connection_line.get_nickname('z' * 20)
-        elif attr == SortAttr.CATEGORY:
-          return Category.index_of(entry.get_type())
-        elif attr == SortAttr.UPTIME:
-          return connection_line.connection.start_time
-        elif attr == SortAttr.COUNTRY:
-          return '' if entry.is_private() else connection_line.get_locale('')
-        else:
-          return ''
-
-      self._entries.sort(key = lambda i: [sort_value(i, attr) for attr in CONFIG['features.connection.order']])
-      self._entry_lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in self._entries]))
-
-  def get_listing_type(self):
-    """
-    Provides the priority content we list connections by.
-    """
-
-    return CONFIG['features.connection.listing_type']
-
-  def set_listing_type(self, listing_type):
-    """
-    Sets the priority information presented by the panel.
-
-    Arguments:
-      listing_type - Listing instance for the primary information to be shown
-    """
-
-    if self.get_listing_type() == listing_type:
-      return
-
-    with self._vals_lock:
-      nyx_config = conf.get_config('nyx')
-      nyx_config.set('features.connection.listing_type', Listing.keys()[Listing.index_of(listing_type)])
-
-      # if we're sorting by the listing then we need to resort
-
-      if SortAttr.LISTING in CONFIG['features.connection.order']:
-        self.set_sort_order()
-
-  def show_sort_dialog(self):
-    """
-    Provides the sort dialog for our connections.
-    """
-
-    # set ordering for connection options
-
-    title_label = 'Connection Ordering:'
-    options = list(SortAttr)
-    old_selection = CONFIG['features.connection.order']
-    option_colors = dict([(attr, SORT_COLORS[attr]) for attr in options])
-    results = nyx.popups.show_sort_dialog(title_label, options, old_selection, option_colors)
-
-    if results:
-      self.set_sort_order(results)
-
-  def handle_key(self, key):
-    with self._vals_lock:
-      user_traffic_allowed = tor_controller().is_user_traffic_allowed()
-
-      if key.is_scroll():
-        page_height = self.get_preferred_size()[0] - 1
-
-        if self._show_details:
-          page_height -= (DETAILS_HEIGHT + 1)
-
-        is_changed = self._scroller.handle_key(key, self._entry_lines, page_height)
-
-        if is_changed:
-          self.redraw(True)
-      elif key.is_selection():
-        self._show_details = not self._show_details
-        self.redraw(True)
-      elif key.match('s'):
-        self.show_sort_dialog()
-      elif key.match('u'):
-        # provides a menu to pick the connection resolver
-
-        title = 'Resolver Util:'
-        options = ['auto'] + list(connection.Resolver)
-        conn_resolver = nyx.util.tracker.get_connection_tracker()
-
-        current_overwrite = conn_resolver.get_custom_resolver()
-
-        if current_overwrite is None:
-          old_selection = 0
-        else:
-          old_selection = options.index(current_overwrite)
-
-        selection = nyx.popups.show_menu(title, options, old_selection)
-
-        # applies new setting
-
-        if selection != -1:
-          selected_option = options[selection] if selection != 0 else None
-          conn_resolver.set_custom_resolver(selected_option)
-      elif key.match('l'):
-        # provides a menu to pick the primary information we list connections by
-
-        title = 'List By:'
-        options = list(Listing)
-
-        old_selection = options.index(self.get_listing_type())
-        selection = nyx.popups.show_menu(title, options, old_selection)
-
-        # applies new setting
-
-        if selection != -1:
-          self.set_listing_type(options[selection])
-      elif key.match('d'):
-        self.set_title_visible(False)
-        self.redraw(True)
-
-        while True:
-          selection = self.get_selection()
-
-          if not selection:
-            break
-
-          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)
-
-          if not key or key.is_selection() or key.match('d'):
-            break  # closes popup
-          elif key.match('left'):
-            self.handle_key(panel.KeyInput(curses.KEY_UP))
-          elif key.match('right'):
-            self.handle_key(panel.KeyInput(curses.KEY_DOWN))
-
-        self.set_title_visible(True)
-        self.redraw(True)
-      elif key.match('c') and user_traffic_allowed.inbound:
-        nyx.popups.show_count_dialog('Client Locales', self._client_locale_usage)
-      elif key.match('e') and user_traffic_allowed.outbound:
-        counts = {}
-        key_width = max(map(len, self._exit_port_usage.keys()))
-
-        for k, v in self._exit_port_usage.items():
-          usage = connection.port_usage(k)
-
-          if usage:
-            k = k.ljust(key_width + 3) + usage.ljust(EXIT_USAGE_WIDTH)
-
-          counts[k] = v
-
-        nyx.popups.show_count_dialog('Exiting Port Usage', counts)
-      else:
-        return False
-
-      return True
-
-  def run(self):
-    """
-    Keeps connections listing updated, checking for new entries at a set rate.
-    """
-
-    last_ran = -1
-
-    while not self._halt:
-      if self.is_paused() or not self._is_tor_running or (time.time() - last_ran) < UPDATE_RATE:
-        with self._pause_condition:
-          if not self._halt:
-            self._pause_condition.wait(0.2)
-
-        continue  # done waiting, try again
-
-      self._update()
-      self.redraw(True)
-
-      # If this is our first run then fill in our fingerprint tracker. This
-      # requires fetching all the router status entries which takes a few
-      # seconds, so best done when we're finished with the rest of the first
-      # iteration to hide the lag.
-
-      if last_ran == -1:
-        nyx.util.tracker.get_consensus_tracker().update(tor_controller().get_network_statuses([]))
-
-      last_ran = time.time()
-
-  def get_help(self):
-    resolver_util = nyx.util.tracker.get_connection_tracker().get_custom_resolver()
-    user_traffic_allowed = tor_controller().is_user_traffic_allowed()
-
-    options = [
-      ('up arrow', 'scroll up a line', None),
-      ('down arrow', 'scroll down a line', None),
-      ('page up', 'scroll up a page', None),
-      ('page down', 'scroll down a page', None),
-      ('enter', 'show connection details', None),
-      ('d', 'raw consensus descriptor', None),
-      ('l', 'listed identity', self.get_listing_type().lower()),
-      ('s', 'sort ordering', None),
-      ('u', 'resolving utility', 'auto' if resolver_util is None else resolver_util),
-    ]
-
-    if user_traffic_allowed.inbound:
-      options.append(('c', 'client locale usage summary', None))
-
-    if user_traffic_allowed.outbound:
-      options.append(('e', 'exit port usage summary', None))
-
-    return options
-
-  def get_selection(self):
-    """
-    Provides the currently selected connection entry.
-    """
-
-    return self._scroller.get_cursor_selection(self._entry_lines)
-
-  def draw(self, width, height):
-    with self._vals_lock:
-      # if we don't have any contents then refuse to show details
-
-      if not self._entries:
-        self._show_details = False
-
-      # extra line when showing the detail panel is for the bottom border
-
-      detail_panel_offset = DETAILS_HEIGHT + 1 if self._show_details else 0
-      is_scrollbar_visible = len(self._entry_lines) > height - detail_panel_offset - 1
-
-      scroll_location = self._scroller.get_scroll_location(self._entry_lines, height - detail_panel_offset - 1)
-      cursor_selection = self.get_selection()
-
-      # draws the detail panel if currently displaying it
-
-      if self._show_details and cursor_selection:
-        # This is a solid border unless the scrollbar is visible, in which case a
-        # 'T' pipe connects the border to the bar.
-
-        ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
-
-        if is_scrollbar_visible:
-          self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
-
-        draw_entries = cursor_selection.get_details(width)
-
-        for i in range(min(len(draw_entries), DETAILS_HEIGHT)):
-          self.addstr(1 + i, 2, draw_entries[i][0], *draw_entries[i][1])
-
-      # title label with connection counts
-
-      if self.is_title_visible():
-        self._draw_title(self._entries)
-
-      scroll_offset = 0
-
-      if is_scrollbar_visible:
-        scroll_offset = 2
-        self.add_scroll_bar(scroll_location, scroll_location + height - detail_panel_offset - 1, len(self._entry_lines), 1 + detail_panel_offset)
-
-      if self.is_paused() or not self._is_tor_running:
-        current_time = self.get_pause_time()
-      else:
-        current_time = time.time()
-
-      for line_number in range(scroll_location, len(self._entry_lines)):
-        entry_line = self._entry_lines[line_number]
-
-        # hilighting if this is the selected line
-
-        extra_format = curses.A_STANDOUT if entry_line == cursor_selection else curses.A_NORMAL
-
-        draw_line = line_number + detail_panel_offset + 1 - scroll_location
-
-        prefix = entry_line.get_listing_prefix()
-
-        for i in range(len(prefix)):
-          self.addch(draw_line, scroll_offset + i, prefix[i])
-
-        x_offset = scroll_offset + len(prefix)
-        draw_entry = entry_line.get_listing_entry(width - scroll_offset - len(prefix), current_time, self.get_listing_type())
-
-        for msg, attr in draw_entry:
-          attr |= extra_format
-          self.addstr(draw_line, x_offset, msg, attr)
-          x_offset += len(msg)
-
-        if draw_line >= height:
-          break
-
-  def _draw_title(self, entries):
-    if self._show_details:
-      title = 'Connection Details:'
-    elif not entries:
-      title = 'Connections:'
-    else:
-      counts = collections.Counter([entry.get_type() for entry in entries])
-      count_labels = ['%i %s' % (counts[category], category.lower()) for category in Category if counts[category]]
-      title = 'Connections (%s):' % ', '.join(count_labels)
-
-    self.addstr(0, 0, title, curses.A_STANDOUT)
-
-  def stop(self):
-    """
-    Halts further resolutions and terminates the thread.
-    """
-
-    with self._pause_condition:
-      self._halt = True
-      self._pause_condition.notifyAll()
-
-  def _update(self):
-    """
-    Fetches the newest resolved connections.
-    """
-
-    conn_resolver = nyx.util.tracker.get_connection_tracker()
-    current_resolution_count = conn_resolver.run_counter()
-
-    if not conn_resolver.is_alive():
-      return  # if we're not fetching connections then this is a no-op
-    elif current_resolution_count == self._last_resource_fetch:
-      return  # no new connections to process
-
-    new_entries = [entries.Entry.from_connection(conn) for conn in conn_resolver.get_value()]
-
-    for circ in tor_controller().get_circuits([]):
-      # Skips established single-hop circuits (these are for directory
-      # fetches, not client circuits)
-
-      if not (circ.status == 'BUILT' and len(circ.path) == 1):
-        new_entries.append(entries.Entry.from_circuit(circ))
-
-    with self._vals_lock:
-      # update stats for client and exit connections
-
-      for entry in new_entries:
-        entry_line = entry.get_lines()[0]
-
-        if entry.is_private() and entry.get_type() == Category.INBOUND:
-          client_locale = entry_line.get_locale(None)
-
-          if client_locale:
-            self._client_locale_usage[client_locale] = self._client_locale_usage.get(client_locale, 0) + 1
-        elif entry.get_type() == Category.EXIT:
-          exit_port = entry_line.connection.remote_port
-          self._exit_port_usage[exit_port] = self._exit_port_usage.get(exit_port, 0) + 1
-
-      self._entries, self._entry_lines = new_entries, list(itertools.chain.from_iterable([entry.get_lines() for entry in new_entries]))
-
-      self.set_sort_order()
-      self._last_resource_fetch = current_resolution_count
-
-    if CONFIG['features.connection.resolveApps']:
-      local_ports, remote_ports = [], []
-
-      for entry in new_entries:
-        line = entry.get_lines()[0]
-
-        if entry.get_type() in (Category.SOCKS, Category.CONTROL):
-          local_ports.append(line.connection.remote_port)
-        elif entry.get_type() == Category.HIDDEN:
-          remote_ports.append(line.connection.local_port)
-
-      nyx.util.tracker.get_port_usage_tracker().query(local_ports, remote_ports)
diff --git a/nyx/connections/entries.py b/nyx/connections/entries.py
index f4cf17e..a372576 100644
--- a/nyx/connections/entries.py
+++ b/nyx/connections/entries.py
@@ -74,7 +74,7 @@ class ConnectionEntry(Entry):
 
   @lru_cache()
   def get_type(self):
-    from nyx.connections.conn_panel import Category
+    from nyx.connection_panel import Category
     controller = tor_controller()
 
     if self._connection.local_port in controller.get_ports(Listener.OR, []):
@@ -111,7 +111,7 @@ class ConnectionEntry(Entry):
 
   @lru_cache()
   def is_private(self):
-    from nyx.connections.conn_panel import Category
+    from nyx.connection_panel import Category
 
     if not CONFIG['features.connection.showIps']:
       return True
@@ -140,7 +140,7 @@ class CircuitEntry(Entry):
     return [CircHeaderLine(self, self._circuit)] + [CircLine(self, self._circuit, fp) for fp, _ in self._circuit.path]
 
   def get_type(self):
-    from nyx.connections.conn_panel import Category
+    from nyx.connection_panel import Category
     return Category.CIRCUIT
 
   def is_private(self):
diff --git a/nyx/controller.py b/nyx/controller.py
index 35b1e89..d0fb7d0 100644
--- a/nyx/controller.py
+++ b/nyx/controller.py
@@ -16,7 +16,7 @@ import nyx.log_panel
 import nyx.config_panel
 import nyx.torrc_panel
 import nyx.graph_panel
-import nyx.connections.conn_panel
+import nyx.connection_panel
 import nyx.util.tracker
 
 import stem
@@ -113,7 +113,7 @@ def init_controller(stdscr, start_time):
 
   # second page: connections
   if CONFIG['features.panels.show.connection']:
-    page_panels.append([nyx.connections.conn_panel.ConnectionPanel(stdscr)])
+    page_panels.append([nyx.connection_panel.ConnectionPanel(stdscr)])
 
     # The DisableDebuggerAttachment will prevent our connection panel from really
     # functioning. It'll have circuits, but little else. If this is the case then





More information about the tor-commits mailing list