| 1 |
# -*- coding: utf-8 -*- |
|---|
| 2 |
# |
|---|
| 3 |
# Copyright (C) 2006-2007 Alec Thomas <alec@swapoff.org> |
|---|
| 4 |
# |
|---|
| 5 |
# This software is licensed as described in the file COPYING, which |
|---|
| 6 |
# you should have received as part of this distribution. |
|---|
| 7 |
# |
|---|
| 8 |
|
|---|
| 9 |
"""Console/terminal interaction classes and functions. |
|---|
| 10 |
|
|---|
| 11 |
This module provides a simple formatting syntax for basic terminal visual |
|---|
| 12 |
control sequences. The syntax is a carat ``^`` followed by a single character. |
|---|
| 13 |
|
|---|
| 14 |
Valid colour escape sequences are: |
|---|
| 15 |
|
|---|
| 16 |
:``^N``: Reset all formatting. |
|---|
| 17 |
:``^B``: Toggle bold. |
|---|
| 18 |
:``^U``: Toggle underline. |
|---|
| 19 |
:``^0``: Set black foreground. |
|---|
| 20 |
:``^1``: Set red foreground. |
|---|
| 21 |
:``^2``: Set green foreground. |
|---|
| 22 |
:``^3``: Set brown foreground. |
|---|
| 23 |
:``^4``: Set blue foreground. |
|---|
| 24 |
:``^5``: Set magenta foreground. |
|---|
| 25 |
:``^6``: Set cyan foreground. |
|---|
| 26 |
:``^7``: Set white foreground. |
|---|
| 27 |
|
|---|
| 28 |
""" |
|---|
| 29 |
|
|---|
| 30 |
import re |
|---|
| 31 |
import sys |
|---|
| 32 |
import os |
|---|
| 33 |
import codecs |
|---|
| 34 |
|
|---|
| 35 |
|
|---|
| 36 |
__all__ = """ |
|---|
| 37 |
cwrite getch cerror cfatal register_codec cinfo cjustify clen cprint cprintstrip |
|---|
| 38 |
csplice cwarning cwraptext print_table rjustify termheight termwidth wraptoterm |
|---|
| 39 |
""".split() |
|---|
| 40 |
|
|---|
| 41 |
__docformat__ = 'restructuredtext en' |
|---|
| 42 |
|
|---|
| 43 |
|
|---|
| 44 |
_decode_re = re.compile(r'\^([N0-7BU])|[^^]+|\^') |
|---|
| 45 |
_encode_re = re.compile(r'\033(?:[^[]|$)|\033\[(.*?)m') |
|---|
| 46 |
_cprint_strip = re.compile(r'\^([N0-7BU])') |
|---|
| 47 |
_cwrap_re = re.compile(r'(\n)|(\s+)|((?:\^[N0-7BU]|\S)+\b[^\n^\w]*)|(.)') |
|---|
| 48 |
_terminal_type = None |
|---|
| 49 |
_terminal_colours = 0 |
|---|
| 50 |
|
|---|
| 51 |
|
|---|
| 52 |
try: |
|---|
| 53 |
_stdout_is_a_tty = sys.stdout.isatty() |
|---|
| 54 |
except: |
|---|
| 55 |
_stdout_is_a_tty = False |
|---|
| 56 |
|
|---|
| 57 |
def mono_cwrite(io, text): |
|---|
| 58 |
io.write(_cprint_strip.sub('', text)) |
|---|
| 59 |
|
|---|
| 60 |
|
|---|
| 61 |
if 'win' in sys.platform: |
|---|
| 62 |
_terminal_type = 'win' |
|---|
| 63 |
import msvcrt |
|---|
| 64 |
def getch(): |
|---|
| 65 |
"""Get a single character from the terminal.""" |
|---|
| 66 |
return msvcrt.getch() |
|---|
| 67 |
|
|---|
| 68 |
# Appropriated from |
|---|
| 69 |
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496901 |
|---|
| 70 |
STD_INPUT_HANDLE = -10 |
|---|
| 71 |
STD_OUTPUT_HANDLE= -11 |
|---|
| 72 |
STD_ERROR_HANDLE = -12 |
|---|
| 73 |
|
|---|
| 74 |
FOREGROUND_BLUE = 0x01 # text color contains blue. |
|---|
| 75 |
FOREGROUND_GREEN= 0x02 # text color contains green. |
|---|
| 76 |
FOREGROUND_RED = 0x04 # text color contains red. |
|---|
| 77 |
FOREGROUND_WHITE = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED |
|---|
| 78 |
FOREGROUND_INTENSITY = 0x08 # text color is intensified. |
|---|
| 79 |
BACKGROUND_BLUE = 0x10 # background color contains blue. |
|---|
| 80 |
BACKGROUND_GREEN= 0x20 # background color contains green. |
|---|
| 81 |
BACKGROUND_RED = 0x40 # background color contains red. |
|---|
| 82 |
BACKGROUND_INTENSITY = 0x80 # background color is intensified. |
|---|
| 83 |
BACKGROUND_WHITE = BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED |
|---|
| 84 |
|
|---|
| 85 |
try: |
|---|
| 86 |
import ctypes |
|---|
| 87 |
|
|---|
| 88 |
_stdout_handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) |
|---|
| 89 |
_stderr_handle = ctypes.windll.kernel32.GetStdHandle(STD_ERROR_HANDLE) |
|---|
| 90 |
|
|---|
| 91 |
def _set_windows_colour(color, fd=None): |
|---|
| 92 |
"""(color) -> BOOL |
|---|
| 93 |
|
|---|
| 94 |
Example: set_color(FOREGROUND_GREEN | FOREGROUND_INTENSITY) |
|---|
| 95 |
""" |
|---|
| 96 |
if not fd or fd is sys.stdout: |
|---|
| 97 |
handle = _stdout_handle |
|---|
| 98 |
elif fd is sys.stderr: |
|---|
| 99 |
handle = _stderr_handle |
|---|
| 100 |
else: |
|---|
| 101 |
return False |
|---|
| 102 |
bool = ctypes.windll.kernel32.SetConsoleTextAttribute(handle, color) |
|---|
| 103 |
return bool |
|---|
| 104 |
|
|---|
| 105 |
|
|---|
| 106 |
def cwrite(io, text): |
|---|
| 107 |
colour_map = { |
|---|
| 108 |
'0': 0, |
|---|
| 109 |
'1': FOREGROUND_RED, |
|---|
| 110 |
'2': FOREGROUND_GREEN, |
|---|
| 111 |
'3': FOREGROUND_RED | FOREGROUND_GREEN, |
|---|
| 112 |
'4': FOREGROUND_BLUE, |
|---|
| 113 |
'5': FOREGROUND_RED | FOREGROUND_BLUE, |
|---|
| 114 |
'6': FOREGROUND_BLUE | FOREGROUND_GREEN, |
|---|
| 115 |
'7': FOREGROUND_WHITE, |
|---|
| 116 |
} |
|---|
| 117 |
|
|---|
| 118 |
for match in _decode_re.finditer(text): |
|---|
| 119 |
code = match.group(1) |
|---|
| 120 |
if not code: |
|---|
| 121 |
io.write(match.group(0)) |
|---|
| 122 |
elif code == 'N': |
|---|
| 123 |
cwrite.state = FOREGROUND_WHITE |
|---|
| 124 |
_set_windows_colour(cwrite.state, io) |
|---|
| 125 |
elif code == 'U': |
|---|
| 126 |
pass |
|---|
| 127 |
elif code == 'B': |
|---|
| 128 |
cwrite.state ^= FOREGROUND_INTENSITY |
|---|
| 129 |
_set_windows_colour(cwrite.state, io) |
|---|
| 130 |
elif code >= '0' and code <= '7': |
|---|
| 131 |
cwrite.state &= ~FOREGROUND_WHITE |
|---|
| 132 |
cwrite.state |= colour_map[code] |
|---|
| 133 |
_set_windows_colour(cwrite.state, io) |
|---|
| 134 |
else: |
|---|
| 135 |
raise NotImplementedError('Unsupported colour code %s' % |
|---|
| 136 |
match.group(0)) |
|---|
| 137 |
|
|---|
| 138 |
cwrite.state = FOREGROUND_WHITE |
|---|
| 139 |
except ImportError: |
|---|
| 140 |
def _set_windows_colour(colour, fd=None): |
|---|
| 141 |
return False |
|---|
| 142 |
|
|---|
| 143 |
elif _stdout_is_a_tty: |
|---|
| 144 |
_terminal_type = 'ansi' |
|---|
| 145 |
try: |
|---|
| 146 |
import curses |
|---|
| 147 |
import signal |
|---|
| 148 |
|
|---|
| 149 |
curses.setupterm() |
|---|
| 150 |
_terminal_colours = curses.tigetnum('colors') |
|---|
| 151 |
|
|---|
| 152 |
# Reconfigure curses on window resize |
|---|
| 153 |
def sigwinch_handler(n, frame): |
|---|
| 154 |
curses.setupterm() |
|---|
| 155 |
|
|---|
| 156 |
signal.signal(signal.SIGWINCH, sigwinch_handler) |
|---|
| 157 |
except: |
|---|
| 158 |
_terminal_colours = 0 |
|---|
| 159 |
|
|---|
| 160 |
def getch(): |
|---|
| 161 |
"""Get a single character from the terminal.""" |
|---|
| 162 |
import tty |
|---|
| 163 |
import termios |
|---|
| 164 |
|
|---|
| 165 |
fd = sys.stdin.fileno() |
|---|
| 166 |
try: |
|---|
| 167 |
old_settings = termios.tcgetattr(fd) |
|---|
| 168 |
except termios.error: |
|---|
| 169 |
return os.read(fd, 1) |
|---|
| 170 |
try: |
|---|
| 171 |
tty.setraw(fd) |
|---|
| 172 |
ch = os.read(fd, 1) |
|---|
| 173 |
finally: |
|---|
| 174 |
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) |
|---|
| 175 |
return ch |
|---|
| 176 |
|
|---|
| 177 |
|
|---|
| 178 |
def cwrite(io, text): |
|---|
| 179 |
io.write(_decode(text)[0]) |
|---|
| 180 |
else: |
|---|
| 181 |
_terminal_type = 'dumb' |
|---|
| 182 |
|
|---|
| 183 |
def getch(): |
|---|
| 184 |
return sys.stdin.read(1) |
|---|
| 185 |
|
|---|
| 186 |
|
|---|
| 187 |
cwrite = mono_cwrite |
|---|
| 188 |
|
|---|
| 189 |
|
|---|
| 190 |
cwrite.__doc__ = \ |
|---|
| 191 |
"""Print using simple colour escape codes. |
|---|
| 192 |
|
|---|
| 193 |
Colour is not automatically reset at the end of output. |
|---|
| 194 |
|
|---|
| 195 |
If ``sys.stdout`` is not a TTY, colour codes will be stripped. |
|---|
| 196 |
""" |
|---|
| 197 |
|
|---|
| 198 |
|
|---|
| 199 |
class _Codec(codecs.Codec): |
|---|
| 200 |
def __init__(self, *args, **kwargs): |
|---|
| 201 |
try: |
|---|
| 202 |
codecs.Codec.__init__(self, *args, **kwargs) |
|---|
| 203 |
except AttributeError: |
|---|
| 204 |
pass |
|---|
| 205 |
self.reset() |
|---|
| 206 |
|
|---|
| 207 |
_encode_mapping = { |
|---|
| 208 |
0: '^N', 1: '^B', 4: '^U', 22: '^B', 24: '^U', 30: '^0', 31: '^1', |
|---|
| 209 |
32: '^2', 33: '^3', 34: '^4', 35: '^5', 36: '^6', 37: '^7', |
|---|
| 210 |
} |
|---|
| 211 |
|
|---|
| 212 |
def decode(self, input, errors='strict'): |
|---|
| 213 |
return _decode_re.sub(self._decode_match, input) |
|---|
| 214 |
|
|---|
| 215 |
def encode(self, input, errors='strict'): |
|---|
| 216 |
return _encode_re.sub(self._encode_match, input) |
|---|
| 217 |
|
|---|
| 218 |
def reset(self): |
|---|
| 219 |
self.bold = False |
|---|
| 220 |
self.underline = False |
|---|
| 221 |
|
|---|
| 222 |
# Internal methods |
|---|
| 223 |
def _encode_match(self, match): |
|---|
| 224 |
c = match.group(1) |
|---|
| 225 |
if c: |
|---|
| 226 |
return self._encode_mapping[int(c)] |
|---|
| 227 |
return match.group(0) |
|---|
| 228 |
|
|---|
| 229 |
def _decode_match(self, match): |
|---|
| 230 |
c = match.group(1) |
|---|
| 231 |
if c: |
|---|
| 232 |
if c == 'B': |
|---|
| 233 |
self.bold = not self.bold |
|---|
| 234 |
if self.bold: |
|---|
| 235 |
return "\033[1m" |
|---|
| 236 |
else: |
|---|
| 237 |
return "\033[22m" |
|---|
| 238 |
elif c == 'U': |
|---|
| 239 |
self.underline = not self.underline |
|---|
| 240 |
if self.underline: |
|---|
| 241 |
return "\033[4m" |
|---|
| 242 |
else: |
|---|
| 243 |
return "\033[24m" |
|---|
| 244 |
elif c == 'N': |
|---|
| 245 |
self.underline = self.bold = 0 |
|---|
| 246 |
return "\033[0m" |
|---|
| 247 |
elif c >= '0' and c <= '7': |
|---|
| 248 |
return "\033[3" + c + "m" |
|---|
| 249 |
else: |
|---|
| 250 |
return match.group(0) |
|---|
| 251 |
return match.group(0) |
|---|
| 252 |
|
|---|
| 253 |
|
|---|
| 254 |
class _CodecStreamWriter(_Codec, codecs.StreamWriter): |
|---|
| 255 |
def __init__(self, stream, errors='strict'): |
|---|
| 256 |
_Codec.__init__(self) |
|---|
| 257 |
codecs.StreamWriter.__init__(self, stream, errors) |
|---|
| 258 |
self.errors = errors |
|---|
| 259 |
|
|---|
| 260 |
def write(self, object): |
|---|
| 261 |
self.stream.write(self.decode(object)) |
|---|
| 262 |
|
|---|
| 263 |
def writelines(self, lines): |
|---|
| 264 |
for line in lines: |
|---|
| 265 |
self.write(line) |
|---|
| 266 |
self.write('\n') |
|---|
| 267 |
|
|---|
| 268 |
|
|---|
| 269 |
class _CodecStreamReader(_Codec, codecs.StreamReader): |
|---|
| 270 |
def __init__(self, stream, errors='strict'): |
|---|
| 271 |
_Codec.__init__(self) |
|---|
| 272 |
codecs.StreamReader.__init__(self, stream, errors) |
|---|
| 273 |
|
|---|
| 274 |
def read(self, size=-1, chars=-1): |
|---|
| 275 |
raise NotImplementedError |
|---|
| 276 |
|
|---|
| 277 |
def readline(self, size=None, keepends=True): |
|---|
| 278 |
raise NotImplementedError |
|---|
| 279 |
|
|---|
| 280 |
def readlines(self, sizehint=None, keepends=True): |
|---|
| 281 |
raise NotImplementedError |
|---|
| 282 |
|
|---|
| 283 |
def seek(self, offset, whence=0): |
|---|
| 284 |
self.stream(offset, whence) |
|---|
| 285 |
self.reset() |
|---|
| 286 |
|
|---|
| 287 |
|
|---|
| 288 |
def _decode(input, errors='strict'): |
|---|
| 289 |
return (_Codec(errors=errors).decode(input), len(input)) |
|---|
| 290 |
|
|---|
| 291 |
|
|---|
| 292 |
def _encode(input, errors='strict'): |
|---|
| 293 |
return (_Codec(errors=errors).encode(input), len(input)) |
|---|
| 294 |
|
|---|
| 295 |
|
|---|
| 296 |
def register_codec(): |
|---|
| 297 |
"""Register the 'cly' codec with Python. |
|---|
| 298 |
|
|---|
| 299 |
The formatting syntax can then be used like any other codec: |
|---|
| 300 |
|
|---|
| 301 |
>>> register_codec() |
|---|
| 302 |
>>> '^Bbold^B'.decode('cly') |
|---|
| 303 |
'\\x1b[1mbold\\x1b[22m' |
|---|
| 304 |
>>> '\\x1b[1mbold\\x1b[22m'.encode('cly') |
|---|
| 305 |
'^Bbold^B' |
|---|
| 306 |
""" |
|---|
| 307 |
def inner_register(encoding): |
|---|
| 308 |
if encoding != 'cly': |
|---|
| 309 |
return None |
|---|
| 310 |
return (_encode, _decode, _CodecStreamReader, _CodecStreamWriter) |
|---|
| 311 |
return codecs.register(inner_register) |
|---|
| 312 |
|
|---|
| 313 |
|
|---|
| 314 |
def cprint(*args): |
|---|
| 315 |
"""Emulate the ``print`` builtin, with terminal shortcuts.""" |
|---|
| 316 |
if args and type(args[0]) is file: |
|---|
| 317 |
stream = args[0] |
|---|
| 318 |
args = args[1:] |
|---|
| 319 |
else: |
|---|
| 320 |
stream = sys.stdout |
|---|
| 321 |
cwrite(stream, ' '.join(map(str, args)) + '\n') |
|---|
| 322 |
|
|---|
| 323 |
|
|---|
| 324 |
def cprintstrip(*args): |
|---|
| 325 |
"""As with cprint, but strip colour codes.""" |
|---|
| 326 |
return _cprint_strip.sub('', ' '.join(map(str, args))) |
|---|
| 327 |
|
|---|
| 328 |
|
|---|
| 329 |
def clen(arg): |
|---|
| 330 |
"""Return the length of arg after colour codes are stripped.""" |
|---|
| 331 |
return len(cprintstrip(arg)) |
|---|
| 332 |
|
|---|
| 333 |
|
|---|
| 334 |
def cerror(*args): |
|---|
| 335 |
"""Print a message in red to stderr.""" |
|---|
| 336 |
cprint(sys.stderr, "^1^B" + ' '.join(map(str, args)) + '^N') |
|---|
| 337 |
|
|---|
| 338 |
|
|---|
| 339 |
def cfatal(*args): |
|---|
| 340 |
"""Print a message in red to stderr then exit with status -1.""" |
|---|
| 341 |
cprint(sys.stderr, "^1^B" + ' '.join(map(str, args)) + '^N') |
|---|
| 342 |
sys.exit(-1) |
|---|
| 343 |
|
|---|
| 344 |
|
|---|
| 345 |
def cwarning(*args): |
|---|
| 346 |
"""Print a yellow warning message to stderr.""" |
|---|
| 347 |
cprint(sys.stderr, "^3^B" + ' '.join(map(str, args)) + '^N') |
|---|
| 348 |
|
|---|
| 349 |
|
|---|
| 350 |
def cinfo(*args): |
|---|
| 351 |
"""Print a green notice.""" |
|---|
| 352 |
cprint("^2" + ' '.join(map(str, args)) + '^N') |
|---|
| 353 |
|
|---|
| 354 |
|
|---|
| 355 |
def termwidth(): |
|---|
| 356 |
"""Guess the current terminal width. |
|---|
| 357 |
|
|---|
| 358 |
Returns -1 if the terminal width can not be determined. |
|---|
| 359 |
""" |
|---|
| 360 |
if not _stdout_is_a_tty: |
|---|
| 361 |
return -1 |
|---|
| 362 |
try: |
|---|
| 363 |
import curses |
|---|
| 364 |
return curses.tigetnum('cols') |
|---|
| 365 |
except: |
|---|
| 366 |
return int(os.environ.get('COLUMNS', -1)) |
|---|
| 367 |
|
|---|
| 368 |
|
|---|
| 369 |
def termheight(): |
|---|
| 370 |
"""Guess the current terminal height. |
|---|
| 371 |
|
|---|
| 372 |
Returns -1 if the terminal height can not be determined. |
|---|
| 373 |
""" |
|---|
| 374 |
if not _stdout_is_a_tty: |
|---|
| 375 |
return -1 |
|---|
| 376 |
try: |
|---|
| 377 |
import curses |
|---|
| 378 |
return curses.tigetnum('lines') |
|---|
| 379 |
except: |
|---|
| 380 |
return int(os.environ.get('LINES', -1)) |
|---|
| 381 |
|
|---|
| 382 |
|
|---|
| 383 |
def csplice(text, start=0, end=-1): |
|---|
| 384 |
"""Splice a colour encoded string.""" |
|---|
| 385 |
out = '' |
|---|
| 386 |
if end == -1: |
|---|
| 387 |
end = len(text) |
|---|
| 388 |
sofar = 0 |
|---|
| 389 |
for token in _decode_re.finditer(text): |
|---|
| 390 |
if sofar > end: break |
|---|
| 391 |
txt = token.group(0) |
|---|
| 392 |
if token.group(1): |
|---|
| 393 |
if start < sofar < end: |
|---|
| 394 |
out += txt |
|---|
| 395 |
else: |
|---|
| 396 |
# Whether beginning and end of segment are in slice |
|---|
| 397 |
bs = start < sofar < end |
|---|
| 398 |
es = start < sofar + len(txt) < end |
|---|
| 399 |
if bs and es: |
|---|
| 400 |
out += txt |
|---|
| 401 |
elif not bs and es: |
|---|
| 402 |
out += txt[start - sofar:] |
|---|
| 403 |
elif bs and not es: |
|---|
| 404 |
out += txt[:end - sofar] |
|---|
| 405 |
break |
|---|
| 406 |
elif sofar <= start and sofar + len(txt) >= end: |
|---|
| 407 |
out += txt[start - sofar:end] |
|---|
| 408 |
break |
|---|
| 409 |
sofar += len(txt) |
|---|
| 410 |
return out |
|---|
| 411 |
|
|---|
| 412 |
|
|---|
| 413 |
def cwraptext(rtext, width=None, subsequent_indent=''): |
|---|
| 414 |
"""Wrap multi-line text to width (defaults to :func:`termwidth`)""" |
|---|
| 415 |
if width is None: |
|---|
| 416 |
width = termwidth() |
|---|
| 417 |
if width == -1: |
|---|
| 418 |
return [rtext] |
|---|
| 419 |
out = [] |
|---|
| 420 |
for text in rtext.splitlines(): |
|---|
| 421 |
tokens = [t.group(0) for t in _cwrap_re.finditer(text)] + [' ' * width] |
|---|
| 422 |
line = tokens.pop(0) |
|---|
| 423 |
first_line = 1 |
|---|
| 424 |
|
|---|
| 425 |
def add_line(line, first_line): |
|---|
| 426 |
if clen(line.rstrip()) > width: |
|---|
| 427 |
tokens.insert(0, csplice(line, width)) |
|---|
| 428 |
line = csplice(line, 0, width) |
|---|
| 429 |
out.append((not first_line and subsequent_indent or '') + line.rstrip()) |
|---|
| 430 |
first_line = 0 |
|---|
| 431 |
if not out[-1]: |
|---|
| 432 |
out.pop() |
|---|
| 433 |
return first_line |
|---|
| 434 |
|
|---|
| 435 |
if tokens: |
|---|
| 436 |
while tokens: |
|---|
| 437 |
if clen(line) + clen(tokens[0].rstrip()) > width: |
|---|
| 438 |
first_line = add_line(line, first_line) |
|---|
| 439 |
line = tokens.pop(0) |
|---|
| 440 |
else: |
|---|
| 441 |
line += tokens.pop(0) |
|---|
| 442 |
if line: |
|---|
| 443 |
add_line(line, first_line) |
|---|
| 444 |
else: |
|---|
| 445 |
out.append('') |
|---|
| 446 |
return out |
|---|
| 447 |
|
|---|
| 448 |
|
|---|
| 449 |
def wraptoterm(text, **kwargs): |
|---|
| 450 |
"""Wrap the given text to the current terminal width""" |
|---|
| 451 |
return '\n'.join(cwraptext(text, **kwargs)) |
|---|
| 452 |
|
|---|
| 453 |
|
|---|
| 454 |
def rjustify(text, width=None): |
|---|
| 455 |
"""Right justify the given text.""" |
|---|
| 456 |
if width is None: |
|---|
| 457 |
width = termwidth() |
|---|
| 458 |
if width == -1: |
|---|
| 459 |
return text |
|---|
| 460 |
text = cwraptext(text, width) |
|---|
| 461 |
out = '' |
|---|
| 462 |
for line in text: |
|---|
| 463 |
out += (' ' * (width - clen(line))) + line + '\n' |
|---|
| 464 |
return out.rstrip() |
|---|
| 465 |
|
|---|
| 466 |
|
|---|
| 467 |
def cjustify(text, width=None): |
|---|
| 468 |
"""Centre the given text.""" |
|---|
| 469 |
if width is None: |
|---|
| 470 |
width = termwidth() |
|---|
| 471 |
if width == -1: |
|---|
| 472 |
return text |
|---|
| 473 |
text = cwraptext(text, width) |
|---|
| 474 |
out = '' |
|---|
| 475 |
for line in text: |
|---|
| 476 |
out += (' ' * ((width - clen(line)) / 2)) + line + '\n' |
|---|
| 477 |
return out.rstrip() |
|---|
| 478 |
|
|---|
| 479 |
|
|---|
| 480 |
def print_table(header, table, sep=u' ', indent=u'', expand_to_fit=True, |
|---|
| 481 |
header_format='^B^U', row_format=('^6', '^B^6'), |
|---|
| 482 |
min_widths=None, term_width=None): |
|---|
| 483 |
"""Print a list of lists as a table, so that columns line up nicely. |
|---|
| 484 |
|
|---|
| 485 |
:param header: List of column headings. Will be printed as the first row. |
|---|
| 486 |
:param table: List of lists for the table body. |
|---|
| 487 |
:param sep: The column separator. |
|---|
| 488 |
:param indent: Table indentation as a string. |
|---|
| 489 |
:param header_format: Formatting to use for header. |
|---|
| 490 |
:param row_format: A tuple specifying cycling formatting colours to use for |
|---|
| 491 |
each row. |
|---|
| 492 |
:param expand_to_fit: If a boolean, signifies whether print_table should |
|---|
| 493 |
expand the table to the width of the terminal or |
|---|
| 494 |
compact it as much as possible. If an integer, |
|---|
| 495 |
specifies the width to expand to. |
|---|
| 496 |
:param min_widths: Columns will be guaranteed to be at least the width of |
|---|
| 497 |
each element in this list. May also be a dictionary of |
|---|
| 498 |
column indices to widths. |
|---|
| 499 |
:param term_width: Override terminal width detection. |
|---|
| 500 |
|
|---|
| 501 |
:returns: List of strings, one per line. |
|---|
| 502 |
|
|---|
| 503 |
Note: In addition to the normal formatting codes supported by :func:`cprint`, |
|---|
| 504 |
:func:`print_table` supports the ``^R`` formatting code, which corresponds |
|---|
| 505 |
to the colour formatting of the current table row. |
|---|
| 506 |
""" |
|---|
| 507 |
def ctlen(s): |
|---|
| 508 |
return clen(s.replace('^R', '')) |
|---|
| 509 |
|
|---|
| 510 |
seplen = len(sep) |
|---|
| 511 |
# Normalise rows |
|---|
| 512 |
rows = [map(unicode, r) for r in [list(header)] + list(table)] |
|---|
| 513 |
columns = len(rows[0]) |
|---|
| 514 |
|
|---|
| 515 |
# Scale size_hints percentages to terminal width |
|---|
| 516 |
if term_width is None: |
|---|
| 517 |
term_width = termwidth() |
|---|
| 518 |
if term_width == -1: |
|---|
| 519 |
term_width = max([sum(map(ctlen, r)) + len(r) for r in rows]) |
|---|
| 520 |
min_widths = reduce(lambda a, b: map(max, zip(a, b)), |
|---|
| 521 |
[map(lambda c: ctlen(c) + 1, r) for r in rows]) |
|---|
| 522 |
else: |
|---|
| 523 |
term_width = term_width - (columns - 1) * seplen - ctlen(indent) |
|---|
| 524 |
if not isinstance(min_widths, dict): |
|---|
| 525 |
min_widths = dict(enumerate(min_widths or [])) |
|---|
| 526 |
|
|---|
| 527 |
# Column widths |
|---|
| 528 |
avg_width = float(term_width) / columns |
|---|
| 529 |
# Use the mid-point between the maximum word width and the maximum length |
|---|
| 530 |
# of the column. |
|---|
| 531 |
widths = [(max([ctlen(w) for cell in column for w in cell.split()]) |
|---|
| 532 |
+ max(map(ctlen, column))) / 2 + seplen |
|---|
| 533 |
for column in zip(*rows)] |
|---|
| 534 |
#widths = [int(min(c, avg_width)) for c in widths] |
|---|
| 535 |
# Apply user-specified column widths. |
|---|
| 536 |
widths = [max(c, min_widths.get(i, 1)) for i, c in enumerate(widths)] |
|---|
| 537 |
width = sum(widths) |
|---|
| 538 |
|
|---|
| 539 |
# Scale columns to fit |
|---|
| 540 |
if width > term_width or expand_to_fit: |
|---|
| 541 |
scale = float(term_width - sum(min_widths.values())) / width |
|---|
| 542 |
widths = [max(int(w * scale), min_widths.get(i, 1)) |
|---|
| 543 |
for i, w in enumerate(widths)] |
|---|
| 544 |
|
|---|
| 545 |
row_alt = -1 |
|---|
| 546 |
for row in rows: |
|---|
| 547 |
# Cycle through row formats |
|---|
| 548 |
if row_alt == -1: |
|---|
| 549 |
format = header_format |
|---|
| 550 |
else: |
|---|
| 551 |
format = row_format[row_alt % len(row_format)] |
|---|
| 552 |
row_alt += 1 |
|---|
| 553 |
|
|---|
| 554 |
wrapped = [cwraptext(c.replace('^R', format), widths[i]) |
|---|
| 555 |
for i, c in enumerate(row)] |
|---|
| 556 |
maxrows = max([0] + map(len, wrapped)) |
|---|
| 557 |
for col in wrapped: |
|---|
| 558 |
col += [''] * (maxrows - len(col)) |
|---|
| 559 |
|
|---|
| 560 |
prefix = indent + format |
|---|
| 561 |
for y in range(len(wrapped[0])): |
|---|
| 562 |
cwrite(sys.stdout, prefix) |
|---|
| 563 |
for x, cell in enumerate(wrapped): |
|---|
| 564 |
cwrite(sys.stdout, cell[y].ljust(widths[x])) |
|---|
| 565 |
if x < columns - 1: |
|---|
| 566 |
cwrite(sys.stdout, ' ') |
|---|
| 567 |
else: |
|---|
| 568 |
cwrite(sys.stdout, '\n') |
|---|
| 569 |
cwrite(sys.stdout, '^N') |
|---|
| 570 |
|
|---|
| 571 |
|
|---|
| 572 |
if __name__ == '__main__': |
|---|
| 573 |
import doctest |
|---|
| 574 |
doctest.testmod() |
|---|