Use dict.(keys|values|items)() instead of dict.(iterkeys|itervalues|iteritems)()...
[portage.git] / pym / _emerge / JobStatusDisplay.py
1 # Copyright 1999-2009 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3 # $Id$
4
5 import formatter
6 import sys
7 import time
8 try:
9         from io import StringIO
10 except ImportError:
11         # Needed for python-2.6 with USE=build since
12         # io imports threading which imports thread
13         # which is unavailable.
14         from StringIO import StringIO
15
16 import portage
17 from portage import os
18 from portage import _encodings
19 from portage import _unicode_decode
20 from portage.output import xtermTitle
21
22 from _emerge.getloadavg import getloadavg
23
24 if sys.hexversion >= 0x3000000:
25         basestring = str
26
27 class JobStatusDisplay(object):
28
29         _bound_properties = ("curval", "failed", "running")
30         _jobs_column_width = 48
31
32         # Don't update the display unless at least this much
33         # time has passed, in units of seconds.
34         _min_display_latency = 2
35
36         _default_term_codes = {
37                 'cr'  : '\r',
38                 'el'  : '\x1b[K',
39                 'nel' : '\n',
40         }
41
42         _termcap_name_map = {
43                 'carriage_return' : 'cr',
44                 'clr_eol'         : 'el',
45                 'newline'         : 'nel',
46         }
47
48         def __init__(self, quiet=False, xterm_titles=True):
49                 object.__setattr__(self, "quiet", quiet)
50                 object.__setattr__(self, "xterm_titles", xterm_titles)
51                 object.__setattr__(self, "maxval", 0)
52                 object.__setattr__(self, "merges", 0)
53                 object.__setattr__(self, "_changed", False)
54                 object.__setattr__(self, "_displayed", False)
55                 object.__setattr__(self, "_last_display_time", 0)
56                 object.__setattr__(self, "width", 80)
57                 self.reset()
58
59                 isatty = hasattr(self.out, "isatty") and self.out.isatty()
60                 object.__setattr__(self, "_isatty", isatty)
61                 if not isatty or not self._init_term():
62                         term_codes = {}
63                         for k, capname in self._termcap_name_map.items():
64                                 term_codes[k] = self._default_term_codes[capname]
65                         object.__setattr__(self, "_term_codes", term_codes)
66                 encoding = sys.getdefaultencoding()
67                 for k, v in self._term_codes.items():
68                         if not isinstance(v, basestring):
69                                 self._term_codes[k] = v.decode(encoding, 'replace')
70
71         @property
72         def out(self):
73                 """Use a lazy reference to sys.stdout, in case the API consumer has
74                 temporarily overridden stdout."""
75                 return sys.stdout
76
77         def _write(self, s):
78                 if sys.hexversion < 0x3000000 and isinstance(s, unicode):
79                         # avoid potential UnicodeEncodeError
80                         s = s.encode(_encodings['stdio'], 'backslashreplace')
81                 self.out.write(s)
82                 self.out.flush()
83
84         def _init_term(self):
85                 """
86                 Initialize term control codes.
87                 @rtype: bool
88                 @returns: True if term codes were successfully initialized,
89                         False otherwise.
90                 """
91
92                 term_type = os.environ.get("TERM", "vt100")
93                 tigetstr = None
94
95                 try:
96                         import curses
97                         try:
98                                 curses.setupterm(term_type, self.out.fileno())
99                                 tigetstr = curses.tigetstr
100                         except curses.error:
101                                 pass
102                 except ImportError:
103                         pass
104
105                 if tigetstr is None:
106                         return False
107
108                 term_codes = {}
109                 for k, capname in self._termcap_name_map.items():
110                         code = tigetstr(capname)
111                         if code is None:
112                                 code = self._default_term_codes[capname]
113                         term_codes[k] = code
114                 object.__setattr__(self, "_term_codes", term_codes)
115                 return True
116
117         def _format_msg(self, msg):
118                 return ">>> %s" % msg
119
120         def _erase(self):
121                 self._write(
122                         self._term_codes['carriage_return'] + \
123                         self._term_codes['clr_eol'])
124                 self._displayed = False
125
126         def _display(self, line):
127                 self._write(line)
128                 self._displayed = True
129
130         def _update(self, msg):
131
132                 if not self._isatty:
133                         self._write(self._format_msg(msg) + self._term_codes['newline'])
134                         self._displayed = True
135                         return
136
137                 if self._displayed:
138                         self._erase()
139
140                 self._display(self._format_msg(msg))
141
142         def displayMessage(self, msg):
143
144                 was_displayed = self._displayed
145
146                 if self._isatty and self._displayed:
147                         self._erase()
148
149                 self._write(self._format_msg(msg) + self._term_codes['newline'])
150                 self._displayed = False
151
152                 if was_displayed:
153                         self._changed = True
154                         self.display()
155
156         def reset(self):
157                 self.maxval = 0
158                 self.merges = 0
159                 for name in self._bound_properties:
160                         object.__setattr__(self, name, 0)
161
162                 if self._displayed:
163                         self._write(self._term_codes['newline'])
164                         self._displayed = False
165
166         def __setattr__(self, name, value):
167                 old_value = getattr(self, name)
168                 if value == old_value:
169                         return
170                 object.__setattr__(self, name, value)
171                 if name in self._bound_properties:
172                         self._property_change(name, old_value, value)
173
174         def _property_change(self, name, old_value, new_value):
175                 self._changed = True
176                 self.display()
177
178         def _load_avg_str(self):
179                 try:
180                         avg = getloadavg()
181                 except OSError:
182                         return 'unknown'
183
184                 max_avg = max(avg)
185
186                 if max_avg < 10:
187                         digits = 2
188                 elif max_avg < 100:
189                         digits = 1
190                 else:
191                         digits = 0
192
193                 return ", ".join(("%%.%df" % digits ) % x for x in avg)
194
195         def display(self):
196                 """
197                 Display status on stdout, but only if something has
198                 changed since the last call.
199                 """
200
201                 if self.quiet:
202                         return
203
204                 current_time = time.time()
205                 time_delta = current_time - self._last_display_time
206                 if self._displayed and \
207                         not self._changed:
208                         if not self._isatty:
209                                 return
210                         if time_delta < self._min_display_latency:
211                                 return
212
213                 self._last_display_time = current_time
214                 self._changed = False
215                 self._display_status()
216
217         def _display_status(self):
218                 # Don't use len(self._completed_tasks) here since that also
219                 # can include uninstall tasks.
220                 curval_str = str(self.curval)
221                 maxval_str = str(self.maxval)
222                 running_str = str(self.running)
223                 failed_str = str(self.failed)
224                 load_avg_str = self._load_avg_str()
225
226                 color_output = StringIO()
227                 plain_output = StringIO()
228                 style_file = portage.output.ConsoleStyleFile(color_output)
229                 style_file.write_listener = plain_output
230                 style_writer = portage.output.StyleWriter(file=style_file, maxcol=9999)
231                 style_writer.style_listener = style_file.new_styles
232                 f = formatter.AbstractFormatter(style_writer)
233
234                 number_style = "INFORM"
235                 f.add_literal_data(_unicode_decode("Jobs: "))
236                 f.push_style(number_style)
237                 f.add_literal_data(_unicode_decode(curval_str))
238                 f.pop_style()
239                 f.add_literal_data(_unicode_decode(" of "))
240                 f.push_style(number_style)
241                 f.add_literal_data(_unicode_decode(maxval_str))
242                 f.pop_style()
243                 f.add_literal_data(_unicode_decode(" complete"))
244
245                 if self.running:
246                         f.add_literal_data(_unicode_decode(", "))
247                         f.push_style(number_style)
248                         f.add_literal_data(_unicode_decode(running_str))
249                         f.pop_style()
250                         f.add_literal_data(_unicode_decode(" running"))
251
252                 if self.failed:
253                         f.add_literal_data(_unicode_decode(", "))
254                         f.push_style(number_style)
255                         f.add_literal_data(_unicode_decode(failed_str))
256                         f.pop_style()
257                         f.add_literal_data(_unicode_decode(" failed"))
258
259                 padding = self._jobs_column_width - len(plain_output.getvalue())
260                 if padding > 0:
261                         f.add_literal_data(padding * _unicode_decode(" "))
262
263                 f.add_literal_data(_unicode_decode("Load avg: "))
264                 f.add_literal_data(_unicode_decode(load_avg_str))
265
266                 # Truncate to fit width, to avoid making the terminal scroll if the
267                 # line overflows (happens when the load average is large).
268                 plain_output = plain_output.getvalue()
269                 if self._isatty and len(plain_output) > self.width:
270                         # Use plain_output here since it's easier to truncate
271                         # properly than the color output which contains console
272                         # color codes.
273                         self._update(plain_output[:self.width])
274                 else:
275                         self._update(color_output.getvalue())
276
277                 if self.xterm_titles:
278                         xtermTitle(" ".join(plain_output.split()))