1be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar#!/usr/bin/env python
2be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
3be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar# Source: http://code.activestate.com/recipes/475116/, with
4be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar# modifications by Daniel Dunbar.
5be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
6be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbarimport sys, re, time
7be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
84db169d31e34df89d10660960070de073f0f28afDaniel Dunbardef to_bytes(str):
94db169d31e34df89d10660960070de073f0f28afDaniel Dunbar    # Encode to Latin1 to get binary data.
104db169d31e34df89d10660960070de073f0f28afDaniel Dunbar    return str.encode('ISO-8859-1')
114db169d31e34df89d10660960070de073f0f28afDaniel Dunbar
12be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbarclass TerminalController:
13be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    """
14be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    A class that can be used to portably generate formatted output to
15be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    a terminal.
16be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
17be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    `TerminalController` defines a set of instance variables whose
18be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    values are initialized to the control sequence necessary to
19be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    perform a given action.  These can be simply included in normal
20be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    output to the terminal:
21be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
22be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        >>> term = TerminalController()
230d038e3e8852bf4fde949136ca9c2815f64febd0Daniel Dunbar        >>> print('This is '+term.GREEN+'green'+term.NORMAL)
24be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
25be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    Alternatively, the `render()` method can used, which replaces
26be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    '${action}' with the string required to perform 'action':
27be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
28be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        >>> term = TerminalController()
290d038e3e8852bf4fde949136ca9c2815f64febd0Daniel Dunbar        >>> print(term.render('This is ${GREEN}green${NORMAL}'))
30be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
31be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    If the terminal doesn't support a given action, then the value of
32be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    the corresponding instance variable will be set to ''.  As a
33be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    result, the above code will still work on terminals that do not
34be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    support color, except that their output will not be colored.
35be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    Also, this means that you can test whether the terminal supports a
36be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    given action by simply testing the truth value of the
37be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    corresponding instance variable:
38be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
39be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        >>> term = TerminalController()
40be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        >>> if term.CLEAR_SCREEN:
410d038e3e8852bf4fde949136ca9c2815f64febd0Daniel Dunbar        ...     print('This terminal supports clearning the screen.')
42be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
43be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    Finally, if the width and height of the terminal are known, then
44be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    they will be stored in the `COLS` and `LINES` attributes.
45be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    """
46be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    # Cursor movement:
47be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    BOL = ''             #: Move the cursor to the beginning of the line
48be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    UP = ''              #: Move the cursor up one line
49be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    DOWN = ''            #: Move the cursor down one line
50be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    LEFT = ''            #: Move the cursor left one char
51be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    RIGHT = ''           #: Move the cursor right one char
52be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
53be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    # Deletion:
54be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    CLEAR_SCREEN = ''    #: Clear the screen and move to home position
55be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    CLEAR_EOL = ''       #: Clear to the end of the line.
56be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    CLEAR_BOL = ''       #: Clear to the beginning of the line.
57be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    CLEAR_EOS = ''       #: Clear to the end of the screen
58be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
59be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    # Output modes:
60be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    BOLD = ''            #: Turn on bold mode
61be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    BLINK = ''           #: Turn on blink mode
62be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    DIM = ''             #: Turn on half-bright mode
63be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    REVERSE = ''         #: Turn on reverse-video mode
64be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    NORMAL = ''          #: Turn off all modes
65be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
66be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    # Cursor display:
67be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    HIDE_CURSOR = ''     #: Make the cursor invisible
68be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    SHOW_CURSOR = ''     #: Make the cursor visible
69be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
70be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    # Terminal size:
71be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    COLS = None          #: Width of the terminal (None for unknown)
72be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    LINES = None         #: Height of the terminal (None for unknown)
73be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
74be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    # Foreground colors:
75be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
76be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
77be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    # Background colors:
78be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
79be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
80be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
81be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    _STRING_CAPABILITIES = """
82be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
83be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
84be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
85be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
86be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
87be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
88be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
89be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def __init__(self, term_stream=sys.stdout):
90be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        """
91be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        Create a `TerminalController` and initialize its attributes
92be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        with appropriate values for the current terminal.
93be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        `term_stream` is the stream that will be used for terminal
94be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        output; if this stream is not a tty, then the terminal is
95be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        assumed to be a dumb terminal (i.e., have no capabilities).
96be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        """
97be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # Curses isn't available on all platforms
98be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        try: import curses
99be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        except: return
100be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
101be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # If the stream isn't a tty, then assume it has no capabilities.
102be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if not term_stream.isatty(): return
103be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
104be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # Check the terminal type.  If we fail, then assume that the
105be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # terminal has no capabilities.
106be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        try: curses.setupterm()
107be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        except: return
108be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
109be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # Look up numeric capabilities.
110be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.COLS = curses.tigetnum('cols')
111be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.LINES = curses.tigetnum('lines')
1128ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi        self.XN = curses.tigetflag('xenl')
113be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
114be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # Look up string capabilities.
115be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        for capability in self._STRING_CAPABILITIES:
116be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            (attrib, cap_name) = capability.split('=')
117be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            setattr(self, attrib, self._tigetstr(cap_name) or '')
118be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
119be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # Colors
120be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        set_fg = self._tigetstr('setf')
121be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if set_fg:
122be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            for i,color in zip(range(len(self._COLORS)), self._COLORS):
1234db169d31e34df89d10660960070de073f0f28afDaniel Dunbar                setattr(self, color, self._tparm(set_fg, i))
124be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        set_fg_ansi = self._tigetstr('setaf')
125be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if set_fg_ansi:
126be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
1274db169d31e34df89d10660960070de073f0f28afDaniel Dunbar                setattr(self, color, self._tparm(set_fg_ansi, i))
128be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        set_bg = self._tigetstr('setb')
129be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if set_bg:
130be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            for i,color in zip(range(len(self._COLORS)), self._COLORS):
1314db169d31e34df89d10660960070de073f0f28afDaniel Dunbar                setattr(self, 'BG_'+color, self._tparm(set_bg, i))
132be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        set_bg_ansi = self._tigetstr('setab')
133be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if set_bg_ansi:
134be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
1354db169d31e34df89d10660960070de073f0f28afDaniel Dunbar                setattr(self, 'BG_'+color, self._tparm(set_bg_ansi, i))
1364db169d31e34df89d10660960070de073f0f28afDaniel Dunbar
1374db169d31e34df89d10660960070de073f0f28afDaniel Dunbar    def _tparm(self, arg, index):
1384db169d31e34df89d10660960070de073f0f28afDaniel Dunbar        import curses
1394db169d31e34df89d10660960070de073f0f28afDaniel Dunbar        return curses.tparm(to_bytes(arg), index).decode('ascii') or ''
140be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
141be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def _tigetstr(self, cap_name):
142be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # String capabilities can include "delays" of the form "$<2>".
143be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # For any modern terminal, we should be able to just ignore
144be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        # these, so strip them out.
145be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        import curses
14605fb743a99ac6a1fc147682a2262a6190f193ab4Daniel Dunbar        cap = curses.tigetstr(cap_name)
14705fb743a99ac6a1fc147682a2262a6190f193ab4Daniel Dunbar        if cap is None:
14805fb743a99ac6a1fc147682a2262a6190f193ab4Daniel Dunbar            cap = ''
14905fb743a99ac6a1fc147682a2262a6190f193ab4Daniel Dunbar        else:
15005fb743a99ac6a1fc147682a2262a6190f193ab4Daniel Dunbar            cap = cap.decode('ascii')
151be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        return re.sub(r'\$<\d+>[/*]?', '', cap)
152be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
153be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def render(self, template):
154be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        """
155be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        Replace each $-substitutions in the given template string with
156be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        the corresponding terminal control string (if it's defined) or
157be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        '' (if it's not).
158be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        """
159be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
160be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
161be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def _render_sub(self, match):
162be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        s = match.group()
163be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if s == '$$': return s
164be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        else: return getattr(self, s[2:-1])
165be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
166be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar#######################################################################
167be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar# Example use case: progress bar
168be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar#######################################################################
169be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
170be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbarclass SimpleProgressBar:
171be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    """
172be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    A simple progress bar which doesn't need any terminal support.
173be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
174be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    This prints out a progress bar like:
175be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar      'Header: 0 .. 10.. 20.. ...'
176be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    """
177be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
178be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def __init__(self, header):
179be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.header = header
180be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.atIndex = None
181be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
182be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def update(self, percent, message):
183be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if self.atIndex is None:
184be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            sys.stdout.write(self.header)
185be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            self.atIndex = 0
186be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
187be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        next = int(percent*50)
188be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if next == self.atIndex:
189be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            return
190be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
191be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        for i in range(self.atIndex, next):
192be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            idx = i % 5
193be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            if idx == 0:
194be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                sys.stdout.write('%-2d' % (i*2))
195be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            elif idx == 1:
196be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                pass # Skip second char
197be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            elif idx < 4:
198be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                sys.stdout.write('.')
199be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            else:
200be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                sys.stdout.write(' ')
201be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        sys.stdout.flush()
202be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.atIndex = next
203be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
204be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def clear(self):
205be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if self.atIndex is not None:
206be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            sys.stdout.write('\n')
207be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            sys.stdout.flush()
208be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            self.atIndex = None
209be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
210be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbarclass ProgressBar:
211be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    """
212be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    A 3-line progress bar, which looks like::
213be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
214be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                                Header
215be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        20% [===========----------------------------------]
216be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                           progress message
217be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
218be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    The progress bar is colored, if the terminal supports color
219be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    output; and adjusts to the width of the terminal.
220be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    """
2218ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi    BAR = '%s${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}%s'
222be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
223be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
224be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def __init__(self, term, header, useETA=True):
225be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.term = term
226be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
227be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            raise ValueError("Terminal isn't capable enough -- you "
228be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                             "should use a simpler progress dispaly.")
2298ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi        self.BOL = self.term.BOL # BoL from col#79
2308ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi        self.XNL = "\n" # Newline from col#79
2318ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi        if self.term.COLS:
2328ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi            self.width = self.term.COLS
2338ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi            if not self.term.XN:
2348ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi                self.BOL = self.term.UP + self.term.BOL
2358ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi                self.XNL = "" # Cursor must be fed to the next line
2368ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi        else:
2378ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi            self.width = 75
238be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.bar = term.render(self.BAR)
239be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.header = self.term.render(self.HEADER % header.center(self.width))
240be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.cleared = 1 #: true if we haven't drawn the bar yet.
241be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.useETA = useETA
242be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if self.useETA:
243be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            self.startTime = time.time()
244be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        self.update(0, '')
245be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
246be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def update(self, percent, message):
247be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if self.cleared:
248be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            sys.stdout.write(self.header)
249be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            self.cleared = 0
250be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        prefix = '%3d%% ' % (percent*100,)
251be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        suffix = ''
252be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if self.useETA:
253be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            elapsed = time.time() - self.startTime
254be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            if percent > .0001 and elapsed > 1:
255be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                total = elapsed / percent
256be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                eta = int(total - elapsed)
257be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                h = eta//3600.
258be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                m = (eta//60) % 60
259be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                s = eta % 60
260be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                suffix = ' ETA: %02d:%02d:%02d'%(h,m,s)
261be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        barWidth = self.width - len(prefix) - len(suffix) - 2
262be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        n = int(barWidth*percent)
263be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if len(message) < self.width:
264be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            message = message + ' '*(self.width - len(message))
265be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        else:
266be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            message = '... ' + message[-(self.width-4):]
267be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        sys.stdout.write(
2688ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi            self.BOL + self.term.UP + self.term.CLEAR_EOL +
269be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            (self.bar % (prefix, '='*n, '-'*(barWidth-n), suffix)) +
2708ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi            self.XNL +
271be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            self.term.CLEAR_EOL + message)
2728ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi        if not self.term.XN:
2738ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi            sys.stdout.flush()
274be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
275be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    def clear(self):
276be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        if not self.cleared:
2778ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi            sys.stdout.write(self.BOL + self.term.CLEAR_EOL +
278be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                             self.term.UP + self.term.CLEAR_EOL +
279be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar                             self.term.UP + self.term.CLEAR_EOL)
2808ea2649fda07b945520f686c6ac0e35a15706ad1NAKAMURA Takumi            sys.stdout.flush()
281be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar            self.cleared = 1
282be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
283be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbardef test():
284be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    tc = TerminalController()
285be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    p = ProgressBar(tc, 'Tests')
286be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    for i in range(101):
287be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        p.update(i/100., str(i))
288be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar        time.sleep(.3)
289be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar
290be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbarif __name__=='__main__':
291be7ada718139b8c840a38ba34c4af492b6a05f9fDaniel Dunbar    test()
292