diff --git a/lib/portage/output.py b/lib/portage/output.py index 4408705c45..4109cf73df 100644 --- a/lib/portage/output.py +++ b/lib/portage/output.py @@ -268,52 +268,86 @@ def nc_len(mystr): _legal_terms_re = re.compile( r"^(xterm|xterm-color|Eterm|aterm|rxvt|screen|kterm|rxvt-unicode|gnome|interix|tmux|st-256color|alacritty|konsole|foot)" ) -_disable_xtermTitle = None -_max_xtermTitle_len = 253 +_xterm_title_supported = None +_max_xterm_title_len = 253 +_title_init_seq = None +_title_finish_seq = None +_default_xterm_title = None + + +def init_xterm_titles(): + global _xterm_title_supported + global _title_init_seq + global _title_finish_seq + + if _xterm_title_supported is not None: + # already initialized + return + + _xterm_title_supported = False + + if not sys.__stderr__.isatty() or "TERM" not in os.environ: + return + + try: + # by default check if we can dynamically query the proper title + # setting sequence via terminfo + curses = init_curses() + + if curses is not None: + tsl = curses.tigetstr("tsl").decode() + fsl = curses.tigetstr("fsl").decode() + if tsl and fsl: + _xterm_title_supported = True + _title_init_seq = tsl + _title_finish_seq = fsl + return + except curses.error: + pass + + if _legal_terms_re.match(os.environ["TERM"]) is not None: + # as a fallback use the well known xterm escape sequences for the hard + # coded suppoted terminal list + _xterm_title_supported = True + _title_init_seq = "\x1b]0;" + _title_finish_seq = "\x07" + + +def format_xterm_title(mystr): + if not _xterm_title_supported: + return mystr + return _title_init_seq + mystr + _title_finish_seq def xtermTitle(mystr, raw=False): - global _disable_xtermTitle - if _disable_xtermTitle is None: - _disable_xtermTitle = not ( - sys.__stderr__.isatty() - and "TERM" in os.environ - and _legal_terms_re.match(os.environ["TERM"]) is not None - ) + init_xterm_titles() - if dotitles and not _disable_xtermTitle: + if dotitles and _xterm_title_supported: # If the title string is too big then the terminal can # misbehave. Therefore, truncate it if it's too big. - if len(mystr) > _max_xtermTitle_len: - mystr = mystr[:_max_xtermTitle_len] + if len(mystr) > _max_xterm_title_len: + mystr = mystr[:_max_xterm_title_len] if not raw: - mystr = f"\x1b]0;{mystr}\x07" + mystr = format_xterm_title(mystr) # avoid potential UnicodeEncodeError mystr = _unicode_encode( mystr, encoding=_encodings["stdio"], errors="backslashreplace" ) - f = sys.stderr.buffer + f = sys.__stderr__.buffer f.write(mystr) f.flush() -default_xterm_title = None - - def xtermTitleReset(): - global default_xterm_title - if default_xterm_title is None: + init_xterm_titles() + global _default_xterm_title + if _default_xterm_title is None: prompt_command = os.environ.get("PROMPT_COMMAND") if prompt_command == "": - default_xterm_title = "" + _default_xterm_title = "" elif prompt_command is not None: - if ( - dotitles - and "TERM" in os.environ - and _legal_terms_re.match(os.environ["TERM"]) is not None - and sys.__stderr__.isatty() - ): + if dotitles and _xterm_title_supported: from portage.process import find_binary, spawn shell = os.environ.get("SHELL") @@ -337,12 +371,16 @@ def xtermTitleReset(): home = os.environ.get("HOME", "") if home != "" and pwd.startswith(home): pwd = "~" + pwd[len(home) :] - default_xterm_title = "\x1b]0;{}@{}:{}\x07".format( - os.environ.get("LOGNAME", ""), - os.environ.get("HOSTNAME", "").split(".", 1)[0], - pwd, + _default_xterm_title = format_xterm_title( + "{}@{}:{}".format( + os.environ.get("LOGNAME", ""), + os.environ.get("HOSTNAME", "").split(".", 1)[0], + pwd, + ) ) - xtermTitle(default_xterm_title, raw=True) + # since PROMPT_COMMAND can already contain escape sequences, output the + # title as a raw sequence without adding any additional sequences. + xtermTitle(_default_xterm_title, raw=True) def notitles(): @@ -509,6 +547,19 @@ def new_styles(self, styles): self.style_listener(styles) +def init_curses(fd=sys.__stderr__): + try: + import curses + + try: + curses.setupterm(term=os.environ.get("TERM", "unknown"), fd=fd.fileno()) + return curses + except curses.error: + return None + except ImportError: + return None + + def get_term_size(fd=None): """ Get the number of lines and columns of the tty that is connected to @@ -522,15 +573,12 @@ def get_term_size(fd=None): fd = sys.stdout if not hasattr(fd, "isatty") or not fd.isatty(): return (0, 0) - try: - import curses - try: - curses.setupterm(term=os.environ.get("TERM", "unknown"), fd=fd.fileno()) + try: + curses = init_curses(fd) + if curses is not None: return curses.tigetnum("lines"), curses.tigetnum("cols") - except curses.error: - pass - except ImportError: + except curses.error: pass try: