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)