Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
# Copyright 2011-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information
"""
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
show_welcome - shows welcome dialog
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
"""
from __future__ import absolute_import
import curses
import math
import operator
import nyx
import nyx.arguments
import nyx.curses
import nyx.log
import nyx.panel
from nyx import nyx_interface
from nyx.curses import RED, GREEN, YELLOW, CYAN, WHITE, NORMAL, BOLD, HIGHLIGHT
import stem.control
import stem.util.str_tools
NO_STATS_MSG = "Usage stats aren't available yet, press any key..."
HEADERS = ['Consensus:', 'Microdescriptor:', 'Server Descriptor:']
HEADER_COLOR = CYAN
LINE_NUMBER_COLOR = YELLOW
BLOCK_START, BLOCK_END = '-----BEGIN ', '-----END '
UNRESOLVED_MSG = 'No consensus data available'
ERROR_MSG = 'Unable to retrieve data'
def show_help():
"""
Presents a popup with the current page's hotkeys.
:returns: :class:`~nyx.curses.KeyInput` that was pressed to close the popup
if it's one panels should act upon, **None** otherwise
"""
interface = nyx_interface()
handlers = []
for panel in reversed(interface.page_panels()[1:]):
handlers += [handler for handler in panel.key_handlers() if handler.description]
def _render(subwindow):
subwindow.box()
subwindow.addstr(0, 0, 'Page %i Commands:' % (interface.get_page() + 1), HIGHLIGHT)
for i, option in enumerate(handlers):
if i // 2 >= subwindow.height - 2:
break
# Entries are shown in the form '<key>: <description>[ (<selection>)]',
# such as...
#
# u: duplicate log entries (hidden)
x = 2 if i % 2 == 0 else 41
y = (i // 2) + 1
x = subwindow.addstr(x, y, option.key, BOLD)
x = subwindow.addstr(x, y, ': ' + option.description)
if option.current:
x = subwindow.addstr(x, y, ' (')
x = subwindow.addstr(x, y, option.current, BOLD)
x = subwindow.addstr(x, y, ')')
# tells user to press a key if the lower left is unoccupied
if len(handlers) < 13 and subwindow.height == 9:
subwindow.addstr(2, 7, 'Press any key...')
with nyx.curses.CURSES_LOCK:
nyx.curses.draw(_render, top = _top(), width = 80, height = 9)
keypress = nyx.curses.key_input()
if keypress.is_selection() or keypress.is_scroll() or keypress.match('left', 'right'):
return None
else:
return keypress
def show_about():
"""
Presents a popup with author and version information.
"""
def _render(subwindow):
subwindow.box()
subwindow.addstr(0, 0, 'About:', HIGHLIGHT)
subwindow.addstr(2, 1, 'Nyx, version %s (released %s)' % (nyx.__version__, nyx.__release_date__), BOLD)
subwindow.addstr(4, 2, 'Written by Damian Johnson (atagar@torproject.org)')
subwindow.addstr(4, 3, 'Project page: %s' % nyx.__url__)
subwindow.addstr(2, 5, 'Released under the GPL v3 (http://www.gnu.org/licenses/gpl.html)')
subwindow.addstr(2, 7, 'Press any key...')
with nyx.curses.CURSES_LOCK:
nyx.curses.draw(_render, top = _top(), width = 80, height = 9)
nyx.curses.key_input()
def show_welcome():
"""
Shows welcome dialog when nyx is run for the first time.
Can be turned off by setting shown in .nyxrc to true.
"""
def _render(subwindow):
subwindow.box()
subwindow.addstr(0, 0, 'Welcome:', HIGHLIGHT)
subwindow.addstr(2, 2, 'To see this message everytime nyx starts add "show_welcome true" in your config file.')
subwindow.addstr(2, 3, 'Use the arrow keys to change between different pages or press m to navigate through options in a menu.')
subwindow.addstr(2, 4, 'Press h if you forget your hotkeys.')
subwindow.addstr(2, 5, 'Press any key...')
with nyx.curses.CURSES_LOCK:
nyx.curses.draw(_render, top = _top(), width = 110, height = 7)
nyx.curses.key_input()
def show_counts(title, counts, fill_char = ' '):
"""
Provides a dialog with bar graphs and percentages for the given set of
counts. Pressing any key closes the dialog.
:param str title: dialog title
:param dict counts: mapping of labels to their value
:param str fill_char: character to use for rendering the bar graph
"""
def _render_no_stats(subwindow):
subwindow.box()
subwindow.addstr(0, 0, title, HIGHLIGHT)
subwindow.addstr(2, 1, NO_STATS_MSG, CYAN, BOLD)
def _render_stats(subwindow):
key_width, val_width, value_total = 3, 1, 0
for k, v in counts.items():
key_width = max(key_width, len(k))
val_width = max(val_width, len(str(v)))
value_total += v
subwindow.box()
subwindow.addstr(0, 0, title, HIGHLIGHT)
graph_width = subwindow.width - key_width - val_width - 11 # border, extra spaces, and percentage column
sorted_counts = sorted(counts.items(), key = operator.itemgetter(1), reverse = True)
for y, (k, v) in enumerate(sorted_counts):
label = '%s %s (%-2i%%)' % (k.ljust(key_width), str(v).rjust(val_width), v * 100 // value_total)
x = subwindow.addstr(2, y + 1, label, GREEN, BOLD)
for j in range(graph_width * v // value_total):
subwindow.addstr(x + j + 1, y + 1, fill_char, RED, HIGHLIGHT)
subwindow.addstr(2, subwindow.height - 2, 'Press any key...')
with nyx.curses.CURSES_LOCK:
if not counts:
nyx.curses.draw(_render_no_stats, top = _top(), width = len(NO_STATS_MSG) + 4, height = 3)
else:
nyx.curses.draw(_render_stats, top = _top(), width = 80, height = 4 + max(1, len(counts)))
nyx.curses.key_input()
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 = len(title), 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.
:param str title: dialog title
:param list options: options that can be selected from
:param str previous_selection: previously selected option
:returns: **str** of selection or **previous_selection** if dialog is canceled
"""
selected_index = options.index(previous_selection) if previous_selection in options else 0
def _render(subwindow):
subwindow.box()
subwindow.addstr(0, 0, title, HIGHLIGHT)
for i, option in enumerate(options):
if option == previous_selection:
subwindow.addstr(2, i + 1, '> ')
attr = HIGHLIGHT if i == selected_index else NORMAL
subwindow.addstr(4, i + 1, ' %s ' % option, attr)
with nyx.curses.CURSES_LOCK:
while True:
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 = max(map(len, options)) + 9, height = len(options) + 2)
key = nyx.curses.key_input()
if key.match('up'):
selected_index = max(0, selected_index - 1)
elif key.match('down'):
selected_index = min(len(options) - 1, selected_index + 1)
elif key.is_selection():
return options[selected_index]
elif key.match('esc'):
return previous_selection
def select_sort_order(title, options, previous_order, option_colors):
"""
Provides sorting dialog of the form...
Current Order: <previous order>
New Order: <selected options>
<option 1> <option 2> <option 3> Cancel
:param str title: dialog title
:param list options: sort options to be provided
:param list previous_order: previous ordering
:param dict option_colors: mapping of options to their color
:returns: **list** of the new sort order or **None** if dialog is canceled
"""
new_order = []
cursor_index = 0
shown_options = list(options) + ['Cancel']
def _draw_selection(subwindow, y, label, selection):
x = subwindow.addstr(2, y, label, BOLD)
for i, option in enumerate(selection):
x = subwindow.addstr(x, y, option, option_colors.get(option, WHITE), BOLD)
if i < len(selection) - 1:
x = subwindow.addstr(x, y, ', ', BOLD)
def _render(subwindow):
subwindow.box()
subwindow.addstr(0, 0, title, HIGHLIGHT)
_draw_selection(subwindow, 1, 'Current Order: ', previous_order)
_draw_selection(subwindow, 2, 'New Order: ', new_order)
# presents remaining options, each row having up to four options
for i, option in enumerate(shown_options):
attr = HIGHLIGHT if i == cursor_index else NORMAL
subwindow.addstr((i % 4) * 19 + 2, (i // 4) + 4, option, attr)
with nyx.curses.CURSES_LOCK:
while len(new_order) < len(previous_order):
nyx.curses.draw(_render, top = _top(), width = 80, height = 9)
key = nyx.curses.key_input()
if key.match('left'):
cursor_index = max(0, cursor_index - 1)
elif key.match('right'):
cursor_index = min(len(shown_options) - 1, cursor_index + 1)
elif key.match('up'):
cursor_index = max(0, cursor_index - 4)
elif key.match('down'):
cursor_index = min(len(shown_options) - 1, cursor_index + 4)
elif key.is_selection():
selection = shown_options[cursor_index]
if selection == 'Cancel':
return None
else:
new_order.append(selection)
shown_options.remove(selection)
cursor_index = min(cursor_index, len(shown_options) - 1)
elif key.match('esc'):
return None
return new_order
def select_event_types(initial_selection):
"""
Presents chart of events for the user to select from.
:param list initial_selection: initial events to be checked
:returns: **set** of event types the user has selected or **None** if dialog
is canceled
"""
event_names = nyx.tor_controller().get_info('events/names', None)
if not event_names:
return
selection = 0
selected_events = list(initial_selection)
events = [event for event in event_names.split() if event not in nyx.log.TOR_RUNLEVELS]
def _render(subwindow):
subwindow.box()
subwindow.addstr(0, 0, 'Event Types:', HIGHLIGHT)
if selection < 10:
description = stem.control.event_description(nyx.log.TOR_RUNLEVELS[selection % 5])
elif selection < (len(events) + 10):
description = stem.control.event_description(events[selection - 10])
else:
description = None
if description:
subwindow.addstr_wrap(1, 1, description, subwindow.width - 1, 1, GREEN, BOLD)
subwindow.hline(1, 3, 78)
subwindow._addch(0, 3, curses.ACS_LTEE)
subwindow._addch(79, 6, curses.ACS_RTEE)
x = subwindow.addstr(1, 4, 'Tor Runlevel:')
for i, event in enumerate(nyx.log.TOR_RUNLEVELS):
x = subwindow.addstr(x + 4, 4, '[X]' if event in selected_events else '[ ]')
x = subwindow.addstr(x + 1, 4, event, HIGHLIGHT if selection == i else NORMAL)
x = subwindow.addstr(1, 5, 'Nyx Runlevel:')
for i, event in enumerate(nyx.log.NYX_RUNLEVELS):
x = subwindow.addstr(x + 4, 5, '[X]' if event in selected_events else '[ ]')
x = subwindow.addstr(x + 1, 5, nyx.log.TOR_RUNLEVELS[i], HIGHLIGHT if selection == (i + 5) else NORMAL)
subwindow.hline(1, 6, 78)
subwindow._addch(0, 6, curses.ACS_LTEE)
subwindow._addch(79, 6, curses.ACS_RTEE)
for i, event in enumerate(events):
x = subwindow.addstr((i % 3) * 25 + 1, i // 3 + 7, '[X]' if event in selected_events else '[ ]')
x = subwindow.addstr(x + 1, i // 3 + 7, event, HIGHLIGHT if selection == (i + 10) else NORMAL)
x = subwindow.width - 14
for i, option in enumerate(['Ok', 'Cancel']):
x = subwindow.addstr(x, subwindow.height - 2, '[')
x = subwindow.addstr(x, subwindow.height - 2, option, BOLD, HIGHLIGHT if selection == len(events) + 10 + i else NORMAL)
x = subwindow.addstr(x, subwindow.height - 2, ']') + 1
with nyx.curses.CURSES_LOCK:
while True:
nyx.curses.draw(_render, top = _top(), width = 80, height = (len(events) // 3) + 10)
key = nyx.curses.key_input()
if key.match('up'):
if selection < 5:
pass
elif selection < 10:
selection -= 5
elif selection < 13:
selection = 5
elif selection < len(events) + 10:
selection -= 3
else:
selection = len(events) + 9
elif key.match('down'):
if selection < 10:
selection = min(selection + 5, 10)
elif selection < len(events) + 10:
selection = min(selection + 3, len(events) + 10)
elif key.match('left'):
selection = max(selection - 1, 0)
elif key.match('right'):
selection = min(selection + 1, len(events) + 11)
elif key.is_selection():
if selection < 5:
selected_event = nyx.log.TOR_RUNLEVELS[selection]
elif selection < 10:
selected_event = nyx.log.NYX_RUNLEVELS[selection - 5]
elif selection == len(events) + 10:
return set(selected_events) # selected 'Ok'
elif selection == len(events) + 11:
return None # selected 'Cancel'
else:
selected_event = events[selection - 10]
if selected_event in selected_events:
selected_events.remove(selected_event)
else:
selected_events.append(selected_event)
elif key.match('esc'):
return None
def confirm_save_torrc(torrc):
"""
Provides a confirmation dialog for saving tor's current configuration.
:param str torrc: torrc that would be saved
:returns: **True** if the torrc should be saved and **False** otherwise
"""
torrc_lines = torrc.splitlines() if torrc else []
selection = 1
def _render(subwindow):
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, '')
subwindow.addstr(1, i + 1, option, GREEN, BOLD)
subwindow.addstr(len(option) + 2, i + 1, arg, CYAN, BOLD)
x = subwindow.width - 16
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, 'Torrc to save:', HIGHLIGHT)
with nyx.curses.CURSES_LOCK:
while True:
nyx.curses.draw(_render, top = _top(), height = len(torrc_lines) + 2)
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():
return selection == 0
elif key.match('esc'):
return False # esc - cancel
def _top():
return nyx_interface().header_panel().get_height()