def format_usage(usage_text): """ Highlight special items in a usage message. :param usage_text: The usage message to process (a string). :returns: The usage message with special items highlighted. This function highlights the following special items: - The initial line of the form "Usage: ..." - Short and long command line options - Environment variables - Meta variables (see :func:`find_meta_variables()`) All items are highlighted in the color defined by :data:`.HIGHLIGHT_COLOR`. """ # Ugly workaround to avoid circular import errors due to interdependencies # between the humanfriendly.terminal and humanfriendly.usage modules. from humanfriendly.terminal import ansi_wrap, HIGHLIGHT_COLOR formatted_lines = [] meta_variables = find_meta_variables(usage_text) for line in usage_text.strip().splitlines(True): if line.startswith(USAGE_MARKER): # Highlight the "Usage: ..." line in bold font and color. formatted_lines.append(ansi_wrap(line, color=HIGHLIGHT_COLOR)) else: # Highlight options, meta variables and environment variables. formatted_lines.append( replace_special_tokens( line, meta_variables, lambda token: ansi_wrap(token, color=HIGHLIGHT_COLOR), )) return ''.join(formatted_lines)
def demonstrate_256_colors(i, j, group=None): """Demonstrate 256 color mode support.""" # Generate the label. label = '256 color mode' if group: label += ' (%s)' % group output('\n' + ansi_wrap('%s:' % label, bold=True)) # Generate a simple rendering of the colors in the requested range and # check if it will fit on a single line (given the terminal's width). single_line = ''.join(' ' + ansi_wrap(str(n), color=n) for n in range(i, j + 1)) lines, columns = find_terminal_size() if columns >= len(ansi_strip(single_line)): output(single_line) else: # Generate a more complex rendering of the colors that will nicely wrap # over multiple lines without using too many lines. width = len(str(j)) + 1 colors_per_line = int(columns / width) colors = [ ansi_wrap(str(n).rjust(width), color=n) for n in range(i, j + 1) ] blocks = [ colors[n:n + colors_per_line] for n in range(0, len(colors), colors_per_line) ] output('\n'.join(''.join(b) for b in blocks))
def format_usage(usage_text): """ Highlight special items in a usage message. :param usage_text: The usage message to process (a string). :returns: The usage message with special items highlighted. This function highlights the following special items: - The initial line of the form "Usage: ..." - Short and long command line options - Environment variables - Meta variables (see :func:`find_meta_variables()`) All items are highlighted in the color defined by :data:`.HIGHLIGHT_COLOR`. """ # Ugly workaround to avoid circular import errors due to interdependencies # between the humanfriendly.terminal and humanfriendly.usage modules. from humanfriendly.terminal import ansi_wrap, HIGHLIGHT_COLOR formatted_lines = [] meta_variables = find_meta_variables(usage_text) for line in usage_text.strip().splitlines(True): if line.startswith(USAGE_MARKER): # Highlight the "Usage: ..." line in bold font and color. formatted_lines.append(ansi_wrap(line, color=HIGHLIGHT_COLOR)) else: # Highlight options, meta variables and environment variables. formatted_lines.append(replace_special_tokens( line, meta_variables, lambda token: ansi_wrap(token, color=HIGHLIGHT_COLOR), )) return ''.join(formatted_lines)
def test_html_conversion(self): """Check the conversion from ANSI escape sequences to HTML.""" # Check conversion of colored text. for color_name, ansi_code in ANSI_COLOR_CODES.items(): ansi_encoded_text = 'plain text followed by %s text' % ansi_wrap(color_name, color=color_name) expected_html = format( '<code>plain text followed by <span style="color:{css}">{name}</span> text</code>', css=EIGHT_COLOR_PALETTE[ansi_code], name=color_name, ) self.assertEquals(expected_html, convert(ansi_encoded_text)) # Check conversion of bright colored text. expected_html = '<code><span style="color:#FF0">bright yellow</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('bright yellow', color='yellow', bright=True))) # Check conversion of text with a background color. expected_html = '<code><span style="background-color:#DE382B">red background</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('red background', background='red'))) # Check conversion of text with a bright background color. expected_html = '<code><span style="background-color:#F00">bright red background</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('bright red background', background='red', bright=True))) # Check conversion of text that uses the 256 color mode palette as a foreground color. expected_html = '<code><span style="color:#FFAF00">256 color mode foreground</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('256 color mode foreground', color=214))) # Check conversion of text that uses the 256 color mode palette as a background color. expected_html = '<code><span style="background-color:#AF0000">256 color mode background</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('256 color mode background', background=124))) # Check that invalid 256 color mode indexes don't raise exceptions. expected_html = '<code>plain text expected</code>' self.assertEquals(expected_html, convert('\x1b[38;5;256mplain text expected\x1b[0m')) # Check conversion of bold text. expected_html = '<code><span style="font-weight:bold">bold text</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('bold text', bold=True))) # Check conversion of underlined text. expected_html = '<code><span style="text-decoration:underline">underlined text</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('underlined text', underline=True))) # Check conversion of strike-through text. expected_html = '<code><span style="text-decoration:line-through">strike-through text</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('strike-through text', strike_through=True))) # Check conversion of inverse text. expected_html = '<code><span style="background-color:#FFC706;color:#000">inverse</span></code>' self.assertEquals(expected_html, convert(ansi_wrap('inverse', color='yellow', inverse=True))) # Check conversion of URLs. for sample_text in 'www.python.org', 'http://coloredlogs.rtfd.org', 'https://coloredlogs.rtfd.org': sample_url = sample_text if '://' in sample_text else ('http://' + sample_text) expected_html = '<code><a href="%s" style="color:inherit">%s</a></code>' % (sample_url, sample_text) self.assertEquals(expected_html, convert(sample_text)) # Check that the capture pattern for URLs doesn't match ANSI escape # sequences and also check that the short hand for the 0 reset code is # supported. These are tests for regressions of bugs found in # coloredlogs <= 8.0. reset_short_hand = '\x1b[0m' blue_underlined = ansi_style(color='blue', underline=True) ansi_encoded_text = '<%shttps://coloredlogs.readthedocs.io%s>' % (blue_underlined, reset_short_hand) expected_html = ( '<code><<span style="color:#006FB8;text-decoration:underline">' '<a href="https://coloredlogs.readthedocs.io" style="color:inherit">' 'https://coloredlogs.readthedocs.io' '</a></span>></code>' ) self.assertEquals(expected_html, convert(ansi_encoded_text))
def test_html_conversion(self): """Check the conversion from ANSI escape sequences to HTML.""" # Check conversion of colored text. for color_name, ansi_code in ANSI_COLOR_CODES.items(): ansi_encoded_text = 'plain text followed by %s text' % ansi_wrap(color_name, color=color_name) expected_html = format( '<code>plain text followed by <span style="color:{css}">{name}</span> text</code>', css=EIGHT_COLOR_PALETTE[ansi_code], name=color_name, ) self.assertEqual(expected_html, convert(ansi_encoded_text)) # Check conversion of bright colored text. expected_html = '<code><span style="color:#FF0">bright yellow</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('bright yellow', color='yellow', bright=True))) # Check conversion of text with a background color. expected_html = '<code><span style="background-color:#DE382B">red background</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('red background', background='red'))) # Check conversion of text with a bright background color. expected_html = '<code><span style="background-color:#F00">bright red background</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('bright red background', background='red', bright=True))) # Check conversion of text that uses the 256 color mode palette as a foreground color. expected_html = '<code><span style="color:#FFAF00">256 color mode foreground</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('256 color mode foreground', color=214))) # Check conversion of text that uses the 256 color mode palette as a background color. expected_html = '<code><span style="background-color:#AF0000">256 color mode background</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('256 color mode background', background=124))) # Check that invalid 256 color mode indexes don't raise exceptions. expected_html = '<code>plain text expected</code>' self.assertEqual(expected_html, convert('\x1b[38;5;256mplain text expected\x1b[0m')) # Check conversion of bold text. expected_html = '<code><span style="font-weight:bold">bold text</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('bold text', bold=True))) # Check conversion of underlined text. expected_html = '<code><span style="text-decoration:underline">underlined text</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('underlined text', underline=True))) # Check conversion of strike-through text. expected_html = '<code><span style="text-decoration:line-through">strike-through text</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('strike-through text', strike_through=True))) # Check conversion of inverse text. expected_html = '<code><span style="background-color:#FFC706;color:#000">inverse</span></code>' self.assertEqual(expected_html, convert(ansi_wrap('inverse', color='yellow', inverse=True))) # Check conversion of URLs. for sample_text in 'www.python.org', 'http://coloredlogs.rtfd.org', 'https://coloredlogs.rtfd.org': sample_url = sample_text if '://' in sample_text else ('http://' + sample_text) expected_html = '<code><a href="%s" style="color:inherit">%s</a></code>' % (sample_url, sample_text) self.assertEqual(expected_html, convert(sample_text)) # Check that the capture pattern for URLs doesn't match ANSI escape # sequences and also check that the short hand for the 0 reset code is # supported. These are tests for regressions of bugs found in # coloredlogs <= 8.0. reset_short_hand = '\x1b[0m' blue_underlined = ansi_style(color='blue', underline=True) ansi_encoded_text = '<%shttps://coloredlogs.readthedocs.io%s>' % (blue_underlined, reset_short_hand) expected_html = ( '<code><<span style="color:#006FB8;text-decoration:underline">' '<a href="https://coloredlogs.readthedocs.io" style="color:inherit">' 'https://coloredlogs.readthedocs.io' '</a></span>></code>' ) self.assertEqual(expected_html, convert(ansi_encoded_text))
def test_ansi_wrap(self): text = "Whatever" # Make sure ansi_wrap() does nothing when no keyword arguments are given. assert text == ansi_wrap(text) # Make sure ansi_wrap() starts the text with the CSI sequence. assert ansi_wrap(text, bold=True).startswith(ANSI_CSI) # Make sure ansi_wrap() ends the text by resetting the ANSI styles. assert ansi_wrap(text, bold=True).endswith(ANSI_RESET)
def test_ansi_wrap(self): """Test :func:`humanfriendly.terminal.ansi_wrap()`.""" text = "Whatever" # Make sure ansi_wrap() does nothing when no keyword arguments are given. assert text == ansi_wrap(text) # Make sure ansi_wrap() starts the text with the CSI sequence. assert ansi_wrap(text, bold=True).startswith(ANSI_CSI) # Make sure ansi_wrap() ends the text by resetting the ANSI styles. assert ansi_wrap(text, bold=True).endswith(ANSI_RESET)
def test_get_pager_command(self): """Test :func:`humanfriendly.terminal.get_pager_command()`.""" # Make sure --RAW-CONTROL-CHARS isn't used when it's not needed. assert '--RAW-CONTROL-CHARS' not in get_pager_command("Usage message") # Make sure --RAW-CONTROL-CHARS is used when it's needed. assert '--RAW-CONTROL-CHARS' in get_pager_command(ansi_wrap("Usage message", bold=True)) # Make sure that less-specific options are only used when valid. options_specific_to_less = ['--no-init', '--quit-if-one-screen'] for pager in 'cat', 'less': original_pager = os.environ.get('PAGER', None) try: # Set $PAGER to `cat' or `less'. os.environ['PAGER'] = pager # Get the pager command line. command_line = get_pager_command() # Check for less-specific options. if pager == 'less': assert all(opt in command_line for opt in options_specific_to_less) else: assert not any(opt in command_line for opt in options_specific_to_less) finally: if original_pager is not None: # Restore the original $PAGER value. os.environ['PAGER'] = original_pager else: # Clear the custom $PAGER value. os.environ.pop('PAGER')
def format(self, record): """ Apply level-specific styling to log records. :param record: A :class:`~logging.LogRecord` object. :returns: The result of :func:`logging.Formatter.format()`. This method injects ANSI escape sequences that are specific to the level of each log record (because such logic cannot be expressed in the syntax of a log format string). It works by making a copy of the log record, changing the `msg` field inside the copy and passing the copy into the :func:`~logging.Formatter.format()` method of the base class. """ style = self.nn.get(self.level_styles, record.levelname) if style: # Due to the way that Python's logging module is structured and # documented the only (IMHO) clean way to customize its behavior is # to change incoming LogRecord objects before they get to the base # formatter. However we don't want to break other formatters and # handlers, so we'll copy the log record. record = copy.copy(record) record.msg = ansi_wrap(coerce_string(record.msg), **style) # Delegate the remaining formatting to the base formatter. return logging.Formatter.format(self, record)
def callback(match): value = match.group(0) is_meta_variable = re.match('^[A-Z][A-Z0-9_]+$', value) if is_meta_variable and value not in meta_variables: return value else: return ansi_wrap(value, color=HIGHLIGHT_COLOR)
def _output(self, text, color, *args, **kw): self._erase_spinner() if terminal_supports_colors(sys.stdout): text = ansi_wrap(text, color=color) auto_encode(sys.stdout, text, ind=_DETAIL_INDENT, *args, **kw) sys.stdout.flush()
def _output(self, text, color, *args, **kw): self._erase_spinner() text = coerce_string(text) if terminal_supports_colors(sys.stdout): text = ansi_wrap(text, color=color) auto_encode(sys.stdout, text, ind=_DETAIL_INDENT, *args, **kw) sys.stdout.flush()
def test_html_conversion(self): """Check the conversion from ANSI escape sequences to HTML.""" ansi_encoded_text = 'I like %s - www.eelstheband.com' % ansi_wrap('birds', bold=True, color='blue') assert ansi_encoded_text == 'I like \x1b[1;34mbirds\x1b[0m - www.eelstheband.com' html_encoded_text = convert(ansi_encoded_text) assert html_encoded_text == ( 'I like <span style="font-weight: bold; color: blue;">birds</span> - ' '<a href="http://www.eelstheband.com" style="color: inherit;">www.eelstheband.com</a>' )
def _output(self, text, color, *args, **kw): if self._need_to_erase_spinner: if self._progress_spinner and self._progress_spinner.interactive: sys.stdout.write(erase_line_code) self._need_to_erase_spinner = False text = coerce_string(text) if terminal_supports_colors(sys.stdout): text = ansi_wrap(text, color=color) auto_encode(sys.stdout, text, ind=_DETAIL_INDENT, *args, **kw)
def format(self, record): style = self.nn.get(self.level_styles, record.levelname) if style and coloredlogs.Empty is not None: copy = coloredlogs.Empty() copy.__class__ = record.__class__ copy.__dict__.update(record.__dict__) copy.levelname = ansi_wrap(coerce_string(record.levelname), **style) record = copy # Delegate the remaining formatting to the base formatter. return logging.Formatter.format(self, record)
def demonstrate_256_colors(i, j, group=None): """Demonstrate 256 color mode support.""" # Generate the label. label = '256 color mode' if group: label += ' (%s)' % group output('\n' + ansi_wrap('%s:' % label, bold=True)) # Generate a simple rendering of the colors in the requested range and # check if it will fit on a single line (given the terminal's width). single_line = ''.join(' ' + ansi_wrap(str(n), color=n) for n in range(i, j + 1)) lines, columns = find_terminal_size() if columns >= len(ansi_strip(single_line)): output(single_line) else: # Generate a more complex rendering of the colors that will nicely wrap # over multiple lines without using too many lines. width = len(str(j)) + 1 colors_per_line = int(columns / width) colors = [ansi_wrap(str(n).rjust(width), color=n) for n in range(i, j + 1)] blocks = [colors[n:n + colors_per_line] for n in range(0, len(colors), colors_per_line)] output('\n'.join(''.join(b) for b in blocks))
def highlight(text): """ Highlight a piece of text using ANSI escape sequences. :param text: The text to highlight (a string). :returns: The highlighted text (when standard output is connected to a terminal) or the original text (when standard output is not connected to a terminal). """ if terminal_supports_colors(sys.stdout): text = ansi_wrap(text, color=HIGHLIGHT_COLOR) return text
def test_ansi_width(self): text = "Whatever" # Make sure ansi_width() works as expected on strings without ANSI escape sequences. assert len(text) == ansi_width(text) # Wrap a text in ANSI escape sequences and make sure ansi_width() treats it as expected. wrapped = ansi_wrap(text, bold=True) # Make sure ansi_wrap() changed the text. assert wrapped != text # Make sure ansi_wrap() added additional bytes. assert len(wrapped) > len(text) # Make sure the result of ansi_width() stays the same. assert len(text) == ansi_width(wrapped)
def test_ansi_width(self): """Test :func:`humanfriendly.terminal.ansi_width()`.""" text = "Whatever" # Make sure ansi_width() works as expected on strings without ANSI escape sequences. assert len(text) == ansi_width(text) # Wrap a text in ANSI escape sequences and make sure ansi_width() treats it as expected. wrapped = ansi_wrap(text, bold=True) # Make sure ansi_wrap() changed the text. assert wrapped != text # Make sure ansi_wrap() added additional bytes. assert len(wrapped) > len(text) # Make sure the result of ansi_width() stays the same. assert len(text) == ansi_width(wrapped)
def demonstrate_ansi_formatting(): """Demonstrate the use of ANSI escape sequences.""" # First we demonstrate the supported text styles. output('%s', ansi_wrap('Text styles:', bold=True)) styles = ['normal', 'bright'] styles.extend(ANSI_TEXT_STYLES.keys()) for style_name in sorted(styles): options = dict(color=HIGHLIGHT_COLOR) if style_name != 'normal': options[style_name] = True style_label = style_name.replace('_', ' ').capitalize() output(' - %s', ansi_wrap(style_label, **options)) # Now we demonstrate named foreground and background colors. for color_type, color_label in (('color', 'Foreground colors'), ('background', 'Background colors')): intensities = [ ('normal', dict()), ('bright', dict(bright=True)), ] if color_type != 'background': intensities.insert(0, ('faint', dict(faint=True))) output('\n%s' % ansi_wrap('%s:' % color_label, bold=True)) output(format_smart_table([ [color_name] + [ ansi_wrap( 'XXXXXX' if color_type != 'background' else (' ' * 6), **dict(list(kw.items()) + [(color_type, color_name)]) ) for label, kw in intensities ] for color_name in sorted(ANSI_COLOR_CODES.keys()) ], column_names=['Color'] + [ label.capitalize() for label, kw in intensities ])) # Demonstrate support for 256 colors as well. demonstrate_256_colors(0, 7, 'standard colors') demonstrate_256_colors(8, 15, 'high-intensity colors') demonstrate_256_colors(16, 231, '216 colors') demonstrate_256_colors(232, 255, 'gray scale colors')
def demonstrate_ansi_formatting(): """Demonstrate the use of ANSI escape sequences.""" # First we demonstrate the supported text styles. output('%s', ansi_wrap('Text styles:', bold=True)) styles = ['normal', 'bright'] styles.extend(ANSI_TEXT_STYLES.keys()) for style_name in sorted(styles): options = dict(color=HIGHLIGHT_COLOR) if style_name != 'normal': options[style_name] = True style_label = style_name.replace('_', ' ').capitalize() output(' - %s', ansi_wrap(style_label, **options)) # Now we demonstrate named foreground and background colors. for color_type, color_label in (('color', 'Foreground colors'), ('background', 'Background colors')): intensities = [ ('normal', dict()), ('bright', dict(bright=True)), ] if color_type != 'background': intensities.insert(0, ('faint', dict(faint=True))) output('\n%s' % ansi_wrap('%s:' % color_label, bold=True)) output( format_smart_table( [[color_name] + [ ansi_wrap( 'XXXXXX' if color_type != 'background' else (' ' * 6), **dict(list(kw.items()) + [(color_type, color_name)])) for label, kw in intensities ] for color_name in sorted(ANSI_COLOR_CODES.keys())], column_names=['Color'] + [label.capitalize() for label, kw in intensities])) # Demonstrate support for 256 colors as well. demonstrate_256_colors(0, 7, 'standard colors') demonstrate_256_colors(8, 15, 'high-intensity colors') demonstrate_256_colors(16, 231, '216 colors') demonstrate_256_colors(232, 255, 'gray scale colors')
def prepare_prompt_text(prompt_text, **options): """ Wrap a text to be rendered as an interactive prompt in ANSI escape sequences. :param prompt_text: The text to render on the prompt (a string). :param options: Any keyword arguments are passed on to :func:`.ansi_wrap()`. :returns: The resulting prompt text (a string). ANSI escape sequences are only used when the standard output stream is connected to a terminal. When the standard input stream is connected to a terminal any escape sequences are wrapped in "readline hints". """ return (ansi_wrap(prompt_text, readline_hints=connected_to_terminal(sys.stdin), **options) if terminal_supports_colors(sys.stdout) else prompt_text)
def format(self, record): """ Apply level-specific styling to log records. :param record: A :class:`~logging.LogRecord` object. :returns: The result of :func:`logging.Formatter.format()`. This method injects ANSI escape sequences that are specific to the level of each log record (because such logic cannot be expressed in the syntax of a log format string). It works by making a copy of the log record, changing the `msg` field inside the copy and passing the copy into the :func:`~logging.Formatter.format()` method of the base class. """ style = self.nn.get(self.level_styles, record.levelname) # After the introduction of the `Empty' class it was reported in issue # 33 that format() can be called when `Empty' has already been garbage # collected. This explains the (otherwise rather out of place) `Empty # is not None' check in the following `if' statement. The reasoning # here is that it's much better to log a message without formatting # then to raise an exception ;-). # # For more details refer to issue 33 on GitHub: # https://github.com/xolox/python-coloredlogs/issues/33 if style and Empty is not None: # Due to the way that Python's logging module is structured and # documented the only (IMHO) clean way to customize its behavior is # to change incoming LogRecord objects before they get to the base # formatter. However we don't want to break other formatters and # handlers, so we copy the log record. # # In the past this used copy.copy() but as reported in issue 29 # (which is reproducible) this can cause deadlocks. The following # Python voodoo is intended to accomplish the same thing as # copy.copy() without all of the generalization and overhead that # we don't need for our -very limited- use case. # # For more details refer to issue 29 on GitHub: # https://github.com/xolox/python-coloredlogs/issues/29 copy = Empty() copy.__class__ = logging.LogRecord copy.__dict__.update(record.__dict__) copy.msg = ansi_wrap(coerce_string(record.msg), **style) record = copy # Delegate the remaining formatting to the base formatter. return logging.Formatter.format(self, record)
def synchronize_channels(self): """Download messages from named channels.""" response = self.client.channels.list() num_channels = len(response.body["channels"]) for i, channel in enumerate(response.body["channels"], start=1): logger.verbose("Synchronizing #%s channel (%s) ..", channel["name"], channel["id"]) self.spinner.label = "Synchronizing channel %s: %s" % ( "%i/%i" % (i, num_channels), ansi_wrap("#%s" % channel["name"], color=HIGHLIGHT_COLOR), ) self.import_messages( self.client.channels, self.get_or_create_conversation(external_id=channel["id"], is_group_conversation=True, name=("#" + channel["name"])), )
def interpret_script(shell_script): """Make it appear as if commands are typed into the terminal.""" with CaptureOutput() as capturer: shell = subprocess.Popen(['bash', '-'], stdin=subprocess.PIPE) with open(shell_script) as handle: for line in handle: sys.stdout.write(ansi_wrap('$', color='green') + ' ' + line) sys.stdout.flush() shell.stdin.write(line) shell.stdin.flush() shell.stdin.close() time.sleep(12) # Get the text that was shown in the terminal. captured_output = capturer.get_text() # Store the text that was shown in the terminal. filename, extension = os.path.splitext(shell_script) transcript_file = '%s.txt' % filename logger.info("Updating %s ..", format_path(transcript_file)) with open(transcript_file, 'w') as handle: handle.write(ansi_strip(captured_output))
def colorize_format(self, fmt): """ Rewrite a logging format string to inject ANSI escape sequences. :param fmt: The log format string. :returns: The logging format string with ANSI escape sequences. This method takes a logging format string like the ones you give to :class:`logging.Formatter`, splits it into whitespace separated tokens and then processes each token as follows: It looks for ``%(...)`` field names in the token (from left to right). For each field name it checks if the field name has a style defined in the `field_styles` dictionary. The first field name that has a style defined determines the style for the complete token. As an example consider the default log format (:data:`DEFAULT_LOG_FORMAT`):: %(asctime)s %(hostname)s %(name)s[%(process)d] %(levelname)s %(message)s The default field styles (:data:`DEFAULT_FIELD_STYLES`) define a style for the `name` field but not for the `process` field, however because both fields are part of the same whitespace separated token they'll be highlighted together in the style defined for the `name` field. """ result = [] for token in fmt.split(): # Look for field names in the token. for match in re.finditer(r'%\((\w+)\)', token): # Check if a style is defined for the matched field name. style = self.nn.get(self.field_styles, match.group(1)) if style: # If a style is defined we apply it. token = ansi_wrap(token, **style) # The style of the first field name that has a style defined # `wins' (within each whitespace separated token). break result.append(token) return ' '.join(result)
def main(argv=None): """Command line interface ``foreman`` for Foreman. :param argv: Override system arguments (sys.argv[1:]). """ argv = argv or sys.argv[1:] args = docopt(__doc__, version="Foreman Tools %s" % foreman_tools.__version__, argv=argv) logging.basicConfig(level=logging.INFO) urllib3.disable_warnings() foreman_url = args["--url"] or DEFAULT_URL fs = ForemanSession(foreman_url=foreman_url) if args["list"] and args["hosts"]: # https://www.theforeman.org/api/1.14/apidoc/v2/hosts.html#description-index count, hosts = fs.get_all("hosts") for host in hosts: last_report = datetime.strptime(host["last_report"], "%Y-%m-%dT%H:%M:%SZ") last_report_age = (datetime.utcnow() - last_report).total_seconds() if last_report_age <= 60 * 60: age_color = "green" elif last_report_age <= 2 * 60 * 60: age_color = "yellow" else: age_color = "red" print( "%-40s %-15s %-10s" % (host["name"], host["ip"], ansi_wrap(format_timespan(last_report_age), color=age_color))) elif args["start"] and args["<host>"]: fs.power(args["<host>"], "start") elif args["stop"] and args["<host>"]: fs.power(args["<host>"], "stop")
def format_usage(usage_text): """ Highlight special items in a usage message. :param usage_text: The usage message to process (a string). :returns: The usage message with special items highlighted. This function highlights the following special items: - The initial line of the form "Usage: ..." - Short and long command line options - Environment variables - Meta variables (see :func:`find_meta_variables()`) All items are highlighted in the color defined by :data:`.HIGHLIGHT_COLOR`. """ # Ugly workaround to avoid circular import errors due to interdependencies # between the humanfriendly.terminal and humanfriendly.usage modules. from humanfriendly.terminal import ansi_wrap, HIGHLIGHT_COLOR formatted_lines = [] meta_variables = find_meta_variables(usage_text) for line in usage_text.strip().splitlines(True): if line.startswith('Usage:'): # Highlight the "Usage: ..." line in bold font and color. formatted_lines.append(ansi_wrap(line, color=HIGHLIGHT_COLOR)) else: # Highlight options, meta variables and environment variables. def callback(match): value = match.group(0) is_meta_variable = re.match('^[A-Z][A-Z0-9_]+$', value) if is_meta_variable and value not in meta_variables: return value else: return ansi_wrap(value, color=HIGHLIGHT_COLOR) formatted_lines.append(USAGE_PATTERN.sub(callback, line)) return ''.join(formatted_lines)
def format(self, width: int) -> str: value = self._format_value(width) if have_ansi: return ansi_wrap(value, color='white', bold=not self.stale) else: return value if not self.stale else f"({value})"
def prompt_for_choice(choices, default=None, padding=True): """ Prompt the user to select a choice from a group of options. :param choices: A sequence of strings with available options. :param default: The default choice if the user simply presses Enter (expected to be a string, defaults to :data:`None`). :param padding: Refer to the documentation of :func:`prompt_for_input()`. :returns: The string corresponding to the user's choice. :raises: - :exc:`~exceptions.ValueError` if `choices` is an empty sequence. - Any exceptions raised by :func:`retry_limit()`. - Any exceptions raised by :func:`prompt_for_input()`. When no options are given an exception is raised: >>> prompt_for_choice([]) Traceback (most recent call last): File "humanfriendly/prompts.py", line 148, in prompt_for_choice raise ValueError("Can't prompt for choice without any options!") ValueError: Can't prompt for choice without any options! If a single option is given the user isn't prompted: >>> prompt_for_choice(['only one choice']) 'only one choice' Here's what the actual prompt looks like by default: >>> prompt_for_choice(['first option', 'second option']) <BLANKLINE> 1. first option 2. second option <BLANKLINE> Enter your choice as a number or unique substring (Control-C aborts): second <BLANKLINE> 'second option' If you don't like the whitespace (empty lines and indentation): >>> prompt_for_choice(['first option', 'second option'], padding=False) 1. first option 2. second option Enter your choice as a number or unique substring (Control-C aborts): first 'first option' """ indent = ' ' if padding else '' # Make sure we can use 'choices' more than once (i.e. not a generator). choices = list(choices) if len(choices) == 1: # If there's only one option there's no point in prompting the user. logger.debug( "Skipping interactive prompt because there's only option (%r).", choices[0]) return choices[0] elif not choices: # We can't render a choice prompt without any options. raise ValueError("Can't prompt for choice without any options!") # Generate the prompt text. prompt_text = ('\n\n' if padding else '\n').join([ # Present the available choices in a user friendly way. "\n".join([(u" %i. %s" % (i, choice)) + (" (default choice)" if choice == default else "") for i, choice in enumerate(choices, start=1)]), # Instructions for the user. "Enter your choice as a number or unique substring (Control-C aborts): ", ]) if terminal_supports_colors(): prompt_text = ansi_wrap(prompt_text, bold=True, readline_hints=True) # Loop until a valid choice is made. logger.debug( "Requesting interactive choice on terminal (options are %s) ..", concatenate(map(repr, choices))) for attempt in retry_limit(): reply = prompt_for_input(prompt_text, '', padding=padding, strip=True) if not reply and default is not None: logger.debug("Default choice selected by empty reply (%r).", default) return default elif reply.isdigit(): index = int(reply) - 1 if 0 <= index < len(choices): logger.debug("Option (%r) selected by numeric reply (%s).", choices[index], reply) return choices[index] # Check for substring matches. matches = [] for choice in choices: lower_reply = reply.lower() lower_choice = choice.lower() if lower_reply == lower_choice: # If we have an 'exact' match we return it immediately. logger.debug("Option (%r) selected by reply (exact match).", choice) return choice elif lower_reply in lower_choice and len(lower_reply) > 0: # Otherwise we gather substring matches. matches.append(choice) if len(matches) == 1: # If a single choice was matched we return it. logger.debug( "Option (%r) selected by reply (substring match on %r).", matches[0], reply) return matches[0] else: # Give the user a hint about what went wrong. if matches: details = format("text '%s' matches more than one choice: %s", reply, concatenate(matches)) elif reply.isdigit(): details = format("number %i is not a valid choice", int(reply)) elif reply and not reply.isspace(): details = format("text '%s' doesn't match any choices", reply) else: details = "there's no default choice" logger.debug("Got %s reply (%s), retrying (%i/%i) ..", "invalid" if reply else "empty", details, attempt, MAX_ATTEMPTS) warning("%sError: Invalid input (%s).", indent, details)
def highlight_column_name(name): return ansi_wrap(name, bold=True, color=HIGHLIGHT_COLOR)
def main(): """Command line interface for the ``apache-manager`` program.""" # Configure logging output. coloredlogs.install() # Command line option defaults. data_file = '/tmp/apache-manager.txt' dry_run = False max_memory_active = None max_memory_idle = None max_ss = None watch = False zabbix_discovery = False verbosity = 0 # Parse the command line options. try: options, arguments = getopt.getopt(sys.argv[1:], 'wa:i:t:f:znvqh', [ 'watch', 'max-memory-active=', 'max-memory-idle=', 'max-ss=', 'max-time=', 'data-file=', 'zabbix-discovery', 'dry-run', 'simulate', 'verbose', 'quiet', 'help', ]) for option, value in options: if option in ('-w', '--watch'): watch = True elif option in ('-a', '--max-memory-active'): max_memory_active = parse_size(value) elif option in ('-i', '--max-memory-idle'): max_memory_idle = parse_size(value) elif option in ('-t', '--max-ss', '--max-time'): max_ss = parse_timespan(value) elif option in ('-f', '--data-file'): data_file = value elif option in ('-z', '--zabbix-discovery'): zabbix_discovery = True elif option in ('-n', '--dry-run', '--simulate'): logger.info("Performing a dry run ..") dry_run = True elif option in ('-v', '--verbose'): coloredlogs.increase_verbosity() verbosity += 1 elif option in ('-q', '--quiet'): coloredlogs.decrease_verbosity() verbosity -= 1 elif option in ('-h', '--help'): usage(__doc__) return except Exception as e: sys.stderr.write("Error: %s!\n" % e) sys.exit(1) # Execute the requested action(s). manager = ApacheManager() try: if max_memory_active or max_memory_idle or max_ss: manager.kill_workers( max_memory_active=max_memory_active, max_memory_idle=max_memory_idle, timeout=max_ss, dry_run=dry_run, ) if watch and connected_to_terminal(sys.stdout): watch_metrics(manager) elif zabbix_discovery: report_zabbix_discovery(manager) elif data_file != '-' and verbosity >= 0: for line in report_metrics(manager): if line_is_heading(line): line = ansi_wrap(line, color=HIGHLIGHT_COLOR) print(line) finally: if (not watch) and (data_file == '-' or not dry_run): manager.save_metrics(data_file)
def wrap_style(self, text, **kw): """ Wrapper for :py:func:`ansi_text()` that's disabled when ``isatty=False``. """ return ansi_wrap(text, **kw) if self.isatty else text
def main(): """Command line interface for the ``apache-manager`` program.""" # Configure logging output. coloredlogs.install(syslog=True) # Command line option defaults. data_file = '/tmp/apache-manager.txt' dry_run = False max_memory_active = None max_memory_idle = None max_ss = None watch = False zabbix_discovery = False verbosity = 0 # Parse the command line options. try: options, arguments = getopt.getopt(sys.argv[1:], 'wa:i:t:f:znvqh', [ 'watch', 'max-memory-active=', 'max-memory-idle=', 'max-ss=', 'max-time=', 'data-file=', 'zabbix-discovery', 'dry-run', 'simulate', 'verbose', 'quiet', 'help', ]) for option, value in options: if option in ('-w', '--watch'): watch = True elif option in ('-a', '--max-memory-active'): max_memory_active = parse_size(value) elif option in ('-i', '--max-memory-idle'): max_memory_idle = parse_size(value) elif option in ('-t', '--max-ss', '--max-time'): max_ss = parse_timespan(value) elif option in ('-f', '--data-file'): data_file = value elif option in ('-z', '--zabbix-discovery'): zabbix_discovery = True elif option in ('-n', '--dry-run', '--simulate'): logger.info("Performing a dry run ..") dry_run = True elif option in ('-v', '--verbose'): coloredlogs.increase_verbosity() verbosity += 1 elif option in ('-q', '--quiet'): coloredlogs.decrease_verbosity() verbosity -= 1 elif option in ('-h', '--help'): usage(__doc__) return except Exception as e: sys.stderr.write("Error: %s!\n" % e) sys.exit(1) # Execute the requested action(s). manager = ApacheManager() try: if max_memory_active or max_memory_idle or max_ss: manager.kill_workers( max_memory_active=max_memory_active, max_memory_idle=max_memory_idle, timeout=max_ss, dry_run=dry_run, ) elif watch and connected_to_terminal(sys.stdout): watch_metrics(manager) elif zabbix_discovery: report_zabbix_discovery(manager) elif data_file != '-' and verbosity >= 0: for line in report_metrics(manager): if line_is_heading(line): line = ansi_wrap(line, color=HIGHLIGHT_COLOR) print(line) finally: if (not watch) and (data_file == '-' or not dry_run): manager.save_metrics(data_file)
def test_html_conversion(self): ansi_encoded_text = 'I like %s - www.eelstheband.com' % ansi_wrap('birds', bold=True, color='blue') assert ansi_encoded_text == 'I like \x1b[1;34mbirds\x1b[0m - www.eelstheband.com' html_encoded_text = coloredlogs.converter.convert(ansi_encoded_text) assert html_encoded_text == 'I like <span style="font-weight: bold; color: blue;">birds</span> - <a href="http://www.eelstheband.com" style="color: inherit;">www.eelstheband.com</a>'
def title(self, value, underline='='): self.print( ansi_wrap(value, bold=True, color=HIGHLIGHT_COLOR), underline * len(value), sep='\n' )
def format_text(self, include_password=True, use_colors=None, padding=True, filters=()): """ Format :attr:`text` for viewing on a terminal. :param include_password: :data:`True` to include the password in the formatted text, :data:`False` to exclude the password from the formatted text. :param use_colors: :data:`True` to use ANSI escape sequences, :data:`False` otherwise. When this is :data:`None` :func:`~humanfriendly.terminal.terminal_supports_colors()` will be used to detect whether ANSI escape sequences are supported. :param padding: :data:`True` to add empty lines before and after the entry and indent the entry's text with two spaces, :data:`False` to skip the padding. :param filters: An iterable of regular expression patterns (defaults to an empty tuple). If a line in the entry's text matches one of these patterns it won't be shown on the terminal. :returns: The formatted entry (a string). """ # Determine whether we can use ANSI escape sequences. if use_colors is None: use_colors = terminal_supports_colors() # Extract the password (first line) from the entry. lines = self.text.splitlines() password = lines.pop(0).strip() # Compile the given patterns to case insensitive regular expressions # and use them to ignore lines that match any of the given filters. patterns = [coerce_pattern(f, re.IGNORECASE) for f in filters] lines = [l for l in lines if not any(p.search(l) for p in patterns)] text = trim_empty_lines("\n".join(lines)) # Include the password in the formatted text? if include_password: text = "Password: %s\n%s" % (password, text) # Add the name to the entry (only when there's something to show). if text and not text.isspace(): title = " / ".join(split(self.name, "/")) if use_colors: title = ansi_wrap(title, bold=True) text = "%s\n\n%s" % (title, text) # Highlight the entry's text using ANSI escape sequences. lines = [] for line in text.splitlines(): # Check for a "Key: Value" line. match = KEY_VALUE_PATTERN.match(line) if match: key = "%s:" % match.group(1).strip() value = match.group(2).strip() if use_colors: # Highlight the key. key = ansi_wrap(key, color=HIGHLIGHT_COLOR) # Underline hyperlinks in the value. tokens = value.split() for i in range(len(tokens)): if "://" in tokens[i]: tokens[i] = ansi_wrap(tokens[i], underline=True) # Replace the line with a highlighted version. line = key + " " + " ".join(tokens) if padding: line = " " + line lines.append(line) text = "\n".join(lines) text = trim_empty_lines(text) if text and padding: text = "\n%s\n" % text return text
def prompt_for_choice(choices, default=None, padding=True): """ Prompt the user to select a choice from a group of options. :param choices: A sequence of strings with available options. :param default: The default choice if the user simply presses Enter (expected to be a string, defaults to :data:`None`). :param padding: Refer to the documentation of :func:`prompt_for_input()`. :returns: The string corresponding to the user's choice. :raises: - :exc:`~exceptions.ValueError` if `choices` is an empty sequence. - Any exceptions raised by :func:`retry_limit()`. - Any exceptions raised by :func:`prompt_for_input()`. When no options are given an exception is raised: >>> prompt_for_choice([]) Traceback (most recent call last): File "humanfriendly/prompts.py", line 148, in prompt_for_choice raise ValueError("Can't prompt for choice without any options!") ValueError: Can't prompt for choice without any options! If a single option is given the user isn't prompted: >>> prompt_for_choice(['only one choice']) 'only one choice' Here's what the actual prompt looks like by default: >>> prompt_for_choice(['first option', 'second option']) <BLANKLINE> 1. first option 2. second option <BLANKLINE> Enter your choice as a number or unique substring (Control-C aborts): second <BLANKLINE> 'second option' If you don't like the whitespace (empty lines and indentation): >>> prompt_for_choice(['first option', 'second option'], padding=False) 1. first option 2. second option Enter your choice as a number or unique substring (Control-C aborts): first 'first option' """ indent = ' ' if padding else '' # Make sure we can use 'choices' more than once (i.e. not a generator). choices = list(choices) if len(choices) == 1: # If there's only one option there's no point in prompting the user. logger.debug("Skipping interactive prompt because there's only option (%r).", choices[0]) return choices[0] elif not choices: # We can't render a choice prompt without any options. raise ValueError("Can't prompt for choice without any options!") # Generate the prompt text. prompt_text = ('\n\n' if padding else '\n').join([ # Present the available choices in a user friendly way. "\n".join([ (u" %i. %s" % (i, choice)) + (" (default choice)" if choice == default else "") for i, choice in enumerate(choices, start=1) ]), # Instructions for the user. "Enter your choice as a number or unique substring (Control-C aborts): ", ]) if terminal_supports_colors(): prompt_text = ansi_wrap(prompt_text, bold=True, readline_hints=True) # Loop until a valid choice is made. logger.debug("Requesting interactive choice on terminal (options are %s) ..", concatenate(map(repr, choices))) for attempt in retry_limit(): reply = prompt_for_input(prompt_text, '', padding=padding, strip=True) if not reply and default is not None: logger.debug("Default choice selected by empty reply (%r).", default) return default elif reply.isdigit(): index = int(reply) - 1 if 0 <= index < len(choices): logger.debug("Option (%r) selected by numeric reply (%s).", choices[index], reply) return choices[index] # Check for substring matches. matches = [] for choice in choices: lower_reply = reply.lower() lower_choice = choice.lower() if lower_reply == lower_choice: # If we have an 'exact' match we return it immediately. logger.debug("Option (%r) selected by reply (exact match).", choice, reply) return choice elif lower_reply in lower_choice and len(lower_reply) > 0: # Otherwise we gather substring matches. matches.append(choice) if len(matches) == 1: # If a single choice was matched we return it. logger.debug("Option (%r) selected by reply (substring match on %r).", matches[0], reply) return matches[0] else: # Give the user a hint about what went wrong. if matches: details = format("text '%s' matches more than one choice: %s", reply, concatenate(matches)) elif reply.isdigit(): details = format("number %i is not a valid choice", int(reply)) elif reply and not reply.isspace(): details = format("text '%s' doesn't match any choices", reply) else: details = "there's no default choice" logger.debug("Got %s reply (%s), retrying (%i/%i) ..", "invalid" if reply else "empty", details, attempt, MAX_ATTEMPTS) warning("%sError: Invalid input (%s).", indent, details)
def format_item(self, key: str, val: Any) -> str: return "{}={}".format( (ansi_wrap(key, **self.style_key) if self.color else key), (ansi_wrap(repr(val), **self.style_val) if self.color else repr(val)))
def render_summary(self): """Render a summary of installed and removable kernel packages on the terminal.""" logger.verbose("Sanity checking meta packages on %s ..", self.context) with AutomaticSpinner(label="Gathering information about %s" % self.context): # Report the installed Linux kernel image meta package(s). if self.installed_image_meta_packages: logger.info( "Found %s installed:", pluralize(len(self.installed_image_meta_packages), "Linux kernel image meta package"), ) for package in self.installed_image_meta_packages: logger.info(" - %s (%s)", package.name, package.version) if len(self.installed_image_meta_packages) > 1: names = concatenate( pkg.name for pkg in self.installed_image_meta_packages) logger.warning( compact( """ You have more than one Linux kernel image meta package installed ({names}) which means automatic package removal can be unreliable! """, names=names, )) logger.verbose( compact(""" I would suggest to stick to one Linux kernel image meta package, preferably the one that matches the newest kernel :-) """)) else: logger.warning( compact(""" It looks like there's no Linux kernel image meta package installed! I hope you've thought about how to handle security updates? """)) # Report the installed Linux kernel header/image package(s). logger.verbose("Checking for removable packages on %s ..", self.context) package_types = ( (self.installed_kernel_packages, "image", True), (self.installed_header_packages, "header", False), (self.installed_modules_packages, "modules", False), ) for collection, label, expected in package_types: if collection: logger.info( "Found %s:", pluralize(len(collection), "installed Linux kernel %s package" % label)) for group in self.installed_package_groups: matching_packages = sorted(package.name for package in group if package in collection) active_group = any( package.name == self.active_kernel_package for package in group) removable_group = group in self.removable_package_groups if matching_packages: logger.info( " - %s (%s)", concatenate(matching_packages), ansi_wrap("removable", color="green") if removable_group else ansi_wrap( "the active kernel" if active_group else ("one of %i newest kernels" % self.preserve_count), color="blue", ), ) elif expected: logger.warning( "No installed %s packages found, this can't be right?!", label) # Report the removable packages. if self.removable_packages: logger.info("Found %s that can be removed.", pluralize(len(self.removable_packages), "package")) # Report the shell command to remove the packages. logger.verbose("Command to remove packages: %s", " ".join(self.cleanup_command)) else: logger.info("No packages need to be removed! :-)")
def title(self, value, underline='='): self.print([ansi_wrap(value, bold=True, color=HIGHLIGHT_COLOR), underline * len(value)], sep='\n')
def prompt_for_confirmation(question, default=None, padding=True): """ Prompt the user for confirmation. :param question: The text that explains what the user is confirming (a string). :param default: The default value (a boolean) or :data:`None`. :param padding: Refer to the documentation of :func:`prompt_for_input()`. :returns: - If the user enters 'yes' or 'y' then :data:`True` is returned. - If the user enters 'no' or 'n' then :data:`False` is returned. - If the user doesn't enter any text or standard input is not connected to a terminal (which makes it impossible to prompt the user) the value of the keyword argument ``default`` is returned (if that value is not :data:`None`). :raises: - Any exceptions raised by :func:`retry_limit()`. - Any exceptions raised by :func:`prompt_for_input()`. When `default` is :data:`False` and the user doesn't enter any text an error message is printed and the prompt is repeated: >>> prompt_for_confirmation("Are you sure?") <BLANKLINE> Are you sure? [y/n] <BLANKLINE> Error: Please enter 'yes' or 'no' (there's no default choice). <BLANKLINE> Are you sure? [y/n] The same thing happens when the user enters text that isn't recognized: >>> prompt_for_confirmation("Are you sure?") <BLANKLINE> Are you sure? [y/n] about what? <BLANKLINE> Error: Please enter 'yes' or 'no' (the text 'about what?' is not recognized). <BLANKLINE> Are you sure? [y/n] """ # Generate the text for the prompt. prompt_text = question if terminal_supports_colors(): prompt_text = ansi_wrap(prompt_text, bold=True, readline_hints=True) # Append the valid replies (and default reply) to the prompt text. hint = "[Y/n]" if default else "[y/N]" if default is not None else "[y/n]" if terminal_supports_colors(): hint = ansi_wrap(hint, color=HIGHLIGHT_COLOR, readline_hints=True) prompt_text += " %s " % hint # Loop until a valid response is given. logger.debug("Requesting interactive confirmation from terminal: %r", ansi_strip(prompt_text).rstrip()) for attempt in retry_limit(): reply = prompt_for_input(prompt_text, '', padding=padding, strip=True) if reply.lower() in ('y', 'yes'): logger.debug("Confirmation granted by reply (%r).", reply) return True elif reply.lower() in ('n', 'no'): logger.debug("Confirmation denied by reply (%r).", reply) return False elif (not reply) and default is not None: logger.debug("Default choice selected by empty reply (%r).", "granted" if default else "denied") return default else: details = ("the text '%s' is not recognized" % reply if reply else "there's no default choice") logger.debug("Got %s reply (%s), retrying (%i/%i) ..", "invalid" if reply else "empty", details, attempt, MAX_ATTEMPTS) warning("{indent}Error: Please enter 'yes' or 'no' ({details}).", indent=' ' if padding else '', details=details)
def format_text(self, include_password=True, use_colors=None, padding=True): """ Format :attr:`text` for viewing on a terminal. :param include_password: :data:`True` to include the password in the formatted text, :data:`False` to exclude the password from the formatted text. :param use_colors: :data:`True` to use ANSI escape sequences, :data:`False` otherwise. When this is :data:`None` :func:`~humanfriendly.terminal.terminal_supports_colors()` will be used to detect whether ANSI escape sequences are supported. :param padding: :data:`True` to add empty lines before and after the entry and indent the entry's text with two spaces, :data:`False` to skip the padding. :returns: The formatted entry (a string). """ # Determine whether we can use ANSI escape sequences. if use_colors is None: use_colors = terminal_supports_colors() # Extract the password (first line) from the entry. lines = self.text.splitlines() password = lines.pop(0).strip() text = trim_empty_lines('\n'.join(lines)) # Include the password in the formatted text? if include_password: text = "Password: %s\n%s" % (password, text) # Add the name to the entry (only when there's something to show). if text and not text.isspace(): title = ' / '.join(split(self.name, '/')) if use_colors: title = ansi_wrap(title, bold=True) text = "%s\n\n%s" % (title, text) # Highlight the entry's text using ANSI escape sequences. lines = [] for line in text.splitlines(): # Check for a "Key: Value" line. match = KEY_VALUE_PATTERN.match(line) if match: key = "%s:" % match.group(1).strip() value = match.group(2).strip() if use_colors: # Highlight the key. key = ansi_wrap(key, color=HIGHLIGHT_COLOR) # Underline hyperlinks in the value. tokens = value.split() for i in range(len(tokens)): if '://' in tokens[i]: tokens[i] = ansi_wrap(tokens[i], underline=True) # Replace the line with a highlighted version. line = key + ' ' + ' '.join(tokens) if padding: line = ' ' + line lines.append(line) text = '\n'.join(lines) if text and padding: text = '\n%s\n' % text return text