From 55bea1c1a01b8aeebcb051c7089b4c2a941fd93c Mon Sep 17 00:00:00 2001 From: Matthias Gerstner Date: Wed, 12 Jun 2024 20:02:18 +0200 Subject: [PATCH 1/3] output.py: try to use curses for setting xterm title Currently only the list of hard coded terminal names found in _legal_terms_re allow usage of the title setting feature. Maintaining this list is a burden and means that some terminals will miss out this feature, although they actually do have support for it. Thus, try to use the curses module to dynamically query the terminfo database for "tsl" (to status line) and "fsl" (from status line) strings. This way all terminals that declare support for the feature will automatically benefit. As a fallback, should curses not be available, still rely on the _legal_terms_re for setting up terminal titles. Signed-off-by: Matthias Gerstner --- lib/portage/output.py | 100 +++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/lib/portage/output.py b/lib/portage/output.py index 4408705c45..b2bd92813e 100644 --- a/lib/portage/output.py +++ b/lib/portage/output.py @@ -270,16 +270,12 @@ def nc_len(mystr): ) _disable_xtermTitle = None _max_xtermTitle_len = 253 +_title_init_seq = None +_title_finish_seq = None 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 the title string is too big then the terminal can @@ -287,7 +283,7 @@ def xtermTitle(mystr, raw=False): if len(mystr) > _max_xtermTitle_len: mystr = mystr[:_max_xtermTitle_len] if not raw: - mystr = f"\x1b]0;{mystr}\x07" + mystr = format_xterm_title(mystr) # avoid potential UnicodeEncodeError mystr = _unicode_encode( @@ -302,18 +298,14 @@ def xtermTitle(mystr, raw=False): def xtermTitleReset(): + 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 = "" 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 not _disable_xtermTitle: from portage.process import find_binary, spawn shell = os.environ.get("SHELL") @@ -337,11 +329,15 @@ 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, + ) ) + # 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) @@ -509,6 +505,63 @@ def new_styles(self, styles): self.style_listener(styles) +def init_curses(fd): + 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 init_xterm_titles(): + global _disable_xtermTitle + global _title_init_seq + global _title_finish_seq + + if _disable_xtermTitle is not None: + # already initialized + return + + _disable_xtermTitle = True + + 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(sys.stderr) + + if curses is not None: + tsl = curses.tigetstr("tsl").decode() + fsl = curses.tigetstr("fsl").decode() + if tsl and fsl: + _xtermTitle_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 + _disable_xtermTitle = False + _title_init_seq = "\x1b]0;" + _title_finish_seq = "\x07" + + +def format_xterm_title(mystr): + if _disable_xtermTitle: + return mystr + return _title_init_seq + mystr + _title_finish_seq + + def get_term_size(fd=None): """ Get the number of lines and columns of the tty that is connected to @@ -522,15 +575,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: From 931c4d0d2ac19b4b476778da9b0142c0bf0a53e7 Mon Sep 17 00:00:00 2001 From: Matthias Gerstner Date: Wed, 12 Jun 2024 20:17:00 +0200 Subject: [PATCH 2/3] output.py: harmonize variable names and group xterm title code The global variables are inconsistently named and spread over the module, similarly the title related code is not grouped together. Harmonize this. Also the double negative logic involving `_disable_xtermTitle` makes the code harder to read. Invert the logic. Signed-off-by: Matthias Gerstner --- lib/portage/output.py | 114 +++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/lib/portage/output.py b/lib/portage/output.py index b2bd92813e..626e91e54a 100644 --- a/lib/portage/output.py +++ b/lib/portage/output.py @@ -268,20 +268,65 @@ 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(sys.stderr) + + 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): 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 = format_xterm_title(mystr) @@ -294,18 +339,15 @@ def xtermTitle(mystr, raw=False): f.flush() -default_xterm_title = None - - def xtermTitleReset(): init_xterm_titles() - global default_xterm_title - if default_xterm_title is None: + 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 not _disable_xtermTitle: + if dotitles and _xterm_title_supported: from portage.process import find_binary, spawn shell = os.environ.get("SHELL") @@ -329,7 +371,7 @@ def xtermTitleReset(): home = os.environ.get("HOME", "") if home != "" and pwd.startswith(home): pwd = "~" + pwd[len(home) :] - default_xterm_title = format_xterm_title( + _default_xterm_title = format_xterm_title( "{}@{}:{}".format( os.environ.get("LOGNAME", ""), os.environ.get("HOSTNAME", "").split(".", 1)[0], @@ -338,7 +380,7 @@ def xtermTitleReset(): ) # 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) + xtermTitle(_default_xterm_title, raw=True) def notitles(): @@ -518,50 +560,6 @@ def init_curses(fd): return None -def init_xterm_titles(): - global _disable_xtermTitle - global _title_init_seq - global _title_finish_seq - - if _disable_xtermTitle is not None: - # already initialized - return - - _disable_xtermTitle = True - - 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(sys.stderr) - - if curses is not None: - tsl = curses.tigetstr("tsl").decode() - fsl = curses.tigetstr("fsl").decode() - if tsl and fsl: - _xtermTitle_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 - _disable_xtermTitle = False - _title_init_seq = "\x1b]0;" - _title_finish_seq = "\x07" - - -def format_xterm_title(mystr): - if _disable_xtermTitle: - return mystr - return _title_init_seq + mystr + _title_finish_seq - - def get_term_size(fd=None): """ Get the number of lines and columns of the tty that is connected to From 8d698e0b501cc0954ed9a6bffa35a7f95af7af50 Mon Sep 17 00:00:00 2001 From: Matthias Gerstner Date: Tue, 18 Jun 2024 23:03:33 +0200 Subject: [PATCH 3/3] output.py: consistently use sys.__stderr__ sys.stderr is sometimes temporarily replaced by other objects, thus rely on sys.__stdserr__ consistently. Also provide a default for `fd` in `init_curses()`. --- lib/portage/output.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/portage/output.py b/lib/portage/output.py index 626e91e54a..4109cf73df 100644 --- a/lib/portage/output.py +++ b/lib/portage/output.py @@ -292,7 +292,7 @@ def init_xterm_titles(): try: # by default check if we can dynamically query the proper title # setting sequence via terminfo - curses = init_curses(sys.stderr) + curses = init_curses() if curses is not None: tsl = curses.tigetstr("tsl").decode() @@ -334,7 +334,7 @@ def xtermTitle(mystr, raw=False): mystr = _unicode_encode( mystr, encoding=_encodings["stdio"], errors="backslashreplace" ) - f = sys.stderr.buffer + f = sys.__stderr__.buffer f.write(mystr) f.flush() @@ -547,7 +547,7 @@ def new_styles(self, styles): self.style_listener(styles) -def init_curses(fd): +def init_curses(fd=sys.__stderr__): try: import curses