Re: [PATCH] emoji on TUI emacs

Eli Zaretskii <[email protected]> Sun, 02 Aug 2026 09:49:20 +0300
Newsgroups gmane.emacs.devel
Message-ID <[email protected]>
> Date: Sun, 2 Aug 2026 02:46:51 +0200
> Cc: [email protected]
> From: Kai Ma <[email protected]>
> 
> >> - string-width=2 and has fe0f: 553
> > And here, does the first codepoint always produce 2 as
> > CHARACTER_WIDTH?
> 
> Unfortunately, no. I found 11 exceptions, and all of them have ZWJ or 
> Emoji_Modifier. For example, ⛹️‍♀️ (string-width=2):
> 
> - U+26F9 PERSON WITH BALL (character width = 1!)
> - U+FE0F VS16
> - U+200D ZWJ
> - U+2640 FEMALE SIGN
> - U+FE0F VS16
> 
> Currently on TUI Emacs it's composed as two composite glyphs [ 26f9 vs16 
> zwj ] [ 2640 fe0f ] (two glyphs), so the whole sequence has string-width=2.

Yes, so this is a separate problem.  What about the sequences where
Emacs does decide to compose: does the first codepoint always produce
2 as CHARACTER_WIDTH in those cases?

> BTW, I experimentally hacked composite.el to compose ZWJ and emoji 
> modifiers, and the result is quite promising -- TUI Emacs works almost 
> always correctly on my emoji catalog file using a modern terminal 
> emulator. I'll test more and send a separate follow-up patch soon.

This should be conditioned by a variable that users can control,
because not all terminals do this.  But yes, when the terminal does do
it, changing compose-gstring-for-terminal in a way that combines them
due to VS-16 is probably a good idea.

> >> --- a/src/term.c
> >> +++ b/src/term.c
> >> @@ -567,6 +567,13 @@ encode_terminal_code (struct glyph *src, int src_len,
> >>     nchars = 0;
> >>     while (src < src_end)
> >>       {
> >> +      /* We must skip glyphs to be padded for a wide character.  */
> >> +      if (CHAR_GLYPH_PADDING_P (*src))
> >> +	{
> >> +	  src++;
> >> +	  continue;
> >> +	}
> >> +
> >>         if (src->type == COMPOSITE_GLYPH)
> >>   	{
> >>   	  struct composition *cmp;
> >> @@ -632,8 +639,7 @@ encode_terminal_code (struct glyph *src, int src_len,
> >>   		nchars++;
> >>   	      }
> >>   	}
> >> -      /* We must skip glyphs to be padded for a wide character.  */
> >> -      else if (! CHAR_GLYPH_PADDING_P (*src))
> >> +      else
> >>   	{
> > Is this part really needed?  AFAICT, it doesn't change anything,
> > because the loop already unconditionally increments 'src', for the
> > padding and non-padding cases alike.  Or what am I missing?
> 
> The padding glyphs constructed for COMPOSITE_GLYPH also has type 
> COMPOSITE_GLYPH, so we have to check padding_p before that branch.

Then if we just change the condition to

  if (src->type == COMPOSITE_GLYPH && ! CHAR_GLYPH_PADDING_P (*src))

then all the rest of the code can be left alone, right?

> I also changed 'while' to 'for' to make the code look better.

I'd prefer to leave the 'while' alone.  Unnecessary code churn makes
it harder to track evolution of code as part of code forensics in the
future.

> > Finally, there's one problem I see after applying the patch:
> > displaying sequences like "\x2699\xfe0f" produce incorrect cursor
> > movement, because Emacs thinks the resulting Emoji is 1-column wide,
> > whereas what the terminal here shows is a 2-column Emoji.  So maybe we
> > need a special treatment for Emoji produced by appending #xFE0F to a
> > 1-column character: consider that to produce a 2-column Emoji.  WDYT?
> 
> So yes, this approach is actually more standard-compliant. The revised 
> patch uses this approach. The current version uses a simple heuristic 
> that checks whether the second glyph is Emoji_Modifier or VS16. I added 
> Emoji_Modifier because ⛹🏻‍♀️ is one example where the second character 
> is not FE0F -- it is [26F9, 1F3FB, 200D, 2640, FE0F].
> 
> Some terminal emulators (e.g. WezTerm) show them using only 1 column, 
> but now I think that's their problem. :)

We should probably have a Lisp variable for this heuristics, so users
could tweak that according to what the terminal does or doesn't do in
these cases.

> +/* Check if the glyph string is an emoji.  In that case, force it to be
> +   shown as wide.  */
> +
> +static bool
> +composite_glyph_is_emoji (struct it *it)
> +{
> +  Lisp_Object gstring, glyph;
> +  int c;
> +
> +  /* 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 >= LGSTRING_GLYPH_LEN (gstring))
> +    return false;
> +
> +  /* Check if the second character is VS16 or an emoji modifier.  */
> +  glyph = LGSTRING_GLYPH (gstring, it->cmp_it.from + 1);
> +  if (! glyph)
> +    return false;
> +  c = LGLYPH_CHAR (glyph);
> +  return (c == 0xFE0F || (0x1F3FB <= c && c <= 0x1F3FF));
> +}

IMO, this function should also look at the character before VS-16.
Not every codepoint produces an Emoji when followed by VS-16, only
characters in the 0x2XXX and 0x303X blocks.  For better accuracy,
maybe we should have a variable similar to
auto-composition-emoji-eligible-codepoints, computed from
admin/unidata/emoji-*.txt files, which will provide the codepoints
that will display as Emoji when followed by VS-16.