3
# Source: http://code.activestate.com/recipes/475116/, with
4
# modifications by Daniel Dunbar.
8
class TerminalController:
10
A class that can be used to portably generate formatted output to
13
`TerminalController` defines a set of instance variables whose
14
values are initialized to the control sequence necessary to
15
perform a given action. These can be simply included in normal
16
output to the terminal:
18
>>> term = TerminalController()
19
>>> print 'This is '+term.GREEN+'green'+term.NORMAL
21
Alternatively, the `render()` method can used, which replaces
22
'${action}' with the string required to perform 'action':
24
>>> term = TerminalController()
25
>>> print term.render('This is ${GREEN}green${NORMAL}')
27
If the terminal doesn't support a given action, then the value of
28
the corresponding instance variable will be set to ''. As a
29
result, the above code will still work on terminals that do not
30
support color, except that their output will not be colored.
31
Also, this means that you can test whether the terminal supports a
32
given action by simply testing the truth value of the
33
corresponding instance variable:
35
>>> term = TerminalController()
36
>>> if term.CLEAR_SCREEN:
37
... print 'This terminal supports clearning the screen.'
39
Finally, if the width and height of the terminal are known, then
40
they will be stored in the `COLS` and `LINES` attributes.
43
BOL = '' #: Move the cursor to the beginning of the line
44
UP = '' #: Move the cursor up one line
45
DOWN = '' #: Move the cursor down one line
46
LEFT = '' #: Move the cursor left one char
47
RIGHT = '' #: Move the cursor right one char
50
CLEAR_SCREEN = '' #: Clear the screen and move to home position
51
CLEAR_EOL = '' #: Clear to the end of the line.
52
CLEAR_BOL = '' #: Clear to the beginning of the line.
53
CLEAR_EOS = '' #: Clear to the end of the screen
56
BOLD = '' #: Turn on bold mode
57
BLINK = '' #: Turn on blink mode
58
DIM = '' #: Turn on half-bright mode
59
REVERSE = '' #: Turn on reverse-video mode
60
NORMAL = '' #: Turn off all modes
63
HIDE_CURSOR = '' #: Make the cursor invisible
64
SHOW_CURSOR = '' #: Make the cursor visible
67
COLS = None #: Width of the terminal (None for unknown)
68
LINES = None #: Height of the terminal (None for unknown)
71
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
74
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
75
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
77
_STRING_CAPABILITIES = """
78
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
79
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
80
BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
81
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
82
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
83
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
85
def __init__(self, term_stream=sys.stdout):
87
Create a `TerminalController` and initialize its attributes
88
with appropriate values for the current terminal.
89
`term_stream` is the stream that will be used for terminal
90
output; if this stream is not a tty, then the terminal is
91
assumed to be a dumb terminal (i.e., have no capabilities).
93
# Curses isn't available on all platforms
97
# If the stream isn't a tty, then assume it has no capabilities.
98
if not term_stream.isatty(): return
100
# Check the terminal type. If we fail, then assume that the
101
# terminal has no capabilities.
102
try: curses.setupterm()
105
# Look up numeric capabilities.
106
self.COLS = curses.tigetnum('cols')
107
self.LINES = curses.tigetnum('lines')
109
# Look up string capabilities.
110
for capability in self._STRING_CAPABILITIES:
111
(attrib, cap_name) = capability.split('=')
112
setattr(self, attrib, self._tigetstr(cap_name) or '')
115
set_fg = self._tigetstr('setf')
117
for i,color in zip(range(len(self._COLORS)), self._COLORS):
118
setattr(self, color, curses.tparm(set_fg, i) or '')
119
set_fg_ansi = self._tigetstr('setaf')
121
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
122
setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
123
set_bg = self._tigetstr('setb')
125
for i,color in zip(range(len(self._COLORS)), self._COLORS):
126
setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
127
set_bg_ansi = self._tigetstr('setab')
129
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
130
setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
132
def _tigetstr(self, cap_name):
133
# String capabilities can include "delays" of the form "$<2>".
134
# For any modern terminal, we should be able to just ignore
135
# these, so strip them out.
137
cap = curses.tigetstr(cap_name) or ''
138
return re.sub(r'\$<\d+>[/*]?', '', cap)
140
def render(self, template):
142
Replace each $-substitutions in the given template string with
143
the corresponding terminal control string (if it's defined) or
146
return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
148
def _render_sub(self, match):
150
if s == '$$': return s
151
else: return getattr(self, s[2:-1])
153
#######################################################################
154
# Example use case: progress bar
155
#######################################################################
157
class SimpleProgressBar:
159
A simple progress bar which doesn't need any terminal support.
161
This prints out a progress bar like:
162
'Header: 0 .. 10.. 20.. ...'
165
def __init__(self, header):
169
def update(self, percent, message):
170
if self.atIndex is None:
171
sys.stdout.write(self.header)
174
next = int(percent*50)
175
if next == self.atIndex:
178
for i in range(self.atIndex, next):
181
sys.stdout.write('%-2d' % (i*2))
183
pass # Skip second char
185
sys.stdout.write('.')
187
sys.stdout.write(' ')
192
if self.atIndex is not None:
193
sys.stdout.write('\n')
199
A 3-line progress bar, which looks like::
202
20% [===========----------------------------------]
205
The progress bar is colored, if the terminal supports color
206
output; and adjusts to the width of the terminal.
208
BAR = '%s${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}%s\n'
209
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
211
def __init__(self, term, header, useETA=True):
213
if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
214
raise ValueError("Terminal isn't capable enough -- you "
215
"should use a simpler progress dispaly.")
216
self.width = self.term.COLS or 75
217
self.bar = term.render(self.BAR)
218
self.header = self.term.render(self.HEADER % header.center(self.width))
219
self.cleared = 1 #: true if we haven't drawn the bar yet.
222
self.startTime = time.time()
225
def update(self, percent, message):
227
sys.stdout.write(self.header)
229
prefix = '%3d%% ' % (percent*100,)
232
elapsed = time.time() - self.startTime
233
if percent > .0001 and elapsed > 1:
234
total = elapsed / percent
235
eta = int(total - elapsed)
239
suffix = ' ETA: %02d:%02d:%02d'%(h,m,s)
240
barWidth = self.width - len(prefix) - len(suffix) - 2
241
n = int(barWidth*percent)
242
if len(message) < self.width:
243
message = message + ' '*(self.width - len(message))
245
message = '... ' + message[-(self.width-4):]
247
self.term.BOL + self.term.UP + self.term.CLEAR_EOL +
248
(self.bar % (prefix, '='*n, '-'*(barWidth-n), suffix)) +
249
self.term.CLEAR_EOL + message)
253
sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
254
self.term.UP + self.term.CLEAR_EOL +
255
self.term.UP + self.term.CLEAR_EOL)
260
tc = TerminalController()
261
p = ProgressBar(tc, 'Tests')
263
p.update(i/100., str(i))
266
if __name__=='__main__':