Warning
although it seems to be working fine, this piece of code still lacks some serious unittesting. Beware!
Note
This might be worth becoming a project..
"""
Nicely format tables for CLI output
:author: Samuele Santi <[email protected]>
:created: 2012-05-09
"""
import os
import textwrap
def have_color_support():
"""Check whether the current terminal has color support"""
TERM = os.environ.get('TERM')
terms_with_color = ['xterm', 'xterm-color', 'xterm-256color', 'linux', 'screen', 'screen-256color', 'screen-bce']
return (TERM in terms_with_color) or TERM.startswith('xterm')
def safe_unicode(text):
try:
return unicode(str(text), "utf8")
except:
return "???"
def getTerminalSize():
def ioctl_GWINSZ(fd):
try:
import fcntl, termios, struct, os
cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
except:
return None
return cr
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
if not cr:
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
cr = ioctl_GWINSZ(fd)
os.close(fd)
except:
pass
if not cr:
try:
cr = (os.environ['LINES'], os.environ['COLUMNS'])
except:
cr = (25, 80)
return int(cr[1]), int(cr[0])
def data_table(data, header_rows=0, use_colors=None, min_field_width=10,
max_field_width=None, min_table_width=None, max_table_width=None):
"""Nicely format some data in a table
:param data: an iterable of iterables, describing rows/columns
:param min_field_width: minimum width for fields. Defaults to 10,
and autoshrinks to fit max_table_width.
:param max_field_width: DEPRECATED
:param max_table_width: Maximum width for the table
:param use_colors: whether to use colors or not. The default (None)
means autodiscover color support from $TERM
:param header_rows: how many rows should be considered as headers
"""
## Convert data to unicode here, as we need exact lengths to calculate
## field widths
#data = [[unicode(str(col), encoding="utf8") for col in row] for row in data]
data = [[safe_unicode(col) for col in row] for row in data]
_columns = max([len(row) for row in data])
_rows = len(data)
if use_colors is None:
use_colors = have_color_support()
if max_table_width is None:
termw, termh = getTerminalSize()
max_table_width = termw
if min_table_width is None:
## Try to fit the 80% of terminal width
termw, termh = getTerminalSize()
min_table_width = min(max_table_width, termw * 4 / 5)
## Make sure we can respect min_field_width
_expected_min_table_width = min_field_width * _columns
if use_colors:
_expected_min_table_width += 2 * _columns
else:
_expected_min_table_width += (3 * _columns) + 1
if _expected_min_table_width > max_table_width:
if use_colors:
min_field_width = int((max_table_width-1) / _columns) - 3
else:
min_field_width = int((max_table_width) / _columns) - 2
if min_field_width < 1:
raise ValueError("Unable to determine a large enough value for min_field_width")
## Compute required field lengths
required_field_lengths = {}
field_lengths = {}
for row in data:
for col_id, col in enumerate(row):
required_field_lengths[col_id] = max(required_field_lengths.get(col_id, 0), len(col))
## Check what the table width will be
_required_table_length = sum(required_field_lengths.itervalues())
if use_colors:
## Only padding, for colorful tables
_required_table_length += 2 * len(required_field_lengths)
else:
## '| <value> ' ... '|'
_required_table_length += (3 * len(required_field_lengths)) + 1
## Check if we need to resize the table..
_required_delta = 0
if _required_table_length > max_table_width:
_required_delta = max_table_width - _required_table_length
elif _required_table_length < min_table_width:
_required_delta = min_table_width - _required_table_length
if _required_delta:
_tot_lengths = sum(required_field_lengths.itervalues())
for k, v in required_field_lengths.items():
_delta = int(round(_required_delta * 1.0 * v / _tot_lengths))
field_lengths[k] = v + _delta
else:
field_lengths = required_field_lengths.copy()
def row_separator():
return u"".join([u"+" + (u"-" * (f + 2)) for k, f in field_lengths.items()]) + u"+"
sep = ""
for k, f in field_lengths.items():
sep += u"+" + (u"-"*(f+2))
sep += u"+"
return sep
str_table = []
for row_id, row in enumerate(data):
if not use_colors:
str_table.append(row_separator())
_wrapped_row = []
#for col_id, col in enumerate(row):
for col_id in range(len(field_lengths)):
try:
col = row[col_id]
_wrapped_row.append(textwrap.wrap(col, field_lengths[col_id]))
except IndexError:
_wrapped_row.append([])
_subrows = max(*[len(c) for c in _wrapped_row])
for _srid in range(_subrows):
_row_str = u""
for col_id, col in enumerate(_wrapped_row):
if use_colors:
if row_id < header_rows:
_row_str += u"\033[1;37;42m"
elif col_id % 2 == 0:
if row_id % 2 == 1:
_row_str += u"\033[48;5;238m"
else:
_row_str += u"\033[48;5;234m"
else:
if row_id % 2 == 1:
_row_str += u"\033[48;5;240m"
else:
_row_str += u"\033[48;5;236m"
else:
_row_str += u"|"
try:
content = u"%s" % _wrapped_row[col_id][_srid]
except IndexError:
content = u""
_row_str += u" %s " % unicode.ljust(content, field_lengths[col_id])
#_row_str += u" "
if use_colors:
_row_str += "\033[0m"
else:
_row_str += u"|"
str_table.append(_row_str)
if not use_colors:
str_table.append(row_separator())
return u"\n".join(str_table)