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')
108 self.XN = curses.tigetflag('xenl')
110 # Look up string capabilities.
111 for capability in self._STRING_CAPABILITIES:
112 (attrib, cap_name) = capability.split('=')
113 setattr(self, attrib, self._tigetstr(cap_name) or '')
116 set_fg = self._tigetstr('setf')
118 for i,color in zip(range(len(self._COLORS)), self._COLORS):
119 setattr(self, color, curses.tparm(set_fg, i) or '')
120 set_fg_ansi = self._tigetstr('setaf')
122 for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
123 setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
124 set_bg = self._tigetstr('setb')
126 for i,color in zip(range(len(self._COLORS)), self._COLORS):
127 setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
128 set_bg_ansi = self._tigetstr('setab')
130 for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
131 setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
133 def _tigetstr(self, cap_name):
134 # String capabilities can include "delays" of the form "$<2>".
135 # For any modern terminal, we should be able to just ignore
136 # these, so strip them out.
138 cap = curses.tigetstr(cap_name)
142 cap = cap.decode('ascii')
143 return re.sub(r'\$<\d+>[/*]?', '', cap)
145 def render(self, template):
147 Replace each $-substitutions in the given template string with
148 the corresponding terminal control string (if it's defined) or
151 return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
153 def _render_sub(self, match):
155 if s == '$$': return s
156 else: return getattr(self, s[2:-1])
158 #######################################################################
159 # Example use case: progress bar
160 #######################################################################
162 class SimpleProgressBar:
164 A simple progress bar which doesn't need any terminal support.
166 This prints out a progress bar like:
167 'Header: 0 .. 10.. 20.. ...'
170 def __init__(self, header):
174 def update(self, percent, message):
175 if self.atIndex is None:
176 sys.stdout.write(self.header)
179 next = int(percent*50)
180 if next == self.atIndex:
183 for i in range(self.atIndex, next):
186 sys.stdout.write('%-2d' % (i*2))
188 pass # Skip second char
190 sys.stdout.write('.')
192 sys.stdout.write(' ')
197 if self.atIndex is not None:
198 sys.stdout.write('\n')
204 A 3-line progress bar, which looks like::
207 20% [===========----------------------------------]
210 The progress bar is colored, if the terminal supports color
211 output; and adjusts to the width of the terminal.
213 BAR = '%s${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}%s'
214 HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
216 def __init__(self, term, header, useETA=True):
218 if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
219 raise ValueError("Terminal isn't capable enough -- you "
220 "should use a simpler progress dispaly.")
221 self.BOL = self.term.BOL # BoL from col#79
222 self.XNL = "\n" # Newline from col#79
224 self.width = self.term.COLS
226 self.BOL = self.term.UP + self.term.BOL
227 self.XNL = "" # Cursor must be fed to the next line
230 self.bar = term.render(self.BAR)
231 self.header = self.term.render(self.HEADER % header.center(self.width))
232 self.cleared = 1 #: true if we haven't drawn the bar yet.
235 self.startTime = time.time()
238 def update(self, percent, message):
240 sys.stdout.write(self.header)
242 prefix = '%3d%% ' % (percent*100,)
245 elapsed = time.time() - self.startTime
246 if percent > .0001 and elapsed > 1:
247 total = elapsed / percent
248 eta = int(total - elapsed)
252 suffix = ' ETA: %02d:%02d:%02d'%(h,m,s)
253 barWidth = self.width - len(prefix) - len(suffix) - 2
254 n = int(barWidth*percent)
255 if len(message) < self.width:
256 message = message + ' '*(self.width - len(message))
258 message = '... ' + message[-(self.width-4):]
260 self.BOL + self.term.UP + self.term.CLEAR_EOL +
261 (self.bar % (prefix, '='*n, '-'*(barWidth-n), suffix)) +
263 self.term.CLEAR_EOL + message)
269 sys.stdout.write(self.BOL + self.term.CLEAR_EOL +
270 self.term.UP + self.term.CLEAR_EOL +
271 self.term.UP + self.term.CLEAR_EOL)
276 tc = TerminalController()
277 p = ProgressBar(tc, 'Tests')
279 p.update(i/100., str(i))
282 if __name__=='__main__':