1
2
3
4
5
6 import re
7 import sys
8
9
11 """
12 A class that can be used to portably generate formatted output to
13 a terminal.
14
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:
19
20 >>> term = TerminalController()
21 >>> print 'This is '+term.GREEN+'green'+term.NORMAL
22
23 Alternatively, the `render()` method can used, which replaces
24 '${action}' with the string required to perform 'action':
25
26 >>> term = TerminalController()
27 >>> print term.render('This is ${GREEN}green${NORMAL}')
28
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:
36
37 >>> term = TerminalController()
38 >>> if term.CLEAR_SCREEN:
39 ... print 'This terminal supports clearning the screen.'
40
41 Finally, if the width and height of the terminal are known, then
42 they will be stored in the `COLS` and `LINES` attributes.
43 """
44
45 BOL = ''
46 UP = ''
47 DOWN = ''
48 LEFT = ''
49 RIGHT = ''
50
51
52 CLEAR_SCREEN = ''
53 CLEAR_EOL = ''
54 CLEAR_BOL = ''
55 CLEAR_EOS = ''
56
57
58 BOLD = ''
59 BLINK = ''
60 DIM = ''
61 REVERSE = ''
62 NORMAL = ''
63
64
65 HIDE_CURSOR = ''
66 SHOW_CURSOR = ''
67
68
69 COLS = None
70 LINES = None
71
72
73 BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
74
75
76 BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
77 BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
78
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()
86
88 """
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).
94 """
95
96 try:
97 import curses
98 except ImportError:
99 return
100
101
102 if not term_stream.isatty():
103 return
104
105
106
107 try:
108 curses.setupterm()
109 except:
110 return
111
112
113 self.COLS = curses.tigetnum('cols')
114 self.LINES = curses.tigetnum('lines')
115
116
117 for capability in self._STRING_CAPABILITIES:
118 (attrib, cap_name) = capability.split('=')
119 setattr(self, attrib, self._tigetstr(cap_name) or '')
120
121
122 set_fg = self._tigetstr('setf')
123 if set_fg:
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')
127 if set_fg_ansi:
128 for i, color in zip(range(len(self._ANSICOLORS)),
129 self._ANSICOLORS):
130 setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
131 set_bg = self._tigetstr('setb')
132 if set_bg:
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')
136 if set_bg_ansi:
137 for i, color in zip(range(len(self._ANSICOLORS)),
138 self._ANSICOLORS):
139 setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
140
142
143
144
145 import curses
146 cap = curses.tigetstr(cap_name) or ''
147 return re.sub(r'\$<\d+>[/*]?', '', cap)
148
150 """
151 Replace each $-substitutions in the given template string with
152 the corresponding terminal control string (if it's defined) or
153 '' (if it's not).
154 """
155 return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
156
158 s = match.group()
159 if s == '$$':
160 return s
161 else:
162 return getattr(self, s[2:-1])
163
164
165
166
167
168
170 """
171 A 3-line progress bar, which looks like::
172
173 Header
174 20% [===========----------------------------------]
175 progress message
176
177 The progress bar is colored, if the terminal supports color
178 output; and adjusts to the width of the terminal.
179 """
180 BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
181 HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
182
184 self.term = term
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
192 self.update(0, '')
193
194 - def update(self, percent, message):
195 if self.cleared:
196 sys.stdout.write(self.header)
197 self.cleared = 0
198 n = int((self.width-10)*percent)
199 sys.stdout.write(
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))
203
210
211 if __name__ == '__main__':
212 term = TerminalController()
213 print term.render('${BOLD}${RED}Error:${NORMAL}'), 'paper is ripped'
214