Source code for lnk.beauty
#!/usr/bin/env python
#! -*- coding: utf-8 -*-
"""Data beautification."""
from __future__ import unicode_literals
from __future__ import division
import math
import os
import re
import sys
from collections import namedtuple
import lnk.errors
MAX_WIDTH = 60 # 3/4 of 80
# If we are connected to a terminal right now
if sys.stdin.isatty():
with os.popen('stty size', 'r') as process:
output = process.read()
# Doesn't always work?
if output:
MAX_WIDTH = 3 * int(output.split()[1])//4
Line = namedtuple('Line', ['raw', 'escaped'])
[docs]def boxify(results):
"""
Formats results of command into a box.
Arguments:
results (list): A list of results, where each result is a list of lines
(e.g. all the lines for the statistics of one URL).
Returns:
The results in a pretty box, as a string.
"""
if not results:
raise lnk.errors.InternalError('Cannot boxify empty results!')
results, width = get_escaped(results)
border = '─' * (width + 2)
lines = ['┌{0}┐'.format(border)]
for n, result in enumerate(results):
m = 0
# We might add lines into the list mid-iteration
while m < len(result):
line = result[m]
if len(line.escaped) > width:
result = result[:m] + wrap(line, width) + result[m + 1:]
else:
adjusted = ljust(line, width)
lines.append('│ {0} │'.format(adjusted))
m += 1
if n + 1 < len(results):
lines.append('├{0}┤'.format(border))
lines += ['└{0}┘'.format(border)]
return '\n'.join(lines)
[docs]def wrap(line, width, indent=None):
"""
Wraps a line to a certain width.
Why not textwrap.wrap? Because in this special situation it is necessary
to wrap not only the escaped, plain-text string but *at the same time*
also the raw string containing escape codes, which, however, should not
count towards the length of a string. This method essentially wraps the
plain-text string, while at the same time moving also through the raw
string, but skipping escape-character-sequences when counting characters.
Arguments:
line (beauty.Line): The beauty.Line object to wrap.
width (int): The width to which to wrap.
indent (str): An indent string with which to indent all lines after the
first wrapped one.
"""
indent = indent or ' ' * int(math.ceil(width/10))
escape_code = re.compile(r'\033\[(\d;?)+m')
r_start = 0
r = escape_code.match(line.raw)
r = r.end() if r else 0
e = 0
e_start = 0
wrapped = []
while e <= len(line.escaped):
w = width - len(indent) if len(wrapped) else width
if (e > 0 and (e - e_start) == w) or (e == len(line.escaped)):
stop = e_start - 1 if e_start else None
reverse = line.escaped[e - 1:stop:-1]
boundary = re.search(r'\w(?=\b)', reverse)
index = e_start + (len(reverse) - boundary.end())
# Only go backwards to the last word boundary if that
# is not where we started (else just split it mid-word here).
# If we went back to the start, we'd get an endless loop
if e < len(line.escaped) and index != e_start:
# Go backwards to the last word boundary
while e > index:
match = re.match(r'm(;?\d)+\[\033', line.raw[e::-1])
if match:
r -= match.end()
else:
e -= 1
r -= 1
# If we already have one string wrapped
if len(wrapped) > 0:
raw = indent + line.raw[r_start:r]
escaped = indent + line.escaped[e_start:e]
else:
raw = line.raw[r_start:r]
escaped = line.escaped[e_start:e]
wrapped.append(Line(raw, escaped))
e_start = e
r_start = r
e += 1
r += 1
# Skip escape codes for the raw string
match = escape_code.match(line.raw, r)
if match:
r = match.end()
return wrapped
[docs]def ljust(line, width, padding=' '):
"""
Adds space-padding to the right of a line.
ljust won't do because of the escaped/raw thing.
Arguments:
line (str): The line to adjust.
width (int): The minimum width the string must have after this function.
padding (str): Optionally, the string with which to pad (usually ' ').
Returns:
The adjusted raw line.
"""
return line.raw + padding * (width - len(line.escaped))
[docs]def get_escaped(results):
"""
Gets escaped lines for the results passed to boxify().
Arguments:
results (list): The results passed to boxify().
Returns:
A list of beauty.Line objects with a raw and escaped component.
"""
width = 0
escaped = []
for result in results:
lines = []
for line in result:
line = escape(line)
if len(line.escaped) > width:
width = len(line.escaped)
lines.append(line)
escaped.append(lines)
return escaped, min([MAX_WIDTH, width])
[docs]def escape(line):
"""
Escapes a raw string.
Arguments:
line (str): The raw string to escape.
Returns:
A beauty.Line object with the original raw component and the same
string, but escaped, i.e. without the formatting codes applied by
ecstasy -- only the text.
"""
pattern = re.compile(r'^(.*)' # anything
r'(?:\033\[(?:\d;?)+m)' # escape codes
r'(.+)' # formatted string
r'(?:\033\[(?:\d;?)+m)' # escape codes
r'(.*)$') # anything
line = line.rstrip()
if '\033' in line:
match = pattern.search(line)
if not match:
raise lnk.errors.InternalError("Could not parse '{0}'".format(line))
escaped = ''.join([i for i in match.groups() if i])
else:
escaped = line
return Line(line, escaped)