Esempio n. 1
0
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))
Esempio n. 2
0
 def test_find_terminal_size(self):
     """Test :func:`humanfriendly.terminal.find_terminal_size()`."""
     lines, columns = find_terminal_size()
     # We really can't assert any minimum or maximum values here because it
     # simply doesn't make any sense; it's impossible for me to anticipate
     # on what environments this test suite will run in the future.
     assert lines > 0
     assert columns > 0
     # The find_terminal_size_using_ioctl() function is the default
     # implementation and it will likely work fine. This makes it hard to
     # test the fall back code paths though. However there's an easy way to
     # make find_terminal_size_using_ioctl() fail ...
     saved_stdin = sys.stdin
     saved_stdout = sys.stdout
     saved_stderr = sys.stderr
     try:
         # What do you mean this is brute force?! ;-)
         sys.stdin = StringIO()
         sys.stdout = StringIO()
         sys.stderr = StringIO()
         # Now find_terminal_size_using_ioctl() should fail even though
         # find_terminal_size_using_stty() might work fine.
         lines, columns = find_terminal_size()
         assert lines > 0
         assert columns > 0
         # There's also an ugly way to make `stty size' fail: The
         # subprocess.Popen class uses os.execvp() underneath, so if we
         # clear the $PATH it will break.
         saved_path = os.environ['PATH']
         try:
             os.environ['PATH'] = ''
             # Now find_terminal_size_using_stty() should fail.
             lines, columns = find_terminal_size()
             assert lines > 0
             assert columns > 0
         finally:
             os.environ['PATH'] = saved_path
     finally:
         sys.stdin = saved_stdin
         sys.stdout = saved_stdout
         sys.stderr = saved_stderr
Esempio n. 3
0
 def test_find_terminal_size(self):
     """Test :func:`humanfriendly.terminal.find_terminal_size()`."""
     lines, columns = find_terminal_size()
     # We really can't assert any minimum or maximum values here because it
     # simply doesn't make any sense; it's impossible for me to anticipate
     # on what environments this test suite will run in the future.
     assert lines > 0
     assert columns > 0
     # The find_terminal_size_using_ioctl() function is the default
     # implementation and it will likely work fine. This makes it hard to
     # test the fall back code paths though. However there's an easy way to
     # make find_terminal_size_using_ioctl() fail ...
     saved_stdin = sys.stdin
     saved_stdout = sys.stdout
     saved_stderr = sys.stderr
     try:
         # What do you mean this is brute force?! ;-)
         sys.stdin = StringIO()
         sys.stdout = StringIO()
         sys.stderr = StringIO()
         # Now find_terminal_size_using_ioctl() should fail even though
         # find_terminal_size_using_stty() might work fine.
         lines, columns = find_terminal_size()
         assert lines > 0
         assert columns > 0
         # There's also an ugly way to make `stty size' fail: The
         # subprocess.Popen class uses os.execvp() underneath, so if we
         # clear the $PATH it will break.
         saved_path = os.environ['PATH']
         try:
             os.environ['PATH'] = ''
             # Now find_terminal_size_using_stty() should fail.
             lines, columns = find_terminal_size()
             assert lines > 0
             assert columns > 0
         finally:
             os.environ['PATH'] = saved_path
     finally:
         sys.stdin = saved_stdin
         sys.stdout = saved_stdout
         sys.stderr = saved_stderr
Esempio n. 4
0
 def render_messages(self, messages):
     """Render the given message(s) on the terminal."""
     previous_conversation = None
     previous_message = None
     # Render a horizontal bar as a delimiter between conversations.
     num_rows, num_columns = find_terminal_size()
     conversation_delimiter = self.generate_html("conversation_delimiter",
                                                 "─" * num_columns)
     for i, msg in enumerate(messages):
         if msg.conversation != previous_conversation:
             # Mark context switches between conversations.
             logger.verbose("Rendering conversation #%i ..",
                            msg.conversation.id)
             self.render_output(conversation_delimiter)
             self.render_output(
                 self.render_conversation_summary(msg.conversation))
             self.render_output(conversation_delimiter)
         elif previous_message and self.keywords:
             # Mark gaps in conversations. This (find_distance()) is a rather
             # heavy check so we only do this when rendering search results.
             distance = msg.find_distance(previous_message)
             if distance > 0:
                 message_delimiter = "── %s omitted " % pluralize(
                     distance, "message")
                 message_delimiter += "─" * int(num_columns -
                                                  len(message_delimiter))
                 self.render_output(
                     self.generate_html("message_delimiter",
                                        message_delimiter))
         # We convert the message metadata and the message text separately,
         # to avoid that a chat message whose HTML contains a single <p> tag
         # causes two newlines to be emitted in between the message metadata
         # and the message text.
         message_metadata = self.prepare_output(" ".join([
             self.render_timestamp(msg.timestamp),
             self.render_backend(msg.conversation.account.backend),
             self.render_contacts(msg),
         ]))
         message_contents = self.normalize_whitespace(
             self.prepare_output(self.render_text(msg)))
         output(message_metadata + " " + message_contents)
         # Keep track of the previous conversation and message.
         previous_conversation = msg.conversation
         previous_message = msg
Esempio n. 5
0
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))
Esempio n. 6
0
def format_smart_table(data, column_names):
    """
    Render tabular data using the most appropriate representation.

    :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
                 containing the rows of the table, where each row is an
                 iterable containing the columns of the table (strings).
    :param column_names: An iterable of column names (strings).
    :returns: The rendered table (a string).

    If you want an easy way to render tabular data on a terminal in a human
    friendly format then this function is for you! It works as follows:

    - If the input data doesn't contain any line breaks the function
      :func:`format_pretty_table()` is used to render a pretty table. If the
      resulting table fits in the terminal without wrapping the rendered pretty
      table is returned.

    - If the input data does contain line breaks or if a pretty table would
      wrap (given the width of the terminal) then the function
      :func:`format_robust_table()` is used to render a more robust table that
      can deal with data containing line breaks and long text.
    """
    # Normalize the input in case we fall back from a pretty table to a robust
    # table (in which case we'll definitely iterate the input more than once).
    data = [normalize_columns(r) for r in data]
    column_names = normalize_columns(column_names)
    # Make sure the input data doesn't contain any line breaks (because pretty
    # tables break horribly when a column's text contains a line break :-).
    if not any(any('\n' in c for c in r) for r in data):
        # Render a pretty table.
        pretty_table = format_pretty_table(data, column_names)
        # Check if the pretty table fits in the terminal.
        table_width = max(map(ansi_width, pretty_table.splitlines()))
        num_rows, num_columns = find_terminal_size()
        if table_width <= num_columns:
            # The pretty table fits in the terminal without wrapping!
            return pretty_table
    # Fall back to a robust table when a pretty table won't work.
    return format_robust_table(data, column_names)
Esempio n. 7
0
def format_smart_table(data, column_names):
    """
    Render tabular data using the most appropriate representation.

    :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
                 containing the rows of the table, where each row is an
                 iterable containing the columns of the table (strings).
    :param column_names: An iterable of column names (strings).
    :returns: The rendered table (a string).

    If you want an easy way to render tabular data on a terminal in a human
    friendly format then this function is for you! It works as follows:

    - If the input data doesn't contain any line breaks the function
      :func:`format_pretty_table()` is used to render a pretty table. If the
      resulting table fits in the terminal without wrapping the rendered pretty
      table is returned.

    - If the input data does contain line breaks or if a pretty table would
      wrap (given the width of the terminal) then the function
      :func:`format_robust_table()` is used to render a more robust table that
      can deal with data containing line breaks and long text.
    """
    # Normalize the input in case we fall back from a pretty table to a robust
    # table (in which case we'll definitely iterate the input more than once).
    data = [normalize_columns(r) for r in data]
    column_names = normalize_columns(column_names)
    # Make sure the input data doesn't contain any line breaks (because pretty
    # tables break horribly when a column's text contains a line break :-).
    if not any(any('\n' in c for c in r) for r in data):
        # Render a pretty table.
        pretty_table = format_pretty_table(data, column_names)
        # Check if the pretty table fits in the terminal.
        table_width = max(map(ansi_width, pretty_table.splitlines()))
        num_rows, num_columns = find_terminal_size()
        if table_width <= num_columns:
            # The pretty table fits in the terminal without wrapping!
            return pretty_table
    # Fall back to a robust table when a pretty table won't work.
    return format_robust_table(data, column_names)
Esempio n. 8
0
def format_robust_table(data, column_names):
    """
    Render tabular data with one column per line (allowing columns with line breaks).

    :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
                 containing the rows of the table, where each row is an
                 iterable containing the columns of the table (strings).
    :param column_names: An iterable of column names (strings).
    :returns: The rendered table (a string).

    Here's an example:

    >>> from humanfriendly.tables import format_robust_table
    >>> column_names = ['Version', 'Uploaded on', 'Downloads']
    >>> humanfriendly_releases = [
    ... ['1.23', '2015-05-25', '218'],
    ... ['1.23.1', '2015-05-26', '1354'],
    ... ['1.24', '2015-05-26', '223'],
    ... ['1.25', '2015-05-26', '4319'],
    ... ['1.25.1', '2015-06-02', '197'],
    ... ]
    >>> print(format_robust_table(humanfriendly_releases, column_names))
    -----------------------
    Version: 1.23
    Uploaded on: 2015-05-25
    Downloads: 218
    -----------------------
    Version: 1.23.1
    Uploaded on: 2015-05-26
    Downloads: 1354
    -----------------------
    Version: 1.24
    Uploaded on: 2015-05-26
    Downloads: 223
    -----------------------
    Version: 1.25
    Uploaded on: 2015-05-26
    Downloads: 4319
    -----------------------
    Version: 1.25.1
    Uploaded on: 2015-06-02
    Downloads: 197
    -----------------------

    The column names are highlighted in bold font and color so they stand out a
    bit more (see :data:`.HIGHLIGHT_COLOR`).
    """
    blocks = []
    column_names = ["%s:" % n for n in normalize_columns(column_names)]
    if terminal_supports_colors():
        column_names = [highlight_column_name(n) for n in column_names]
    # Convert each row into one or more `name: value' lines (one per column)
    # and group each `row of lines' into a block (i.e. rows become blocks).
    for row in data:
        lines = []
        for column_index, column_text in enumerate(normalize_columns(row)):
            stripped_column = column_text.strip()
            if '\n' not in stripped_column:
                # Columns without line breaks are formatted inline.
                lines.append("%s %s" % (column_names[column_index], stripped_column))
            else:
                # Columns with line breaks could very well contain indented
                # lines, so we'll put the column name on a separate line. This
                # way any indentation remains intact, and it's easier to
                # copy/paste the text.
                lines.append(column_names[column_index])
                lines.extend(column_text.rstrip().splitlines())
        blocks.append(lines)
    # Calculate the width of the row delimiter.
    num_rows, num_columns = find_terminal_size()
    longest_line = max(max(map(ansi_width, lines)) for lines in blocks)
    delimiter = u"\n%s\n" % ('-' * min(longest_line, num_columns))
    # Force a delimiter at the start and end of the table.
    blocks.insert(0, "")
    blocks.append("")
    # Embed the row delimiter between every two blocks.
    return delimiter.join(u"\n".join(b) for b in blocks).strip()
Esempio n. 9
0
def format_robust_table(data, column_names):
    """
    Render tabular data with one column per line (allowing columns with line breaks).

    :param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
                 containing the rows of the table, where each row is an
                 iterable containing the columns of the table (strings).
    :param column_names: An iterable of column names (strings).
    :returns: The rendered table (a string).

    Here's an example:

    >>> from humanfriendly.tables import format_robust_table
    >>> column_names = ['Version', 'Uploaded on', 'Downloads']
    >>> humanfriendly_releases = [
    ... ['1.23', '2015-05-25', '218'],
    ... ['1.23.1', '2015-05-26', '1354'],
    ... ['1.24', '2015-05-26', '223'],
    ... ['1.25', '2015-05-26', '4319'],
    ... ['1.25.1', '2015-06-02', '197'],
    ... ]
    >>> print(format_robust_table(humanfriendly_releases, column_names))
    -----------------------
    Version: 1.23
    Uploaded on: 2015-05-25
    Downloads: 218
    -----------------------
    Version: 1.23.1
    Uploaded on: 2015-05-26
    Downloads: 1354
    -----------------------
    Version: 1.24
    Uploaded on: 2015-05-26
    Downloads: 223
    -----------------------
    Version: 1.25
    Uploaded on: 2015-05-26
    Downloads: 4319
    -----------------------
    Version: 1.25.1
    Uploaded on: 2015-06-02
    Downloads: 197
    -----------------------

    The column names are highlighted in bold font and color so they stand out a
    bit more (see :data:`.HIGHLIGHT_COLOR`).
    """
    blocks = []
    column_names = ["%s:" % n for n in normalize_columns(column_names)]
    if terminal_supports_colors():
        column_names = [highlight_column_name(n) for n in column_names]
    # Convert each row into one or more `name: value' lines (one per column)
    # and group each `row of lines' into a block (i.e. rows become blocks).
    for row in data:
        lines = []
        for column_index, column_text in enumerate(normalize_columns(row)):
            stripped_column = column_text.strip()
            if '\n' not in stripped_column:
                # Columns without line breaks are formatted inline.
                lines.append("%s %s" %
                             (column_names[column_index], stripped_column))
            else:
                # Columns with line breaks could very well contain indented
                # lines, so we'll put the column name on a separate line. This
                # way any indentation remains intact, and it's easier to
                # copy/paste the text.
                lines.append(column_names[column_index])
                lines.extend(column_text.rstrip().splitlines())
        blocks.append(lines)
    # Calculate the width of the row delimiter.
    num_rows, num_columns = find_terminal_size()
    longest_line = max(max(map(ansi_width, lines)) for lines in blocks)
    delimiter = u"\n%s\n" % ('-' * min(longest_line, num_columns))
    # Force a delimiter at the start and end of the table.
    blocks.insert(0, "")
    blocks.append("")
    # Embed the row delimiter between every two blocks.
    return delimiter.join(u"\n".join(b) for b in blocks).strip()