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