commit 7847a7521ba3423db2ac070bf8c52f145e7ac9b3 Author: Damian Johnson atagar@torproject.org Date: Tue Oct 7 09:51:51 2014 -0700
Replacing key integer codes with KeyInput class
Curses' getch() method returns an integer code for the pressed key. This is well and good, but needed both ui_tools helper functions and a lotta checks like...
if key in (ord('s'), ord('S')):
Wrapping user input with a KeyInput class that both makes this nicer...
if key.match('s'):
... and includes the ui_tools helpers. --- arm/config_panel.py | 22 +++++++------ arm/connections/conn_panel.py | 16 +++++----- arm/connections/count_popup.py | 2 +- arm/connections/descriptor_popup.py | 10 +++--- arm/controller.py | 25 +++++++++------ arm/graphing/graph_panel.py | 54 +++++++++++++++---------------- arm/header_panel.py | 4 +-- arm/log_panel.py | 14 ++++---- arm/menu/menu.py | 17 ++++------ arm/popups.py | 60 +++++++++++++++++------------------ arm/torrc_panel.py | 8 ++--- arm/util/panel.py | 56 ++++++++++++++++++++++++++++++++ arm/util/ui_tools.py | 39 ++++------------------- 13 files changed, 181 insertions(+), 146 deletions(-)
diff --git a/arm/config_panel.py b/arm/config_panel.py index 637b354..dbae8de 100644 --- a/arm/config_panel.py +++ b/arm/config_panel.py @@ -361,7 +361,7 @@ class ConfigPanel(panel.Panel):
def handle_key(self, key): with self.vals_lock: - if ui_tools.is_scroll_key(key): + if key.is_scroll(): page_height = self.get_preferred_size()[0] - 1 detail_panel_height = CONFIG["features.config.selectionDetails.height"]
@@ -372,7 +372,7 @@ class ConfigPanel(panel.Panel):
if is_changed: self.redraw(True) - elif ui_tools.is_selection_key(key) and self._get_config_options(): + elif key.is_selection() and self._get_config_options(): # Prompts the user to edit the selected configuration value. The # interface is locked to prevent updates between setting the value # and showing any errors. @@ -417,12 +417,12 @@ class ConfigPanel(panel.Panel): self.redraw(True) except Exception as exc: popups.show_msg("%s (press any key)" % exc) - elif key in (ord('a'), ord('A')): + elif key.match('a'): self.show_all = not self.show_all self.redraw(True) - elif key in (ord('s'), ord('S')): + elif key.match('s'): self.show_sort_dialog() - elif key in (ord('v'), ord('V')): + elif key.match('v'): self.show_write_dialog() else: return False @@ -468,9 +468,9 @@ class ConfigPanel(panel.Panel): height = new_height is_option_line_separate = True
- key, selection = 0, 2 + selection = 2
- while not ui_tools.is_selection_key(key): + while True: # if the popup has been resized then recreate it (needed for the # proper border height)
@@ -525,12 +525,14 @@ class ConfigPanel(panel.Panel):
popup.win.refresh()
- key = arm.controller.get_controller().get_screen().getch() + key = arm.controller.get_controller().key_input()
- if key == curses.KEY_LEFT: + if key.match('left'): selection = max(0, selection - 1) - elif key == curses.KEY_RIGHT: + elif key.match('right'): selection = min(len(selection_options) - 1, selection + 1) + elif key.is_selection(): + break
if selection in (0, 1): loaded_torrc, prompt_canceled = tor_config.get_torrc(), False diff --git a/arm/connections/conn_panel.py b/arm/connections/conn_panel.py index 57c64f5..76b2807 100644 --- a/arm/connections/conn_panel.py +++ b/arm/connections/conn_panel.py @@ -262,7 +262,7 @@ class ConnectionPanel(panel.Panel, threading.Thread):
def handle_key(self, key): with self.vals_lock: - if ui_tools.is_scroll_key(key): + if key.is_scroll(): page_height = self.get_preferred_size()[0] - 1
if self._show_details: @@ -272,12 +272,12 @@ class ConnectionPanel(panel.Panel, threading.Thread):
if is_changed: self.redraw(True) - elif ui_tools.is_selection_key(key): + elif key.is_selection(): self._show_details = not self._show_details self.redraw(True) - elif key in (ord('s'), ord('S')): + elif key.match('s'): self.show_sort_dialog() - elif key in (ord('u'), ord('U')): + elif key.match('u'): # provides a menu to pick the connection resolver
title = "Resolver Util:" @@ -298,7 +298,7 @@ class ConnectionPanel(panel.Panel, threading.Thread): if selection != -1: selected_option = options[selection] if selection != 0 else None conn_resolver.set_custom_resolver(selected_option) - elif key in (ord('l'), ord('L')): + elif key.match('l'): # provides a menu to pick the primary information we list connections by
title = "List By:" @@ -315,12 +315,12 @@ class ConnectionPanel(panel.Panel, threading.Thread):
if selection != -1: self.set_listing_type(options[selection]) - elif key in (ord('d'), ord('D')): + elif key.match('d'): # presents popup for raw consensus data descriptor_popup.show_descriptor_popup(self) - elif key in (ord('c'), ord('C')) and self.is_clients_allowed(): + elif key.match('c') and self.is_clients_allowed(): count_popup.showCountDialog(count_popup.CountType.CLIENT_LOCALE, self._client_locale_usage) - elif key in (ord('e'), ord('E')) and self.is_exits_allowed(): + elif key.match('e') and self.is_exits_allowed(): count_popup.showCountDialog(count_popup.CountType.EXIT_PORT, self._exit_port_usage) else: return False diff --git a/arm/connections/count_popup.py b/arm/connections/count_popup.py index 4decf46..1e22870 100644 --- a/arm/connections/count_popup.py +++ b/arm/connections/count_popup.py @@ -106,6 +106,6 @@ def showCountDialog(count_type, counts): popup.win.refresh()
curses.cbreak() - control.get_screen().getch() + control.key_input() finally: arm.popups.finalize() diff --git a/arm/connections/descriptor_popup.py b/arm/connections/descriptor_popup.py index af6ee69..d169309 100644 --- a/arm/connections/descriptor_popup.py +++ b/arm/connections/descriptor_popup.py @@ -79,9 +79,9 @@ def show_descriptor_popup(conn_panel): draw(popup, fingerprint, display_text, display_color, scroll, show_line_number) is_changed = False
- key = control.get_screen().getch() + key = control.key_input()
- if ui_tools.is_scroll_key(key): + if key.is_scroll(): # TODO: This is a bit buggy in that scrolling is by display_text # lines rather than the displayed lines, causing issues when # content wraps. The result is that we can't have a scrollbar and @@ -94,12 +94,12 @@ def show_descriptor_popup(conn_panel):
if scroll != new_scroll: scroll, is_changed = new_scroll, True - elif ui_tools.is_selection_key(key) or key in (ord('d'), ord('D')): + elif key.is_selection() or key.match('d'): is_done = True # closes popup - elif key in (curses.KEY_LEFT, curses.KEY_RIGHT): + elif key.match('left', 'right'): # navigation - pass on to conn_panel and recreate popup
- conn_panel.handle_key(curses.KEY_UP if key == curses.KEY_LEFT else curses.KEY_DOWN) + conn_panel.handle_key(panel.KeyInput(curses.KEY_UP) if key.match('left') else panel.KeyInput(curses.KEY_DOWN)) break finally: arm.popups.finalize() diff --git a/arm/controller.py b/arm/controller.py index 09f0f83..48034e9 100644 --- a/arm/controller.py +++ b/arm/controller.py @@ -273,6 +273,13 @@ class Controller:
return self._screen
+ def key_input(self): + """ + Gets keystroke from the user. + """ + + return panel.KeyInput(self.get_screen().getch()) + def get_page_count(self): """ Provides the number of pages the interface has. This may be zero if all @@ -652,29 +659,29 @@ def start_arm(stdscr): key, override_key = override_key, None else: curses.halfdelay(CONFIG["features.redrawRate"] * 10) - key = stdscr.getch() + key = panel.KeyInput(stdscr.getch())
- if key == curses.KEY_RIGHT: + if key.match('right'): control.next_page() - elif key == curses.KEY_LEFT: + elif key.match('left'): control.prev_page() - elif key in (ord('p'), ord('P')): + elif key.match('p'): control.set_paused(not control.is_paused()) - elif key in (ord('m'), ord('M')): + elif key.match('m'): arm.menu.menu.show_menu() - elif key in (ord('q'), ord('Q')): + elif key.match('q'): # provides prompt to confirm that arm should exit
if CONFIG["features.confirmQuit"]: msg = "Are you sure (q again to confirm)?" confirmation_key = arm.popups.show_msg(msg, attr = curses.A_BOLD) - quit_confirmed = confirmation_key in (ord('q'), ord('Q')) + quit_confirmed = confirmation_key.match('q') else: quit_confirmed = True
if quit_confirmed: control.quit() - elif key in (ord('x'), ord('X')): + elif key.match('x'): # provides prompt to confirm that arm should issue a sighup
msg = "This will reset Tor's internal state. Are you sure (x again to confirm)?" @@ -685,7 +692,7 @@ def start_arm(stdscr): tor_controller().signal(stem.Signal.RELOAD) except IOError as exc: log.error("Error detected when reloading tor: %s" % exc.strerror) - elif key in (ord('h'), ord('H')): + elif key.match('h'): override_key = arm.popups.show_help_popup() elif key == ord('l') - 96: # force redraw when ctrl+l is pressed diff --git a/arm/graphing/graph_panel.py b/arm/graphing/graph_panel.py index 68328b8..a034e56 100644 --- a/arm/graphing/graph_panel.py +++ b/arm/graphing/graph_panel.py @@ -24,7 +24,7 @@ import arm.controller
import stem.control
-from arm.util import panel, tor_controller, ui_tools +from arm.util import panel, tor_controller
from stem.util import conf, enum, str_tools
@@ -315,42 +315,40 @@ class GraphPanel(panel.Panel):
control = arm.controller.get_controller()
- panel.CURSES_LOCK.acquire() + with panel.CURSES_LOCK: + try: + while True: + msg = 'press the down/up to resize the graph, and enter when done' + control.set_msg(msg, curses.A_BOLD, True) + curses.cbreak() + key = control.key_input()
- try: - while True: - msg = 'press the down/up to resize the graph, and enter when done' - control.set_msg(msg, curses.A_BOLD, True) - curses.cbreak() - key = control.get_screen().getch() + if key.match('down'): + # don't grow the graph if it's already consuming the whole display + # (plus an extra line for the graph/log gap)
- if key == curses.KEY_DOWN: - # don't grow the graph if it's already consuming the whole display - # (plus an extra line for the graph/log gap) + max_height = self.parent.getmaxyx()[0] - self.top + current_height = self.get_height()
- max_height = self.parent.getmaxyx()[0] - self.top - current_height = self.get_height() + if current_height < max_height + 1: + self.set_graph_height(self.graph_height + 1) + elif key.match('up'): + self.set_graph_height(self.graph_height - 1) + elif key.is_selection(): + break
- if current_height < max_height + 1: - self.set_graph_height(self.graph_height + 1) - elif key == curses.KEY_UP: - self.set_graph_height(self.graph_height - 1) - elif ui_tools.is_selection_key(key): - break - - control.redraw() - finally: - control.set_msg() - panel.CURSES_LOCK.release() + control.redraw() + finally: + control.set_msg()
def handle_key(self, key): - if key in (ord('r'), ord('R')): + if key.match('r'): self.resize_graph() - elif key in (ord('b'), ord('B')): + elif key.match('b'): # uses the next boundary type self.bounds = Bounds.next(self.bounds) self.redraw(True) - elif key in (ord('s'), ord('S')): + elif key.match('s'): # provides a menu to pick the graphed stats
available_stats = self.stats.keys() @@ -377,7 +375,7 @@ class GraphPanel(panel.Panel): self.set_stats(None) elif selection != -1: self.set_stats(available_stats[selection - 1]) - elif key in (ord('i'), ord('I')): + elif key.match('i'): # provides menu to pick graph panel update interval
options = [label for (label, _) in UPDATE_INTERVALS] diff --git a/arm/header_panel.py b/arm/header_panel.py index 5278ca1..7206870 100644 --- a/arm/header_panel.py +++ b/arm/header_panel.py @@ -87,9 +87,9 @@ class HeaderPanel(panel.Panel, threading.Thread): arm.popups.show_msg('Requesting a new identity', 1)
def handle_key(self, key): - if key in (ord('n'), ord('N')): + if key.match('n'): self.send_newnym() - elif key in (ord('r'), ord('R')) and not self._vals.is_connected: + elif key.match('r') and not self._vals.is_connected: # TODO: This is borked. Not quite sure why but our attempt to call # PROTOCOLINFO fails with a socket error, followed by completely freezing # arm. This is exposing two bugs... diff --git a/arm/log_panel.py b/arm/log_panel.py index e78ecc7..f0e2a23 100644 --- a/arm/log_panel.py +++ b/arm/log_panel.py @@ -863,7 +863,7 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): raise exc
def handle_key(self, key): - if ui_tools.is_scroll_key(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)
@@ -872,18 +872,18 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler): self.scroll = new_scroll self.redraw(True) self.vals_lock.release() - elif key in (ord('u'), ord('U')): + elif key.match('u'): self.vals_lock.acquire() self.set_duplicate_visability(not CONFIG["features.log.showDuplicateEntries"]) self.redraw(True) self.vals_lock.release() - elif key in (ord('c'), ord('C')): + elif key.match('c'): msg = "This will clear the log. Are you sure (c again to confirm)?" key_press = arm.popups.show_msg(msg, attr = curses.A_BOLD)
- if key_press in (ord('c'), ord('C')): + if key_press.match('c'): self.clear() - elif key in (ord('f'), ord('F')): + elif key.match('f'): # Provides menu to pick regular expression filters or adding new ones: # for syntax see: http://docs.python.org/library/re.html#regular-expression-syntax
@@ -912,9 +912,9 @@ class LogPanel(panel.Panel, threading.Thread, logging.Handler):
if len(self.filter_options) > MAX_REGEX_FILTERS: del self.filter_options[MAX_REGEX_FILTERS:] - elif key in (ord('e'), ord('E')): + elif key.match('e'): self.show_event_selection_prompt() - elif key in (ord('a'), ord('A')): + elif key.match('a'): self.show_snapshot_prompt() else: return False diff --git a/arm/menu/menu.py b/arm/menu/menu.py index 435725b..e8fe297 100644 --- a/arm/menu/menu.py +++ b/arm/menu/menu.py @@ -40,17 +40,17 @@ class MenuCursor: is_selection_submenu = isinstance(self._selection, arm.menu.item.Submenu) selection_hierarchy = self._selection.get_hierarchy()
- if ui_tools.is_selection_key(key): + if key.is_selection(): if is_selection_submenu: if not self._selection.is_empty(): self._selection = self._selection.get_children()[0] else: self._is_done = self._selection.select() - elif key == curses.KEY_UP: + elif key.match('up'): self._selection = self._selection.prev() - elif key == curses.KEY_DOWN: + elif key.match('down'): self._selection = self._selection.next() - elif key == curses.KEY_LEFT: + elif key.match('left'): if len(selection_hierarchy) <= 3: # shift to the previous main submenu
@@ -60,7 +60,7 @@ class MenuCursor: # go up a submenu level
self._selection = self._selection.get_parent() - elif key == curses.KEY_RIGHT: + elif key.match('right'): if is_selection_submenu: # open submenu (same as making a selection)
@@ -71,9 +71,7 @@ class MenuCursor:
next_submenu = selection_hierarchy[1].next() self._selection = next_submenu.get_children()[0] - elif key in (27, ord('m'), ord('M')): - # close menu - + elif key.match('esc', 'm'): self._is_done = True
@@ -127,8 +125,7 @@ def show_menu(): popup.win.refresh()
curses.cbreak() - key = control.get_screen().getch() - cursor.handle_key(key) + cursor.handle_key(control.key_input())
# redraws the rest of the interface if we're rendering on it again
diff --git a/arm/popups.py b/arm/popups.py index 59cf40a..e852b95 100644 --- a/arm/popups.py +++ b/arm/popups.py @@ -91,20 +91,18 @@ def show_msg(msg, max_wait = -1, attr = curses.A_STANDOUT): attr - attributes with which to draw the message """
- panel.CURSES_LOCK.acquire() - control = arm.controller.get_controller() - control.set_msg(msg, attr, True) + with panel.CURSES_LOCK: + control = arm.controller.get_controller() + control.set_msg(msg, attr, True)
- if max_wait == -1: - curses.cbreak() - else: - curses.halfdelay(max_wait * 10) + if max_wait == -1: + curses.cbreak() + else: + curses.halfdelay(max_wait * 10)
- key_press = control.get_screen().getch() - control.set_msg() - panel.CURSES_LOCK.release() - - return key_press + key_press = control.key_input() + control.set_msg() + return key_press
def show_help_popup(): @@ -173,13 +171,12 @@ def show_help_popup():
popup.win.refresh() curses.cbreak() - exit_key = control.get_screen().getch() + exit_key = control.key_input() finally: finalize()
- if not ui_tools.is_selection_key(exit_key) and \ - not ui_tools.is_scroll_key(exit_key) and \ - exit_key not in (curses.KEY_LEFT, curses.KEY_RIGHT): + if not exit_key.is_selection() and not exit_key.is_scroll() and \ + not exit_key.match('left', 'right'): return exit_key else: return None @@ -208,7 +205,7 @@ def show_about_popup(): popup.win.refresh()
curses.cbreak() - control.get_screen().getch() + control.key_input() finally: finalize()
@@ -270,17 +267,17 @@ def show_sort_dialog(title, options, old_selection, option_colors):
popup.win.refresh()
- key = arm.controller.get_controller().get_screen().getch() + key = arm.controller.get_controller().key_input()
- if key == curses.KEY_LEFT: + if key.match('left'): cursor_location = max(0, cursor_location - 1) - elif key == curses.KEY_RIGHT: + elif key.match('right'): cursor_location = min(len(selection_options) - 1, cursor_location + 1) - elif key == curses.KEY_UP: + elif key.match('up'): cursor_location = max(0, cursor_location - 4) - elif key == curses.KEY_DOWN: + elif key.match('down'): cursor_location = min(len(selection_options) - 1, cursor_location + 4) - elif ui_tools.is_selection_key(key): + elif key.is_selection(): selection = selection_options[cursor_location]
if selection == "Cancel": @@ -351,7 +348,7 @@ def show_menu(title, options, old_selection): if not popup: return
- key, selection = 0, old_selection if old_selection != -1 else 0 + selection = old_selection if old_selection != -1 else 0
try: # hides the title of the first panel on the page @@ -363,7 +360,7 @@ def show_menu(title, options, old_selection):
curses.cbreak() # wait indefinitely for key presses (no timeout)
- while not ui_tools.is_selection_key(key): + while True: popup.win.erase() popup.win.box() popup.addstr(0, 0, title, curses.A_STANDOUT) @@ -377,14 +374,17 @@ def show_menu(title, options, old_selection):
popup.win.refresh()
- key = control.get_screen().getch() + key = control.key_input()
- if key == curses.KEY_UP: + if key.match('up'): selection = max(0, selection - 1) - elif key == curses.KEY_DOWN: + elif key.match('down'): selection = min(len(options) - 1, selection + 1) - elif key == 27: - selection, key = -1, curses.KEY_ENTER # esc - cancel + elif key.is_selection(): + break + elif key.match('esc'): + selection = -1 + break finally: top_panel.set_title_visible(True) finalize() diff --git a/arm/torrc_panel.py b/arm/torrc_panel.py index 2a3fb23..b7fa71a 100644 --- a/arm/torrc_panel.py +++ b/arm/torrc_panel.py @@ -123,18 +123,18 @@ class TorrcPanel(panel.Panel):
def handle_key(self, key): with self.vals_lock: - if ui_tools.is_scroll_key(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)
if self.scroll != new_scroll: self.scroll = new_scroll self.redraw(True) - elif key in (ord('n'), ord('N')): + elif key.match('n'): self.set_line_number_visible(not self.show_line_num) - elif key in (ord('s'), ord('S')): + elif key.match('s'): self.set_comments_visible(self.strip_comments) - elif key in (ord('r'), ord('R')): + elif key.match('r'): self.reload_torrc() else: return False diff --git a/arm/util/panel.py b/arm/util/panel.py index d68268a..fb6ae0e 100644 --- a/arm/util/panel.py +++ b/arm/util/panel.py @@ -18,6 +18,20 @@ from stem.util import log
CURSES_LOCK = RLock()
+SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END) + +SPECIAL_KEYS = { + 'up': curses.KEY_UP, + 'down': curses.KEY_DOWN, + 'left': curses.KEY_LEFT, + 'right': curses.KEY_RIGHT, + 'home': curses.KEY_HOME, + 'end': curses.KEY_END, + 'page_up': curses.KEY_PPAGE, + 'page_down': curses.KEY_NPAGE, + 'esc': 27, +} +
# tags used by addfstr - this maps to functor/argument combinations since the # actual values (in the case of color attributes) might not yet be initialized @@ -806,3 +820,45 @@ class Panel(): log.debug("recreating panel '%s' with the dimensions of %i/%i" % (self.get_name(), new_height, new_width))
return recreate + + +class KeyInput(object): + """ + Keyboard input by the user. + """ + + def __init__(self, key): + self._key = key # pressed key as an integer + + def match(self, *keys): + """ + Checks if we have a case insensitive match with the given key. Beside + characters, this also recognizes: up, down, left, right, home, end, + page_up, page_down, and esc. + """ + + for key in keys: + if key in SPECIAL_KEYS: + if self._key == SPECIAL_KEYS[key]: + return True + elif len(key) == 1: + if self._key in (ord(key.lower()), ord(key.upper())): + return True + else: + raise ValueError("%s wasn't among our recognized key codes" % key) + + return False + + def is_scroll(self): + """ + True if the key is used for scrolling, false otherwise. + """ + + return self._key in SCROLL_KEYS + + def is_selection(self): + """ + True if the key matches the enter or space keys. + """ + + return self._key in (curses.KEY_ENTER, 10, ord(' ')) diff --git a/arm/util/ui_tools.py b/arm/util/ui_tools.py index d2a6f43..a1064ee 100644 --- a/arm/util/ui_tools.py +++ b/arm/util/ui_tools.py @@ -24,8 +24,6 @@ COLOR_LIST = { DEFAULT_COLOR_ATTR = dict([(color, 0) for color in COLOR_LIST]) COLOR_ATTR = None
-SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END) -
def conf_handler(key, value): if key == 'features.color_override': @@ -211,29 +209,6 @@ def draw_box(panel, top, left, width, height, attr=curses.A_NORMAL): panel.addch(top + height - 1, left, curses.ACS_LLCORNER, attr)
-def is_selection_key(key): - """ - Returns true if the keycode matches the enter or space keys. - - Argument: - key - keycode to be checked - """ - - return key in (curses.KEY_ENTER, 10, ord(' ')) - - -def is_scroll_key(key): - """ - Returns true if the keycode is recognized by the get_scroll_position function - for scrolling. - - Argument: - key - keycode to be checked - """ - - return key in SCROLL_KEYS - - def get_scroll_position(key, position, page_height, content_height, is_cursor = False): """ Parses navigation keys, providing the new scroll possition the panel should @@ -254,20 +229,20 @@ def get_scroll_position(key, position, page_height, content_height, is_cursor = is_cursor - tracks a cursor position rather than scroll if true """
- if is_scroll_key(key): + if key.is_scroll(): shift = 0
- if key == curses.KEY_UP: + if key.match('up'): shift = -1 - elif key == curses.KEY_DOWN: + elif key.match('down'): shift = 1 - elif key == curses.KEY_PPAGE: + elif key.match('page_up'): shift = -page_height + 1 if is_cursor else -page_height - elif key == curses.KEY_NPAGE: + elif key.match('page_down'): shift = page_height - 1 if is_cursor else page_height - elif key == curses.KEY_HOME: + elif key.match('home'): shift = -content_height - elif key == curses.KEY_END: + elif key.match('end'): shift = content_height
# returns the shift, restricted to valid bounds
tor-commits@lists.torproject.org