[tor-commits] [nyx/master] Revise confirmation dialog for saving the torrc
atagar at torproject.org
atagar at torproject.org
Tue Apr 5 01:47:44 UTC 2016
commit da49b908fb1ee3dc6a7cbd0e90a5ed76ec890e2c
Author: Damian Johnson <atagar at 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'))
More information about the tor-commits
mailing list