Enable colored output for the CLI on windows as well

This commit is contained in:
Kovid Goyal 2012-11-20 16:17:44 +05:30
parent 4e76a78171
commit d00a37e66a
8 changed files with 290 additions and 266 deletions

View File

@ -14,14 +14,6 @@ Various run time constants.
import sys, locale, codecs, os, importlib, collections import sys, locale, codecs, os, importlib, collections
_tc = None
def terminal_controller():
global _tc
if _tc is None:
from calibre.utils.terminfo import TerminalController
_tc = TerminalController(sys.stdout)
return _tc
_plat = sys.platform.lower() _plat = sys.platform.lower()
iswindows = 'win32' in _plat or 'win64' in _plat iswindows = 'win32' in _plat or 'win64' in _plat
isosx = 'darwin' in _plat isosx = 'darwin' in _plat
@ -37,6 +29,8 @@ isportable = os.environ.get('CALIBRE_PORTABLE_BUILD', None) is not None
ispy3 = sys.version_info.major > 2 ispy3 = sys.version_info.major > 2
isxp = iswindows and sys.getwindowsversion().major < 6 isxp = iswindows and sys.getwindowsversion().major < 6
isworker = os.environ.has_key('CALIBRE_WORKER') or os.environ.has_key('CALIBRE_SIMPLE_WORKER') isworker = os.environ.has_key('CALIBRE_WORKER') or os.environ.has_key('CALIBRE_SIMPLE_WORKER')
if isworker:
os.environ.pop('CALIBRE_FORCE_ANSI', None)
try: try:
preferred_encoding = locale.getpreferredencoding() preferred_encoding = locale.getpreferredencoding()

View File

@ -1716,7 +1716,7 @@ if __name__ == '__main__':
ret = 0 ret = 0
for x in ('lxml', 'calibre.ebooks.BeautifulSoup', 'uuid', for x in ('lxml', 'calibre.ebooks.BeautifulSoup', 'uuid',
'calibre.utils.terminfo', 'calibre.utils.magick', 'PIL', 'Image', 'calibre.utils.terminal', 'calibre.utils.magick', 'PIL', 'Image',
'sqlite3', 'mechanize', 'httplib', 'xml'): 'sqlite3', 'mechanize', 'httplib', 'xml'):
if x in sys.modules: if x in sys.modules:
ret = 1 ret = 1

View File

@ -11,7 +11,6 @@ from optparse import OptionParser
from calibre import __version__, __appname__, human_readable from calibre import __version__, __appname__, human_readable
from calibre.devices.errors import PathError from calibre.devices.errors import PathError
from calibre.utils.terminfo import TerminalController
from calibre.devices.errors import ArgumentError, DeviceError, DeviceLocked from calibre.devices.errors import ArgumentError, DeviceError, DeviceLocked
from calibre.customize.ui import device_plugins from calibre.customize.ui import device_plugins
from calibre.devices.scanner import DeviceScanner from calibre.devices.scanner import DeviceScanner
@ -20,8 +19,7 @@ from calibre.utils.config import device_prefs
MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output
class FileFormatter(object): class FileFormatter(object):
def __init__(self, file, term): def __init__(self, file):
self.term = term
self.is_dir = file.is_dir self.is_dir = file.is_dir
self.is_readonly = file.is_readonly self.is_readonly = file.is_readonly
self.size = file.size self.size = file.size
@ -94,7 +92,7 @@ def info(dev):
print "Software version:", info[2] print "Software version:", info[2]
print "Mime type: ", info[3] print "Mime type: ", info[3]
def ls(dev, path, term, recurse=False, color=False, human_readable_size=False, ll=False, cols=0): def ls(dev, path, recurse=False, human_readable_size=False, ll=False, cols=0):
def col_split(l, cols): # split list l into columns def col_split(l, cols): # split list l into columns
rows = len(l) / cols rows = len(l) / cols
if len(l) % cols: if len(l) % cols:
@ -126,14 +124,13 @@ def ls(dev, path, term, recurse=False, color=False, human_readable_size=False, l
for file in files: for file in files:
size = len(str(file.size)) size = len(str(file.size))
if human_readable_size: if human_readable_size:
file = FileFormatter(file, term) file = FileFormatter(file)
size = len(file.human_readable_size) size = len(file.human_readable_size)
if size > maxlen: maxlen = size if size > maxlen: maxlen = size
for file in files: for file in files:
file = FileFormatter(file, term) file = FileFormatter(file)
name = file.name if ll else file.isdir_name name = file.name if ll else file.isdir_name
lsoutput.append(name) lsoutput.append(name)
if color: name = file.name_in_color
lscoloutput.append(name) lscoloutput.append(name)
if ll: if ll:
size = str(file.size) size = str(file.size)
@ -173,10 +170,8 @@ def shutdown_plugins():
pass pass
def main(): def main():
term = TerminalController() from calibre.utils.terminal import geometry
cols = term.COLS cols = geometry()[0]
if not cols: # On windows terminal width is unknown
cols = 80
parser = OptionParser(usage="usage: %prog [options] command args\n\ncommand "+ parser = OptionParser(usage="usage: %prog [options] command args\n\ncommand "+
"is one of: info, books, df, ls, cp, mkdir, touch, cat, rm, eject, test_file\n\n"+ "is one of: info, books, df, ls, cp, mkdir, touch, cat, rm, eject, test_file\n\n"+
@ -260,7 +255,6 @@ def main():
dev.mkdir(args[0]) dev.mkdir(args[0])
elif command == "ls": elif command == "ls":
parser = OptionParser(usage="usage: %prog ls [options] path\nList files on the device\n\npath must begin with / or card:/") parser = OptionParser(usage="usage: %prog ls [options] path\nList files on the device\n\npath must begin with / or card:/")
parser.add_option("--color", help="show ls output in color", dest="color", action="store_true", default=False)
parser.add_option("-l", help="In addition to the name of each file, print the file type, permissions, and timestamp (the modification time, in the local timezone). Times are local.", dest="ll", action="store_true", default=False) parser.add_option("-l", help="In addition to the name of each file, print the file type, permissions, and timestamp (the modification time, in the local timezone). Times are local.", dest="ll", action="store_true", default=False)
parser.add_option("-R", help="Recursively list subdirectories encountered. /dev and /proc are omitted", dest="recurse", action="store_true", default=False) parser.add_option("-R", help="Recursively list subdirectories encountered. /dev and /proc are omitted", dest="recurse", action="store_true", default=False)
parser.remove_option("-h") parser.remove_option("-h")
@ -269,7 +263,7 @@ def main():
if len(args) != 1: if len(args) != 1:
parser.print_help() parser.print_help()
return 1 return 1
print ls(dev, args[0], term, color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols), print ls(dev, args[0], recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols),
elif command == "info": elif command == "info":
info(dev) info(dev)
elif command == "cp": elif command == "cp":

View File

@ -65,8 +65,7 @@ def get_db(dbpath, options):
def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, separator, def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, separator,
prefix, subtitle='Books in the calibre database'): prefix, subtitle='Books in the calibre database'):
from calibre.constants import terminal_controller as tc from calibre.utils.terminal import ColoredStream, geometry
terminal_controller = tc()
if sort_by: if sort_by:
db.sort(sort_by, ascending) db.sort(sort_by, ascending)
if search_text: if search_text:
@ -101,7 +100,7 @@ def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, se
for j, field in enumerate(fields): for j, field in enumerate(fields):
widths[j] = max(widths[j], len(unicode(i[field]))) widths[j] = max(widths[j], len(unicode(i[field])))
screen_width = terminal_controller.COLS if line_width < 0 else line_width screen_width = geometry()[0] if line_width < 0 else line_width
if not screen_width: if not screen_width:
screen_width = 80 screen_width = 80
field_width = screen_width//len(fields) field_width = screen_width//len(fields)
@ -120,7 +119,8 @@ def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, se
widths = list(base_widths) widths = list(base_widths)
titles = map(lambda x, y: '%-*s%s'%(x-len(separator), y, separator), titles = map(lambda x, y: '%-*s%s'%(x-len(separator), y, separator),
widths, title_fields) widths, title_fields)
print terminal_controller.GREEN + ''.join(titles)+terminal_controller.NORMAL with ColoredStream(sys.stdout, fg='green'):
print ''.join(titles)
wrappers = map(lambda x: TextWrapper(x-1), widths) wrappers = map(lambda x: TextWrapper(x-1), widths)
o = cStringIO.StringIO() o = cStringIO.StringIO()
@ -1288,8 +1288,7 @@ def command_list_categories(args, dbpath):
fields = ['category', 'tag_name', 'count', 'rating'] fields = ['category', 'tag_name', 'count', 'rating']
def do_list(): def do_list():
from calibre.constants import terminal_controller as tc from calibre.utils.terminal import geometry, ColoredStream
terminal_controller = tc()
separator = ' ' separator = ' '
widths = list(map(lambda x : 0, fields)) widths = list(map(lambda x : 0, fields))
@ -1297,7 +1296,7 @@ def command_list_categories(args, dbpath):
for j, field in enumerate(fields): for j, field in enumerate(fields):
widths[j] = max(widths[j], max(len(field), len(unicode(i[field])))) widths[j] = max(widths[j], max(len(field), len(unicode(i[field]))))
screen_width = terminal_controller.COLS if opts.width < 0 else opts.width screen_width = geometry()[0]
if not screen_width: if not screen_width:
screen_width = 80 screen_width = 80
field_width = screen_width//len(fields) field_width = screen_width//len(fields)
@ -1316,7 +1315,8 @@ def command_list_categories(args, dbpath):
widths = list(base_widths) widths = list(base_widths)
titles = map(lambda x, y: '%-*s%s'%(x-len(separator), y, separator), titles = map(lambda x, y: '%-*s%s'%(x-len(separator), y, separator),
widths, fields) widths, fields)
print terminal_controller.GREEN + ''.join(titles)+terminal_controller.NORMAL with ColoredStream(sys.stdout, fg='green'):
print ''.join(titles)
wrappers = map(lambda x: TextWrapper(x-1), widths) wrappers = map(lambda x: TextWrapper(x-1), widths)
o = cStringIO.StringIO() o = cStringIO.StringIO()

View File

@ -12,7 +12,7 @@ from optparse import OptionParser as _OptionParser, OptionGroup
from optparse import IndentedHelpFormatter from optparse import IndentedHelpFormatter
from calibre.constants import (config_dir, CONFIG_DIR_MODE, __appname__, from calibre.constants import (config_dir, CONFIG_DIR_MODE, __appname__,
__version__, __author__, terminal_controller) __version__, __author__)
from calibre.utils.lock import ExclusiveFile from calibre.utils.lock import ExclusiveFile
from calibre.utils.config_base import (make_config_dir, Option, OptionValues, from calibre.utils.config_base import (make_config_dir, Option, OptionValues,
OptionSet, ConfigInterface, Config, prefs, StringConfig, ConfigProxy, OptionSet, ConfigInterface, Config, prefs, StringConfig, ConfigProxy,
@ -30,28 +30,28 @@ def check_config_write_access():
class CustomHelpFormatter(IndentedHelpFormatter): class CustomHelpFormatter(IndentedHelpFormatter):
def format_usage(self, usage): def format_usage(self, usage):
tc = terminal_controller() from calibre.utils.terminal import colored
return "%s%s%s: %s\n" % (tc.BLUE, _('Usage'), tc.NORMAL, usage) return colored(_('Usage'), fg='blue', bold=True) + ': ' + usage
def format_heading(self, heading): def format_heading(self, heading):
tc = terminal_controller() from calibre.utils.terminal import colored
return "%*s%s%s%s:\n" % (self.current_indent, tc.BLUE, return "%*s%s:\n" % (self.current_indent, '',
"", heading, tc.NORMAL) colored(heading, fg='blue'))
def format_option(self, option): def format_option(self, option):
import textwrap import textwrap
tc = terminal_controller() from calibre.utils.terminal import colored
result = [] result = []
opts = self.option_strings[option] opts = self.option_strings[option]
opt_width = self.help_position - self.current_indent - 2 opt_width = self.help_position - self.current_indent - 2
if len(opts) > opt_width: if len(opts) > opt_width:
opts = "%*s%s\n" % (self.current_indent, "", opts = "%*s%s\n" % (self.current_indent, "",
tc.GREEN+opts+tc.NORMAL) colored(opts, fg='green'))
indent_first = self.help_position indent_first = self.help_position
else: # start help on same line as opts else: # start help on same line as opts
opts = "%*s%-*s " % (self.current_indent, "", opt_width + opts = "%*s%-*s " % (self.current_indent, "", opt_width +
len(tc.GREEN + tc.NORMAL), tc.GREEN + opts + tc.NORMAL) len(colored('', fg='green')), colored(opts, fg='green'))
indent_first = 0 indent_first = 0
result.append(opts) result.append(opts)
if option.help: if option.help:
@ -78,11 +78,11 @@ class OptionParser(_OptionParser):
conflict_handler='resolve', conflict_handler='resolve',
**kwds): **kwds):
import textwrap import textwrap
tc = terminal_controller() from calibre.utils.terminal import colored
usage = textwrap.dedent(usage) usage = textwrap.dedent(usage)
if epilog is None: if epilog is None:
epilog = _('Created by ')+tc.RED+__author__+tc.NORMAL epilog = _('Created by ')+colored(__author__, fg='cyan')
usage += '\n\n'+_('''Whenever you pass arguments to %prog that have spaces in them, ''' usage += '\n\n'+_('''Whenever you pass arguments to %prog that have spaces in them, '''
'''enclose the arguments in quotation marks.''') '''enclose the arguments in quotation marks.''')
_OptionParser.__init__(self, usage=usage, version=version, epilog=epilog, _OptionParser.__init__(self, usage=usage, version=version, epilog=epilog,
@ -95,6 +95,21 @@ class OptionParser(_OptionParser):
_("show this help message and exit") _("show this help message and exit")
_("show program's version number and exit") _("show program's version number and exit")
def print_usage(self, file=None):
from calibre.utils.terminal import ANSIStream
s = ANSIStream(file)
_OptionParser.print_usage(self, file=s)
def print_help(self, file=None):
from calibre.utils.terminal import ANSIStream
s = ANSIStream(file)
_OptionParser.print_help(self, file=s)
def print_version(self, file=None):
from calibre.utils.terminal import ANSIStream
s = ANSIStream(file)
_OptionParser.print_version(self, file=s)
def error(self, msg): def error(self, msg):
if self.gui_mode: if self.gui_mode:
raise Exception(msg) raise Exception(msg)

View File

@ -33,21 +33,18 @@ class ANSIStream(Stream):
def __init__(self, stream=sys.stdout): def __init__(self, stream=sys.stdout):
Stream.__init__(self, stream) Stream.__init__(self, stream)
from calibre.utils.terminfo import TerminalController
tc = TerminalController(stream)
self.color = { self.color = {
DEBUG: bytes(tc.GREEN), DEBUG: u'green',
INFO: bytes(''), INFO: None,
WARN: bytes(tc.YELLOW), WARN: u'yellow',
ERROR: bytes(tc.RED) ERROR: u'red',
} }
self.normal = bytes(tc.NORMAL)
def prints(self, level, *args, **kwargs): def prints(self, level, *args, **kwargs):
self.stream.write(self.color[level])
kwargs['file'] = self.stream kwargs['file'] = self.stream
self._prints(*args, **kwargs) from calibre.utils.terminal import ColoredStream
self.stream.write(self.normal) with ColoredStream(self.stream, self.color[level]):
self._prints(*args, **kwargs)
def flush(self): def flush(self):
self.stream.flush() self.stream.flush()

View File

@ -0,0 +1,239 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, sys, re
from itertools import izip
from calibre.constants import iswindows
def fmt(code):
return ('\033[%dm'%code).encode('ascii')
RATTRIBUTES = dict(
izip(xrange(1, 9), (
'bold',
'dark',
'',
'underline',
'blink',
'',
'reverse',
'concealed'
)
))
ATTRIBUTES = {v:fmt(k) for k, v in RATTRIBUTES.iteritems()}
del ATTRIBUTES['']
RBACKGROUNDS = dict(
izip(xrange(41, 48), (
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white'
),
))
BACKGROUNDS = {v:fmt(k) for k, v in RBACKGROUNDS.iteritems()}
RCOLORS = dict(
izip(xrange(31, 38), (
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white',
),
))
COLORS = {v:fmt(k) for k, v in RCOLORS.iteritems()}
RESET = fmt(0)
if iswindows:
# From wincon.h
WCOLORS = {c:i for i, c in enumerate((
'black', 'blue', 'green', 'cyan', 'red', 'magenta', 'yellow', 'white'))}
def to_flag(fg, bg, bold):
val = 0
if bold:
val |= 0x08
if fg in WCOLORS:
val |= WCOLORS[fg]
if bg in WCOLORS:
val |= (WCOLORS[bg] << 4)
return val
def colored(text, fg=None, bg=None, bold=False):
prefix = []
if fg is not None:
prefix.append(COLORS[fg])
if bg is not None:
prefix.append(BACKGROUNDS[bg])
if bold:
prefix.append(ATTRIBUTES['bold'])
prefix = b''.join(prefix)
suffix = RESET
if isinstance(text, type(u'')):
prefix = prefix.decode('ascii')
suffix = suffix.decode('ascii')
return prefix + text + suffix
class Detect(object):
def __init__(self, stream):
self.stream = stream or sys.stdout
self.isatty = getattr(self.stream, 'isatty', lambda : False)()
force_ansi = os.environ.has_key('CALIBRE_FORCE_ANSI')
if not self.isatty and force_ansi:
self.isatty = True
self.isansi = force_ansi or not iswindows
self.set_console = None
if not self.isansi:
try:
import msvcrt
self.msvcrt = msvcrt
self.file_handle = msvcrt.get_osfhandle(self.stream.fileno())
from ctypes import windll
self.set_console = windll.kernel32.SetConsoleTextAttribute
except:
pass
class ColoredStream(Detect):
def __init__(self, stream=None, fg=None, bg=None, bold=False):
super(ColoredStream, self).__init__(stream)
self.fg, self.bg, self.bold = fg, bg, bold
if self.set_console is not None:
self.wval = to_flag(self.fg, self.bg, bold)
def __enter__(self):
if not self.isatty:
return
if self.isansi:
if self.bold:
self.stream.write(ATTRIBUTES['bold'])
if self.bg is not None:
self.stream.write(BACKGROUNDS[self.bg])
if self.fg is not None:
self.stream.write(COLORS[self.fg])
elif self.set_console is not None:
if self.wval != 0:
self.set_console(self.file_handle, self.wval)
def __exit__(self, *args, **kwargs):
if not self.isatty:
return
if not self.fg and not self.bg and not self.bold:
return
if self.isansi:
self.stream.write(RESET)
elif self.set_console is not None:
self.set_console(self.file_handle, WCOLORS['white'])
class ANSIStream(Detect):
ANSI_RE = re.compile(br'\033\[((?:\d|;)*)([a-zA-Z])')
def __init__(self, stream=None):
super(ANSIStream, self).__init__(stream)
self.encoding = getattr(self.stream, 'encoding', 'utf-8')
def write(self, text):
if isinstance(text, type(u'')):
text = text.encode(self.encoding, 'replace')
if not self.isatty:
return self.strip_and_write(text)
if self.isansi:
return self.stream.write(text)
if not self.isansi and self.set_console is None:
return self.strip_and_write(text)
self.write_and_convert(text)
def strip_and_write(self, text):
self.stream.write(self.ANSI_RE.sub(b'', text))
def write_and_convert(self, text):
'''
Write the given text to our wrapped stream, stripping any ANSI
sequences from the text, and optionally converting them into win32
calls.
'''
self.last_state = (None, None, False)
cursor = 0
for match in self.ANSI_RE.finditer(text):
start, end = match.span()
self.write_plain_text(text, cursor, start)
self.convert_ansi(*match.groups())
cursor = end
self.write_plain_text(text, cursor, len(text))
def write_plain_text(self, text, start, end):
if start < end:
self.stream.write(text[start:end])
self.stream.flush()
def convert_ansi(self, paramstring, command):
params = self.extract_params(paramstring)
self.call_win32(command, params)
def extract_params(self, paramstring):
def split(paramstring):
for p in paramstring.split(b';'):
if p:
yield int(p)
return tuple(split(paramstring))
def call_win32(self, command, params):
if command != b'm': return
fg, bg, bold = self.last_state
for param in params:
if param in RCOLORS:
fg = RCOLORS[param]
elif param in RBACKGROUNDS:
bg = RBACKGROUNDS[param]
elif param == 1:
bold = True
elif param == 0:
fg = 'white'
bg, bold = None, False
self.last_state = (fg, bg, bold)
if fg or bg or bold:
self.set_console(self.file_handle, to_flag(fg, bg, bold))
else:
self.set_console(self.file_handle, WCOLORS['white'])
def geometry():
try:
import curses
curses.setupterm()
except:
return 80, 80
else:
width = curses.tigetnum('cols') or 80
height = curses.tigetnum('lines') or 80
return width, height
def test():
s = ANSIStream()
text = [colored(t, fg=t)+'. '+colored(t, fg=t, bold=True)+'.' for t in
('red', 'yellow', 'green', 'white', 'cyan', 'magenta', 'blue',)]
s.write('\n'.join(text))
print()

View File

@ -1,215 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, re, os
""" Get information about the terminal we are running in """
class TerminalController:
"""
A class that can be used to portably generate formatted output to
a terminal.
`TerminalController` defines a set of instance variables whose
values are initialized to the control sequence necessary to
perform a given action. These can be simply included in normal
output to the terminal:
>>> term = TerminalController()
>>> print 'This is '+term.GREEN+'green'+term.NORMAL
Alternatively, the `render()` method can used, which replaces
'${action}' with the string required to perform 'action':
>>> term = TerminalController()
>>> print term.render('This is ${GREEN}green${NORMAL}')
If the terminal doesn't support a given action, then the value of
the corresponding instance variable will be set to ''. As a
result, the above code will still work on terminals that do not
support color, except that their output will not be colored.
Also, this means that you can test whether the terminal supports a
given action by simply testing the truth value of the
corresponding instance variable:
>>> term = TerminalController()
>>> if term.CLEAR_SCREEN:
... print 'This terminal supports clearing the screen.'
Finally, if the width and height of the terminal are known, then
they will be stored in the `COLS` and `LINES` attributes.
"""
# Cursor movement:
BOL = '' #: Move the cursor to the beginning of the line
UP = '' #: Move the cursor up one line
DOWN = '' #: Move the cursor down one line
LEFT = '' #: Move the cursor left one char
RIGHT = '' #: Move the cursor right one char
# Deletion:
CLEAR_SCREEN = '' #: Clear the screen and move to home position
CLEAR_EOL = '' #: Clear to the end of the line.
CLEAR_BOL = '' #: Clear to the beginning of the line.
CLEAR_EOS = '' #: Clear to the end of the screen
# Output modes:
BOLD = '' #: Turn on bold mode
BLINK = '' #: Turn on blink mode
DIM = '' #: Turn on half-bright mode
REVERSE = '' #: Turn on reverse-video mode
NORMAL = '' #: Turn off all modes
# Cursor display:
HIDE_CURSOR = '' #: Make the cursor invisible
SHOW_CURSOR = '' #: Make the cursor visible
# Terminal size:
COLS = None #: Width of the terminal (None for unknown)
LINES = None #: Height of the terminal (None for unknown)
# Foreground colors:
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
# Background colors:
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
_STRING_CAPABILITIES = """
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
def __init__(self, term_stream=sys.stdout):
"""
Create a `TerminalController` and initialize its attributes
with appropriate values for the current terminal.
`term_stream` is the stream that will be used for terminal
output; if this stream is not a tty, then the terminal is
assumed to be a dumb terminal (i.e., have no capabilities).
"""
# Curses isn't available on all platforms
try: import curses
except: return
# If the stream isn't a tty, then assume it has no capabilities.
if os.environ.get('CALIBRE_WORKER', None) is not None or not hasattr(term_stream, 'isatty') or not term_stream.isatty(): return
# Check the terminal type. If we fail, then assume that the
# terminal has no capabilities.
try: curses.setupterm()
except: return
# Look up numeric capabilities.
self.COLS = curses.tigetnum('cols')
self.LINES = curses.tigetnum('lines')
# Look up string capabilities.
for capability in self._STRING_CAPABILITIES:
(attrib, cap_name) = capability.split('=')
setattr(self, attrib, self._tigetstr(cap_name) or '')
# Colors
set_fg = self._tigetstr('setf')
if set_fg:
for i,color in zip(range(len(self._COLORS)), self._COLORS):
setattr(self, color, curses.tparm(set_fg, i) or '')
set_fg_ansi = self._tigetstr('setaf')
if set_fg_ansi:
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
set_bg = self._tigetstr('setb')
if set_bg:
for i,color in zip(range(len(self._COLORS)), self._COLORS):
setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
set_bg_ansi = self._tigetstr('setab')
if set_bg_ansi:
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
def _tigetstr(self, cap_name):
# String capabilities can include "delays" of the form "$<2>".
# For any modern terminal, we should be able to just ignore
# these, so strip them out.
import curses
cap = curses.tigetstr(cap_name) or ''
return re.sub(r'\$<\d+>[/*]?', '', cap)
def render(self, template):
"""
Replace each $-substitutions in the given template string with
the corresponding terminal control string (if it's defined) or
'' (if it's not).
"""
return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
def _render_sub(self, match):
s = match.group()
if s == '$$': return s
else: return getattr(self, s[2:-1])
#######################################################################
# Example use case: progress bar
#######################################################################
class ProgressBar:
"""
A 3-line progress bar, which looks like::
Header
20% [===========----------------------------------]
progress message
The progress bar is colored, if the terminal supports color
output; and adjusts to the width of the terminal.
If the terminal doesn't have the required capabilities, it uses a
simple progress bar.
"""
BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
def __init__(self, term, header, no_progress_bar = False):
self.term, self.no_progress_bar = term, no_progress_bar
self.fancy = self.term.CLEAR_EOL and self.term.UP and self.term.BOL
if self.fancy:
self.width = self.term.COLS or 75
self.bar = term.render(self.BAR)
self.header = self.term.render(self.HEADER % header.center(self.width))
if isinstance(self.header, unicode):
self.header = self.header.encode('utf-8')
self.cleared = 1 #: true if we haven't drawn the bar yet.
def update(self, percent, message=''):
if isinstance(message, unicode):
message = message.encode('utf-8', 'replace')
if self.no_progress_bar:
if message:
print message
elif self.fancy:
if self.cleared:
sys.stdout.write(self.header)
self.cleared = 0
n = int((self.width-10)*percent)
msg = message.center(self.width)
sys.stdout.write(
self.term.BOL + self.term.UP + self.term.CLEAR_EOL +
(self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) +
self.term.CLEAR_EOL + msg)
sys.stdout.flush()
else:
if not message:
print '%d%%'%(percent*100),
else:
print '%d%%'%(percent*100), message
sys.stdout.flush()
def clear(self):
if self.fancy and not self.cleared:
sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
self.term.UP + self.term.CLEAR_EOL +
self.term.UP + self.term.CLEAR_EOL)
self.cleared = 1