Re: [PATCH] emoji on TUI emacs

Kai Ma <[email protected]>
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
Gentle ping.

(For convenience, I re-attached the current patches in this mail.)

> Kai Ma <[email protected]> writes:
>
>> 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.
>
> Apparently, this is incorrect, and it crashes for me occasionally
> because nglyphs != nchars in general.
>
> Here is a new patch that supersedes the wrong one.  It reuses
> 'composition_gstring_width' as cmp_it->width for automatic compositions.
0001-Change-widths-of-East-Asian-Neutral-characters-to-1.patch (text/x-diff, 1.8 KB)
From 5648bddae136121f40b1532635952faf1097167b 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 ddab6b65acbb7160250fd07ecfe9a11ee92f79f1 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..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, 8.8 KB)
From 91e3571a15a435e11d980711e9e948e0f7264e87 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..fe2e9912866 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 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 f8a43e68efd724cd9aa7c762d39e0edb69dac62c 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 a3b8f8b991b5fcd5ec9a8323ac9946624e058dd2 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.4 KB)
From d3fdd855f1a204a3f06b1c0b86faf40ae9e5009b 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 c0656510677..263ffb1166b 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
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.