sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit d21b4bebeb134b1cccefdb1c669542cc2d109b3e
parent 735593960308648000dbe99d706221c24a306746
Author: Dan Callaghan <djc@djc.id.au>
Date:   Mon,  5 Oct 2026 19:46:37 +1100

improve xterm title handling

Check for the XT termcap when deciding whether to emit the title, since
that is the "proper" way to detect support. But also match on TERM=tmux
since its terminfo doesn't advertise XT for some reason.

Escape control characters in the title.

Strip out all characters above U+00FF on Screen due to a bug in its
handling of Unicode in xterm titles.

Fixes #572.

Diffstat:
M lib/sup/buffer.rb | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb
@@ -151,7 +151,7 @@ EOS
     @textfields = {}
     @flash = nil
     @shelled = @asking = false
-    @in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
+    @have_xterm_title = Ncurses.tigetflag("XT") > 0 || ENV["TERM"] =~ /(screen|tmux)/
     @sigwinch_happened = false
     @sigwinch_mutex = Mutex.new
   end
@@ -262,8 +262,7 @@ EOS
         get_status_and_title @focus_buf # must be called outside of the ncurses lock
       end
 
-    ## http://rtfm.etla.org/xterm/ctlseq.html (see Operating System Controls)
-    print "\033]0;#{title}\07" if title && @in_x
+    emit_xterm_title title if title
 
     Ncurses.mutex.lock unless opts[:sync] == false
 
@@ -292,6 +291,17 @@ EOS
     Ncurses.mutex.unlock unless opts[:sync] == false
   end
 
+  def emit_xterm_title title
+    return unless @have_xterm_title
+    ## Escape terminal control characters.
+    title = title.gsub(/[[:cntrl:]]/) { |c| Ncurses.unctrl c.ord }
+    ## Screen does not properly handle characters above U+FF in the title.
+    ## It truncates them to 8 bits which may cause it to emit control
+    ## characters into the terminal title.
+    title = title.gsub /[^\u0000-\u00ff]/, "?" if ENV["TERM"] =~ /screen/
+    print "\033]0;#{title}\07"
+  end
+
   ## if the named buffer already exists, pops it to the front without
   ## calling the block. otherwise, gets the mode from the block and
   ## creates a new buffer. returns two things: the buffer, and a boolean