Use portage.subprocess_getstatusoutput() more.
[portage.git] / pym / portage / output.py
1 # Copyright 1998-2011 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3
4 __docformat__ = "epytext"
5
6 import errno
7 import io
8 import formatter
9 import re
10 import sys
11
12 import portage
13 portage.proxy.lazyimport.lazyimport(globals(),
14         'portage.util:writemsg',
15 )
16
17 from portage import os
18 from portage import _encodings
19 from portage import _unicode_encode
20 from portage import _unicode_decode
21 from portage.const import COLOR_MAP_FILE
22 from portage.exception import CommandNotFound, FileNotFound, \
23         ParseError, PermissionDenied, PortageException
24 from portage.localization import _
25
26 havecolor=1
27 dotitles=1
28
29 _styles = {}
30 """Maps style class to tuple of attribute names."""
31
32 codes = {}
33 """Maps attribute name to ansi code."""
34
35 esc_seq = "\x1b["
36
37 codes["normal"]       =  esc_seq + "0m"
38 codes["reset"]        =  esc_seq + "39;49;00m"
39
40 codes["bold"]         =  esc_seq + "01m"
41 codes["faint"]        =  esc_seq + "02m"
42 codes["standout"]     =  esc_seq + "03m"
43 codes["underline"]    =  esc_seq + "04m"
44 codes["blink"]        =  esc_seq + "05m"
45 codes["overline"]     =  esc_seq + "06m"
46 codes["reverse"]      =  esc_seq + "07m"
47 codes["invisible"]    =  esc_seq + "08m"
48
49 codes["no-attr"]      = esc_seq + "22m"
50 codes["no-standout"]  = esc_seq + "23m"
51 codes["no-underline"] = esc_seq + "24m"
52 codes["no-blink"]     = esc_seq + "25m"
53 codes["no-overline"]  = esc_seq + "26m"
54 codes["no-reverse"]   = esc_seq + "27m"
55
56 codes["bg_black"]      = esc_seq + "40m"
57 codes["bg_darkred"]    = esc_seq + "41m"
58 codes["bg_darkgreen"]  = esc_seq + "42m"
59 codes["bg_brown"]      = esc_seq + "43m"
60 codes["bg_darkblue"]   = esc_seq + "44m"
61 codes["bg_purple"]     = esc_seq + "45m"
62 codes["bg_teal"]       = esc_seq + "46m"
63 codes["bg_lightgray"]  = esc_seq + "47m"
64 codes["bg_default"]    = esc_seq + "49m"
65 codes["bg_darkyellow"] = codes["bg_brown"]
66
67 def color(fg, bg="default", attr=["normal"]):
68         mystr = codes[fg]
69         for x in [bg]+attr:
70                 mystr += codes[x]
71         return mystr
72
73
74 ansi_codes = []
75 for x in range(30, 38):
76         ansi_codes.append("%im" % x)
77         ansi_codes.append("%i;01m" % x)
78
79 rgb_ansi_colors = ['0x000000', '0x555555', '0xAA0000', '0xFF5555', '0x00AA00',
80         '0x55FF55', '0xAA5500', '0xFFFF55', '0x0000AA', '0x5555FF', '0xAA00AA',
81         '0xFF55FF', '0x00AAAA', '0x55FFFF', '0xAAAAAA', '0xFFFFFF']
82
83 for x in range(len(rgb_ansi_colors)):
84         codes[rgb_ansi_colors[x]] = esc_seq + ansi_codes[x]
85
86 del x
87
88 codes["black"]     = codes["0x000000"]
89 codes["darkgray"]  = codes["0x555555"]
90
91 codes["red"]       = codes["0xFF5555"]
92 codes["darkred"]   = codes["0xAA0000"]
93
94 codes["green"]     = codes["0x55FF55"]
95 codes["darkgreen"] = codes["0x00AA00"]
96
97 codes["yellow"]    = codes["0xFFFF55"]
98 codes["brown"]     = codes["0xAA5500"]
99
100 codes["blue"]      = codes["0x5555FF"]
101 codes["darkblue"]  = codes["0x0000AA"]
102
103 codes["fuchsia"]   = codes["0xFF55FF"]
104 codes["purple"]    = codes["0xAA00AA"]
105
106 codes["turquoise"] = codes["0x55FFFF"]
107 codes["teal"]      = codes["0x00AAAA"]
108
109 codes["white"]     = codes["0xFFFFFF"]
110 codes["lightgray"] = codes["0xAAAAAA"]
111
112 codes["darkteal"]   = codes["turquoise"]
113 # Some terminals have darkyellow instead of brown.
114 codes["0xAAAA00"]   = codes["brown"]
115 codes["darkyellow"] = codes["0xAAAA00"]
116
117
118
119 # Colors from /etc/init.d/functions.sh
120 _styles["NORMAL"]     = ( "normal", )
121 _styles["GOOD"]       = ( "green", )
122 _styles["WARN"]       = ( "yellow", )
123 _styles["BAD"]        = ( "red", )
124 _styles["HILITE"]     = ( "teal", )
125 _styles["BRACKET"]    = ( "blue", )
126
127 # Portage functions
128 _styles["INFORM"]                  = ( "darkgreen", )
129 _styles["UNMERGE_WARN"]            = ( "red", )
130 _styles["SECURITY_WARN"]           = ( "red", )
131 _styles["MERGE_LIST_PROGRESS"]     = ( "yellow", )
132 _styles["PKG_BLOCKER"]             = ( "red", )
133 _styles["PKG_BLOCKER_SATISFIED"]   = ( "darkblue", )
134 _styles["PKG_MERGE"]               = ( "darkgreen", )
135 _styles["PKG_MERGE_SYSTEM"]        = ( "darkgreen", )
136 _styles["PKG_MERGE_WORLD"]         = ( "green", )
137 _styles["PKG_BINARY_MERGE"]        = ( "purple", )
138 _styles["PKG_BINARY_MERGE_SYSTEM"] = ( "purple", )
139 _styles["PKG_BINARY_MERGE_WORLD"]  = ( "fuchsia", )
140 _styles["PKG_UNINSTALL"]           = ( "red", )
141 _styles["PKG_NOMERGE"]             = ( "darkblue", )
142 _styles["PKG_NOMERGE_SYSTEM"]      = ( "darkblue", )
143 _styles["PKG_NOMERGE_WORLD"]       = ( "blue", )
144 _styles["PROMPT_CHOICE_DEFAULT"]   = ( "green", )
145 _styles["PROMPT_CHOICE_OTHER"]     = ( "red", )
146
147 def _parse_color_map(config_root='/', onerror=None):
148         """
149         Parse /etc/portage/color.map and return a dict of error codes.
150
151         @param onerror: an optional callback to handle any ParseError that would
152                 otherwise be raised
153         @type onerror: callable
154         @rtype: dict
155         @return: a dictionary mapping color classes to color codes
156         """
157         global codes, _styles
158         myfile = os.path.join(config_root, COLOR_MAP_FILE)
159         ansi_code_pattern = re.compile("^[0-9;]*m$") 
160         quotes = '\'"'
161         def strip_quotes(token):
162                 if token[0] in quotes and token[0] == token[-1]:
163                         token = token[1:-1]
164                 return token
165         try:
166                 lineno=0
167                 for line in io.open(_unicode_encode(myfile,
168                         encoding=_encodings['fs'], errors='strict'),
169                         mode='r', encoding=_encodings['content'], errors='replace'):
170                         lineno += 1
171
172                         commenter_pos = line.find("#")
173                         line = line[:commenter_pos].strip()
174                         
175                         if len(line) == 0:
176                                 continue
177                         
178                         split_line = line.split("=")
179                         if len(split_line) != 2:
180                                 e = ParseError(_("'%s', line %s: expected exactly one occurrence of '=' operator") % \
181                                         (myfile, lineno))
182                                 raise e
183                                 if onerror:
184                                         onerror(e)
185                                 else:
186                                         raise e
187                                 continue
188
189                         k = strip_quotes(split_line[0].strip())
190                         v = strip_quotes(split_line[1].strip())
191                         if not k in _styles and not k in codes:
192                                 e = ParseError(_("'%s', line %s: Unknown variable: '%s'") % \
193                                         (myfile, lineno, k))
194                                 if onerror:
195                                         onerror(e)
196                                 else:
197                                         raise e
198                                 continue
199                         if ansi_code_pattern.match(v):
200                                 if k in _styles:
201                                         _styles[k] = ( esc_seq + v, )
202                                 elif k in codes:
203                                         codes[k] = esc_seq + v
204                         else:
205                                 code_list = []
206                                 for x in v.split():
207                                         if x in codes:
208                                                 if k in _styles:
209                                                         code_list.append(x)
210                                                 elif k in codes:
211                                                         code_list.append(codes[x])
212                                         else:
213                                                 e = ParseError(_("'%s', line %s: Undefined: '%s'") % \
214                                                         (myfile, lineno, x))
215                                                 if onerror:
216                                                         onerror(e)
217                                                 else:
218                                                         raise e
219                                 if k in _styles:
220                                         _styles[k] = tuple(code_list)
221                                 elif k in codes:
222                                         codes[k] = "".join(code_list)
223         except (IOError, OSError) as e:
224                 if e.errno == errno.ENOENT:
225                         raise FileNotFound(myfile)
226                 elif e.errno == errno.EACCES:
227                         raise PermissionDenied(myfile)
228                 raise
229
230 def nc_len(mystr):
231         tmp = re.sub(esc_seq + "^m]+m", "", mystr);
232         return len(tmp)
233
234 _legal_terms_re = re.compile(r'^(xterm|xterm-color|Eterm|aterm|rxvt|screen|kterm|rxvt-unicode|gnome|interix)')
235 _disable_xtermTitle = None
236 _max_xtermTitle_len = 253
237
238 def xtermTitle(mystr, raw=False):
239         global _disable_xtermTitle
240         if _disable_xtermTitle is None:
241                 _disable_xtermTitle = not (sys.stderr.isatty() and \
242                 'TERM' in os.environ and \
243                 _legal_terms_re.match(os.environ['TERM']) is not None)
244
245         if dotitles and not _disable_xtermTitle:
246                 # If the title string is too big then the terminal can
247                 # misbehave. Therefore, truncate it if it's too big.
248                 if len(mystr) > _max_xtermTitle_len:
249                         mystr = mystr[:_max_xtermTitle_len]
250                 if not raw:
251                         mystr = '\x1b]0;%s\x07' % mystr
252
253                 # avoid potential UnicodeEncodeError
254                 mystr = _unicode_encode(mystr,
255                         encoding=_encodings['stdio'], errors='backslashreplace')
256                 f = sys.stderr
257                 if sys.hexversion >= 0x3000000:
258                         f = f.buffer
259                 f.write(mystr)
260                 f.flush()
261
262 default_xterm_title = None
263
264 def xtermTitleReset():
265         global default_xterm_title
266         if default_xterm_title is None:
267                 prompt_command = os.environ.get('PROMPT_COMMAND')
268                 if prompt_command == "":
269                         default_xterm_title = ""
270                 elif prompt_command is not None:
271                         if dotitles and \
272                                 'TERM' in os.environ and \
273                                 _legal_terms_re.match(os.environ['TERM']) is not None and \
274                                 sys.stderr.isatty():
275                                 from portage.process import find_binary, spawn
276                                 shell = os.environ.get("SHELL")
277                                 if not shell or not os.access(shell, os.EX_OK):
278                                         shell = find_binary("sh")
279                                 if shell:
280                                         spawn([shell, "-c", prompt_command], env=os.environ,
281                                                 fd_pipes={0:sys.stdin.fileno(),1:sys.stderr.fileno(),
282                                                 2:sys.stderr.fileno()})
283                                 else:
284                                         os.system(prompt_command)
285                         return
286                 else:
287                         pwd = os.environ.get('PWD','')
288                         home = os.environ.get('HOME', '')
289                         if home != '' and pwd.startswith(home):
290                                 pwd = '~' + pwd[len(home):]
291                         default_xterm_title = '\x1b]0;%s@%s:%s\x07' % (
292                                 os.environ.get('LOGNAME', ''),
293                                 os.environ.get('HOSTNAME', '').split('.', 1)[0], pwd)
294         xtermTitle(default_xterm_title, raw=True)
295
296 def notitles():
297         "turn off title setting"
298         dotitles=0
299
300 def nocolor():
301         "turn off colorization"
302         global havecolor
303         havecolor=0
304
305 def resetColor():
306         return codes["reset"]
307
308 def style_to_ansi_code(style):
309         """
310         @param style: A style name
311         @type style: String
312         @rtype: String
313         @return: A string containing one or more ansi escape codes that are
314                 used to render the given style.
315         """
316         ret = ""
317         for attr_name in _styles[style]:
318                 # allow stuff that has found it's way through ansi_code_pattern
319                 ret += codes.get(attr_name, attr_name)
320         return ret
321
322 def colorize(color_key, text):
323         global havecolor
324         if havecolor:
325                 if color_key in codes:
326                         return codes[color_key] + text + codes["reset"]
327                 elif color_key in _styles:
328                         return style_to_ansi_code(color_key) + text + codes["reset"]
329                 else:
330                         return text
331         else:
332                 return text
333
334 compat_functions_colors = ["bold","white","teal","turquoise","darkteal",
335         "fuchsia","purple","blue","darkblue","green","darkgreen","yellow",
336         "brown","darkyellow","red","darkred"]
337
338 def create_color_func(color_key):
339         def derived_func(*args):
340                 newargs = list(args)
341                 newargs.insert(0, color_key)
342                 return colorize(*newargs)
343         return derived_func
344
345 for c in compat_functions_colors:
346         globals()[c] = create_color_func(c)
347
348 class ConsoleStyleFile(object):
349         """
350         A file-like object that behaves something like
351         the colorize() function. Style identifiers
352         passed in via the new_styles() method will be used to
353         apply console codes to output.
354         """
355         def __init__(self, f):
356                 self._file = f
357                 self._styles = None
358                 self.write_listener = None
359
360         def new_styles(self, styles):
361                 self._styles = styles
362
363         def write(self, s):
364                 # In python-2.6, DumbWriter.send_line_break() can write
365                 # non-unicode '\n' which fails with TypeError if self._file
366                 # is a text stream such as io.StringIO. Therefore, make sure
367                 # input is converted to unicode when necessary.
368                 s = _unicode_decode(s)
369                 global havecolor
370                 if havecolor and self._styles:
371                         styled_s = []
372                         for style in self._styles:
373                                 styled_s.append(style_to_ansi_code(style))
374                         styled_s.append(s)
375                         styled_s.append(codes["reset"])
376                         self._write(self._file, "".join(styled_s))
377                 else:
378                         self._write(self._file, s)
379                 if self.write_listener:
380                         self._write(self.write_listener, s)
381
382         def _write(self, f, s):
383                 # avoid potential UnicodeEncodeError
384                 if f in (sys.stdout, sys.stderr):
385                         s = _unicode_encode(s,
386                                 encoding=_encodings['stdio'], errors='backslashreplace')
387                         if sys.hexversion >= 0x3000000:
388                                 f = f.buffer
389                 f.write(s)
390
391         def writelines(self, lines):
392                 for s in lines:
393                         self.write(s)
394
395         def flush(self):
396                 self._file.flush()
397
398         def close(self):
399                 self._file.close()
400
401 class StyleWriter(formatter.DumbWriter):
402         """
403         This is just a DumbWriter with a hook in the new_styles() method
404         that passes a styles tuple as a single argument to a callable
405         style_listener attribute.
406         """
407         def __init__(self, **kwargs):
408                 formatter.DumbWriter.__init__(self, **kwargs)
409                 self.style_listener = None
410
411         def new_styles(self, styles):
412                 formatter.DumbWriter.new_styles(self, styles)
413                 if self.style_listener:
414                         self.style_listener(styles)
415
416 def get_term_size():
417         """
418         Get the number of lines and columns of the tty that is connected to
419         stdout.  Returns a tuple of (lines, columns) or (-1, -1) if an error
420         occurs. The curses module is used if available, otherwise the output of
421         `stty size` is parsed.
422         """
423         if not sys.stdout.isatty():
424                 return -1, -1
425         try:
426                 import curses
427                 try:
428                         curses.setupterm()
429                         return curses.tigetnum('lines'), curses.tigetnum('cols')
430                 except curses.error:
431                         pass
432         except ImportError:
433                 pass
434         st, out = portage.subprocess_getstatusoutput('stty size')
435         if st == os.EX_OK:
436                 out = out.split()
437                 if len(out) == 2:
438                         try:
439                                 return int(out[0]), int(out[1])
440                         except ValueError:
441                                 pass
442         return -1, -1
443
444 def set_term_size(lines, columns, fd):
445         """
446         Set the number of lines and columns for the tty that is connected to fd.
447         For portability, this simply calls `stty rows $lines columns $columns`.
448         """
449         from portage.process import spawn
450         cmd = ["stty", "rows", str(lines), "columns", str(columns)]
451         try:
452                 spawn(cmd, env=os.environ, fd_pipes={0:fd})
453         except CommandNotFound:
454                 writemsg(_("portage: stty: command not found\n"), noiselevel=-1)
455
456 class EOutput(object):
457         """
458         Performs fancy terminal formatting for status and informational messages.
459
460         The provided methods produce identical terminal output to the eponymous
461         functions in the shell script C{/sbin/functions.sh} and also accept
462         identical parameters.
463
464         This is not currently a drop-in replacement however, as the output-related
465         functions in C{/sbin/functions.sh} are oriented for use mainly by system
466         init scripts and ebuilds and their output can be customized via certain
467         C{RC_*} environment variables (see C{/etc/conf.d/rc}). B{EOutput} is not
468         customizable in this manner since it's intended for more general uses.
469         Likewise, no logging is provided.
470
471         @ivar quiet: Specifies if output should be silenced.
472         @type quiet: BooleanType
473         @ivar term_columns: Width of terminal in characters. Defaults to the value
474                 specified by the shell's C{COLUMNS} variable, else to the queried tty
475                 size, else to C{80}.
476         @type term_columns: IntType
477         """
478
479         def __init__(self, quiet=False):
480                 self.__last_e_cmd = ""
481                 self.__last_e_len = 0
482                 self.quiet = quiet
483                 lines, columns = get_term_size()
484                 if columns <= 0:
485                         columns = 80
486                 self.term_columns = columns
487                 sys.stdout.flush()
488                 sys.stderr.flush()
489
490         def _write(self, f, s):
491                 # avoid potential UnicodeEncodeError
492                 writemsg(s, noiselevel=-1, fd=f)
493
494         def __eend(self, caller, errno, msg):
495                 if errno == 0:
496                         status_brackets = colorize("BRACKET", "[ ") + colorize("GOOD", "ok") + colorize("BRACKET", " ]")
497                 else:
498                         status_brackets = colorize("BRACKET", "[ ") + colorize("BAD", "!!") + colorize("BRACKET", " ]")
499                         if msg:
500                                 if caller == "eend":
501                                         self.eerror(msg[0])
502                                 elif caller == "ewend":
503                                         self.ewarn(msg[0])
504                 if self.__last_e_cmd != "ebegin":
505                         self.__last_e_len = 0
506                 if not self.quiet:
507                         out = sys.stdout
508                         self._write(out,
509                                 "%*s%s\n" % ((self.term_columns - self.__last_e_len - 7),
510                                 "", status_brackets))
511
512         def ebegin(self, msg):
513                 """
514                 Shows a message indicating the start of a process.
515
516                 @param msg: A very brief (shorter than one line) description of the
517                         starting process.
518                 @type msg: StringType
519                 """
520                 msg += " ..."
521                 if not self.quiet:
522                         self.einfon(msg)
523                 self.__last_e_len = len(msg) + 3
524                 self.__last_e_cmd = "ebegin"
525
526         def eend(self, errno, *msg):
527                 """
528                 Indicates the completion of a process, optionally displaying a message
529                 via L{eerror} if the process's exit status isn't C{0}.
530
531                 @param errno: A standard UNIX C{errno} code returned by processes upon
532                         exit.
533                 @type errno: IntType
534                 @param msg: I{(optional)} An error message, typically a standard UNIX
535                         error string corresponding to C{errno}.
536                 @type msg: StringType
537                 """
538                 if not self.quiet:
539                         self.__eend("eend", errno, msg)
540                 self.__last_e_cmd = "eend"
541
542         def eerror(self, msg):
543                 """
544                 Shows an error message.
545
546                 @param msg: A very brief (shorter than one line) error message.
547                 @type msg: StringType
548                 """
549                 out = sys.stderr
550                 if not self.quiet:
551                         if self.__last_e_cmd == "ebegin":
552                                 self._write(out, "\n")
553                         self._write(out, colorize("BAD", " * ") + msg + "\n")
554                 self.__last_e_cmd = "eerror"
555
556         def einfo(self, msg):
557                 """
558                 Shows an informative message terminated with a newline.
559
560                 @param msg: A very brief (shorter than one line) informative message.
561                 @type msg: StringType
562                 """
563                 out = sys.stdout
564                 if not self.quiet:
565                         if self.__last_e_cmd == "ebegin":
566                                 self._write(out, "\n")
567                         self._write(out, colorize("GOOD", " * ") + msg + "\n")
568                 self.__last_e_cmd = "einfo"
569
570         def einfon(self, msg):
571                 """
572                 Shows an informative message terminated without a newline.
573
574                 @param msg: A very brief (shorter than one line) informative message.
575                 @type msg: StringType
576                 """
577                 out = sys.stdout
578                 if not self.quiet:
579                         if self.__last_e_cmd == "ebegin":
580                                 self._write(out, "\n")
581                         self._write(out, colorize("GOOD", " * ") + msg)
582                 self.__last_e_cmd = "einfon"
583
584         def ewarn(self, msg):
585                 """
586                 Shows a warning message.
587
588                 @param msg: A very brief (shorter than one line) warning message.
589                 @type msg: StringType
590                 """
591                 out = sys.stderr
592                 if not self.quiet:
593                         if self.__last_e_cmd == "ebegin":
594                                 self._write(out, "\n")
595                         self._write(out, colorize("WARN", " * ") + msg + "\n")
596                 self.__last_e_cmd = "ewarn"
597
598         def ewend(self, errno, *msg):
599                 """
600                 Indicates the completion of a process, optionally displaying a message
601                 via L{ewarn} if the process's exit status isn't C{0}.
602
603                 @param errno: A standard UNIX C{errno} code returned by processes upon
604                         exit.
605                 @type errno: IntType
606                 @param msg: I{(optional)} A warning message, typically a standard UNIX
607                         error string corresponding to C{errno}.
608                 @type msg: StringType
609                 """
610                 if not self.quiet:
611                         self.__eend("ewend", errno, msg)
612                 self.__last_e_cmd = "ewend"
613
614 class ProgressBar(object):
615         """The interface is copied from the ProgressBar class from the EasyDialogs
616         module (which is Mac only)."""
617         def __init__(self, title=None, maxval=0, label=None):
618                 self._title = title
619                 self._maxval = maxval
620                 self._label = maxval
621                 self._curval = 0
622
623         @property
624         def curval(self):
625                 """
626                 The current value (of type integer or long integer) of the progress
627                 bar. The normal access methods coerce curval between 0 and maxval. This
628                 attribute should not be altered directly.
629                 """
630                 return self._curval
631
632         @property
633         def maxval(self):
634                 """
635                 The maximum value (of type integer or long integer) of the progress
636                 bar; the progress bar (thermometer style) is full when curval equals
637                 maxval. If maxval is 0, the bar will be indeterminate (barber-pole).
638                 This attribute should not be altered directly.
639                 """
640                 return self._maxval
641
642         def title(self, newstr):
643                 """Sets the text in the title bar of the progress dialog to newstr."""
644                 self._title = newstr
645
646         def label(self, newstr):
647                 """Sets the text in the progress box of the progress dialog to newstr."""
648                 self._label = newstr
649
650         def set(self, value, maxval=None):
651                 """
652                 Sets the progress bar's curval to value, and also maxval to max if the
653                 latter is provided. value is first coerced between 0 and maxval. The
654                 thermometer bar is updated to reflect the changes, including a change
655                 from indeterminate to determinate or vice versa.
656                 """
657                 if maxval is not None:
658                         self._maxval = maxval
659                 if value < 0:
660                         value = 0
661                 elif value > self._maxval:
662                         value = self._maxval
663                 self._curval = value
664
665         def inc(self, n=1):
666                 """Increments the progress bar's curval by n, or by 1 if n is not
667                 provided. (Note that n may be negative, in which case the effect is a
668                 decrement.) The progress bar is updated to reflect the change. If the
669                 bar is indeterminate, this causes one ``spin'' of the barber pole. The
670                 resulting curval is coerced between 0 and maxval if incrementing causes
671                 it to fall outside this range.
672                 """
673                 self.set(self._curval+n)
674
675 class TermProgressBar(ProgressBar):
676         """A tty progress bar similar to wget's."""
677         def __init__(self, **kwargs):
678                 ProgressBar.__init__(self, **kwargs)
679                 lines, self.term_columns = get_term_size()
680                 self.file = sys.stdout
681                 self._min_columns = 11
682                 self._max_columns = 80
683                 # for indeterminate mode, ranges from 0.0 to 1.0
684                 self._position = 0.0
685
686         def set(self, value, maxval=None):
687                 ProgressBar.set(self, value, maxval=maxval)
688                 self._display_image(self._create_image())
689
690         def _display_image(self, image):
691                 self.file.write('\r')
692                 self.file.write(image)
693                 self.file.flush()
694
695         def _create_image(self):
696                 cols = self.term_columns
697                 if cols > self._max_columns:
698                         cols = self._max_columns
699                 min_columns = self._min_columns
700                 curval = self._curval
701                 maxval = self._maxval
702                 position = self._position
703                 percentage_str_width = 4
704                 square_brackets_width = 2
705                 if cols < percentage_str_width:
706                         return ""
707                 bar_space = cols - percentage_str_width - square_brackets_width
708                 if maxval == 0:
709                         max_bar_width = bar_space-3
710                         image = "    "
711                         if cols < min_columns:
712                                 return image
713                         if position <= 0.5:
714                                 offset = 2 * position
715                         else:
716                                 offset = 2 * (1 - position)
717                         delta = 0.5 / max_bar_width
718                         position += delta
719                         if position >= 1.0:
720                                 position = 0.0
721                         # make sure it touches the ends
722                         if 1.0 - position < delta:
723                                 position = 1.0
724                         if position < 0.5 and 0.5 - position < delta:
725                                 position = 0.5
726                         self._position = position
727                         bar_width = int(offset * max_bar_width)
728                         image = image + "[" + (bar_width * " ") + \
729                                 "<=>" + ((max_bar_width - bar_width) * " ") + "]"
730                         return image
731                 else:
732                         percentage = int(100 * float(curval) / maxval)
733                         if percentage == 100:
734                                 percentage_str_width += 1
735                                 bar_space -= 1
736                         max_bar_width = bar_space - 1
737                         image = ("%d%% " % percentage).rjust(percentage_str_width)
738                         if cols < min_columns:
739                                 return image
740                         offset = float(curval) / maxval
741                         bar_width = int(offset * max_bar_width)
742                         image = image + "[" + (bar_width * "=") + \
743                                 ">" + ((max_bar_width - bar_width) * " ") + "]"
744                         return image
745
746 _color_map_loaded = False
747
748 def _init(config_root='/'):
749         """
750         Load color.map from the given config_root. This is called automatically
751         on first access of the codes or _styles attributes (unless it has already
752         been called for some other reason).
753         """
754
755         global _color_map_loaded, codes, _styles
756         if _color_map_loaded:
757                 return
758
759         _color_map_loaded = True
760         codes = object.__getattribute__(codes, '_attr')
761         _styles = object.__getattribute__(_styles, '_attr')
762
763         for k, v in codes.items():
764                 codes[k] = _unicode_decode(v)
765
766         for k, v in _styles.items():
767                 _styles[k] = _unicode_decode(v)
768
769         try:
770                 _parse_color_map(config_root=config_root,
771                         onerror=lambda e: writemsg("%s\n" % str(e), noiselevel=-1))
772         except FileNotFound:
773                 pass
774         except PermissionDenied as e:
775                 writemsg(_("Permission denied: '%s'\n") % str(e), noiselevel=-1)
776                 del e
777         except PortageException as e:
778                 writemsg("%s\n" % str(e), noiselevel=-1)
779                 del e
780
781 class _LazyInitColorMap(portage.proxy.objectproxy.ObjectProxy):
782
783         __slots__ = ('_attr',)
784
785         def __init__(self, attr):
786                 portage.proxy.objectproxy.ObjectProxy.__init__(self)
787                 object.__setattr__(self, '_attr', attr)
788
789         def _get_target(self):
790                 _init()
791                 return object.__getattribute__(self, '_attr')
792
793 codes = _LazyInitColorMap(codes)
794 _styles = _LazyInitColorMap(_styles)