commit b6908b1fa5092eab691b261e06be34df1e1d3eb7 Author: Damian Johnson atagar@torproject.org Date: Sat Jul 23 10:50:07 2016 -0700
Rewrite backlog handling
Mutable function globals are code stink. They break thread safety and are just about never the right thing to do. The arm 1.4.5 HistoryValidator class was a lot closer to what we want...
https://gitweb.torproject.org/nyx.git/tree/src/util/textInput.py?h=release#n...
Cleaning that up and replacing our _handle_history_key() with it. This is invisible to callers (they're both private helpers). Improving our tests along the way too. --- nyx/curses.py | 86 +++++++++++++++++++++++++------------------------------ test/subwindow.py | 48 +++++++++++++++---------------- 2 files changed, 63 insertions(+), 71 deletions(-)
diff --git a/nyx/curses.py b/nyx/curses.py index ecc2644..b33abd3 100644 --- a/nyx/curses.py +++ b/nyx/curses.py @@ -150,8 +150,6 @@ SPECIAL_KEYS = {
Dimensions = collections.namedtuple('Dimensions', ['width', 'height'])
-HISTORY_DICT = {'selection_index': -1, 'custom_input': ''} -
def conf_handler(key, value): if key == 'features.colorOverride': @@ -286,7 +284,7 @@ def str_input(x, y, initial_text = '', backlog = None, tab_completion = None): handler = _handle_key
if backlog: - handler = functools.partial(_handle_history_key, handler, backlog) + handler = functools.partial(_TextBacklog(backlog)._handler, handler)
if tab_completion: handler = functools.partial(_handle_tab_completion, handler, tab_completion) @@ -335,50 +333,6 @@ def _handle_key(textbox, key): return key
-def _handle_history_key(next_handler, backlog, textbox, key): - """ - Allows user to select previous inputs when pressing up/down. - - :param func next_handler: handler to invoke after this - :param list backlog: backlog of all previous commands - :param Textbox textbox: current textbox context - :param int key: key pressed - - :returns: **None** if up/down is pressed, otherwise invokes next handler - """ - - global HISTORY_DICT - - if key in (curses.KEY_UP, curses.KEY_DOWN): - offset = 1 if key == curses.KEY_UP else -1 - new_selection = HISTORY_DICT['selection_index'] + offset - - new_selection = max(-1, new_selection) - new_selection = min(len(backlog) - 1, new_selection) - - if HISTORY_DICT['selection_index'] == new_selection: - return None - - if HISTORY_DICT['selection_index'] == -1: - HISTORY_DICT['custom_input'] = textbox.gather().strip() - - if new_selection == -1: - new_input = HISTORY_DICT['custom_input'] - else: - new_input = backlog[new_selection] - - y, _ = textbox.win.getyx() - _, max_x = textbox.win.getmaxyx() - textbox.win.clear() - textbox.win.addstr(y, 0, new_input[:max_x - 1]) - textbox.win.move(y, min(len(new_input), max_x - 1)) - - HISTORY_DICT['selection_index'] = new_selection - return None - - return next_handler(textbox, key) - - def _handle_tab_completion(next_handler, tab_completion, textbox, key): """ Allows user to tab complete commands if sufficient context is provided to @@ -419,6 +373,44 @@ def _handle_tab_completion(next_handler, tab_completion, textbox, key): return next_handler(textbox, key)
+class _TextBacklog(object): + """ + History backlog that allows the :func:`~nyx.curses.str_input` function to + scroll through prior inputs when pressing the up and down arrow keys. + """ + + def __init__(self, backlog = []): + self._backlog = backlog # backlog contents, newest to oldest + self._selection = -1 # selected item, -1 if we're not on the backlog + self._custom_input = '' # field's input prior to selecting a backlog item + + def _handler(self, next_handler, textbox, key): + if key in (curses.KEY_UP, curses.KEY_DOWN): + if key == curses.KEY_UP: + new_selection = min(len(self._backlog) - 1, self._selection + 1) + else: + new_selection = max(-1, self._selection - 1) + + if self._selection == new_selection: + return None + + if self._selection == -1: + self._custom_input = textbox.gather().strip() # save custom input + + new_input = self._custom_input if new_selection == -1 else self._backlog[new_selection] + + y, _ = textbox.win.getyx() + _, max_x = textbox.win.getmaxyx() + textbox.win.clear() + textbox.win.addstr(y, 0, new_input[:max_x - 1]) + textbox.win.move(y, min(len(new_input), max_x - 1)) + self._selection = new_selection + + return None + + return next_handler(textbox, key) + + def curses_attr(*attributes): """ Provides encoding for the given curses text attributes. diff --git a/test/subwindow.py b/test/subwindow.py index b72aeb7..2df946e 100644 --- a/test/subwindow.py +++ b/test/subwindow.py @@ -174,30 +174,6 @@ class TestCurses(unittest.TestCase): def test_handle_key_when_resized(self): self.assertEqual(curses.ascii.BEL, nyx.curses._handle_key(_textbox(), 410))
- def test_handle_history_key(self): - backlog = ['GETINFO version'] - - textbox = Mock() - textbox.win.getyx.return_value = DIMENSIONS - self.assertIsNone(nyx.curses._handle_history_key(NO_OP_HANDLER, [], textbox, curses.KEY_UP)) - - textbox = Mock() - textbox.win.getyx.return_value = DIMENSIONS - textbox.win.getmaxyx.return_value = DIMENSIONS - textbox.win.addstr = Mock() - textbox.win.move = Mock() - nyx.curses._handle_history_key(NO_OP_HANDLER, backlog, textbox, curses.KEY_UP) - self.assertTrue(textbox.win.clear.called) - expected_addstr_call = call(DIMENSIONS[0], 0, backlog[0]) - self.assertEqual(expected_addstr_call, textbox.win.addstr.call_args) - expected_move_call = call(DIMENSIONS[0], len(backlog[0])) - self.assertEqual(expected_move_call, textbox.win.move.call_args) - - textbox = Mock() - mock_handle_key = Mock() - nyx.curses._handle_history_key(mock_handle_key, [], textbox, curses.KEY_LEFT) - self.assertTrue(mock_handle_key.called) - def test_handle_tab_completion_no_op(self): tab_completion = lambda txt_input: ['GETINFO version'] result = nyx.curses._handle_tab_completion(NO_OP_HANDLER, tab_completion, _textbox(), ord('a')) @@ -228,3 +204,27 @@ class TestCurses(unittest.TestCase): self.assertEqual(None, result) # consumes input self.assertEquals(call(0, 8), textbox.win.move.call_args) # move cursor to end self.assertEqual(call(0, 0, 'GETINFO '), textbox.win.addstr.call_args) + + def test_text_backlog_no_op(self): + backlog = nyx.curses._TextBacklog(['GETINFO version']) + textbox = _textbox() + + self.assertEqual(ord('a'), backlog._handler(NO_OP_HANDLER, textbox, ord('a'))) + self.assertFalse(textbox.win.addstr.called) + + def test_text_backlog_fills_history(self): + backlog = nyx.curses._TextBacklog(['GETINFO version']) + textbox = _textbox() + + self.assertEqual(None, backlog._handler(NO_OP_HANDLER, textbox, curses.KEY_UP)) + self.assertEqual(call(0, 0, 'GETINFO version'), textbox.win.addstr.call_args) + + def test_text_backlog_remembers_custom_input(self): + backlog = nyx.curses._TextBacklog(['GETINFO version']) + textbox = _textbox(text = 'hello') + + self.assertEqual(None, backlog._handler(NO_OP_HANDLER, textbox, curses.KEY_UP)) + self.assertEqual(call(0, 0, 'GETINFO version'), textbox.win.addstr.call_args) + + self.assertEqual(None, backlog._handler(NO_OP_HANDLER, textbox, curses.KEY_DOWN)) + self.assertEqual(call(0, 0, 'hello'), textbox.win.addstr.call_args)
tor-commits@lists.torproject.org