Re: [PATCH] emoji on TUI emacs

Kai Ma <[email protected]> Sun, 09 Aug 2026 20:42:12 +0200
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
Thanks.  Please find the updated patch series below.

Eli Zaretskii <[email protected]> writes:

>> However, the column number change due to C-f, C-b, etc. is still the
>> total sum of CHARACTER_WIDTH (not necessarily 2).  Do we want to change
>> it as well?
>
> Yes, definitely.  current-column and move-to-column should both work
> correctly in these cases.

Fixed.  Previously in composition_update_it, the width is computed by
summing character widths.  I'm not entirely sure whether it's correct to
sum glyph widths here.

>> --- 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)))))
>
> I think the above logic could benefit from comments explaining why we
> dispatch the different cases as we do.

Done.

>> +  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.
>
> This line is too long.  Our conventions are not to exceed 79
> characters.  I suggest to shorten it as follows:
>
>   List of characters displayed as Emoji on TTY frames when followed by
>   VS-16.

Fixed.


>> +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.
>                                                  ^^^^^
> "Emoji", capitalized.
>
>> +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
>              ^^^^^
> "Emoji"

Fixed.

>
>> +considered as a narrow glyph to be compatible with some non-compliant
>> +terminal emulators.  */);
>> +  tty_display_emoji_force_wide = true;
>
> I think these two variables are related (basically, the first one
> controls which sequences will be considered to be wide, right?).  So I
> think they should each mention the other one.

Fixed.

>
>> @@ -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;
>
> This is not quite right: in an Emacs build --without-x some of the
> functions mentioned in the code fragment which was previously under
> HAVE_WINDOW_SYSTEM do not exist, so this will fail to link.  What you
> need is to move the HAVE_WINDOW_SYSTEM condition inside this fragment,
> such that it is still in effect when functions like Ffont_info are
> called.  I think the entire block which starts with
>
> 	  if (font_width < 0)
>
> should be under the HAVE_WINDOW_SYSTEM condition, since it makes no
> sense on a TTY frame.

Fixed.

>
> Finally, I think these changes and the new variables warrant a NEWS
> entry.

Done.
0001-Change-widths-of-East-Asian-Neutral-characters-to-1.patch (text/x-diff, 1.8 KB)
From 10d1fa2b424d67d1111abc12114e285f95ae0b43 Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Wed, 5 Aug 2026 00:56:18 +0200
Subject: [PATCH 1/6] 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.4 KB)
From d7930b3641a227acef357c9cde37bddb31b808cc Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Sat, 8 Aug 2026 18:46:09 +0200
Subject: [PATCH 2/6] 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           | 33 ++++++++++++++++++++++++++++-----
 3 files changed, 48 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..ce22b4bbbce 100644
--- a/lisp/composite.el
+++ b/lisp/composite.el
@@ -899,11 +899,22 @@ 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)
+     ;; The graphical composition function already handles the
+     ;; composition of Emoji sequences based on the font.  On textual
+     ;; terminals, we compose Emoji sequences and leave the actual
+     ;; rendering to the terminal emulator.
+     ((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 +947,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.8 KB)
From cd1b6e2144bffac7b655b45e891df899fbde3c19 Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Thu, 30 Jul 2026 16:24:28 +0200
Subject: [PATCH 3/6] 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 | 169 ++++++++++++++++++++++++++++++++++++++++-------------
 1 file changed, 127 insertions(+), 42 deletions(-)

diff --git a/src/term.c b/src/term.c
index 7e05f5b5139..7eb7d2a2a7d 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);
 }
 
 
@@ -5277,6 +5335,33 @@ syms_of_term (void)
 using TABs for cursor motion.  */);
   tty_cursor_movement_use_TAB = 1;
 
+  DEFVAR_LISP ("auto-composition-emoji-tty-eligible-codepoints", Vauto_composition_emoji_tty_eligible_codepoints,
+    doc: /* List of characters displayed as Emoji on TTY frames when followed by VS-16.
+
+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.
+
+Some initial code points may have character width 1, and non-compliant
+terminals may render them narrow.  In that case, set
+`tty-display-emoji-force-wide' to nil.
+
+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.
+
+On tty, an Emoji sequence considered for composition may begin with any
+code point in `auto-composition-emoji-tty-eligible-codepoints', which
+does not necessarily has width 2.  A compliant terminal emulator
+typically render an Emoji sequence as a wide glyph.
+
+When nil, an Emoji sequence 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.9 KB)
From 1b0135a3b0159d0c2d0ce90f0ec61ab1b42ee648 Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Sat, 8 Aug 2026 19:42:31 +0200
Subject: [PATCH 4/6] 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   | 15 +++++++++------
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/lisp/composite.el b/lisp/composite.el
index ce22b4bbbce..b2a832fb771 100644
--- a/lisp/composite.el
+++ b/lisp/composite.el
@@ -956,7 +956,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..4d6ad9a0b8e 100644
--- a/src/character.c
+++ b/src/character.c
@@ -353,12 +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;
+#ifdef HAVE_WINDOW_SYSTEM
   Lisp_Object default_font, frame_font;
 #endif
 
@@ -379,9 +379,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)
@@ -394,9 +393,10 @@ lisp_string_width (Lisp_Object string, ptrdiff_t from, ptrdiff_t to,
 
 	  int pixelwidth = composition_gstring_width (val, 0, j, NULL);
 
+#ifdef HAVE_WINDOW_SYSTEM
 	  /* 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 +417,14 @@ lisp_string_width (Lisp_Object string, ptrdiff_t from, ptrdiff_t to,
 		    }
 		}
 	    }
-	  thiswidth = (double) pixelwidth / font_width + 0.5;
+#endif
+
+	  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
0005-Compute-composition-width-using-glyph-widths.patch (text/x-diff, 1.1 KB)
From cafe5e982bc9ab32b3b506b6563b279d92e2244a Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Sun, 9 Aug 2026 19:44:49 +0200
Subject: [PATCH 5/6] Compute composition width using glyph widths.

* src/composite.c (composition_update_it): Set cmp_it->width to be the
sum of all glyph widths, instead of character widths.
---
 src/composite.c | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/composite.c b/src/composite.c
index 55841d08cb5..ce596be9d2c 100644
--- a/src/composite.c
+++ b/src/composite.c
@@ -1536,11 +1536,13 @@ composition_update_it (struct composition_it *cmp_it, ptrdiff_t charpos, ptrdiff
       cmp_it->nchars = LGLYPH_TO (glyph) + 1 - from;
       cmp_it->nbytes = 0;
       cmp_it->width = 0;
-      for (i = cmp_it->nchars - 1; i >= 0; i--)
+
+      for (i = cmp_it->nglyphs - 1; i >= 0; i--)
 	{
-	  c = XFIXNUM (LGSTRING_CHAR (gstring, from + i));
+	  glyph = LGSTRING_GLYPH (gstring, from + i);
+	  c = LGLYPH_CHAR (glyph);
 	  cmp_it->nbytes += CHAR_BYTES (c);
-	  cmp_it->width += CHARACTER_WIDTH (c);
+	  cmp_it->width += LGLYPH_WIDTH (glyph);
 	}
     }
   return c;
-- 
2.53.0
0006-etc-NEWS-Document-improved-Emoji-support-on-textual-.patch (text/x-diff, 1.4 KB)
From 2042315056defff267fcfbccacc651d09754a5d4 Mon Sep 17 00:00:00 2001
From: Kai Ma <[email protected]>
Date: Sun, 9 Aug 2026 20:31:26 +0200
Subject: [PATCH 6/6] ; * etc/NEWS: Document improved Emoji support on textual
 terminals.

---
 etc/NEWS | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/etc/NEWS b/etc/NEWS
index 9ddf8d41028..e5ebcc6f413 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -107,6 +107,25 @@ Therefore, the instructions in the NEWS file for Emacs 30.1 to add
 
 to your initialization file no longer apply, and you may remove it.
 
+---
+** Improved Emoji support on textual terminals
+Emacs now composes Emoji sequences as two-column glyphs on textual
+terminals when 'auto-composition-mode' is enabled.  This should correct
+a number of movement and display issues on an Emoji-capable terminal
+emulator.
+
+However, the actual rendering of Emoji still depends on the capability
+of the terminal emulator:
+
+- If the terminal emulator does not support grapheme clusters at all,
+  composed Emoji sequences will not work.  In this case, disable
+  'auto-composition-mode'.
+
+- Non-compliant terminal emulators may not always render an Emoji
+  sequence as a wide glyph, depending on the character width of the
+  first code point in the sequence.  In this case, it might help to set
+  'tty-display-emoji-force-wide' to nil.
+
 
 * Editing Changes in Emacs 32.1
 
-- 
2.53.0