def get_label(self, option_width, value_width, summary_width): """ Provides display string of the configuration entry with the given constraints on the width of the contents. Arguments: option_width - width of the option column value_width - width of the value column summary_width - width of the summary column """ # Fetching the display entries is very common so this caches the values. # Doing this substantially drops cpu usage when scrolling (by around 40%). arg_set = (option_width, value_width, summary_width) if not self.label_cache or self.label_cache_args != arg_set: option_label = str_tools.crop(self.get(Field.OPTION), option_width) value_label = str_tools.crop(self.get(Field.VALUE), value_width) summary_label = str_tools.crop(self.get(Field.SUMMARY), summary_width, None) line_text_layout = '%%-%is %%-%is %%-%is' % (option_width, value_width, summary_width) self.label_cache = line_text_layout % (option_label, value_label, summary_label) self.label_cache_args = arg_set return self.label_cache
def test_crop(self): # test the pydoc examples self.assertEqual('This is a looo...', str_tools.crop('This is a looooong message', 17)) self.assertEqual('This is a...', str_tools.crop('This is a looooong message', 12)) self.assertEqual('', str_tools.crop('This is a looooong message', 3))
def _draw_selection_details(subwindow, selected): """ Shows details of the currently selected option. """ attr = ', '.join(('custom' if selected.is_set() else 'default', selected.value_type, 'usage: %s' % selected.manual.usage)) selected_color = CONFIG['attr.config.category_color'].get(selected.manual.category, WHITE) subwindow.box(0, 0, subwindow.width, DETAILS_HEIGHT) subwindow.addstr(2, 1, '%s (%s Option)' % (selected.name, selected.manual.category), selected_color, BOLD) subwindow.addstr(2, 2, 'Value: %s (%s)' % (selected.value(), str_tools.crop(attr, subwindow.width - len(selected.value()) - 13)), selected_color, BOLD) description = 'Description: %s' % selected.manual.description for i in range(DETAILS_HEIGHT - 4): if not description: break # done writing description line, description = description.split('\n', 1) if '\n' in description else (description, '') if i < DETAILS_HEIGHT - 5: line, remainder = str_tools.crop(line, subwindow.width - 3, 4, 4, str_tools.Ending.HYPHEN, True) description = ' ' + remainder.strip() + description subwindow.addstr(2, 3 + i, line, selected_color, BOLD) else: subwindow.addstr(2, 3 + i, str_tools.crop(line, subwindow.width - 3, 4, 4), selected_color, BOLD)
def _draw_line(subwindow, x, y, entry, is_selected, value_width, description_width): """ Show an individual configuration line. """ attr = [CONFIG['attr.config.category_color'].get(entry.manual.category, WHITE)] attr.append(BOLD if entry.is_set() else NORMAL) attr.append(HIGHLIGHT if is_selected else NORMAL) option_label = str_tools.crop(entry.name, NAME_WIDTH).ljust(NAME_WIDTH + 1) value_label = str_tools.crop(entry.value(), value_width).ljust(value_width + 1) summary_label = str_tools.crop(entry.manual.summary, description_width).ljust(description_width) subwindow.addstr(x, y, option_label + value_label + summary_label, *attr)
def _draw_address_column(subwindow, x, y, line, attr): src = tor_controller().get_info('address', line.connection.local_address) if line.line_type == LineType.CONNECTION: src = '%s:%s' % (src, line.connection.local_port) if line.line_type == LineType.CIRCUIT_HEADER and line.circuit.status != 'BUILT': dst = 'Building...' else: dst = '<scrubbed>' if line.entry.is_private( ) else line.connection.remote_address dst += ':%s' % line.connection.remote_port if line.entry.get_type() == Category.EXIT: purpose = connection.port_usage(line.connection.remote_port) if purpose: dst += ' (%s)' % str_tools.crop(purpose, 26 - len(dst) - 3) elif not tor_controller().is_geoip_unavailable( ) and not line.entry.is_private(): dst += ' (%s)' % (line.locale if line.locale else '??') src = '%-21s' % src dst = '%-21s' % dst if tor_controller().is_geoip_unavailable( ) else '%-26s' % dst if line.entry.get_type() in (Category.INBOUND, Category.SOCKS, Category.CONTROL): dst, src = src, dst if line.line_type == LineType.CIRCUIT: return subwindow.addstr(x, y, dst, *attr) else: return subwindow.addstr(x, y, '%s --> %s' % (src, dst), *attr)
def _draw_address_column(subwindow, x, y, line, attr): src = tor_controller().get_info('address', line.connection.local_address) src += ':%s' % line.connection.local_port if line.line_type == LineType.CONNECTION else '' if line.line_type == LineType.CIRCUIT_HEADER and line.circuit.status != 'BUILT': dst = 'Building...' else: dst = '<scrubbed>' if line.entry.is_private() else line.connection.remote_address dst += ':%s' % line.connection.remote_port if line.entry.get_type() == Category.EXIT: purpose = connection.port_usage(line.connection.remote_port) if purpose: dst += ' (%s)' % str_tools.crop(purpose, 26 - len(dst) - 3) elif not tor_controller().is_geoip_unavailable() and not line.entry.is_private(): dst += ' (%s)' % (line.locale if line.locale else '??') if line.entry.get_type() in (Category.INBOUND, Category.SOCKS, Category.CONTROL): dst, src = src, dst if line.line_type == LineType.CIRCUIT: subwindow.addstr(x, y, dst, *attr) else: subwindow.addstr(x, y, '%-21s --> %-26s' % (src, dst), *attr)
def format(self, message, crop_width = None): formatted_msg = message.format(**self._attr) if crop_width: formatted_msg = str_tools.crop(formatted_msg, crop_width) return formatted_msg
def _draw_selection_details(subwindow, selected): """ Shows details of the currently selected option. """ attr = ['custom' if selected.is_set() else 'default', selected.value_type] if selected.usage: attr.append('usage: %s' % selected.usage) selected_color = CONFIG['attr.config.category_color'].get( selected.category, WHITE) subwindow.box(0, 0, subwindow.width, DETAILS_HEIGHT) if selected.category: subwindow.addstr(2, 1, '%s (%s Option)' % (selected.name, selected.category), selected_color, BOLD) else: subwindow.addstr(2, 1, selected.name, selected_color, BOLD) subwindow.addstr( 2, 2, 'Value: %s (%s)' % (selected.value(), str_tools.crop(', '.join(attr), max(0, subwindow.width - len(selected.value()) - 13))), selected_color, BOLD) description = 'Description: %s' % selected.description for i in range(DETAILS_HEIGHT - 4): if not description: break # done writing description line, description = description.split( '\n', 1) if '\n' in description else (description, '') if i < DETAILS_HEIGHT - 5: line, remainder = str_tools.crop(line, subwindow.width - 3, 4, 4, str_tools.Ending.HYPHEN, True) description = ' ' + remainder.strip() + description subwindow.addstr(2, 3 + i, line, selected_color, BOLD) else: subwindow.addstr(2, 3 + i, str_tools.crop(line, subwindow.width - 3, 4, 4), selected_color, BOLD)
def _draw_line(subwindow, x, y, entry, is_selected, value_width, description_width): """ Show an individual configuration line. """ attr = [CONFIG['attr.config.category_color'].get(entry.category, WHITE)] attr.append(BOLD if entry.is_set() else NORMAL) attr.append(HIGHLIGHT if is_selected else NORMAL) option_label = str_tools.crop(entry.name, NAME_WIDTH).ljust(NAME_WIDTH + 1) value_label = str_tools.crop(entry.value(), value_width).ljust(value_width + 1) summary_label = str_tools.crop(entry.summary, description_width).ljust(description_width) subwindow.addstr(x, y, option_label + value_label + summary_label, *attr)
def addstr_wrap(self, y, x, msg, width, min_x = 0, *attr): orig_y = y while msg: draw_msg, msg = str_tools.crop(msg, width - x, None, ending = None, get_remainder = True) if not draw_msg: draw_msg, msg = str_tools.crop(msg, width - x), '' # first word is longer than the line x = self.addstr(y, x, draw_msg, *attr) if (y - orig_y + 1) >= CONFIG['features.maxLineWrap']: break # maximum number we'll wrap if msg: x, y = min_x, y + 1 return x, y
def test_crop(self): # test the pydoc examples self.assertEqual('This is a looo...', str_tools.crop('This is a looooong message', 17)) self.assertEqual('This is a...', str_tools.crop('This is a looooong message', 12)) self.assertEqual('', str_tools.crop('This is a looooong message', 3)) self.assertEqual('', str_tools.crop('This is a looooong message', 0))
def draw(self, width, height): with self._vals_lock: # If true, we assume that the cached value in self._last_content_height is # still accurate, and stop drawing when there's nothing more to display. # Otherwise the self._last_content_height is suspect, and we'll process all # the content to check if it's right (and redraw again with the corrected # height if not). trust_last_content_height = self._last_content_height_args == (width, height) # restricts scroll location to valid bounds self.scroll = max(0, min(self.scroll, self._last_content_height - height + 1)) rendered_contents, corrections, conf_location = None, {}, None if self.config_type == Config.TORRC: loaded_torrc = tor_config.get_torrc() with loaded_torrc.get_lock(): conf_location = loaded_torrc.get_config_location() if not loaded_torrc.is_loaded(): rendered_contents = ['### Unable to load the torrc ###'] else: rendered_contents = loaded_torrc.get_display_contents(self.strip_comments) # constructs a mapping of line numbers to the issue on it corrections = dict((line_number, (issue, msg)) for line_number, issue, msg in loaded_torrc.get_corrections()) else: loaded_nyxrc = conf.get_config('nyx') conf_location = loaded_nyxrc._path rendered_contents = list(loaded_nyxrc._raw_contents) # offset to make room for the line numbers line_number_offset = 0 if self.show_line_num: if len(rendered_contents) == 0: line_number_offset = 2 else: line_number_offset = int(math.log10(len(rendered_contents))) + 2 # draws left-hand scroll bar if content's longer than the height scroll_offset = 0 if CONFIG['features.config.file.showScrollbars'] and self._last_content_height > height - 1: scroll_offset = 3 self.add_scroll_bar(self.scroll, self.scroll + height - 1, self._last_content_height, 1) display_line = -self.scroll + 1 # line we're drawing on # draws the top label if self.is_title_visible(): source_label = 'Tor' if self.config_type == Config.TORRC else 'Nyx' location_label = ' (%s)' % conf_location if conf_location else '' self.addstr(0, 0, '%s Configuration File%s:' % (source_label, location_label), curses.A_STANDOUT) is_multiline = False # true if we're in the middle of a multiline torrc entry for line_number in range(0, len(rendered_contents)): line_text = rendered_contents[line_number] line_text = line_text.rstrip() # remove ending whitespace # blank lines are hidden when stripping comments if self.strip_comments and not line_text: continue # splits the line into its component (msg, format) tuples line_comp = { 'option': ['', (curses.A_BOLD, 'green')], 'argument': ['', (curses.A_BOLD, 'cyan')], 'correction': ['', (curses.A_BOLD, 'cyan')], 'comment': ['', ('white',)], } # parses the comment comment_index = line_text.find('#') if comment_index != -1: line_comp['comment'][0] = line_text[comment_index:] line_text = line_text[:comment_index] # splits the option and argument, preserving any whitespace around them stripped_line = line_text.strip() option_index = stripped_line.find(' ') if is_multiline: # part of a multiline entry started on a previous line so everything # is part of the argument line_comp['argument'][0] = line_text elif option_index == -1: # no argument provided line_comp['option'][0] = line_text else: option_text = stripped_line[:option_index] option_end = line_text.find(option_text) + len(option_text) line_comp['option'][0] = line_text[:option_end] line_comp['argument'][0] = line_text[option_end:] # flags following lines as belonging to this multiline entry if it ends # with a slash if stripped_line: is_multiline = stripped_line.endswith('\\') # gets the correction if line_number in corrections: line_issue, line_issue_msg = corrections[line_number] if line_issue in (tor_config.ValidationError.DUPLICATE, tor_config.ValidationError.IS_DEFAULT): line_comp['option'][1] = (curses.A_BOLD, 'blue') line_comp['argument'][1] = (curses.A_BOLD, 'blue') elif line_issue == tor_config.ValidationError.MISMATCH: line_comp['argument'][1] = (curses.A_BOLD, 'red') line_comp['correction'][0] = ' (%s)' % line_issue_msg else: # For some types of configs the correction field is simply used to # provide extra data (for instance, the type for tor state fields). line_comp['correction'][0] = ' (%s)' % line_issue_msg line_comp['correction'][1] = (curses.A_BOLD, 'magenta') # draws the line number if self.show_line_num and display_line < height and display_line >= 1: line_number_str = ('%%%ii' % (line_number_offset - 1)) % (line_number + 1) self.addstr(display_line, scroll_offset, line_number_str, curses.A_BOLD, 'yellow') # draws the rest of the components with line wrap cursor_location, line_offset = line_number_offset + scroll_offset, 0 max_lines_per_entry = CONFIG['features.config.file.max_lines_per_entry'] display_queue = [line_comp[entry] for entry in ('option', 'argument', 'correction', 'comment')] while display_queue: msg, format = display_queue.pop(0) max_msg_size, include_break = width - cursor_location, False if len(msg) >= max_msg_size: # message is too long - break it up if line_offset == max_lines_per_entry - 1: msg = str_tools.crop(msg, max_msg_size) else: include_break = True msg, remainder = str_tools.crop(msg, max_msg_size, 4, 4, str_tools.Ending.HYPHEN, True) display_queue.insert(0, (remainder.strip(), format)) draw_line = display_line + line_offset if msg and draw_line < height and draw_line >= 1: self.addstr(draw_line, cursor_location, msg, *format) # If we're done, and have added content to this line, then start # further content on the next line. cursor_location += len(msg) include_break |= not display_queue and cursor_location != line_number_offset + scroll_offset if include_break: line_offset += 1 cursor_location = line_number_offset + scroll_offset display_line += max(line_offset, 1) if trust_last_content_height and display_line >= height: break if not trust_last_content_height: self._last_content_height_args = (width, height) new_content_height = display_line + self.scroll - 1 if self._last_content_height != new_content_height: self._last_content_height = new_content_height self.redraw(True)
def draw_line(x, y, width, msg, *attr): msg, remaining_lines = msg.split('\n', 1) if ('\n' in msg) else (msg, '') msg, cropped = str_tools.crop(msg, width - x - 1, min_crop = 4, ending = str_tools.Ending.HYPHEN, get_remainder = True) x = self.addstr(y, x, msg, *attr) return x, (cropped + '\n' + remaining_lines).strip()
def _draw_selection_panel(self, selection, width, detail_panel_height, is_scrollbar_visible): """ Renders a panel for the selected configuration option. """ # This is a solid border unless the scrollbar is visible, in which case a # 'T' pipe connects the border to the bar. ui_tools.draw_box(self, 0, 0, width, detail_panel_height + 1) if is_scrollbar_visible: self.addch(detail_panel_height, 1, curses.ACS_TTEE) selection_format = (curses.A_BOLD, CATEGORY_COLOR[selection.get(Field.CATEGORY)]) # first entry: # <option> (<category> Option) option_label = ' (%s Option)' % selection.get(Field.CATEGORY) self.addstr(1, 2, selection.get(Field.OPTION) + option_label, *selection_format) # second entry: # Value: <value> ([default|custom], <type>, usage: <argument usage>) if detail_panel_height >= 3: value_attr = [] value_attr.append('default' if selection.get(Field.IS_DEFAULT) else 'custom') value_attr.append(selection.get(Field.TYPE)) value_attr.append('usage: %s' % (selection.get(Field.ARG_USAGE))) value_attr_label = ', '.join(value_attr) value_label_width = width - 12 - len(value_attr_label) value_label = str_tools.crop(selection.get(Field.VALUE), value_label_width) self.addstr(2, 2, 'Value: %s (%s)' % (value_label, value_attr_label), *selection_format) # remainder is filled with the man page description description_height = max(0, detail_panel_height - 3) description_content = 'Description: ' + selection.get(Field.DESCRIPTION) for i in range(description_height): # checks if we're done writing the description if not description_content: break # there's a leading indent after the first line if i > 0: description_content = ' ' + description_content # we only want to work with content up until the next newline if '\n' in description_content: line_content, description_content = description_content.split('\n', 1) else: line_content, description_content = description_content, '' if i != description_height - 1: # there's more lines to display msg, remainder = str_tools.crop(line_content, width - 3, 4, 4, str_tools.Ending.HYPHEN, True) description_content = remainder.strip() + description_content else: # this is the last line, end it with an ellipse msg = str_tools.crop(line_content, width - 3, 4, 4) self.addstr(3 + i, 2, msg, *selection_format)
def show_write_dialog(self): """ Provies an interface to confirm if the configuration is saved and, if so, where. """ # display a popup for saving the current configuration config_lines = tor_config.get_custom_options(True) popup, width, height = nyx.popups.init(len(config_lines) + 2) if not popup: return try: # displayed options (truncating the labels if there's limited room) if width >= 30: selection_options = ('Save', 'Save As...', 'Cancel') else: selection_options = ('Save', 'Save As', 'X') # checks if we can show options beside the last line of visible content is_option_line_separate = False last_index = min(height - 2, len(config_lines) - 1) # if we don't have room to display the selection options and room to # grow then display the selection options on its own line if width < (30 + len(config_lines[last_index])): popup.set_height(height + 1) popup.redraw(True) # recreates the window instance new_height, _ = popup.get_preferred_size() if new_height > height: height = new_height is_option_line_separate = True selection = 2 while True: # if the popup has been resized then recreate it (needed for the # proper border height) new_height, new_width = popup.get_preferred_size() if (height, width) != (new_height, new_width): height, width = new_height, new_width popup.redraw(True) # if there isn't room to display the popup then cancel it if height <= 2: selection = 2 break popup.win.erase() popup.win.box() popup.addstr(0, 0, 'Configuration being saved:', curses.A_STANDOUT) visible_config_lines = height - 3 if is_option_line_separate else height - 2 for i in range(visible_config_lines): line = str_tools.crop(config_lines[i], width - 2) if ' ' in line: option, arg = line.split(' ', 1) popup.addstr(i + 1, 1, option, curses.A_BOLD, 'green') popup.addstr(i + 1, len(option) + 2, arg, curses.A_BOLD, 'cyan') else: popup.addstr(i + 1, 1, line, curses.A_BOLD, 'green') # draws selection options (drawn right to left) draw_x = width - 1 for i in range(len(selection_options) - 1, -1, -1): option_label = selection_options[i] draw_x -= (len(option_label) + 2) # if we've run out of room then drop the option (this will only # occure on tiny displays) if draw_x < 1: break selection_format = curses.A_STANDOUT if i == selection else curses.A_NORMAL popup.addstr(height - 2, draw_x, '[') popup.addstr(height - 2, draw_x + 1, option_label, selection_format, curses.A_BOLD) popup.addstr(height - 2, draw_x + len(option_label) + 1, ']') draw_x -= 1 # space gap between the options popup.win.refresh() key = nyx.controller.get_controller().key_input() if key.match('left'): selection = max(0, selection - 1) 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 try: config_location = loaded_torrc.get_config_location() except IOError: config_location = '' if selection == 1: # prompts user for a configuration location config_location = nyx.popups.input_prompt('Save to (esc to cancel): ', config_location) if not config_location: prompt_canceled = True if not prompt_canceled: try: tor_config.save_conf(config_location, config_lines) msg = 'Saved configuration to %s' % config_location except IOError as exc: msg = 'Unable to save configuration (%s)' % exc.strerror nyx.popups.show_msg(msg, 2) finally: nyx.popups.finalize()