Re: [PATCH] emoji on TUI emacs

Eli Zaretskii <[email protected]> Sat, 08 Aug 2026 16:34:46 +0300
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
> Date: Wed, 5 Aug 2026 04:45:27 +0200
> Cc: [email protected]
> From: Kai Ma <[email protected]>
> 
> Attached are the three patches that make textual Emacs work with emoji 
> on modern terminal emulators.

Thanks.  Please find a few comments to the patch below.

> For your enjoyment, a smaller file containing many different kinds of 
> emoji sequences is also attached.
> 
> First, the test results. So far, there are 3 main issues with Emoji 
> support in the textual terminals.
> 
> * Issue 1: Keycap sequences like 3️⃣ (#x33 #xfe0f #x20e3) are not 
> supported. They are explicitly excluded in emoji-zwj.awk: "FIXME: add 
> support for Emoji_Keycap_Sequence once we learn how to respect 
> FE0F/VS-16 for ASCII characters." But I'm not very sure what is missing. 
> Could someone enlighten me here?

See the (very long) discussion in bug#39799.  The problem here is that
Emacs can only support a composition of a sequence of characters if
all of those characters have a glyph in the same font used for all of
them.  But ASCII characters are always displayed by the default face's
font, which is normally not an Emoji font, and so doesn't usually
support VS-16.  So compositions that begin with an ASCII codepoint
don't work.

However, this doesn't have to affect TTY frames, see below.

> * Issue 2: Bare-bones emoji modifiers (like 🏻 #x1f3fb) are composed 
> onto the preceding space characters by some terminal emulators, but 
> Emacs does not compose them. I think Emacs is correct here, base #x20 is 
> not an emoji modifier base.

Right.

> * Issue 3: Some terminal emulator's Unicode support is not up to date, 
> and thus calculates incorrect widths for new emojis. Only Kitty supports 
> Unicode 17, the version that Emacs supports.
> 
> IMO all of them should appear very rare in real texts. But Issue 1 in 
> principle could be fixed. Issue 2 and 3 are not Emacs problems.

Right.

> (BTW, during testing I find some East_Asian_Neutral characters misalign 
> because Emacs marks them as wide. I'm not sure about know the rationale. 
> Patch 2 removes them so they get width 1, in accordance to the Unicode 
> standard. But perhaps there's some background I'm missing?)

We generally go by the data in the file EastAsianWidth.txt, but at
least #x2690..#x2692 look almost double-width on my system.  Isn't
that what you see?

> A follow-up question is: should we make string-width recognize emoji 
> sequences and return 2 instead of 1? Or at least update org-string-width.

string-width considers compositions, so if it produces results
different from the actual display, it means composition_gstring_width
needs to be fixed, IMO.

> +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))
> +    {
> +      Lisp_Object tail = Vauto_composition_emoji_eligible_codepoints;
> +
> +      FOR_EACH_TAIL (tail)
>  	{
> -	  glyph->resolved_level = 0;
> -	  glyph->bidi_type = UNKNOWN_BT;
> +	  Lisp_Object item = XCAR (tail);
> +	  if (FIXNUMP (item) && XFIXNUM (item) == first)
> +	    return true;

Any reason not to use Fmember here?

Also, I think we should have a separate variable for the TTY case, see
below.

> +  DEFVAR_BOOL ("tty-display-emoji-force-wide", tty_display_emoji_force_wide,
> +    doc: /* Whether TTY frames should always render emoji sequences as
> +wide glyphs.

The first line of a doc string should always be a single complete
sentence.  Also, "render" is not accurate here (it's the terminal that
renders them).  So I suggest

  Whether Emoji sequences on TTY frames should always be considered wide.

> +When nil, emoji sequences whose first character's width is 1 may be
> +rendered as a narrow glyph to be compatible with some non-compliant
   ^^^^^^^^
Again, "considered"

> +  tty_display_emoji_force_wide = 1;

Please use 'true' here instead of 1.

> --- 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)

See comments above.

> --- 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

This should perhaps be reconsidered according to comments below.

> --- a/admin/unidata/emoji-zwj.awk
> +++ b/admin/unidata/emoji-zwj.awk
> @@ -60,29 +60,22 @@
>      vec[elts[1]] = vec[elts[1]] "\""
>  }
>  
> +# The following codepoints are not emoji, but they are part of
> +# emoji sequences.  We have code in font.c:font_range that will
> +# try to display those sequences with the emoji font anyway.
> +# Generated from emoji-variation-sequences.txt.
> +/^[0-9A-F]+ FE0F *; *emoji style;/ {
> +    trigger_codepoints[++ntriggers] = $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"
>       print "(eval-when-compile (require 'regexp-opt))"
>  
> -     # The following codepoints are not emoji, but they are part of
> -     # emoji sequences.  We have code in font.c:font_range that will
> -     # try to display those sequences with the emoji font anyway.
> -
> -     trigger_codepoints[1] = "261D"
> -     trigger_codepoints[2] = "26F9"
> -     trigger_codepoints[3] = "270C"
> -     trigger_codepoints[4] = "270D"
> -     trigger_codepoints[5] = "2764"
> -     trigger_codepoints[6] = "1F3CB"
> -     trigger_codepoints[7] = "1F3CC"
> -     trigger_codepoints[8] = "1F3F3"
> -     trigger_codepoints[9] = "1F3F4"
> -     trigger_codepoints[10] = "1F441"
> -     trigger_codepoints[11] = "1F574"
> -     trigger_codepoints[12] = "1F575"
> -     trigger_codepoints[13] = "1F590"
> -     trigger_codepoints[14] = "20E3"
> +     # Keycap combining
> +     trigger_codepoints[++ntriggers] = "20E3"  # COMBINING ENCLOSING KEYCAP
>  
>       print "(setq auto-composition-emoji-eligible-codepoints"
>       print "'("
> @@ -114,7 +107,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-emoji-gstring)))))"

I think we should leave auto-composition-emoji-eligible-codepoints
alone, and introduce a separate variable for TTY frames.  The reason
is a bit subtle, and is again related to the way Emacs selects fonts
for non-ASCII characters on GUI frames.  Since we want the codepoints
hard-coded above to be shown as their normal non-Emoji glyphs,
preferably using the default face's font, we cannot add them to the
'emoji' script, thus forcing them to be displayed by fonts used for
Emoji.  But OTOH, when they are followed by VS-16, we must force Emacs
to use the Emoji font for them, or else there will be no composition.
The comment above tells you that we have a trick in the code for that.

But all of this is not relevant for TTY frames, because we don't care
about the fonts, which are the responsibility of the terminal.  So I
think we should have a separate variable which lists all of the
codepoints, but used only in term.c, so as not to inadvertently affect
GUI frames.  For TTY frames, we should simply have there all the
codepoints in emoji-variation-sequences.txt.  Does this make sense?

> -     print "                                           #'compose-gstring-for-graphic))))"
> +     print "                                           #'compose-emoji-gstring))))"

The name of the function compose-emoji-gstring is somewhat misleading.
I'd prefer something like compose-gstring-and-emoji instead.

> +(defvar auto-compose-emoji-for-terminal t
> +  "Enable auto composition of Emoji sequences on text terminals.

Once again, this is not about enabling such composition.  This is
about Emacs being in sync with what the terminal actually does with
Emoji sequences of more than one codepoint.  A non-nil value makes
Emacs assume the terminal composes them.

And I think when the user sets this to nil, he/she should also disable
auto-mode, right?  Or at least to consider that?

> +(defun compose-emoji-gstring (gstring _direction)
> +  "Compose Emoji sequences into a grapheme cluster.
> +This function is redirected to `compose-glyph-for-graphic' by
> +`auto-compose-chars' on graphical display."

There's something wrong with the last sentence.  "Redirected to" is
incorrect, I think, and there's no such thing as
compose-glyph-for-graphic.