[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [nyx/master] Merge ui_tools.py into curses.py
commit aa58b0d3e2b032e85c1295028ee5d3865bd74508
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date: Sat Mar 12 16:32:15 2016 -0800
Merge ui_tools.py into curses.py
Merging our old util in, revising the functions (especially the Scroller) along
the way. The draw_box() method better fits with the panel for now.
---
nyx/controller.py | 5 +-
nyx/curses.py | 214 ++++++++++++++++++++++++++++++++++++++
nyx/panel/config.py | 46 ++++-----
nyx/panel/connection.py | 36 +++----
nyx/panel/log.py | 17 ++-
nyx/panel/torrc.py | 29 ++++--
nyx/popups.py | 14 ++-
nyx/starter.py | 4 +-
nyx/util/__init__.py | 1 -
nyx/util/panel.py | 29 ++++++
nyx/util/ui_tools.py | 268 ------------------------------------------------
11 files changed, 320 insertions(+), 343 deletions(-)
diff --git a/nyx/controller.py b/nyx/controller.py
index 1478779..f188bb1 100644
--- a/nyx/controller.py
+++ b/nyx/controller.py
@@ -9,6 +9,7 @@ import time
import curses
import threading
+import nyx.curses
import nyx.menu.menu
import nyx.popups
import nyx.util.tracker
@@ -25,7 +26,7 @@ import stem
from stem.util import conf, log
from nyx.curses import NORMAL, BOLD, HIGHLIGHT
-from nyx.util import panel, tor_controller, ui_tools
+from nyx.util import panel, tor_controller
NYX_CONTROLLER = None
@@ -389,7 +390,7 @@ def start_nyx(stdscr):
control = get_controller()
if not CONFIG['features.acsSupport']:
- ui_tools.disable_acs()
+ nyx.curses.disable_acs()
# provides notice about any unused config keys
diff --git a/nyx/curses.py b/nyx/curses.py
index 1280fc8..af3484b 100644
--- a/nyx/curses.py
+++ b/nyx/curses.py
@@ -14,6 +14,17 @@ if we want Windows support in the future too.
get_color_override - provides color we override requests with
set_color_override - sets color we override requests with
+ disable_acs - renders replacements for ACS characters
+ is_wide_characters_supported - checks if curses supports wide character
+
+ Scroller - scrolls content with keyboard navigation
+ |- location - present scroll location
+ +- handle_key - moves scroll based on user input
+
+ CursorScroller - scrolls content with a cursor for selecting items
+ |- selection - present selection and scroll location
+ +- handle_key - moves cursor based on user input
+
.. data:: Color (enum)
Terminal colors.
@@ -51,6 +62,7 @@ import curses
import stem.util.conf
import stem.util.enum
+import stem.util.system
from nyx.util import msg, log
@@ -189,3 +201,205 @@ def _color_attr():
COLOR_ATTR = DEFAULT_COLOR_ATTR
return COLOR_ATTR
+
+
+def disable_acs():
+ """
+ Replaces ACS characters used for showing borders. This can be preferable if
+ curses is `unable to render them
+ <https://www.atagar.com/arm/images/acs_display_failure.png>`_.
+ """
+
+ for item in curses.__dict__:
+ if item.startswith('ACS_'):
+ curses.__dict__[item] = ord('+')
+
+ # replace common border pipe cahracters
+
+ curses.ACS_SBSB = ord('|')
+ curses.ACS_VLINE = ord('|')
+ curses.ACS_BSBS = ord('-')
+ curses.ACS_HLINE = ord('-')
+
+
+def is_wide_characters_supported():
+ """
+ Checks if our version of curses has wide character support. This is required
+ to print unicode.
+
+ :returns: **bool** that's **True** if curses supports wide characters, and
+ **False** if it either can't or this can't be determined
+ """
+
+ try:
+ # Gets the dynamic library used by the interpretor for curses. This uses
+ # 'ldd' on Linux or 'otool -L' on OSX.
+ #
+ # atagar@fenrir:~/Desktop$ ldd /usr/lib/python2.6/lib-dynload/_curses.so
+ # linux-gate.so.1 => (0x00a51000)
+ # libncursesw.so.5 => /lib/libncursesw.so.5 (0x00faa000)
+ # libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0x002f1000)
+ # libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00158000)
+ # libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0x00398000)
+ # /lib/ld-linux.so.2 (0x00ca8000)
+ #
+ # atagar$ otool -L /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so
+ # /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so:
+ # /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
+ # /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
+ # /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.6)
+
+ import _curses
+
+ if stem.util.system.is_available('ldd'):
+ return 'libncursesw' in '\n'.join(lib_dependency_lines = stem.util.system.call('ldd %s' % _curses.__file__))
+ elif stem.util.system.is_available('otool'):
+ return 'libncursesw' in '\n'.join(lib_dependency_lines = stem.util.system.call('otool -L %s' % _curses.__file__))
+ except:
+ pass
+
+ return False
+
+
+class Scroller(object):
+ """
+ Simple scroller that provides keyboard navigation of content.
+ """
+
+ def __init__(self):
+ self._location = 0
+
+ def location(self, content_height = None, page_height = None):
+ """
+ Provides the position we've scrolled to.
+
+ If a **content_height** and **page_height** are provided this ensures our
+ scroll position falls within a valid range. This should be done when the
+ content changes or panel resized.
+
+ :param int content_height: height of the content being renered
+ :param int page_height: height visible on the page
+
+ :returns: **int** position we've scrolled to
+ """
+
+ if content_height is not None and page_height is not None:
+ self._location = max(0, min(self._location, content_height - page_height))
+
+ return self._location
+
+ def handle_key(self, key, content_height, page_height):
+ """
+ Moves scrolling location according to the given input...
+
+ * up / down - scrolls one position up or down
+ * page up / page down - scrolls by the page_height
+ * home / end - moves to the top or bottom
+
+ :param nyx.util.panel.KeyInput key: pressed key
+ :param int content_height: height of the content being renered
+ :param int page_height: height visible on the page
+
+ :returns: **bool** that's **True** if the scrolling position changed and
+ **False** otherwise
+ """
+
+ new_location = _scroll_position(self._location, key, content_height, page_height, False)
+
+ if new_location != self._location:
+ self._location = new_location
+ return True
+ else:
+ return False
+
+
+class CursorScroller(object):
+ """
+ Scroller that tracks a cursor's position.
+ """
+
+ def __init__(self):
+ self._location = 0
+
+ # We track the cursor location by the item we have selected, so it stays
+ # selected as the content changes. We also keep track of its last location
+ # so we can fall back to that if it disappears.
+
+ self._cursor_location = 0
+ self._cursor_selection = None
+
+ def selection(self, content, page_height = None):
+ """
+ Provides the item from the content that's presently selected. If provided
+ the height of our page this provides the scroll position as well...
+
+ ::
+
+ selected, scroll = my_scroller.selection(content, page_height)
+
+ :param list content: content the scroller is tracking
+ :param int page_height: height visible on the page
+
+ :returns: **tuple** of the form **(cursor, scroll)**, the cursor is
+ **None** if content is empty
+ """
+
+ content = list(content) # shallow copy for thread safety
+
+ if not content:
+ self._cursor_location = 0
+ self._cursor_selection = None
+ return None if page_height is None else None, 0
+
+ if self._cursor_selection in content:
+ # moves cursor location to track the selection
+ self._cursor_location = content.index(self._cursor_selection)
+ else:
+ # select the next closest entry
+ self._cursor_location = max(0, min(self._cursor_location, len(content) - 1))
+ self._cursor_selection = content[self._cursor_location]
+
+ # ensure our cursor is visible
+
+ if page_height:
+ if self._cursor_location < self._location:
+ self._location = self._cursor_location
+ elif self._cursor_location > self._location + page_height - 1:
+ self._location = self._cursor_location - page_height + 1
+
+ if page_height is None:
+ return self._cursor_selection
+ else:
+ return self._cursor_selection, self._location
+
+ def handle_key(self, key, content, page_height):
+ self.selection(content, page_height) # reset cursor position
+ new_location = _scroll_position(self._cursor_location, key, len(content), page_height, True)
+
+ if new_location != self._cursor_location:
+ self._cursor_location = new_location
+ self._cursor_selection = content[new_location]
+
+ return True
+ else:
+ return False
+
+
+def _scroll_position(location, key, content_height, page_height, is_cursor):
+ if key.match('up'):
+ shift = -1
+ elif key.match('down'):
+ shift = 1
+ elif key.match('page_up'):
+ shift = -page_height + 1 if is_cursor else -page_height
+ elif key.match('page_down'):
+ shift = page_height - 1 if is_cursor else page_height
+ elif key.match('home'):
+ shift = -content_height
+ elif key.match('end'):
+ shift = content_height
+ else:
+ return location
+
+ max_position = content_height - 1 if is_cursor else content_height - page_height
+ return max(0, min(location + shift, max_position))
diff --git a/nyx/panel/config.py b/nyx/panel/config.py
index fd529ca..2c09a35 100644
--- a/nyx/panel/config.py
+++ b/nyx/panel/config.py
@@ -7,13 +7,14 @@ import curses
import os
import nyx.controller
+import nyx.curses
import nyx.popups
import stem.control
import stem.manual
from nyx.curses import GREEN, CYAN, WHITE, NORMAL, BOLD, HIGHLIGHT
-from nyx.util import DATA_DIR, panel, tor_controller, ui_tools
+from nyx.util import DATA_DIR, panel, tor_controller
from stem.util import conf, enum, log, str_tools
@@ -120,7 +121,7 @@ class ConfigPanel(panel.Panel):
panel.Panel.__init__(self, stdscr, 'configuration', 0)
self._contents = []
- self._scroller = ui_tools.Scroller(True)
+ self._scroller = nyx.curses.CursorScroller()
self._sort_order = CONFIG['features.config.order']
self._show_all = False # show all options, or just the important ones
@@ -237,23 +238,23 @@ class ConfigPanel(panel.Panel):
if is_changed:
self.redraw(True)
elif key.is_selection():
- selection = self._scroller.get_cursor_selection(self._get_config_options())
- initial_value = selection.value() if selection.is_set() else ''
- new_value = nyx.popups.input_prompt('%s Value (esc to cancel): ' % selection.name, initial_value)
+ selected = self._scroller.selection(self._get_config_options())
+ initial_value = selected.value() if selected.is_set() else ''
+ new_value = nyx.popups.input_prompt('%s Value (esc to cancel): ' % selected.name, initial_value)
if new_value != initial_value:
try:
- if selection.value_type == 'Boolean':
+ if selected.value_type == 'Boolean':
# if the value's a boolean then allow for 'true' and 'false' inputs
if new_value.lower() == 'true':
new_value = '1'
elif new_value.lower() == 'false':
new_value = '0'
- elif selection.value_type == 'LineList':
+ elif selected.value_type == 'LineList':
new_value = new_value.split(',') # set_conf accepts list inputs
- tor_controller().set_conf(selection.name, new_value)
+ tor_controller().set_conf(selected.name, new_value)
self.redraw(True)
except Exception as exc:
nyx.popups.show_msg('%s (press any key)' % exc)
@@ -283,12 +284,11 @@ class ConfigPanel(panel.Panel):
def draw(self, width, height):
contents = self._get_config_options()
- selection = self._scroller.get_cursor_selection(contents)
- scroll_location = self._scroller.get_scroll_location(contents, height - DETAILS_HEIGHT)
+ selected, scroll = self._scroller.selection(contents, height - DETAILS_HEIGHT)
is_scrollbar_visible = len(contents) > height - DETAILS_HEIGHT
- if selection is not None:
- self._draw_selection_details(selection, width)
+ if selected is not None:
+ self._draw_selection_details(selected, width)
if self.is_title_visible():
hidden_msg = "press 'a' to hide most options" if self._show_all else "press 'a' to show all options"
@@ -298,9 +298,9 @@ class ConfigPanel(panel.Panel):
if is_scrollbar_visible:
scroll_offset = 3
- self.add_scroll_bar(scroll_location, scroll_location + height - DETAILS_HEIGHT, len(contents), DETAILS_HEIGHT)
+ self.add_scroll_bar(scroll, scroll + height - DETAILS_HEIGHT, len(contents), DETAILS_HEIGHT)
- if selection is not None:
+ if selected is not None:
self.addch(DETAILS_HEIGHT - 1, 1, curses.ACS_TTEE)
# Description column can grow up to eighty characters. After that any extra
@@ -314,10 +314,10 @@ class ConfigPanel(panel.Panel):
else:
value_width = VALUE_WIDTH
- for i, entry in enumerate(contents[scroll_location:]):
+ for i, entry in enumerate(contents[scroll:]):
attr = [CONFIG['attr.config.category_color'].get(entry.manual.category, WHITE)]
attr.append(BOLD if entry.is_set() else NORMAL)
- attr.append(HIGHLIGHT if entry == selection else NORMAL)
+ attr.append(HIGHLIGHT if entry == selected else NORMAL)
option_label = str_tools.crop(entry.name, NAME_WIDTH).ljust(NAME_WIDTH + 1)
value_label = str_tools.crop(entry.value(), value_width).ljust(value_width + 1)
@@ -331,18 +331,18 @@ class ConfigPanel(panel.Panel):
def _get_config_options(self):
return self._contents if self._show_all else filter(lambda entry: stem.manual.is_important(entry.name) or entry.is_set(), self._contents)
- def _draw_selection_details(self, selection, width):
+ def _draw_selection_details(self, selected, width):
"""
Shows details of the currently selected option.
"""
- description = 'Description: %s' % (selection.manual.description)
- attr = ', '.join(('custom' if selection.is_set() else 'default', selection.value_type, 'usage: %s' % selection.manual.usage))
- selected_color = CONFIG['attr.config.category_color'].get(selection.manual.category, WHITE)
- ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT)
+ description = 'Description: %s' % (selected.manual.description)
+ attr = ', '.join(('custom' if selected.is_set() else 'default', selected.value_type, 'usage: %s' % selected.manual.usage))
+ selected_color = CONFIG['attr.config.category_color'].get(selected.manual.category, WHITE)
+ self.draw_box(0, 0, width, DETAILS_HEIGHT)
- self.addstr(1, 2, '%s (%s Option)' % (selection.name, selection.manual.category), selected_color, BOLD)
- self.addstr(2, 2, 'Value: %s (%s)' % (selection.value(), str_tools.crop(attr, width - len(selection.value()) - 13)), selected_color, BOLD)
+ self.addstr(1, 2, '%s (%s Option)' % (selected.name, selected.manual.category), selected_color, BOLD)
+ self.addstr(2, 2, 'Value: %s (%s)' % (selected.value(), str_tools.crop(attr, width - len(selected.value()) - 13)), selected_color, BOLD)
for i in range(DETAILS_HEIGHT - 4):
if not description:
diff --git a/nyx/panel/connection.py b/nyx/panel/connection.py
index 2c8fce9..7e01674 100644
--- a/nyx/panel/connection.py
+++ b/nyx/panel/connection.py
@@ -9,12 +9,12 @@ import curses
import itertools
import threading
+import nyx.curses
import nyx.popups
import nyx.util.tracker
-import nyx.util.ui_tools
from nyx.curses import WHITE, NORMAL, BOLD, HIGHLIGHT
-from nyx.util import panel, tor_controller, ui_tools
+from nyx.util import panel, tor_controller
from stem.control import Listener
from stem.util import datetime_to_unix, conf, connection, enum, str_tools
@@ -263,7 +263,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
threading.Thread.__init__(self)
self.setDaemon(True)
- self._scroller = ui_tools.Scroller(True)
+ self._scroller = nyx.curses.CursorScroller()
self._entries = [] # last fetched display entries
self._show_details = False # presents the details panel if true
self._sort_order = CONFIG['features.connection.order']
@@ -338,10 +338,10 @@ class ConnectionPanel(panel.Panel, threading.Thread):
resolver = connection_tracker.get_custom_resolver()
selected_index = 0 if resolver is None else options.index(resolver)
- selection = nyx.popups.show_menu('Connection Resolver:', options, selected_index)
+ selected = nyx.popups.show_menu('Connection Resolver:', options, selected_index)
- if selection != -1:
- connection_tracker.set_custom_resolver(None if selection == 0 else options[selection])
+ if selected != -1:
+ connection_tracker.set_custom_resolver(None if selected == 0 else options[selected])
elif key.match('d'):
self.set_title_visible(False)
self.redraw(True)
@@ -349,16 +349,16 @@ class ConnectionPanel(panel.Panel, threading.Thread):
while True:
lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in entries]))
- selection = self._scroller.get_cursor_selection(lines)
+ selected = self._scroller.selection(lines)
- if not selection:
+ if not selected:
break
def is_close_key(key):
return key.is_selection() or key.match('d') or key.match('left') or key.match('right')
- color = CONFIG['attr.connection.category_color'].get(selection.entry.get_type(), WHITE)
- key = nyx.popups.show_descriptor_popup(selection.fingerprint, color, self.max_x, is_close_key)
+ color = CONFIG['attr.connection.category_color'].get(selected.entry.get_type(), WHITE)
+ key = nyx.popups.show_descriptor_popup(selected.fingerprint, color, self.max_x, is_close_key)
if not key or key.is_selection() or key.match('d'):
break # closes popup
@@ -445,7 +445,9 @@ class ConnectionPanel(panel.Panel, threading.Thread):
entries = self._entries
lines = list(itertools.chain.from_iterable([entry.get_lines() for entry in entries]))
- selected = self._scroller.get_cursor_selection(lines)
+ is_showing_details = self._show_details and lines
+ details_offset = DETAILS_HEIGHT + 1 if is_showing_details else 0
+ selected, scroll = self._scroller.selection(lines, height - details_offset - 1)
if self.is_paused():
current_time = self.get_pause_time()
@@ -454,12 +456,8 @@ class ConnectionPanel(panel.Panel, threading.Thread):
else:
current_time = time.time()
- is_showing_details = self._show_details and selected
- details_offset = DETAILS_HEIGHT + 1 if is_showing_details else 0
-
is_scrollbar_visible = len(lines) > height - details_offset - 1
scroll_offset = 2 if is_scrollbar_visible else 0
- scroll_location = self._scroller.get_scroll_location(lines, height - details_offset - 1)
if self.is_title_visible():
self._draw_title(entries, self._show_details)
@@ -468,10 +466,10 @@ class ConnectionPanel(panel.Panel, threading.Thread):
self._draw_details(selected, width, is_scrollbar_visible)
if is_scrollbar_visible:
- self.add_scroll_bar(scroll_location, scroll_location + height - details_offset - 1, len(lines), 1 + details_offset)
+ self.add_scroll_bar(scroll, scroll + height - details_offset - 1, len(lines), 1 + details_offset)
- for line_number in range(scroll_location, len(lines)):
- y = line_number + details_offset + 1 - scroll_location
+ for line_number in range(scroll, len(lines)):
+ y = line_number + details_offset + 1 - scroll
self._draw_line(scroll_offset, y, lines[line_number], lines[line_number] == selected, width - scroll_offset, current_time)
if y >= height:
@@ -547,7 +545,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
# draw the border, with a 'T' pipe if connecting with the scrollbar
- ui_tools.draw_box(self, 0, 0, width, DETAILS_HEIGHT + 2)
+ self.draw_box(0, 0, width, DETAILS_HEIGHT + 2)
if is_scrollbar_visible:
self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
diff --git a/nyx/panel/log.py b/nyx/panel/log.py
index e674ac1..ba22d3c 100644
--- a/nyx/panel/log.py
+++ b/nyx/panel/log.py
@@ -11,11 +11,12 @@ import threading
import stem.response.events
import nyx.arguments
+import nyx.curses
import nyx.popups
import nyx.util.log
from nyx.curses import GREEN, YELLOW, WHITE, NORMAL, BOLD, HIGHLIGHT
-from nyx.util import join, panel, tor_controller, ui_tools
+from nyx.util import join, panel, tor_controller
from stem.util import conf, log
@@ -76,6 +77,7 @@ class LogPanel(panel.Panel, threading.Thread):
self.set_pause_attr('_event_log')
+ self._scroller = nyx.curses.Scroller()
self._halt = False # terminates thread if true
self._pause_condition = threading.Condition()
self._has_new_event = False
@@ -96,7 +98,6 @@ class LogPanel(panel.Panel, threading.Thread):
log.info(str(exc))
self._last_content_height = len(self._event_log) # height of the rendered content when last drawn
- self._scroll = 0
# merge NYX_LOGGER into us, and listen for its future events
@@ -223,10 +224,9 @@ class LogPanel(panel.Panel, threading.Thread):
def handle_key(self, key):
if key.is_scroll():
page_height = self.get_preferred_size()[0] - 1
- new_scroll = ui_tools.get_scroll_position(key, self._scroll, page_height, self._last_content_height)
+ is_changed = self._scroller.handle_key(key, self._last_content_height, page_height)
- if self._scroll != new_scroll:
- self._scroll = new_scroll
+ if is_changed:
self.redraw(True)
elif key.match('u'):
self.set_duplicate_visability(not self._show_duplicates)
@@ -271,12 +271,11 @@ class LogPanel(panel.Panel, threading.Thread):
]
def draw(self, width, height):
- self._scroll = max(0, min(self._scroll, self._last_content_height - height + 1))
+ scroll = self._scroller.location(self._last_content_height, height)
event_log = list(self.get_attr('_event_log'))
event_filter = self._filter.clone()
event_types = list(self._event_types)
- scroll = int(self._scroll)
last_content_height = self._last_content_height
show_duplicates = self._show_duplicates
@@ -309,7 +308,7 @@ class LogPanel(panel.Panel, threading.Thread):
for entry in day_to_entries[day]:
y = self._draw_entry(x, y, width, entry, show_duplicates)
- ui_tools.draw_box(self, original_y, x - 1, width - x + 1, y - original_y + 1, YELLOW, BOLD)
+ self.draw_box(original_y, x - 1, width - x + 1, y - original_y + 1, YELLOW, BOLD)
time_label = time.strftime(' %B %d, %Y ', time.localtime(day_to_entries[day][0].timestamp))
self.addstr(original_y, x + 1, time_label, YELLOW, BOLD)
@@ -330,7 +329,7 @@ class LogPanel(panel.Panel, threading.Thread):
if content_height_delta >= CONTENT_HEIGHT_REDRAW_THRESHOLD:
force_redraw_reason = 'estimate was off by %i' % content_height_delta
- elif new_content_height > height and self._scroll + height - 1 > new_content_height:
+ elif new_content_height > height and scroll + height - 1 > new_content_height:
force_redraw_reason = 'scrolled off the bottom of the page'
elif not is_scrollbar_visible and new_content_height > height - 1:
force_redraw_reason = "scroll bar wasn't previously visible"
diff --git a/nyx/panel/torrc.py b/nyx/panel/torrc.py
index ae26c83..bf10efb 100644
--- a/nyx/panel/torrc.py
+++ b/nyx/panel/torrc.py
@@ -3,9 +3,12 @@ Panel displaying the torrc or nyxrc with the validation done against it.
"""
import math
+import string
+
+import nyx.curses
from nyx.curses import RED, GREEN, YELLOW, CYAN, WHITE, BOLD, HIGHLIGHT
-from nyx.util import expand_path, msg, panel, tor_controller, ui_tools
+from nyx.util import expand_path, msg, panel, tor_controller
from stem import ControllerError
from stem.control import State
@@ -20,7 +23,7 @@ class TorrcPanel(panel.Panel):
def __init__(self, stdscr):
panel.Panel.__init__(self, stdscr, 'torrc', 0)
- self._scroll = 0
+ self._scroller = nyx.curses.Scroller()
self._show_line_numbers = True # shows left aligned line numbers
self._show_comments = True # shows comments and extra whitespace
self._last_content_height = 0
@@ -41,9 +44,14 @@ class TorrcPanel(panel.Panel):
if event_type == State.RESET:
try:
self._torrc_location = expand_path(controller.get_info('config-file'))
+ contents = []
with open(self._torrc_location) as torrc_file:
- self._torrc_content = [ui_tools.get_printable(line.replace('\t', ' ')).rstrip() for line in torrc_file.readlines()]
+ for line in torrc_file.readlines():
+ line = line.replace('\t', ' ').replace('\xc2', "'").rstrip()
+ contents.append(filter(lambda char: char in string.printable, line))
+
+ self._torrc_content = contents
except ControllerError as exc:
self._torrc_load_error = msg('panel.torrc.unable_to_find_torrc', error = exc)
self._torrc_location = None
@@ -75,10 +83,9 @@ class TorrcPanel(panel.Panel):
def handle_key(self, key):
if key.is_scroll():
page_height = self.get_preferred_size()[0] - 1
- new_scroll = ui_tools.get_scroll_position(key, self._scroll, page_height, self._last_content_height)
+ is_changed = self._scroller.handle_key(key, self._last_content_height, page_height)
- if self._scroll != new_scroll:
- self._scroll = new_scroll
+ if is_changed:
self.redraw(True)
elif key.match('l'):
self.set_line_number_visible(not self._show_line_numbers)
@@ -101,12 +108,12 @@ class TorrcPanel(panel.Panel):
]
def draw(self, width, height):
+ scroll = self._scroller.location(self._last_content_height, height)
+
if self._torrc_content is None:
self.addstr(1, 0, self._torrc_load_error, RED, BOLD)
new_content_height = 1
else:
- self._scroll = max(0, min(self._scroll, self._last_content_height - height + 1))
-
if not self._show_line_numbers:
line_number_offset = 0
elif len(self._torrc_content) == 0:
@@ -118,9 +125,9 @@ class TorrcPanel(panel.Panel):
if self._last_content_height > height - 1:
scroll_offset = 3
- self.add_scroll_bar(self._scroll, self._scroll + height - 1, self._last_content_height, 1)
+ self.add_scroll_bar(scroll, scroll + height - 1, self._last_content_height, 1)
- y = 1 - self._scroll
+ y = 1 - scroll
is_multiline = False # true if we're in the middle of a multiline torrc entry
for line_number, line in enumerate(self._torrc_content):
@@ -159,7 +166,7 @@ class TorrcPanel(panel.Panel):
y += 1
- new_content_height = y + self._scroll - 1
+ new_content_height = y + scroll - 1
if self.is_title_visible():
self.addstr(0, 0, ' ' * width) # clear line
diff --git a/nyx/popups.py b/nyx/popups.py
index 1a711c7..c05db05 100644
--- a/nyx/popups.py
+++ b/nyx/popups.py
@@ -9,10 +9,11 @@ import curses
import operator
import nyx.controller
+import nyx.curses
from nyx import __version__, __release_date__
from nyx.curses import RED, GREEN, YELLOW, CYAN, WHITE, NORMAL, BOLD, HIGHLIGHT
-from nyx.util import tor_controller, panel, ui_tools
+from nyx.util import tor_controller, panel
NO_STATS_MSG = "Usage stats aren't available yet, press any key..."
@@ -463,20 +464,17 @@ def show_descriptor_popup(fingerprint, color, max_width, is_close_key):
if not popup:
return None
- scroll, redraw = 0, True
+ scroller, redraw = nyx.curses.Scroller(), True
while True:
if redraw:
- _draw(popup, title, lines, color, scroll, show_line_numbers)
+ _draw(popup, title, lines, color, scroller.location(), 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
+ redraw = scroller.handle_key(key, len(lines), height - 2)
elif is_close_key(key):
return key
@@ -560,7 +558,7 @@ def _draw(popup, title, lines, entry_color, scroll, show_line_numbers):
if show_line_numbers:
popup.addstr(y, 2, str(i + 1).rjust(line_number_width), LINE_NUMBER_COLOR, BOLD)
- x, y = popup.addstr_wrap(y, width, keyword, width, offset, color, BOLD)
+ x, y = popup.addstr_wrap(y, 3 + line_number_width, keyword, width, offset, color, BOLD)
x, y = popup.addstr_wrap(y, x + 1, value, width, offset, color)
y += 1
diff --git a/nyx/starter.py b/nyx/starter.py
index e46d494..83825e0 100644
--- a/nyx/starter.py
+++ b/nyx/starter.py
@@ -17,9 +17,9 @@ import time
import nyx
import nyx.arguments
import nyx.controller
+import nyx.curses
import nyx.util.panel
import nyx.util.tracker
-import nyx.util.ui_tools
import stem
import stem.util.log
@@ -237,7 +237,7 @@ def _use_unicode(config):
is_lang_unicode = 'utf-' in os.getenv('LANG', '').lower()
- if is_lang_unicode and nyx.util.ui_tools.is_wide_characters_supported():
+ if is_lang_unicode and nyx.curses.is_wide_characters_supported():
locale.setlocale(locale.LC_ALL, '')
diff --git a/nyx/util/__init__.py b/nyx/util/__init__.py
index 1bc3d71..176753b 100644
--- a/nyx/util/__init__.py
+++ b/nyx/util/__init__.py
@@ -18,7 +18,6 @@ __all__ = [
'log',
'panel',
'tracker',
- 'ui_tools',
]
TOR_CONTROLLER = None
diff --git a/nyx/util/panel.py b/nyx/util/panel.py
index 26b0af1..43e0236 100644
--- a/nyx/util/panel.py
+++ b/nyx/util/panel.py
@@ -769,6 +769,35 @@ class Panel(object):
return recreate
+ def draw_box(self, top, left, width, height, *attributes):
+ """
+ Draws a box in the panel with the given bounds.
+
+ Arguments:
+ top - vertical position of the box's top
+ left - horizontal position of the box's left side
+ width - width of the drawn box
+ height - height of the drawn box
+ attr - text attributes
+ """
+
+ # draws the top and bottom
+
+ self.hline(top, left + 1, width - 2, *attributes)
+ self.hline(top + height - 1, left + 1, width - 2, *attributes)
+
+ # draws the left and right sides
+
+ self.vline(top + 1, left, height - 2, *attributes)
+ self.vline(top + 1, left + width - 1, height - 2, *attributes)
+
+ # draws the corners
+
+ self.addch(top, left, curses.ACS_ULCORNER, *attributes)
+ self.addch(top, left + width - 1, curses.ACS_URCORNER, *attributes)
+ self.addch(top + height - 1, left, curses.ACS_LLCORNER, *attributes)
+ self.addch(top + height - 1, left + width - 1, curses.ACS_LRCORNER, *attributes)
+
class KeyInput(object):
"""
diff --git a/nyx/util/ui_tools.py b/nyx/util/ui_tools.py
deleted file mode 100644
index ccea602..0000000
--- a/nyx/util/ui_tools.py
+++ /dev/null
@@ -1,268 +0,0 @@
-"""
-Toolkit for working with curses.
-"""
-
-import curses
-
-from curses.ascii import isprint
-
-from stem.util import system
-
-
-def disable_acs():
- """
- Replaces the curses ACS characters. This can be preferable if curses is
- unable to render them...
-
- http://www.atagar.com/nyx/images/acs_display_failure.png
- """
-
- for item in curses.__dict__:
- if item.startswith('ACS_'):
- curses.__dict__[item] = ord('+')
-
- # replace a few common border pipes that are better rendered as '|' or
- # '-' instead
-
- curses.ACS_SBSB = ord('|')
- curses.ACS_VLINE = ord('|')
- curses.ACS_BSBS = ord('-')
- curses.ACS_HLINE = ord('-')
-
-
-def get_printable(line, keep_newlines = True):
- """
- Provides the line back with non-printable characters stripped.
-
- :param str line: string to be processed
- :param str keep_newlines: retains newlines if **True**, stripped otherwise
-
- :returns: **str** of the line with only printable content
- """
-
- line = line.replace('\xc2', "'")
- line = filter(lambda char: isprint(char) or (keep_newlines and char == '\n'), line)
-
- return line
-
-
-def draw_box(panel, top, left, width, height, *attributes):
- """
- Draws a box in the panel with the given bounds.
-
- Arguments:
- panel - panel in which to draw
- top - vertical position of the box's top
- left - horizontal position of the box's left side
- width - width of the drawn box
- height - height of the drawn box
- attr - text attributes
- """
-
- # draws the top and bottom
-
- panel.hline(top, left + 1, width - 2, *attributes)
- panel.hline(top + height - 1, left + 1, width - 2, *attributes)
-
- # draws the left and right sides
-
- panel.vline(top + 1, left, height - 2, *attributes)
- panel.vline(top + 1, left + width - 1, height - 2, *attributes)
-
- # draws the corners
-
- panel.addch(top, left, curses.ACS_ULCORNER, *attributes)
- panel.addch(top, left + width - 1, curses.ACS_URCORNER, *attributes)
- panel.addch(top + height - 1, left, curses.ACS_LLCORNER, *attributes)
- panel.addch(top + height - 1, left + width - 1, curses.ACS_LRCORNER, *attributes)
-
-
-def get_scroll_position(key, position, page_height, content_height, is_cursor = False):
- """
- Parses navigation keys, providing the new scroll possition the panel should
- use. Position is always between zero and (content_height - page_height). This
- handles the following keys:
- Up / Down - scrolls a position up or down
- Page Up / Page Down - scrolls by the page_height
- Home - top of the content
- End - bottom of the content
-
- This provides the input position if the key doesn't correspond to the above.
-
- Arguments:
- key - keycode for the user's input
- position - starting position
- page_height - size of a single screen's worth of content
- content_height - total lines of content that can be scrolled
- is_cursor - tracks a cursor position rather than scroll if true
- """
-
- if key.is_scroll():
- shift = 0
-
- if key.match('up'):
- shift = -1
- elif key.match('down'):
- shift = 1
- elif key.match('page_up'):
- shift = -page_height + 1 if is_cursor else -page_height
- elif key.match('page_down'):
- shift = page_height - 1 if is_cursor else page_height
- elif key.match('home'):
- shift = -content_height
- elif key.match('end'):
- shift = content_height
-
- # returns the shift, restricted to valid bounds
-
- max_location = content_height - 1 if is_cursor else content_height - page_height
- return max(0, min(position + shift, max_location))
- else:
- return position
-
-
-class Scroller:
- """
- Tracks the scrolling position when there might be a visible cursor. This
- expects that there is a single line displayed per an entry in the contents.
- """
-
- def __init__(self, is_cursor_enabled):
- self.scroll_location, self.cursor_location = 0, 0
- self.cursor_selection = None
- self.is_cursor_enabled = is_cursor_enabled
-
- def get_scroll_location(self, content, page_height):
- """
- Provides the scrolling location, taking into account its cursor's location
- content size, and page height.
-
- Arguments:
- content - displayed content
- page_height - height of the display area for the content
- """
-
- if content and page_height:
- self.scroll_location = max(0, min(self.scroll_location, len(content) - page_height + 1))
-
- if self.is_cursor_enabled:
- self.get_cursor_selection(content) # resets the cursor location
-
- # makes sure the cursor is visible
-
- if self.cursor_location < self.scroll_location:
- self.scroll_location = self.cursor_location
- elif self.cursor_location > self.scroll_location + page_height - 1:
- self.scroll_location = self.cursor_location - page_height + 1
-
- # checks if the bottom would run off the content (this could be the
- # case when the content's size is dynamic and entries are removed)
-
- if len(content) > page_height:
- self.scroll_location = min(self.scroll_location, len(content) - page_height)
-
- return self.scroll_location
-
- def get_cursor_selection(self, content):
- """
- Provides the selected item in the content. This is the same entry until
- the cursor moves or it's no longer available (in which case it moves on to
- the next entry).
-
- Arguments:
- content - displayed content
- """
-
- # TODO: needs to handle duplicate entries when using this for the
- # connection panel
-
- if not self.is_cursor_enabled:
- return None
- elif not content:
- self.cursor_location, self.cursor_selection = 0, None
- return None
-
- self.cursor_location = min(self.cursor_location, len(content) - 1)
-
- if self.cursor_selection is not None and self.cursor_selection in content:
- # moves cursor location to track the selection
- self.cursor_location = content.index(self.cursor_selection)
- else:
- # select the next closest entry
- self.cursor_selection = content[self.cursor_location]
-
- return self.cursor_selection
-
- def handle_key(self, key, content, page_height):
- """
- Moves either the scroll or cursor according to the given input.
-
- Arguments:
- key - key code of user input
- content - displayed content
- page_height - height of the display area for the content
- """
-
- if self.is_cursor_enabled:
- self.get_cursor_selection(content) # resets the cursor location
- start_location = self.cursor_location
- else:
- start_location = self.scroll_location
-
- new_location = get_scroll_position(key, start_location, page_height, len(content), self.is_cursor_enabled)
-
- if start_location != new_location:
- if self.is_cursor_enabled:
- self.cursor_selection = content[new_location]
- else:
- self.scroll_location = new_location
-
- return True
- else:
- return False
-
-
-def is_wide_characters_supported():
- """
- Checks if our version of curses has wide character support. This is required
- to print unicode.
-
- :returns: **bool** that's **True** if curses supports wide characters, and
- **False** if it either can't or this can't be determined
- """
-
- try:
- # Gets the dynamic library used by the interpretor for curses. This uses
- # 'ldd' on Linux or 'otool -L' on OSX.
- #
- # atagar@fenrir:~/Desktop$ ldd /usr/lib/python2.6/lib-dynload/_curses.so
- # linux-gate.so.1 => (0x00a51000)
- # libncursesw.so.5 => /lib/libncursesw.so.5 (0x00faa000)
- # libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0x002f1000)
- # libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00158000)
- # libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0x00398000)
- # /lib/ld-linux.so.2 (0x00ca8000)
- #
- # atagar$ otool -L /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so
- # /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so:
- # /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
- # /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
- # /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.6)
-
- import _curses
-
- lib_dependency_lines = None
-
- if system.is_available('ldd'):
- lib_dependency_lines = system.call('ldd %s' % _curses.__file__)
- elif system.is_available('otool'):
- lib_dependency_lines = system.call('otool -L %s' % _curses.__file__)
-
- if lib_dependency_lines:
- for line in lib_dependency_lines:
- if 'libncursesw' in line:
- return True
- except:
- pass
-
- return False
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits