commit aa58b0d3e2b032e85c1295028ee5d3865bd74508 Author: Damian Johnson atagar@torproject.org 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@lists.torproject.org