3 # Source: http://code.activestate.com/recipes/475116/, with
4 # modifications by Daniel Dunbar.
9 # Encode to UTF-8 to get binary data.
10 return str.encode('utf-8')
12 class TerminalController:
14 A class that can be used to portably generate formatted output to
17 `TerminalController` defines a set of instance variables whose
18 values are initialized to the control sequence necessary to
19 perform a given action. These can be simply included in normal
20 output to the terminal:
22 >>> term = TerminalController()
23 >>> print('This is '+term.GREEN+'green'+term.NORMAL)
25 Alternatively, the `render()` method can used, which replaces
26 '${action}' with the string required to perform 'action':
28 >>> term = TerminalController()
29 >>> print(term.render('This is ${GREEN}green${NORMAL}'))
31 If the terminal doesn't support a given action, then the value of
32 the corresponding instance variable will be set to ''. As a
33 result, the above code will still work on terminals that do not
34 support color, except that their output will not be colored.
35 Also, this means that you can test whether the terminal supports a
36 given action by simply testing the truth value of the
37 corresponding instance variable:
39 >>> term = TerminalController()
40 >>> if term.CLEAR_SCREEN:
41 ... print('This terminal supports clearning the screen.')
43 Finally, if the width and height of the terminal are known, then
44 they will be stored in the `COLS` and `LINES` attributes.
47 BOL = '' #: Move the cursor to the beginning of the line
48 UP = '' #: Move the cursor up one line
49 DOWN = '' #: Move the cursor down one line
50 LEFT = '' #: Move the cursor left one char
51 RIGHT = '' #: Move the cursor right one char
54 CLEAR_SCREEN = '' #: Clear the screen and move to home position
55 CLEAR_EOL = '' #: Clear to the end of the line.
56 CLEAR_BOL = '' #: Clear to the beginning of the line.
57 CLEAR_EOS = '' #: Clear to the end of the screen
60 BOLD = '' #: Turn on bold mode
61 BLINK = '' #: Turn on blink mode
62 DIM = '' #: Turn on half-bright mode
63 REVERSE = '' #: Turn on reverse-video mode
64 NORMAL = '' #: Turn off all modes
67 HIDE_CURSOR = '' #: Make the cursor invisible
68 SHOW_CURSOR = '' #: Make the cursor visible
71 COLS = None #: Width of the terminal (None for unknown)
72 LINES = None #: Height of the terminal (None for unknown)
75 BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
78 BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
79 BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
81 _STRING_CAPABILITIES = """
82 BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
83 CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
84 BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
85 HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
86 _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
87 _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
89 def __init__(self, term_stream=sys.stdout):
91 Create a `TerminalController` and initialize its attributes
92 with appropriate values for the current terminal.
93 `term_stream` is the stream that will be used for terminal
94 output; if this stream is not a tty, then the terminal is
95 assumed to be a dumb terminal (i.e., have no capabilities).
97 # 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(): return
104 # Check the terminal type. If we fail, then assume that the
105 # terminal has no capabilities.
106 try: curses.setupterm()
109 # Look up numeric capabilities.
110 self.COLS = curses.tigetnum('cols')
111 self.LINES = curses.tigetnum('lines')
112 self.XN = curses.tigetflag('xenl')
114 # Look up string capabilities.
115 for capability in self._STRING_CAPABILITIES:
116 (attrib, cap_name) = capability.split('=')
117 setattr(self, attrib, self._tigetstr(cap_name) or '')
120 set_fg = self._tigetstr('setf')
122 for i,color in zip(range(len(self._COLORS)), self._COLORS):
123 setattr(self, color, self._tparm(set_fg, i))
124 set_fg_ansi = self._tigetstr('setaf')
126 for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
127 setattr(self, color, self._tparm(set_fg_ansi, i))
128 set_bg = self._tigetstr('setb')
130 for i,color in zip(range(len(self._COLORS)), self._COLORS):
131 setattr(self, 'BG_'+color, self._tparm(set_bg, i))
132 set_bg_ansi = self._tigetstr('setab')
134 for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
135 setattr(self, 'BG_'+color, self._tparm(set_bg_ansi, i))
137 def _tparm(self, arg, index):
139 return curses.tparm(to_bytes(arg), index).decode('utf-8') 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)
150 cap = cap.decode('utf-8')
151 return re.sub(r'\$<\d+>[/*]?', '', cap)
153 def render(self, template):
155 Replace each $-substitutions in the given template string with
156 the corresponding terminal control string (if it's defined) or
159 return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
161 def _render_sub(self, match):
163 if s == '$$': return s
164 else: return getattr(self, s[2:-1])
166 #######################################################################
167 # Example use case: progress bar
168 #######################################################################
170 class SimpleProgressBar:
172 A simple progress bar which doesn't need any terminal support.
174 This prints out a progress bar like:
175 'Header: 0 .. 10.. 20.. ...'
178 def __init__(self, header):
182 def update(self, percent, message):
183 if self.atIndex is None:
184 sys.stdout.write(self.header)
187 next = int(percent*50)
188 if next == self.atIndex:
191 for i in range(self.atIndex, next):
194 sys.stdout.write('%-2d' % (i*2))
196 pass # Skip second char
198 sys.stdout.write('.')
200 sys.stdout.write(' ')
205 if self.atIndex is not None:
206 sys.stdout.write('\n')
212 A 3-line progress bar, which looks like::
215 20% [===========----------------------------------]
218 The progress bar is colored, if the terminal supports color
219 output; and adjusts to the width of the terminal.
221 BAR = '%s${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}%s'
222 HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
224 def __init__(self, term, header, useETA=True):
226 if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
227 raise ValueError("Terminal isn't capable enough -- you "
228 "should use a simpler progress dispaly.")
229 self.BOL = self.term.BOL # BoL from col#79
230 self.XNL = "\n" # Newline from col#79
232 self.width = self.term.COLS
234 self.BOL = self.term.UP + self.term.BOL
235 self.XNL = "" # Cursor must be fed to the next line
238 self.bar = term.render(self.BAR)
239 self.header = self.term.render(self.HEADER % header.center(self.width))
240 self.cleared = 1 #: true if we haven't drawn the bar yet.
243 self.startTime = time.time()
246 def update(self, percent, message):
248 sys.stdout.write(self.header)
250 prefix = '%3d%% ' % (percent*100,)
253 elapsed = time.time() - self.startTime
254 if percent > .0001 and elapsed > 1:
255 total = elapsed / percent
256 eta = int(total - elapsed)
260 suffix = ' ETA: %02d:%02d:%02d'%(h,m,s)
261 barWidth = self.width - len(prefix) - len(suffix) - 2
262 n = int(barWidth*percent)
263 if len(message) < self.width:
264 message = message + ' '*(self.width - len(message))
266 message = '... ' + message[-(self.width-4):]
268 self.BOL + self.term.UP + self.term.CLEAR_EOL +
269 (self.bar % (prefix, '='*n, '-'*(barWidth-n), suffix)) +
271 self.term.CLEAR_EOL + message)
277 sys.stdout.write(self.BOL + self.term.CLEAR_EOL +
278 self.term.UP + self.term.CLEAR_EOL +
279 self.term.UP + self.term.CLEAR_EOL)
284 tc = TerminalController()
285 p = ProgressBar(tc, 'Tests')
287 p.update(i/100., str(i))
290 if __name__=='__main__':