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