commit da49b908fb1ee3dc6a7cbd0e90a5ed76ec890e2c Author: Damian Johnson atagar@torproject.org Date: Mon Apr 4 18:14:36 2016 -0700
Revise confirmation dialog for saving the torrc
Overhauling our last popup. Now that the popup module has everything picking a uniform naming scheme. --- nyx/panel/config.py | 60 +++--------- nyx/panel/connection.py | 4 +- nyx/panel/graph.py | 4 +- nyx/panel/log.py | 4 +- nyx/popups.py | 248 +++++++++++++++++++++++++++++------------------- test/popups.py | 63 +++++++++--- 6 files changed, 219 insertions(+), 164 deletions(-)
diff --git a/nyx/panel/config.py b/nyx/panel/config.py index 6de9e61..1b991b6 100644 --- a/nyx/panel/config.py +++ b/nyx/panel/config.py @@ -17,7 +17,7 @@ import nyx.popups import stem.control import stem.manual
-from nyx.curses import GREEN, CYAN, WHITE, NORMAL, BOLD, HIGHLIGHT +from nyx.curses import WHITE, NORMAL, BOLD, HIGHLIGHT from nyx import DATA_DIR, tor_controller
from stem.util import conf, enum, log, str_tools @@ -175,7 +175,7 @@ class ConfigPanel(nyx.panel.Panel): """
sort_colors = dict([(attr, CONFIG['attr.config.sort_color'].get(attr, WHITE)) for attr in SortAttr]) - results = nyx.popups.show_sort_dialog('Config Option Ordering:', SortAttr, self._sort_order, sort_colors) + results = nyx.popups.select_sort_order('Config Option Ordering:', SortAttr, self._sort_order, sort_colors)
if results: self._sort_order = results @@ -186,53 +186,15 @@ class ConfigPanel(nyx.panel.Panel): Confirmation dialog for saving tor's configuration. """
- selection, controller = 1, tor_controller() - config_text = controller.get_info('config-text', None) - config_lines = config_text.splitlines() if config_text else [] - - with nyx.popups.popup_window(len(config_lines) + 2) as (popup, width, height): - if not popup or height <= 2: - return - - while True: - height, width = popup.get_preferred_size() # allow us to be resized - popup.win.erase() - - for i, full_line in enumerate(config_lines): - line = str_tools.crop(full_line, width - 2) - option, arg = line.split(' ', 1) if ' ' in line else (line, '') - - popup.addstr(i + 1, 1, option, GREEN, BOLD) - popup.addstr(i + 1, len(option) + 2, arg, CYAN, BOLD) - - x = width - 16 - - for i, option in enumerate(['Save', 'Cancel']): - x = popup.addstr(height - 2, x, '[') - x = popup.addstr(height - 2, x, option, BOLD, HIGHLIGHT if i == selection else NORMAL) - x = popup.addstr(height - 2, x, '] ') - - popup.draw_box() - popup.addstr(0, 0, 'Torrc to save:', HIGHLIGHT) - popup.win.refresh() - - key = nyx.curses.key_input() - - if key.match('left'): - selection = max(0, selection - 1) - elif key.match('right'): - selection = min(1, selection + 1) - elif key.is_selection(): - if selection == 0: - try: - controller.save_conf() - nyx.controller.show_message('Saved configuration to %s' % controller.get_info('config-file', '<unknown>'), HIGHLIGHT, max_wait = 2) - except IOError as exc: - nyx.controller.show_message('Unable to save configuration (%s)' % exc.strerror, HIGHLIGHT, max_wait = 2) - - break - elif key.match('esc'): - break # esc - cancel + controller = tor_controller() + torrc = controller.get_info('config-text', None) + + if nyx.popups.confirm_save_torrc(torrc): + try: + controller.save_conf() + nyx.controller.show_message('Saved configuration to %s' % controller.get_info('config-file', '<unknown>'), HIGHLIGHT, max_wait = 2) + except IOError as exc: + nyx.controller.show_message('Unable to save configuration (%s)' % exc.strerror, HIGHLIGHT, max_wait = 2)
self.redraw(True)
diff --git a/nyx/panel/connection.py b/nyx/panel/connection.py index afd1da1..709cf52 100644 --- a/nyx/panel/connection.py +++ b/nyx/panel/connection.py @@ -311,7 +311,7 @@ class ConnectionPanel(nyx.panel.Panel, threading.Thread): """
sort_colors = dict([(attr, CONFIG['attr.connection.sort_color'].get(attr, WHITE)) for attr in SortAttr]) - results = nyx.popups.show_sort_dialog('Connection Ordering:', SortAttr, self._sort_order, sort_colors) + results = nyx.popups.select_sort_order('Connection Ordering:', SortAttr, self._sort_order, sort_colors)
if results: self._sort_order = results @@ -392,7 +392,7 @@ class ConnectionPanel(nyx.panel.Panel, threading.Thread): resolver = connection_tracker.get_custom_resolver() options = ['auto'] + list(connection.Resolver) + list(nyx.tracker.CustomResolver)
- selected = nyx.popups.show_list_selector('Connection Resolver:', options, resolver if resolver else 'auto') + selected = nyx.popups.select_from_list('Connection Resolver:', options, resolver if resolver else 'auto') connection_tracker.set_custom_resolver(None if selected == 'auto' else selected)
self.redraw(True) diff --git a/nyx/panel/graph.py b/nyx/panel/graph.py index cad754b..7c53675 100644 --- a/nyx/panel/graph.py +++ b/nyx/panel/graph.py @@ -510,7 +510,7 @@ class GraphPanel(nyx.panel.Panel): options = ['None'] + [stat.capitalize() for stat in available_stats] previous_selection = options[available_stats.index(self.displayed_stat) + 1] if self.displayed_stat else 'None'
- selection = nyx.popups.show_list_selector('Graphed Stats:', options, previous_selection) + selection = nyx.popups.select_from_list('Graphed Stats:', options, previous_selection) self.displayed_stat = None if selection == 'None' else available_stats[options.index(selection) - 1]
def _next_bounds(): @@ -518,7 +518,7 @@ class GraphPanel(nyx.panel.Panel): self.redraw(True)
def _pick_interval(): - self.update_interval = nyx.popups.show_list_selector('Update Interval:', list(Interval), self.update_interval) + self.update_interval = nyx.popups.select_from_list('Update Interval:', list(Interval), self.update_interval) self.redraw(True)
return ( diff --git a/nyx/panel/log.py b/nyx/panel/log.py index 364878f..dc26dc3 100644 --- a/nyx/panel/log.py +++ b/nyx/panel/log.py @@ -141,7 +141,7 @@ class LogPanel(nyx.panel.Panel, threading.Thread): Prompts the user to select the events being listened for. """
- event_types = nyx.popups.show_event_selector() + event_types = nyx.popups.select_event_types()
if event_types != self._event_types: self._event_types = nyx.log.listen_for_events(self._register_tor_event, event_types) @@ -214,7 +214,7 @@ class LogPanel(nyx.panel.Panel, threading.Thread): with nyx.curses.CURSES_LOCK: options = ['None'] + self._filter.latest_selections() + ['New...'] initial_selection = self._filter.selection() if self._filter.selection() else 'None' - selection = nyx.popups.show_list_selector('Log Filter:', options, initial_selection) + selection = nyx.popups.select_from_list('Log Filter:', options, initial_selection)
if selection == 'None': self._filter.select(None) diff --git a/nyx/popups.py b/nyx/popups.py index 3160949..b7ad7b1 100644 --- a/nyx/popups.py +++ b/nyx/popups.py @@ -2,7 +2,20 @@ # See LICENSE for licensing information
""" -Functions for displaying popups in the interface. +Popup dialogs provided by our interface. + +:: + + show_help - keybindings provided by the current page + show_about - basic information about our application + show_counts - listing of counts with bar graphs + show_descriptor - presents descriptors for a relay + + select_from_list - selects from a list of options + select_sort_order - selects attributes by which to sort by + select_event_types - select from a list of event types + + confirm_save_torrc - confirmation dialog for saving the torrc """
import math @@ -204,7 +217,117 @@ def show_counts(title, counts, fill_char = ' '): nyx.curses.key_input()
-def show_list_selector(title, options, previous_selection): +def show_descriptor(fingerprint, color, is_close_key): + """ + Provides a dialog showing descriptors for a relay. + + :param str fingerprint: fingerprint of the relay to be shown + :param str color: text color of the dialog + :param function is_close_key: method to indicate if a key should close the + dialog or not + + :returns: :class:`~nyx.curses.KeyInput` for the keyboard input that + closed the dialog + """ + + if fingerprint: + title = 'Consensus Descriptor (%s):' % fingerprint + lines = _descriptor_text(fingerprint) + show_line_numbers = True + else: + title = 'Consensus Descriptor:' + lines = [UNRESOLVED_MSG] + show_line_numbers = False + + scroller = nyx.curses.Scroller() + line_number_width = int(math.log10(len(lines))) + 1 if show_line_numbers else 0 + + def _render(subwindow): + in_block = False # flag indicating if we're currently in crypto content + y, offset = 1, line_number_width + 3 if show_line_numbers else 2 + + for i, line in enumerate(lines): + keyword, value = line, '' + line_color = color + + if line in HEADERS: + line_color = HEADER_COLOR + elif line.startswith(BLOCK_START): + in_block = True + elif line.startswith(BLOCK_END): + in_block = False + elif in_block: + keyword, value = '', line + elif ' ' in line and line != UNRESOLVED_MSG and line != ERROR_MSG: + keyword, value = line.split(' ', 1) + keyword = keyword + ' ' + + if i < scroller.location(): + continue + + if show_line_numbers: + subwindow.addstr(2, y, str(i + 1).rjust(line_number_width), LINE_NUMBER_COLOR, BOLD) + + x, y = subwindow.addstr_wrap(3 + line_number_width, y, keyword, subwindow.width - 2, offset, line_color, BOLD) + x, y = subwindow.addstr_wrap(x, y, value, subwindow.width - 2, offset, line_color) + y += 1 + + if y > subwindow.height - 2: + break + + subwindow.box() + subwindow.addstr(0, 0, title, HIGHLIGHT) + + width, height = 0, len(lines) + 2 + screen_size = nyx.curses.screen_size() + + for line in lines: + width = min(screen_size.width, max(width, len(line) + line_number_width + 5)) + height += len(line) / (screen_size.width - line_number_width - 5) # extra lines due to text wrap + + with nyx.curses.CURSES_LOCK: + nyx.curses.draw(lambda subwindow: subwindow.addstr(0, 0, ' ' * 500), top = _top(), height = 1) # hides title below us + nyx.curses.draw(_render, top = _top(), width = width, height = height) + popup_height = min(screen_size.height - _top(), height) + + while True: + key = nyx.curses.key_input() + + if key.is_scroll(): + is_changed = scroller.handle_key(key, len(lines), popup_height - 2) + + if is_changed: + nyx.curses.draw(_render, top = _top(), width = width, height = height) + elif is_close_key(key): + return key + + +def _descriptor_text(fingerprint): + """ + Provides the descriptors for a relay. + + :param str fingerprint: relay fingerprint to be looked up + + :returns: **list** with the lines that should be displayed in the dialog + """ + + controller = nyx.tor_controller() + router_status_entry = controller.get_network_status(fingerprint, None) + microdescriptor = controller.get_microdescriptor(fingerprint, None) + server_descriptor = controller.get_server_descriptor(fingerprint, None) + + description = 'Consensus:\n\n%s' % (router_status_entry if router_status_entry else ERROR_MSG) + + if server_descriptor: + description += '\n\nServer Descriptor:\n\n%s' % server_descriptor + + if microdescriptor: + description += '\n\nMicrodescriptor:\n\n%s' % microdescriptor + + return description.split('\n') + + +def select_from_list(title, options, previous_selection): """ Provides list of items the user can choose from.
@@ -244,7 +367,7 @@ def show_list_selector(title, options, previous_selection): return previous_selection
-def show_sort_dialog(title, options, previous_order, option_colors): +def select_sort_order(title, options, previous_order, option_colors): """ Provides sorting dialog of the form...
@@ -315,7 +438,7 @@ def show_sort_dialog(title, options, previous_order, option_colors): return new_order
-def show_event_selector(): +def select_event_types(): """ Presents a chart of event types we support, with a prompt for the user to select a set. @@ -345,114 +468,49 @@ def show_event_selector(): return None
-def show_descriptor(fingerprint, color, is_close_key): +def confirm_save_torrc(torrc): """ - Provides a dialog showing descriptors for a relay. + Provides a confirmation dialog for saving tor's current configuration.
- :param str fingerprint: fingerprint of the relay to be shown - :param str color: text color of the dialog - :param function is_close_key: method to indicate if a key should close the - dialog or not + :param str torrc: torrc that would be saved
- :returns: :class:`~nyx.curses.KeyInput` for the keyboard input that - closed the dialog + :returns: **True** if the torrc should be saved and **False** otherwise """
- if fingerprint: - title = 'Consensus Descriptor (%s):' % fingerprint - lines = _descriptor_text(fingerprint) - show_line_numbers = True - else: - title = 'Consensus Descriptor:' - lines = [UNRESOLVED_MSG] - show_line_numbers = False - - scroller = nyx.curses.Scroller() - line_number_width = int(math.log10(len(lines))) + 1 if show_line_numbers else 0 + torrc_lines = torrc.splitlines() if torrc else [] + selection = 1
def _render(subwindow): - in_block = False # flag indicating if we're currently in crypto content - y, offset = 1, line_number_width + 3 if show_line_numbers else 2 - - for i, line in enumerate(lines): - keyword, value = line, '' - line_color = color - - if line in HEADERS: - line_color = HEADER_COLOR - elif line.startswith(BLOCK_START): - in_block = True - elif line.startswith(BLOCK_END): - in_block = False - elif in_block: - keyword, value = '', line - elif ' ' in line and line != UNRESOLVED_MSG and line != ERROR_MSG: - keyword, value = line.split(' ', 1) - keyword = keyword + ' ' + for i, full_line in enumerate(torrc_lines): + line = stem.util.str_tools.crop(full_line, subwindow.width - 2) + option, arg = line.split(' ', 1) if ' ' in line else (line, '')
- if i < scroller.location(): - continue - - if show_line_numbers: - subwindow.addstr(2, y, str(i + 1).rjust(line_number_width), LINE_NUMBER_COLOR, BOLD) + subwindow.addstr(1, i + 1, option, GREEN, BOLD) + subwindow.addstr(len(option) + 2, i + 1, arg, CYAN, BOLD)
- x, y = subwindow.addstr_wrap(3 + line_number_width, y, keyword, subwindow.width - 2, offset, line_color, BOLD) - x, y = subwindow.addstr_wrap(x, y, value, subwindow.width - 2, offset, line_color) - y += 1 + x = subwindow.width - 16
- if y > subwindow.height - 2: - break + for i, option in enumerate(['Save', 'Cancel']): + x = subwindow.addstr(x, subwindow.height - 2, '[') + x = subwindow.addstr(x, subwindow.height - 2, option, BOLD, HIGHLIGHT if i == selection else NORMAL) + x = subwindow.addstr(x, subwindow.height - 2, '] ')
subwindow.box() - subwindow.addstr(0, 0, title, HIGHLIGHT) - - width, height = 0, len(lines) + 2 - screen_size = nyx.curses.screen_size() - - for line in lines: - width = min(screen_size.width, max(width, len(line) + line_number_width + 5)) - height += len(line) / (screen_size.width - line_number_width - 5) # extra lines due to text wrap + subwindow.addstr(0, 0, 'Torrc to save:', HIGHLIGHT)
with nyx.curses.CURSES_LOCK: - nyx.curses.draw(lambda subwindow: subwindow.addstr(0, 0, ' ' * 500), top = _top(), height = 1) # hides title below us - nyx.curses.draw(_render, top = _top(), width = width, height = height) - popup_height = min(screen_size.height - _top(), height) - while True: + nyx.curses.draw(_render, top = _top(), height = len(torrc_lines) + 2) key = nyx.curses.key_input()
- if key.is_scroll(): - is_changed = scroller.handle_key(key, len(lines), popup_height - 2) - - if is_changed: - nyx.curses.draw(_render, top = _top(), width = width, height = height) - elif is_close_key(key): - return key - - -def _descriptor_text(fingerprint): - """ - Provides the descriptors for a relay. - - :param str fingerprint: relay fingerprint to be looked up - - :returns: **list** with the lines that should be displayed in the dialog - """ - - controller = nyx.tor_controller() - router_status_entry = controller.get_network_status(fingerprint, None) - microdescriptor = controller.get_microdescriptor(fingerprint, None) - server_descriptor = controller.get_server_descriptor(fingerprint, None) - - description = 'Consensus:\n\n%s' % (router_status_entry if router_status_entry else ERROR_MSG) - - if server_descriptor: - description += '\n\nServer Descriptor:\n\n%s' % server_descriptor - - if microdescriptor: - description += '\n\nMicrodescriptor:\n\n%s' % microdescriptor - - return description.split('\n') + if key.match('left'): + selection = max(0, selection - 1) + elif key.match('right'): + selection = min(1, selection + 1) + elif key.is_selection(): + return selection == 0 + elif key.match('esc'): + return False # esc - cancel
def _top(): diff --git a/test/popups.py b/test/popups.py index e34398b..34ff587 100644 --- a/test/popups.py +++ b/test/popups.py @@ -109,10 +109,24 @@ Event Types:-------------------------------------------------------------------+ +------------------------------------------------------------------------------+ """.strip()
-EXPECTED_DESCRIPTOR_WITHOUT_FINGERPRINT = """ -Consensus Descriptor:----------+ -| No consensus data available | -+------------------------------+ +TORRC = """ +ControlPort 9051 +CookieAuthentication 1 +ExitPolicy reject *:* +DataDirectory /home/atagar/.tor +Log notice file /home/atagar/.tor/log +ORPort 7000 +""".strip() + +EXPECTED_SAVE_TORRC_CONFIRMATION = """ +Torrc to save:-----------------------------------------------------------------+ +|ControlPort 9051 | +|CookieAuthentication 1 | +|ExitPolicy reject *:* | +|DataDirectory /home/atagar/.tor | +|Log notice file /home/atagar/.tor/log | +|ORPort 7000 [Save] [Cancel]| ++------------------------------------------------------------------------------+ """.strip()
DESCRIPTOR_TEXT = """ @@ -142,6 +156,12 @@ NCGI042p6+7UgCVT1x3WcLnq3ScV//s1wXHrUXa7vi0= -----END SIGNATURE----- """.strip().split('\n')
+EXPECTED_DESCRIPTOR_WITHOUT_FINGERPRINT = """ +Consensus Descriptor:----------+ +| No consensus data available | ++------------------------------+ +""".strip() + EXPECTED_DESCRIPTOR = """ Consensus Descriptor (29787760145CD1A473552A2FC64C72A9A130820E):---------------------------------------------------+ | 1 Consensus: | @@ -232,23 +252,23 @@ class TestPopups(unittest.TestCase): self.assertEqual(EXPECTED_COUNTS, rendered.content)
@patch('nyx.popups._top', Mock(return_value = 0)) - def test_selector(self): + def test_select_from_list(self): options = ['each second', '5 seconds', '30 seconds', 'minutely', '15 minute', '30 minute', 'hourly', 'daily'] - rendered = test.render(nyx.popups.show_list_selector, 'Update Interval:', options, 'each second') + rendered = test.render(nyx.popups.select_from_list, 'Update Interval:', options, 'each second') self.assertEqual(EXPECTED_LIST_SELECTOR, rendered.content) self.assertEqual('each second', rendered.return_value)
@patch('nyx.popups._top', Mock(return_value = 0)) - def test_sort_dialog(self): + def test_select_sort_order(self): previous_order = ['Man Page Entry', 'Name', 'Is Set'] options = ['Name', 'Value', 'Value Type', 'Category', 'Usage', 'Summary', 'Description', 'Man Page Entry', 'Is Set']
- rendered = test.render(nyx.popups.show_sort_dialog, 'Config Option Ordering:', options, previous_order, {}) + rendered = test.render(nyx.popups.select_sort_order, 'Config Option Ordering:', options, previous_order, {}) self.assertEqual(EXPECTED_SORT_DIALOG_START, rendered.content) self.assertEqual(None, rendered.return_value)
@patch('nyx.popups._top', Mock(return_value = 0)) - def test_sort_dialog_selecting(self): + def test_select_sort_order_usage(self): # Use the dialog to make a selection. At the end we render two options as # being selected (rather than three) because the act of selecing the third # closed the popup. @@ -262,7 +282,7 @@ class TestPopups(unittest.TestCase):
def draw_func(): with patch('nyx.curses.key_input', side_effect = keypresses): - return nyx.popups.show_sort_dialog('Config Option Ordering:', options, previous_order, {}) + return nyx.popups.select_sort_order('Config Option Ordering:', options, previous_order, {})
previous_order = ['Man Page Entry', 'Name', 'Is Set'] options = ['Name', 'Value', 'Value Type', 'Category', 'Usage', 'Summary', 'Description', 'Man Page Entry', 'Is Set'] @@ -273,18 +293,33 @@ class TestPopups(unittest.TestCase):
@patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.controller.input_prompt', Mock(return_value = None)) - def test_event_selector_when_canceled(self): - rendered = test.render(nyx.popups.show_event_selector) + def test_select_event_types_when_canceled(self): + rendered = test.render(nyx.popups.select_event_types) self.assertEqual(EXPECTED_EVENT_SELECTOR, rendered.content) self.assertEqual(None, rendered.return_value)
@patch('nyx.popups._top', Mock(return_value = 0)) @patch('nyx.controller.input_prompt', Mock(return_value = '2bwe')) - def test_event_selector_with_input(self): - rendered = test.render(nyx.popups.show_event_selector) + def test_select_event_types_with_input(self): + rendered = test.render(nyx.popups.select_event_types) self.assertEqual(EXPECTED_EVENT_SELECTOR, rendered.content) self.assertEqual(set(['NYX_INFO', 'ERR', 'WARN', 'BW', 'NYX_ERR', 'NYX_WARN', 'NYX_NOTICE']), rendered.return_value)
+ @patch('nyx.curses.screen_size', Mock(return_value = nyx.curses.Dimensions(80, 60))) + @patch('nyx.popups._top', Mock(return_value = 0)) + def test_confirm_save_torrc(self): + rendered = test.render(nyx.popups.confirm_save_torrc, TORRC) + self.assertEqual(EXPECTED_SAVE_TORRC_CONFIRMATION, rendered.content) + self.assertEqual(False, rendered.return_value) + + def draw_func(): + with patch('nyx.curses.key_input', side_effect = [nyx.curses.KeyInput(curses.KEY_LEFT), nyx.curses.KeyInput(curses.KEY_ENTER)]): + return nyx.popups.confirm_save_torrc(TORRC) + + rendered = test.render(draw_func) + self.assertEqual(EXPECTED_SAVE_TORRC_CONFIRMATION, rendered.content) + self.assertEqual(True, rendered.return_value) + @patch('nyx.popups._top', Mock(return_value = 0)) def test_descriptor_without_fingerprint(self): rendered = test.render(nyx.popups.show_descriptor, None, nyx.curses.Color.RED, lambda key: key.match('esc'))