1 # Copyright 1998-2011 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
4 __docformat__ = "epytext"
13 portage.proxy.lazyimport.lazyimport(globals(),
14 'portage.util:writemsg',
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 _
30 """Maps style class to tuple of attribute names."""
33 """Maps attribute name to ansi code."""
37 codes["normal"] = esc_seq + "0m"
38 codes["reset"] = esc_seq + "39;49;00m"
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"
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"
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"]
67 def color(fg, bg="default", attr=["normal"]):
75 for x in range(30, 38):
76 ansi_codes.append("%im" % x)
77 ansi_codes.append("%i;01m" % x)
79 rgb_ansi_colors = ['0x000000', '0x555555', '0xAA0000', '0xFF5555', '0x00AA00',
80 '0x55FF55', '0xAA5500', '0xFFFF55', '0x0000AA', '0x5555FF', '0xAA00AA',
81 '0xFF55FF', '0x00AAAA', '0x55FFFF', '0xAAAAAA', '0xFFFFFF']
83 for x in range(len(rgb_ansi_colors)):
84 codes[rgb_ansi_colors[x]] = esc_seq + ansi_codes[x]
88 codes["black"] = codes["0x000000"]
89 codes["darkgray"] = codes["0x555555"]
91 codes["red"] = codes["0xFF5555"]
92 codes["darkred"] = codes["0xAA0000"]
94 codes["green"] = codes["0x55FF55"]
95 codes["darkgreen"] = codes["0x00AA00"]
97 codes["yellow"] = codes["0xFFFF55"]
98 codes["brown"] = codes["0xAA5500"]
100 codes["blue"] = codes["0x5555FF"]
101 codes["darkblue"] = codes["0x0000AA"]
103 codes["fuchsia"] = codes["0xFF55FF"]
104 codes["purple"] = codes["0xAA00AA"]
106 codes["turquoise"] = codes["0x55FFFF"]
107 codes["teal"] = codes["0x00AAAA"]
109 codes["white"] = codes["0xFFFFFF"]
110 codes["lightgray"] = codes["0xAAAAAA"]
112 codes["darkteal"] = codes["turquoise"]
113 # Some terminals have darkyellow instead of brown.
114 codes["0xAAAA00"] = codes["brown"]
115 codes["darkyellow"] = codes["0xAAAA00"]
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", )
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", )
147 def _parse_color_map(config_root='/', onerror=None):
149 Parse /etc/portage/color.map and return a dict of error codes.
151 @param onerror: an optional callback to handle any ParseError that would
153 @type onerror: callable
155 @return: a dictionary mapping color classes to color codes
157 global codes, _styles
158 myfile = os.path.join(config_root, COLOR_MAP_FILE)
159 ansi_code_pattern = re.compile("^[0-9;]*m$")
161 def strip_quotes(token):
162 if token[0] in quotes and token[0] == token[-1]:
167 for line in io.open(_unicode_encode(myfile,
168 encoding=_encodings['fs'], errors='strict'),
169 mode='r', encoding=_encodings['content'], errors='replace'):
172 commenter_pos = line.find("#")
173 line = line[:commenter_pos].strip()
178 split_line = line.split("=")
179 if len(split_line) != 2:
180 e = ParseError(_("'%s', line %s: expected exactly one occurrence of '=' operator") % \
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'") % \
199 if ansi_code_pattern.match(v):
201 _styles[k] = ( esc_seq + v, )
203 codes[k] = esc_seq + v
211 code_list.append(codes[x])
213 e = ParseError(_("'%s', line %s: Undefined: '%s'") % \
220 _styles[k] = tuple(code_list)
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)
231 tmp = re.sub(esc_seq + "^m]+m", "", mystr);
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
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)
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]
251 mystr = '\x1b]0;%s\x07' % mystr
253 # avoid potential UnicodeEncodeError
254 mystr = _unicode_encode(mystr,
255 encoding=_encodings['stdio'], errors='backslashreplace')
257 if sys.hexversion >= 0x3000000:
262 default_xterm_title = None
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:
272 'TERM' in os.environ and \
273 _legal_terms_re.match(os.environ['TERM']) is not None and \
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")
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()})
284 os.system(prompt_command)
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)
297 "turn off title setting"
301 "turn off colorization"
306 return codes["reset"]
308 def style_to_ansi_code(style):
310 @param style: A style name
313 @return: A string containing one or more ansi escape codes that are
314 used to render the given style.
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)
322 def colorize(color_key, text):
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"]
334 compat_functions_colors = ["bold","white","teal","turquoise","darkteal",
335 "fuchsia","purple","blue","darkblue","green","darkgreen","yellow",
336 "brown","darkyellow","red","darkred"]
338 def create_color_func(color_key):
339 def derived_func(*args):
341 newargs.insert(0, color_key)
342 return colorize(*newargs)
345 for c in compat_functions_colors:
346 globals()[c] = create_color_func(c)
348 class ConsoleStyleFile(object):
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.
355 def __init__(self, f):
358 self.write_listener = None
360 def new_styles(self, styles):
361 self._styles = styles
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)
370 if havecolor and self._styles:
372 for style in self._styles:
373 styled_s.append(style_to_ansi_code(style))
375 styled_s.append(codes["reset"])
376 self._write(self._file, "".join(styled_s))
378 self._write(self._file, s)
379 if self.write_listener:
380 self._write(self.write_listener, s)
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:
391 def writelines(self, lines):
401 class StyleWriter(formatter.DumbWriter):
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.
407 def __init__(self, **kwargs):
408 formatter.DumbWriter.__init__(self, **kwargs)
409 self.style_listener = None
411 def new_styles(self, styles):
412 formatter.DumbWriter.new_styles(self, styles)
413 if self.style_listener:
414 self.style_listener(styles)
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.
423 if not sys.stdout.isatty():
429 return curses.tigetnum('lines'), curses.tigetnum('cols')
434 st, out = portage.subprocess_getstatusoutput('stty size')
439 return int(out[0]), int(out[1])
444 def set_term_size(lines, columns, fd):
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`.
449 from portage.process import spawn
450 cmd = ["stty", "rows", str(lines), "columns", str(columns)]
452 spawn(cmd, env=os.environ, fd_pipes={0:fd})
453 except CommandNotFound:
454 writemsg(_("portage: stty: command not found\n"), noiselevel=-1)
456 class EOutput(object):
458 Performs fancy terminal formatting for status and informational messages.
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.
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.
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
476 @type term_columns: IntType
479 def __init__(self, quiet=False):
480 self.__last_e_cmd = ""
481 self.__last_e_len = 0
483 lines, columns = get_term_size()
486 self.term_columns = columns
490 def _write(self, f, s):
491 # avoid potential UnicodeEncodeError
492 writemsg(s, noiselevel=-1, fd=f)
494 def __eend(self, caller, errno, msg):
496 status_brackets = colorize("BRACKET", "[ ") + colorize("GOOD", "ok") + colorize("BRACKET", " ]")
498 status_brackets = colorize("BRACKET", "[ ") + colorize("BAD", "!!") + colorize("BRACKET", " ]")
502 elif caller == "ewend":
504 if self.__last_e_cmd != "ebegin":
505 self.__last_e_len = 0
509 "%*s%s\n" % ((self.term_columns - self.__last_e_len - 7),
510 "", status_brackets))
512 def ebegin(self, msg):
514 Shows a message indicating the start of a process.
516 @param msg: A very brief (shorter than one line) description of the
518 @type msg: StringType
523 self.__last_e_len = len(msg) + 3
524 self.__last_e_cmd = "ebegin"
526 def eend(self, errno, *msg):
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}.
531 @param errno: A standard UNIX C{errno} code returned by processes upon
534 @param msg: I{(optional)} An error message, typically a standard UNIX
535 error string corresponding to C{errno}.
536 @type msg: StringType
539 self.__eend("eend", errno, msg)
540 self.__last_e_cmd = "eend"
542 def eerror(self, msg):
544 Shows an error message.
546 @param msg: A very brief (shorter than one line) error message.
547 @type msg: StringType
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"
556 def einfo(self, msg):
558 Shows an informative message terminated with a newline.
560 @param msg: A very brief (shorter than one line) informative message.
561 @type msg: StringType
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"
570 def einfon(self, msg):
572 Shows an informative message terminated without a newline.
574 @param msg: A very brief (shorter than one line) informative message.
575 @type msg: StringType
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"
584 def ewarn(self, msg):
586 Shows a warning message.
588 @param msg: A very brief (shorter than one line) warning message.
589 @type msg: StringType
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"
598 def ewend(self, errno, *msg):
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}.
603 @param errno: A standard UNIX C{errno} code returned by processes upon
606 @param msg: I{(optional)} A warning message, typically a standard UNIX
607 error string corresponding to C{errno}.
608 @type msg: StringType
611 self.__eend("ewend", errno, msg)
612 self.__last_e_cmd = "ewend"
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):
619 self._maxval = maxval
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.
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.
642 def title(self, newstr):
643 """Sets the text in the title bar of the progress dialog to newstr."""
646 def label(self, newstr):
647 """Sets the text in the progress box of the progress dialog to newstr."""
650 def set(self, value, maxval=None):
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.
657 if maxval is not None:
658 self._maxval = maxval
661 elif value > self._maxval:
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.
673 self.set(self._curval+n)
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
686 def set(self, value, maxval=None):
687 ProgressBar.set(self, value, maxval=maxval)
688 self._display_image(self._create_image())
690 def _display_image(self, image):
691 self.file.write('\r')
692 self.file.write(image)
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:
707 bar_space = cols - percentage_str_width - square_brackets_width
709 max_bar_width = bar_space-3
711 if cols < min_columns:
714 offset = 2 * position
716 offset = 2 * (1 - position)
717 delta = 0.5 / max_bar_width
721 # make sure it touches the ends
722 if 1.0 - position < delta:
724 if position < 0.5 and 0.5 - position < delta:
726 self._position = position
727 bar_width = int(offset * max_bar_width)
728 image = image + "[" + (bar_width * " ") + \
729 "<=>" + ((max_bar_width - bar_width) * " ") + "]"
732 percentage = int(100 * float(curval) / maxval)
733 if percentage == 100:
734 percentage_str_width += 1
736 max_bar_width = bar_space - 1
737 image = ("%d%% " % percentage).rjust(percentage_str_width)
738 if cols < min_columns:
740 offset = float(curval) / maxval
741 bar_width = int(offset * max_bar_width)
742 image = image + "[" + (bar_width * "=") + \
743 ">" + ((max_bar_width - bar_width) * " ") + "]"
746 _color_map_loaded = False
748 def _init(config_root='/'):
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).
755 global _color_map_loaded, codes, _styles
756 if _color_map_loaded:
759 _color_map_loaded = True
760 codes = object.__getattribute__(codes, '_attr')
761 _styles = object.__getattribute__(_styles, '_attr')
763 for k, v in codes.items():
764 codes[k] = _unicode_decode(v)
766 for k, v in _styles.items():
767 _styles[k] = _unicode_decode(v)
770 _parse_color_map(config_root=config_root,
771 onerror=lambda e: writemsg("%s\n" % str(e), noiselevel=-1))
774 except PermissionDenied as e:
775 writemsg(_("Permission denied: '%s'\n") % str(e), noiselevel=-1)
777 except PortageException as e:
778 writemsg("%s\n" % str(e), noiselevel=-1)
781 class _LazyInitColorMap(portage.proxy.objectproxy.ObjectProxy):
783 __slots__ = ('_attr',)
785 def __init__(self, attr):
786 portage.proxy.objectproxy.ObjectProxy.__init__(self)
787 object.__setattr__(self, '_attr', attr)
789 def _get_target(self):
791 return object.__getattribute__(self, '_attr')
793 codes = _LazyInitColorMap(codes)
794 _styles = _LazyInitColorMap(_styles)