1
# Copyright 2006 Edward Loper. May be distributed under the same terms
4
# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116
10
class TerminalController:
12
A class that can be used to portably generate formatted output to
15
`TerminalController` defines a set of instance variables whose
16
values are initialized to the control sequence necessary to
17
perform a given action. These can be simply included in normal
18
output to the terminal:
20
>>> term = TerminalController()
21
>>> print 'This is '+term.GREEN+'green'+term.NORMAL
23
Alternatively, the `render()` method can used, which replaces
24
'${action}' with the string required to perform 'action':
26
>>> term = TerminalController()
27
>>> print term.render('This is ${GREEN}green${NORMAL}')
29
If the terminal doesn't support a given action, then the value of
30
the corresponding instance variable will be set to ''. As a
31
result, the above code will still work on terminals that do not
32
support color, except that their output will not be colored.
33
Also, this means that you can test whether the terminal supports a
34
given action by simply testing the truth value of the
35
corresponding instance variable:
37
>>> term = TerminalController()
38
>>> if term.CLEAR_SCREEN:
39
... print 'This terminal supports clearning the screen.'
41
Finally, if the width and height of the terminal are known, then
42
they will be stored in the `COLS` and `LINES` attributes.
45
BOL = '' #: Move the cursor to the beginning of the line
46
UP = '' #: Move the cursor up one line
47
DOWN = '' #: Move the cursor down one line
48
LEFT = '' #: Move the cursor left one char
49
RIGHT = '' #: Move the cursor right one char
52
CLEAR_SCREEN = '' #: Clear the screen and move to home position
53
CLEAR_EOL = '' #: Clear to the end of the line.
54
CLEAR_BOL = '' #: Clear to the beginning of the line.
55
CLEAR_EOS = '' #: Clear to the end of the screen
58
BOLD = '' #: Turn on bold mode
59
BLINK = '' #: Turn on blink mode
60
DIM = '' #: Turn on half-bright mode
61
REVERSE = '' #: Turn on reverse-video mode
62
NORMAL = '' #: Turn off all modes
65
HIDE_CURSOR = '' #: Make the cursor invisible
66
SHOW_CURSOR = '' #: Make the cursor visible
69
COLS = None #: Width of the terminal (None for unknown)
70
LINES = None #: Height of the terminal (None for unknown)
73
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
76
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
77
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
79
_STRING_CAPABILITIES = """
80
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
81
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
82
BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
83
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
84
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
85
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
87
def __init__(self, term_stream=sys.stdout):
89
Create a `TerminalController` and initialize its attributes
90
with appropriate values for the current terminal.
91
`term_stream` is the stream that will be used for terminal
92
output; if this stream is not a tty, then the terminal is
93
assumed to be a dumb terminal (i.e., have no capabilities).
95
# Curses isn't available on all platforms
101
# If the stream isn't a tty, then assume it has no capabilities.
102
if not term_stream.isatty():
105
# Check the terminal type. If we fail, then assume that the
106
# terminal has no capabilities.
112
# Look up numeric capabilities.
113
self.COLS = curses.tigetnum('cols')
114
self.LINES = curses.tigetnum('lines')
116
# Look up string capabilities.
117
for capability in self._STRING_CAPABILITIES:
118
(attrib, cap_name) = capability.split('=')
119
setattr(self, attrib, self._tigetstr(cap_name) or '')
122
set_fg = self._tigetstr('setf')
124
for i, color in zip(range(len(self._COLORS)), self._COLORS):
125
setattr(self, color, curses.tparm(set_fg, i) or '')
126
set_fg_ansi = self._tigetstr('setaf')
128
for i, color in zip(range(len(self._ANSICOLORS)),
130
setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
131
set_bg = self._tigetstr('setb')
133
for i, color in zip(range(len(self._COLORS)), self._COLORS):
134
setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
135
set_bg_ansi = self._tigetstr('setab')
137
for i, color in zip(range(len(self._ANSICOLORS)),
139
setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
141
def _tigetstr(self, cap_name):
142
# String capabilities can include "delays" of the form "$<2>".
143
# For any modern terminal, we should be able to just ignore
144
# these, so strip them out.
146
cap = curses.tigetstr(cap_name) or ''
147
return re.sub(r'\$<\d+>[/*]?', '', cap)
149
def render(self, template):
151
Replace each $-substitutions in the given template string with
152
the corresponding terminal control string (if it's defined) or
155
return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
157
def _render_sub(self, match):
162
return getattr(self, s[2:-1])
164
#######################################################################
165
# Example use case: progress bar
166
#######################################################################
171
A 3-line progress bar, which looks like::
174
20% [===========----------------------------------]
177
The progress bar is colored, if the terminal supports color
178
output; and adjusts to the width of the terminal.
180
BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
181
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
183
def __init__(self, term, header):
185
if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
186
raise ValueError("Terminal isn't capable enough -- you "
187
"should use a simpler progress dispaly.")
188
self.width = self.term.COLS or 75
189
self.bar = term.render(self.BAR)
190
self.header = self.term.render(self.HEADER % header.center(self.width))
191
self.cleared = 1 #: true if we haven't drawn the bar yet.
194
def update(self, percent, message):
196
sys.stdout.write(self.header)
198
n = int((self.width-10)*percent)
200
self.term.BOL + self.term.UP + self.term.CLEAR_EOL +
201
(self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) +
202
self.term.CLEAR_EOL + message.center(self.width))
206
sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
207
self.term.UP + self.term.CLEAR_EOL +
208
self.term.UP + self.term.CLEAR_EOL)
211
if __name__ == '__main__':
212
term = TerminalController()
213
print term.render('${BOLD}${RED}Error:${NORMAL}'), 'paper is ripped'