Re: [PATCH] emoji on TUI emacs

Kai Ma <[email protected]>
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
Thanks for the review.  I've updated the patches according to the comments.

Eli Zaretskii <[email protected]> writes:

> Thanks, I have a couple of nits and one comment, and then there are a
> few problems that still need to be fixed:
>
>> --- a/src/composite.c
>> +++ b/src/composite.c
>> @@ -1535,12 +1535,12 @@ composition_update_it (struct composition_it *cmp_it, ptrdiff_t charpos, ptrdiff
>>        glyph = LGSTRING_GLYPH (gstring, cmp_it->from);
>>        cmp_it->nchars = LGLYPH_TO (glyph) + 1 - from;
>>        cmp_it->nbytes = 0;
>> -      cmp_it->width = 0;
>> +      cmp_it->width = composition_gstring_width (gstring, cmp_it->from, cmp_it->to, NULL);
>> +
>>        for (i = cmp_it->nchars - 1; i >= 0; i--)
>>  	{
>>  	  c = XFIXNUM (LGSTRING_CHAR (gstring, from + i));
>>  	  cmp_it->nbytes += CHAR_BYTES (c);
>> -	  cmp_it->width += CHARACTER_WIDTH (c);
>>  	}
>>      }
>>    return c;
>
>
> Hmmm... is this guaranteed to produce correct results for both Emoji
> and non-Emoji compositions?  Did you verify we are not breaking
> anything here, for example with compositions like 'a' followed by an
> accent?

Yes, they work for me.

> It would help if you could talk me through the code and
> explain how what composition_gstring_width does is equivalent to
> summing CHARACTER_WIDTH for TTY frames, in cases other than Emoji.  We
> need to convince ourselves that we don't introduce regressions here.
> If we are not sure, perhaps special-casing Emoji would be safer.

They should be equal for ordinary non-Emoji compositions (those in the
form of {BASE} {MODIFIER}+):

  before the change: cmp_it->width = base-char-width + 0 + 0 ...
                     (modifier-width = 0)

  after the change:  cmp_it->width = base-glyph-width + 0 + 0 ...
                     (modifier-width = 0)

I think the reason why it works for TTY frames is that
compose-gstring-terminal tries to extend the gstring as long as the next
glyph has width 0.  Therefore, for the ordinary cases, it should usually
be in the expected form.

The exceptions here are standalone combining characters.  I checked
single-char strings by enumerating codepoints in
composition-function-table, and found that the width of a string
containing only a modifier changes from 0 to 1, which IMO makes sense,
because Emacs adds a preceding space as the base when they appear
standalone (also in compose-gstring-terminal).  Previously, if a line
contains a single FE0F,

   {x} ?\xFE0F {y}             ({x} and {y} are points)

both x and y report column number 0, despite the fact that it is
visually a space, and you can move the cursor around it.

After this change, x reports column number 0 and y reports 1, which is
more intuitive.  And if there is a preceding character,
compose-gstring-terminal will compose them together as a grapheme
cluster, and the width of this composition is just the width of the
base character.

Curiously, I found two exceptions during this testing,

U+061B ARABIC SEMICOLON
U+06DE ARABIC START OF RUB EL HIZB

  (currently both (char-width ?\x061B) and (string-width "\x061B") are 0)

Both have visual appearance, their East_Asian_Width is Neutral and their
canonical-combining-class is 0, so I think they should have width 1, so
as to prevent them from being composed unexpectedly.  I updated Patch 1
to correct them.  After this change, they work fine for me.  But I don't
know Arabic, so I might be wrong here.

>> * admin/unidata/Makefile.in: Changed.
>
> This should describe the change.

Fixed.

>> * src/composite.c (composition_update_it): set the width of an automatic
>> composition using composition_gstring_width.
>
> The description of the change should begin with a capital letter
> ("Set", not "set").

Fixed.

> I applied the patches and tried to use Emacs in a -nw session.  I got
> assertion violation when scrolling through the HELLO file ("C-h h").
> See the backtrace and some data from GDB below.  As result, I think
> this code is incorrect:
>
>   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));
>
> See the doc string of composition-get-gstring regarding the structure
> of a gstring object.  My reading of that is that you always need to
> use 0 and 1 to access the first and the second codepoint of a composed
> sequence; FROM and TO are indices of the _glyphs_ in the composed
> grapheme cluster, which is not what you want.
>
> As you can see from the GDB session of an Emacs that hit assertion
> violation, this gstring has only 2 composed characters, but FROM is 2
> and TO is 4, so you are accessing a 3-component vector with indices
> 2+1 = 3 and 3+1 = 4, which is not right.

Indeed, I reproduced the assertion failure after enabling the
assertions.  I didn't expect the glyph index does not equal the
character index.

> And I'm not sure you need the condition about FROM+1 >= TO, is that
> perhaps some kind of defense against crashes you've seen?  If not, why
> do you need this condition there?

I added it because composite_glyph_is_emoji_sequence is meant to check
whether the composite glyph is a result of an Emoji _sequence_, so I
simply took the approach that seemed to make more sense: make the
function return false as early as possible if it does not have (at
least) two chars...

> I think the above fragment should be replaced with this:
>
>   first = XFIXNUM (LGSTRING_CHAR (gstring, 0));
>   if (LGSTRING_CHAR_LEN (gstring) <= 1)
>     second = 0;
>   else
>     second = XFIXNUM (LGSTRING_CHAR (gstring, 1));
>
> because I've seen cases that the function gets a composition with only
> one codepoint (which is strange, but we should not crash or hit
> assertions).

... and that approach happened to defend against this case as well.  So
I weren't bitten by this during testing.

> After fixing these two problems, I still see some incorrect
> compositions in the various admin/unidata/emoji-*.txt files.  First,
> cursor movement through Emoji sequences around line 3600 of
> emoji-test.txt is wrong: it seems that the terminal thinks the "heart"
> Emoji is two separate characters, or that its width is not 2.

Indeed.  This is due to emoji-zwj.awk only generating composition rules
from emoji-zwj-sequences.txt, which only contains fully-qualified RGI
sequences.  I've updated the patches to also generate composition rules
for both minimally-qualified and unqualified RGI sequences from
emoji-test.txt.  This should be correct according to the file header:

 • The listed minimally-qualified and unqualified cover all cases where
   an element of the RGI set is missing one or more emoji presentation
   selectors.

> The second problem is with the Keycap sequences around line 550 of
> emoji-sequences: it seems Emacs is not composing these sequences for
> some reason?  Do you see this on your terminal?

Should be fixed now.  It was because I added all codepoints in
auto-composition-emoji-tty-eligible-codepoints to 'ch' in emoji-zwj.awk.
The problem is that auto-composition-emoji-tty-eligible-codepoints
contains initial codepoints of keycap sequences, and then 'ch' generated
composition rules like 002A FE0F, which took priority over
002A FE0F 20E3.

> Apart of the above two issues, the display seems correct, which is a
> nice improvement.

Thanks.  Hopefully the updated patches make it even nicer.
0001-Change-widths-of-some-East-Asian-Neutral-characters-.patch (text/x-diff, 2.2 KB)
From 312b52a37011cd3f47b183701543b2b2c44d12e5 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 some 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.
Do not mark U+061B and U+06DE as zero-width.
---
 lisp/international/characters.el | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/lisp/international/characters.el b/lisp/international/characters.el
index d3024b95cd3..2b53b99afed 100644
--- a/lisp/international/characters.el
+++ b/lisp/international/characters.el
@@ -1001,10 +1001,12 @@ ?L
 	   (#x05C4 . #x05C5)
 	   (#x05C7 . #x05C7)
 	   (#x0600 . #x0605)
-	   (#x0610 . #x061C)
+           (#x0610 . #x061A)
+           (#x061C . #x061C)
 	   (#x064B . #x065F)
 	   (#x0670 . #x0670)
-	   (#x06D6 . #x06E4)
+	   (#x06D6 . #x06DD)
+	   (#x06DF . #x06E4)
 	   (#x06E7 . #x06E8)
 	   (#x06EA . #x06ED)
 	   (#x070F . #x070F)
@@ -1304,7 +1306,7 @@ ?L
 	   (#x2648 . #x2653)
 	   (#x267F . #x267F)
            (#x268A . #x268F)
-	   (#x2690 . #x2693)
+	   (#x2693 . #x2693)
 	   (#x26A1 . #x26A1)
 	   (#x26AA . #x26AB)
 	   (#x26BD . #x26BE)
@@ -1374,7 +1376,6 @@ ?L
 	   (#x1F0CF . #x1F0CF)
 	   (#x1F18E . #x1F18E)
 	   (#x1F191 . #x1F19A)
-	   (#x1F1AD . #x1F1AD)
 	   (#x1F200 . #x1F202)
            (#x1F210 . #x1F23B)
            (#x1F240 . #x1F248)
@@ -1413,8 +1414,6 @@ ?L
 	   (#x1F90C . #x1F93A)
 	   (#x1F93C . #x1F945)
 	   (#x1F947 . #x1F9FF)
-	   (#x1FA00 . #x1FA53)
-	   (#x1FA60 . #x1FA6D)
 	   (#x1FA70 . #x1FA7C)
 	   (#x1FA80 . #x1FA8A)
 	   (#x1FA8E . #x1FAC6)
@@ -1422,7 +1421,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, 7.2 KB)
From a40e2c3e8ee0fb755b28ffb4012d7e84862b052d 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: Add emoji-variation-sequences.txt and
emoji-test.txt to zwj_sources.
* admin/unidata/emoji-zwj.awk: Scan emoji-variation-sequences.txt to
generate auto-composition-emoji-tty-eligible-codepoints.  Scan
emoji-test.txt to generate
auto-composition-emoji-unqualified-tail-initials and the composition
rules for unqualified and minimally-qualified RGI sequences.  Update the
default composition function to compose-gstring-and-emoji.
* lisp/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 | 39 ++++++++++++++++++++++++++++++++-----
 lisp/composite.el           | 33 ++++++++++++++++++++++++++-----
 3 files changed, 63 insertions(+), 11 deletions(-)
 mode change 100644 => 100755 admin/unidata/emoji-zwj.awk

diff --git a/admin/unidata/Makefile.in b/admin/unidata/Makefile.in
index b954990cf9e..66f486c2417 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 $(srcdir)/emoji-test.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..d383597d169
--- a/admin/unidata/emoji-zwj.awk
+++ b/admin/unidata/emoji-zwj.awk
@@ -39,7 +39,9 @@
 
 ### Code:
 
-/^[0-9A-F].*; RGI_Emoji_(ZWJ|Modifier)_Sequence/ {
+(/^[0-9A-F].*; RGI_Emoji_(ZWJ|Modifier)_Sequence/)||
+(/^[0-9A-F].*; (un|minimally-)qualified/ && /.+200D.+;/) {
+    is_unqualified = $0 ~ /; unqualified/
     sub(/ *;.*/, "", $0)
     num = split($0, elts)
     if (ch[elts[1]] == "")
@@ -58,6 +60,14 @@
         vec[elts[1]] = vec[elts[1]] c
     }
     vec[elts[1]] = vec[elts[1]] "\""
+
+    if (is_unqualified && elts[2] == "200D") {
+        unqualified_tail_initials[++n_unqualified_tail_initials] = elts[3]
+    }
+}
+
+/^[0-9A-F]+ FE0F *; *emoji style;/ {
+    tty_trigger_codepoints[++n_tty_triggers] = $1
 }
 
 END {
@@ -86,13 +96,32 @@ 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 "))"
+
+     # Generate unqualified tail initials.
+     print "(setq auto-composition-emoji-unqualified-tail-initials"
+     print "'("
+     for (trig in unqualified_tail_initials)
+     {
+         print "?\\N{U+" unqualified_tail_initials[trig] "}"
+     }
+     print "))"
+
      #  We add entries for 'codepoint U+FE0F' here to ensure that the
      # code in font_range is triggered.
 
@@ -114,7 +143,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 +155,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 +163,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..d510b92d0f6 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 frames.  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, 9.7 KB)
From 6a51154abf0eb9db5326db78b05fffb368aa7b68 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 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.
(auto-composition-emoji-unqualified-tail-initials): Add.
---
 src/term.c | 188 +++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 145 insertions(+), 43 deletions(-)

diff --git a/src/term.c b/src/term.c
index 7e05f5b5139..983d3c701c5 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,131 @@ 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;
+	  ++it->glyph_row->used[it->area];
+	  ++glyph;
 	}
-      else
-	{
-	  glyph->resolved_level = 0;
-	  glyph->bidi_type = UNKNOWN_BT;
-	}
-
-      ++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;
+  ptrdiff_t len;
+  int first, second, third;
+
+  /* Skip static compositions.  */
+  if (it->cmp_it.ch < 0)
+    return false;
+
+  gstring = composition_gstring_from_id (it->cmp_it.id);
+  len = LGSTRING_CHAR_LEN (gstring);
+  first = XFIXNUM (LGSTRING_CHAR (gstring, 0));
+  second = len < 2 ? 0 : XFIXNUM (LGSTRING_CHAR (gstring, 1));
+
+  /* Regional Indicators ("Flags" in emoji_zwj.awk).  */
+  if (0x1F1E6 <= first && first <= 0x1F1FF
+      && 0x1F1E6 <= second && second <= 0x1F1FF)
+    return true;
+
+  /* The rest two cases both require Base.  */
+  if (NILP (Fmemq (make_fixnum (first),
+		   Vauto_composition_emoji_tty_eligible_codepoints)))
+    return false;
+
+  /* Base + VS16/Modifier.  */
+  if (second == 0xFE0F || (0x1F3FB <= second && second <= 0x1F3FF))
+    return true;
+
+  /* Unqualified RGI sequences: Base + ZWJ + TailInitial ...  */
+  third = len < 3 ? 0 : XFIXNUM (LGSTRING_CHAR (gstring, 2));
+  return second == 0x200D
+    && ! NILP (Fmemq (make_fixnum (third),
+		      Vauto_composition_emoji_unqualified_tail_initials));
+}
+
+
 /* 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 +1922,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 +5341,44 @@ 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_LISP ("auto-composition-emoji-unqualified-tail-initials", Vauto_composition_emoji_unqualified_tail_initials,
+    doc: /* List of characters that may begin the tail of an unqualified RGI sequence.
+
+An unqualified RGI sequence begins with a codepoint in
+`auto-composition-emoji-tty-eligible-codepoints', followed by a
+Zero-Width Joiner.  The first character after the first ZWJ must be in
+this list to be recognized as an Emoji sequence.
+
+This list is auto-generated, you should not need to modify it.  */);
+  Vauto_composition_emoji_unqualified_tail_initials = 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 frames, 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 223bb791fd2ed64b7f3706392f7c41510e5663db 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 d510b92d0f6..6770ba6e504 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-Use-the-gstring-width-as-the-width-of-an-automatic-c.patch (text/x-diff, 1.1 KB)
From 737a3115f70e8734b6a694a407975c6a65d0f8d7 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] Use the gstring width as the width of an automatic
 composition.

* src/composite.c (composition_update_it): Set the width of an automatic
composition using composition_gstring_width.
---
 src/composite.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/composite.c b/src/composite.c
index 55841d08cb5..b4706015c32 100644
--- a/src/composite.c
+++ b/src/composite.c
@@ -1535,12 +1535,12 @@ composition_update_it (struct composition_it *cmp_it, ptrdiff_t charpos, ptrdiff
       glyph = LGSTRING_GLYPH (gstring, cmp_it->from);
       cmp_it->nchars = LGLYPH_TO (glyph) + 1 - from;
       cmp_it->nbytes = 0;
-      cmp_it->width = 0;
+      cmp_it->width = composition_gstring_width (gstring, cmp_it->from, cmp_it->to, NULL);
+
       for (i = cmp_it->nchars - 1; i >= 0; i--)
 	{
 	  c = XFIXNUM (LGSTRING_CHAR (gstring, from + i));
 	  cmp_it->nbytes += CHAR_BYTES (c);
-	  cmp_it->width += CHARACTER_WIDTH (c);
 	}
     }
   return c;
-- 
2.53.0
0006-etc-NEWS-Document-improved-Emoji-support-on-textual-.patch (text/x-diff, 1.5 KB)
From 62d62c8ad3980bfad765103a6eeed4bae2962b0d 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 6f95672272b..66ef0e7dbb6 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -109,6 +109,25 @@ to your initialization file no longer apply, and you may remove it.
 If you still want to show the "*Completions*" buffer, then customize
 the user option 'icomplete-in-buffer' to the value 'with-completions-popup'.
 
+---
+** 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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.