Re: [PATCH] emoji on TUI emacs

Kai Ma <[email protected]> Sat, 08 Aug 2026 20:52:03 +0200
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
Sorry, forgot to attach the patches..
0001-Change-widths-of-East-Asian-Neutral-characters-to-1.patch (text/x-diff, 1.8 KB)
From ace2aed26a39db49f7ad7982d1f4b45905216f7d Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Wed, 5 Aug 2026 00:56:18 +0200
Subject: [PATCH 1/4] Change widths of East Asian Neutral characters to 1.

In accordance to UAX #11 ED7
(https://www.unicode.org/reports/tr11/tr11-44.html#ED7)

> [...] because for all practical purposes they behave like Na, they are
> treated as narrow characters (the same as Na) under the
> recommendations below.

* lisp/international/characters.el: Do not mark U+2690--U+2692, U+1F1AD,
U+1FA00--U+1FA53, U+1FA60--U+1FA6D, and U+1FB00--U+1FB92 as
double-width, since they have East_Asian_Width=Neutral in Unicode 17.
---
 lisp/international/characters.el | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/lisp/international/characters.el b/lisp/international/characters.el
index d3024b95cd3..4f66aca6ebf 100644
--- a/lisp/international/characters.el
+++ b/lisp/international/characters.el
@@ -1304,7 +1304,7 @@ ?L
 	   (#x2648 . #x2653)
 	   (#x267F . #x267F)
            (#x268A . #x268F)
-	   (#x2690 . #x2693)
+	   (#x2693 . #x2693)
 	   (#x26A1 . #x26A1)
 	   (#x26AA . #x26AB)
 	   (#x26BD . #x26BE)
@@ -1374,7 +1374,6 @@ ?L
 	   (#x1F0CF . #x1F0CF)
 	   (#x1F18E . #x1F18E)
 	   (#x1F191 . #x1F19A)
-	   (#x1F1AD . #x1F1AD)
 	   (#x1F200 . #x1F202)
            (#x1F210 . #x1F23B)
            (#x1F240 . #x1F248)
@@ -1413,8 +1412,6 @@ ?L
 	   (#x1F90C . #x1F93A)
 	   (#x1F93C . #x1F945)
 	   (#x1F947 . #x1F9FF)
-	   (#x1FA00 . #x1FA53)
-	   (#x1FA60 . #x1FA6D)
 	   (#x1FA70 . #x1FA7C)
 	   (#x1FA80 . #x1FA8A)
 	   (#x1FA8E . #x1FAC6)
@@ -1422,7 +1419,6 @@ ?L
 	   (#x1FACD . #x1FADC)
 	   (#x1FADF . #x1FAEA)
 	   (#x1FAEF . #x1FAF8)
-	   (#x1FB00 . #x1FB92)
 	   (#x20000 . #x2FFFF)
 	   (#x30000 . #x3FFFF))))
   (dolist (elt l)
-- 
2.53.0
0002-Compose-emoji-sequences-on-tty-frames.patch (text/x-diff, 6.2 KB)
From 68d6ed74b3fb6c1c5769a1705382839cb5cfeab6 Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Sat, 8 Aug 2026 18:46:09 +0200
Subject: [PATCH 2/4] Compose emoji sequences on tty frames.

* admin/unidata/Makefile.in: Changed.
* admin/unidata/emoji-zwj.awk: Scan emoji-variation-sequences.txt to
generate auto-composition-emoji-tty-eligible-codepoints.  Update the
default composition function to compose-gstring-and-emoji.
* composite.el (compose-gstring-and-emoji): Add.
(auto-compose-chars): Changed to consider compose-gstring-and-emoji.
---
 admin/unidata/Makefile.in   |  2 +-
 admin/unidata/emoji-zwj.awk | 23 +++++++++++++++++++----
 lisp/composite.el           | 29 ++++++++++++++++++++++++-----
 3 files changed, 44 insertions(+), 10 deletions(-)
 mode change 100644 => 100755 admin/unidata/emoji-zwj.awk

diff --git a/admin/unidata/Makefile.in b/admin/unidata/Makefile.in
index b954990cf9e..6407abd1c4b 100644
--- a/admin/unidata/Makefile.in
+++ b/admin/unidata/Makefile.in
@@ -116,7 +116,7 @@ .PHONY:
 emoji-zwj.el: ${unidir}/emoji-zwj.el
 
 zwj = ${srcdir}/emoji-zwj.awk
-zwj_sources = ${srcdir}/emoji-zwj-sequences.txt $(srcdir)/emoji-sequences.txt
+zwj_sources = ${srcdir}/emoji-zwj-sequences.txt $(srcdir)/emoji-sequences.txt $(srcdir)/emoji-variation-sequences.txt
 ${unidir}/emoji-zwj.el: ${zwj}
 
 # Don't use $^, since that includes the awk script.
diff --git a/admin/unidata/emoji-zwj.awk b/admin/unidata/emoji-zwj.awk
old mode 100644
new mode 100755
index 1cb7508e443..1795152a391
--- a/admin/unidata/emoji-zwj.awk
+++ b/admin/unidata/emoji-zwj.awk
@@ -60,6 +60,11 @@
     vec[elts[1]] = vec[elts[1]] "\""
 }
 
+/^[0-9A-F]+ FE0F *; *emoji style;/ {
+    tty_trigger_codepoints[++n_tty_triggers] = $1
+    ch[$1] = $1
+}
+
 END {
      print ";;; emoji-zwj.el --- emoji zwj character composition table  -*- lexical-binding:t -*-"
      print ";;; Automatically generated from admin/unidata/emoji-{zwj-,}sequences.txt"
@@ -86,13 +91,23 @@ END {
 
      print "(setq auto-composition-emoji-eligible-codepoints"
      print "'("
-
      for (trig in trigger_codepoints)
      {
          print "?\\N{U+" trigger_codepoints[trig] "}"
      }
      print "))"
 
+     # On tty terminals, emoji sequences are simply composed and the
+     # rendering is delegated to the terminal emulators.  All codepoints
+     # in the variation sequence is considered a valid beginning.
+     print "(setq auto-composition-emoji-tty-eligible-codepoints"
+     print "'("
+     for (trig in tty_trigger_codepoints)
+     {
+         print "?\\N{U+" tty_trigger_codepoints[trig] "}"
+     }
+     print "))"
+
      #  We add entries for 'codepoint U+FE0F' here to ensure that the
      # code in font_range is triggered.
 
@@ -114,7 +129,7 @@ END {
      print "                        (nconc (char-table-range composition-function-table (car elt))"
      print "                               (list (vector (cdr elt)"
      print "                                             0"
-     print "                                             #'compose-gstring-for-graphic)))))"
+     print "                                             #'compose-gstring-and-emoji)))))"
 
      print ";; The following two blocks are derived by hand from emoji-sequences.txt"
      print ";; FIXME: add support for Emoji_Keycap_Sequence once we learn how to respect FE0F/VS-16"
@@ -126,7 +141,7 @@ END {
      print "                      (nconc (char-table-range composition-function-table '(#x1F1E6 . #x1F1FF))"
      print "                             (list (vector \"[\\U0001F1E6-\\U0001F1FF][\\U0001F1E6-\\U0001F1FF]\""
      print "                                           0"
-     print "                                           #'compose-gstring-for-graphic))))"
+     print "                                           #'compose-gstring-and-emoji))))"
 
      print ";; UK Flags"
      print "(set-char-table-range composition-function-table"
@@ -134,7 +149,7 @@ END {
      print "                      (nconc (char-table-range composition-function-table #x1F3F4)"
      print "                             (list (vector \"\\U0001F3F4\\U000E0067\\U000E0062\\\\(?:\\U000E0065\\U000E006E\\U000E0067\\\\|\\U000E0073\\U000E0063\\U000E0074\\\\|\\U000E0077\\U000E006C\\U000E0073\\\\)\\U000E007F\""
      print "                                           0"
-     print "                                           #'compose-gstring-for-graphic))))"
+     print "                                           #'compose-gstring-and-emoji))))"
 
      printf "\n(provide 'emoji-zwj)"
 }
diff --git a/lisp/composite.el b/lisp/composite.el
index d6e923f7f28..512bd0bdd7d 100644
--- a/lisp/composite.el
+++ b/lisp/composite.el
@@ -899,11 +899,18 @@ auto-compose-chars
 
 This function is the default value of `auto-composition-function' (which see)."
   (let ((gstring (composition-get-gstring from to font-object string)))
-    (if (lgstring-shaped-p gstring)
-	gstring
-      (or (fontp font-object 'font-object)
-	  (setq func 'compose-gstring-for-terminal))
-      (funcall func gstring direction))))
+    (cond
+     ((lgstring-shaped-p gstring)
+      gstring)
+     ((and (eq func #'compose-gstring-and-emoji)
+           (fontp font-object 'font-object))
+      (compose-gstring-for-graphic gstring direction))
+     ((eq func #'compose-gstring-and-emoji)
+      (compose-gstring-and-emoji gstring direction))
+     ((fontp font-object 'font-object)
+      (funcall func gstring direction))
+     (t
+      (compose-gstring-for-terminal gstring direction)))))
 
 (put 'auto-composition-mode 'permanent-local t)
 
@@ -936,6 +943,18 @@ global-auto-composition-mode
 
 (defalias 'toggle-auto-composition 'auto-composition-mode)
 
+(defun compose-gstring-and-emoji (gstring _direction)
+  "Compose Emoji sequences into a grapheme cluster.
+This function is only called on tty terminals.  On graphical displays,
+`auto-compose-chars' will call `compose-gstring-for-graphic' instead."
+  ;; Emoji sequences are matched exactly.
+  (let ((nglyphs (lgstring-char-len gstring)))
+    (dotimes (i nglyphs)
+      (let ((glyph (lgstring-glyph gstring i)))
+        (when glyph
+          (lglyph-set-from-to glyph 0 (1- nglyphs))))))
+  gstring)
+
 (provide 'composite)
 
 ;;; composite.el ends here
-- 
2.53.0
0003-Produce-padding-glyphs-for-composite-glyphs.patch (text/x-diff, 8.4 KB)
From 564a059d72e5b9910baacacf61f39ef82f58779f Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Thu, 30 Jul 2026 16:24:28 +0200
Subject: [PATCH 3/4] Produce padding glyphs for composite glyphs.

* src/term.c (append_composite_glyph): Add `padding' argument.
(produce_composite_glyph): Request one padding glyph if the composite
glyph has pixel_width >= 2 and the first character has width >= 2, or it
is considered for an emoji.
(encode_terminal_code): Skip padding glyphs for composite glyphs as
well.
(composite_glyph_is_emoji_sequence): Add.
(tty-display-emoji-force-wide): Add.
(auto-composition-emoji-tty-eligible-codepoints): Add.
---
 src/term.c | 160 +++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 118 insertions(+), 42 deletions(-)

diff --git a/src/term.c b/src/term.c
index 52ae4f61814..b574149d4f0 100644
--- a/src/term.c
+++ b/src/term.c
@@ -567,7 +567,7 @@ encode_terminal_code (struct glyph *src, int src_len,
   nchars = 0;
   while (src < src_end)
     {
-      if (src->type == COMPOSITE_GLYPH)
+      if (src->type == COMPOSITE_GLYPH && ! CHAR_GLYPH_PADDING_P (*src))
 	{
 	  struct composition *cmp;
 	  Lisp_Object gstring UNINIT;
@@ -1528,7 +1528,7 @@ #define CONDITIONAL_REASSIGN(cap1, cap2, sym)				\
    available from the initial frame as in batch mode.  */
 
 static void append_glyph (struct it *);
-static void append_composite_glyph (struct it *);
+static void append_composite_glyph (struct it *, int padding);
 static void produce_composite_glyph (struct it *);
 static void append_glyphless_glyph (struct it *, int, const char *);
 static void produce_glyphless_glyph (struct it *, Lisp_Object);
@@ -1775,9 +1775,10 @@ produce_glyphs (struct it *it)
    face.  */
 
 static void
-append_composite_glyph (struct it *it)
+append_composite_glyph (struct it *it, int padding)
 {
-  struct glyph *glyph;
+  struct glyph *glyph, *end;
+  int i, width = 1 + padding;
 
   eassert (it->glyph_row);
   glyph = it->glyph_row->glyphs[it->area] + it->glyph_row->used[it->area];
@@ -1789,74 +1790,125 @@ append_composite_glyph (struct it *it)
     && !it->glyph_row->full_width_p
     && !WINDOW_RIGHTMOST_P (it->w)
     && WINDOW_RIGHT_MARGIN_WIDTH (it->w) == 0;
-  if (glyph < it->glyph_row->glyphs[1 + it->area] - reserve_last)
+  end = it->glyph_row->glyphs[1 + it->area] - reserve_last;
+  if (glyph < end)
     {
       /* If the glyph row is reversed, we need to prepend the glyph
 	 rather than append it.  */
       if (it->glyph_row->reversed_p && it->area == TEXT_AREA)
 	{
 	  struct glyph *g;
+	  int move_by = width;
 
 	  /* Make room for the new glyph.  */
+	  if (move_by > end - glyph)
+	    move_by = end - glyph;
 	  for (g = glyph - 1; g >= it->glyph_row->glyphs[it->area]; g--)
-	    g[1] = *g;
+	    g[move_by] = *g;
 	  glyph = it->glyph_row->glyphs[it->area];
+	  end = glyph + move_by;
 	}
-      glyph->type = COMPOSITE_GLYPH;
-      eassert (it->pixel_width <= SHRT_MAX);
-      glyph->pixel_width = it->pixel_width;
-      glyph->u.cmp.id = it->cmp_it.id;
-      if (it->cmp_it.ch < 0)
-	{
-	  glyph->u.cmp.automatic = 0;
-	  glyph->u.cmp.id = it->cmp_it.id;
-	}
-      else
+
+      eassert(it->pixel_width <= SHRT_MAX);
+      for (i = 0;
+	   i < width && glyph < end;
+	   ++i)
 	{
-	  glyph->u.cmp.automatic = 1;
+	  glyph->type = COMPOSITE_GLYPH;
+	  glyph->pixel_width = it->pixel_width - padding;
 	  glyph->u.cmp.id = it->cmp_it.id;
-	  glyph->slice.cmp.from = it->cmp_it.from;
-	  glyph->slice.cmp.to = it->cmp_it.to - 1;
-	}
+	  if (it->cmp_it.ch < 0)
+	    {
+	      glyph->u.cmp.automatic = 0;
+	      glyph->u.cmp.id = it->cmp_it.id;
+	    }
+	  else
+	    {
+	      glyph->u.cmp.automatic = 1;
+	      glyph->u.cmp.id = it->cmp_it.id;
+	      glyph->slice.cmp.from = it->cmp_it.from;
+	      glyph->slice.cmp.to = it->cmp_it.to - 1;
+	    }
+	  glyph->avoid_cursor_p = it->avoid_cursor_p;
+	  glyph->multibyte_p = it->multibyte_p;
+	  glyph->frame = it->f;
+	  glyph->face_id = it->face_id;
+	  glyph->padding_p = i > 0;
+	  glyph->charpos = CHARPOS (it->position);
+	  glyph->object = it->object;
+	  if (it->bidi_p)
+	    {
+	      glyph->resolved_level = it->bidi_it.resolved_level;
+	      eassert ((it->bidi_it.type & 7) == it->bidi_it.type);
+	      glyph->bidi_type = it->bidi_it.type;
+	    }
+	  else
+	    {
+	      glyph->resolved_level = 0;
+	      glyph->bidi_type = UNKNOWN_BT;
+	    }
 
-      glyph->avoid_cursor_p = it->avoid_cursor_p;
-      glyph->multibyte_p = it->multibyte_p;
-      glyph->frame = it->f;
-      glyph->face_id = it->face_id;
-      glyph->padding_p = false;
-      glyph->charpos = CHARPOS (it->position);
-      glyph->object = it->object;
-      if (it->bidi_p)
-	{
-	  glyph->resolved_level = it->bidi_it.resolved_level;
-	  eassert ((it->bidi_it.type & 7) == it->bidi_it.type);
-	  glyph->bidi_type = it->bidi_it.type;
-	}
-      else
-	{
-	  glyph->resolved_level = 0;
-	  glyph->bidi_type = UNKNOWN_BT;
+	  ++it->glyph_row->used[it->area];
+	  ++glyph;
 	}
+    }
+}
 
-      ++it->glyph_row->used[it->area];
-      ++glyph;
+
+/* For some emoji sequences, the first character width might be 1.
+   Possibly render them as wide glyphs if it's requested.  */
+
+static bool
+composite_glyph_is_emoji_sequence (struct it *it)
+{
+  Lisp_Object gstring;
+  int first, second;
+
+  /* Skip static compositions.  */
+  if (it->cmp_it.ch < 0)
+    return false;
+
+  gstring = composition_gstring_from_id (it->cmp_it.id);
+  if (it->cmp_it.from + 1 >= it->cmp_it.to)
+    return false;
+  first = XFIXNUM (LGSTRING_CHAR (gstring, it->cmp_it.from));
+  second = XFIXNUM (LGSTRING_CHAR (gstring, it->cmp_it.from + 1));
+
+  /* Base + VS16/Modifier.  */
+  if (second == 0xFE0F || (0x1F3FB <= second && second <= 0x1F3FF))
+    {
+      return ! NILP (Fmemq (make_fixnum (first),
+			    Vauto_composition_emoji_tty_eligible_codepoints));
     }
+
+  /* Regional Indicators ("Flags" in emoji_zwj.awk).  */
+  if (0x1F1E6 <= first && first <= 0x1F1FF
+      && 0x1F1E6 <= second && second <= 0x1F1FF)
+    return true;
+
+  return false;
 }
 
 
 /* Produce a composite glyph for iterator IT.  IT->cmp_id is the ID of
    the composition.  We simply produces components of the composition
    assuming that the terminal has a capability to layout/render it
-   correctly.  */
+   correctly.  A padding glyph is added if both the composition and the
+   first character has width >= 2.  As a special case, if the composite
+   glyph is determined to be an emoji sequence, it may also add a
+   padding glyph.  */
 
 static void
 produce_composite_glyph (struct it *it)
 {
+  int padding, first_char;
+
   if (it->cmp_it.ch < 0)
     {
       struct composition *cmp = composition_table[it->cmp_it.id];
 
       it->pixel_width = cmp->width;
+      first_char = it->c;
     }
   else
     {
@@ -1864,10 +1916,16 @@ produce_composite_glyph (struct it *it)
 
       it->pixel_width = composition_gstring_width (gstring, it->cmp_it.from,
 						   it->cmp_it.to, NULL);
+      first_char = LGLYPH_CHAR (LGSTRING_GLYPH (gstring,
+						it->cmp_it.from));
     }
-  it->nglyphs = 1;
+
+  padding = (it->pixel_width >= 2 && CHARACTER_WIDTH (first_char) >= 2)
+    || (tty_display_emoji_force_wide
+	&& composite_glyph_is_emoji_sequence (it));
+  it->nglyphs = 1 + padding;
   if (it->glyph_row)
-    append_composite_glyph (it);
+    append_composite_glyph (it, padding);
 }
 
 
@@ -5266,6 +5324,24 @@ syms_of_term (void)
 non-nil to enable this optimization.  */);
   tty_cursor_movement_use_TAB_BS = 0;
 
+  DEFVAR_LISP ("auto-composition-emoji-tty-eligible-codepoints", Vauto_composition_emoji_tty_eligible_codepoints,
+	       doc: /* List of codepoints for which auto-composition will check for an emoji font on tty display.
+
+These are codepoints which have Emoji_Presentation = No, and thus by
+default are not displayed as emoji.  When followed by U+FE0F (VS-16),
+they may be considered as the beginning of an emoji sequence instead.
+
+This list is auto-generated, you should not need to modify it.  */);
+  Vauto_composition_emoji_tty_eligible_codepoints = Qnil;
+
+  DEFVAR_BOOL ("tty-display-emoji-force-wide", tty_display_emoji_force_wide,
+    doc: /* Whether Emoji sequences on TTY frames should always be considered wide.
+
+When nil, emoji sequences whose first character's width is 1 may be
+considered as a narrow glyph to be compatible with some non-compliant
+terminal emulators.  */);
+  tty_display_emoji_force_wide = true;
+
   defsubr (&Stty_display_color_p);
   defsubr (&Stty_display_color_cells);
   defsubr (&Stty_no_underline);
-- 
2.53.0
0004-Make-string-width-return-2-for-an-emoji.patch (text/x-diff, 2.8 KB)
From cdd561b30ab1b1b6278f902189ab54bc573d70ad Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Sat, 8 Aug 2026 19:42:31 +0200
Subject: [PATCH 4/4] Make string-width return 2 for an emoji.

* lisp/composite.el (compose-gstring-and-emoji): Adjust the widths of
glyphs.
* src/character.c (lisp_string_width): Handle auto_comp when
HAVE_WINDOW_SYSTEM is not defined.
---
 lisp/composite.el |  4 +++-
 src/character.c   | 13 ++++++-------
 2 files changed, 9 insertions(+), 8 deletions(-)

diff --git a/lisp/composite.el b/lisp/composite.el
index 512bd0bdd7d..8ac29bb71aa 100644
--- a/lisp/composite.el
+++ b/lisp/composite.el
@@ -952,7 +952,9 @@ compose-gstring-and-emoji
     (dotimes (i nglyphs)
       (let ((glyph (lgstring-glyph gstring i)))
         (when glyph
-          (lglyph-set-from-to glyph 0 (1- nglyphs))))))
+          (lglyph-set-from-to glyph 0 (1- nglyphs))
+          ;; Adjust the total width of the gstring to 2.
+          (lglyph-set-width glyph (if (= i 0) 2 0))))))
   gstring)
 
 (provide 'composite)
diff --git a/src/character.c b/src/character.c
index 6347617d508..d3fd3bdb944 100644
--- a/src/character.c
+++ b/src/character.c
@@ -353,14 +353,12 @@ lisp_string_width (Lisp_Object string, ptrdiff_t from, ptrdiff_t to,
   ptrdiff_t from_byte = i_byte;
   ptrdiff_t width = 0;
   struct Lisp_Char_Table *dp = buffer_display_table ();
-#ifdef HAVE_WINDOW_SYSTEM
   struct frame *f =
     (FRAMEP (selected_frame) && FRAME_LIVE_P (XFRAME (selected_frame)))
     ? XFRAME (selected_frame)
     : NULL;
   int font_width = -1;
   Lisp_Object default_font, frame_font;
-#endif
 
   eassert (precision <= 0 || (nchars && nbytes));
 
@@ -379,9 +377,8 @@ lisp_string_width (Lisp_Object string, ptrdiff_t from, ptrdiff_t to,
 	  chars = end - i;
 	  bytes = string_char_to_byte (string, end) - i_byte;
 	}
-#ifdef HAVE_WINDOW_SYSTEM
       else if (auto_comp
-	       && f && FRAME_WINDOW_P (f)
+	       && f
 	       && multibyte
 	       && find_automatic_composition (i, -1, i, &ignore,
 					      &end, &val, string)
@@ -396,7 +393,7 @@ lisp_string_width (Lisp_Object string, ptrdiff_t from, ptrdiff_t to,
 
 	  /* The below is somewhat expensive, so compute it only once
 	     for the entire loop, and only if needed.  */
-	  if (font_width < 0)
+	  if (FRAME_WINDOW_P (f) && font_width < 0)
 	    {
 	      font_width = FRAME_COLUMN_WIDTH (f);
 	      default_font = Fface_font (Qdefault, Qnil, Qnil);
@@ -417,11 +414,13 @@ lisp_string_width (Lisp_Object string, ptrdiff_t from, ptrdiff_t to,
 		    }
 		}
 	    }
-	  thiswidth = (double) pixelwidth / font_width + 0.5;
+
+	  thiswidth = FRAME_WINDOW_P (f)
+	    ? ((double) pixelwidth / font_width + 0.5)
+	    : pixelwidth;
 	  chars = end - i;
 	  bytes = string_char_to_byte (string, end) - i_byte;
 	}
-#endif	/* HAVE_WINDOW_SYSTEM */
       else
 	{
 	  int c;
-- 
2.53.0