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 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
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
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_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)
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()