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