mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Enable colored output for the CLI on windows as well
This commit is contained in:
parent
4e76a78171
commit
d00a37e66a
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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":
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
239
src/calibre/utils/terminal.py
Normal file
239
src/calibre/utils/terminal.py
Normal 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()
|
||||||
|
|
@ -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
|
|
Loading…
x
Reference in New Issue
Block a user