Re: [PATCH] emoji on TUI emacs
Kai Ma <[email protected]> Sat, 08 Aug 2026 20:52:03 +0200
| Newsgroups | gmane.emacs.devel |
|---|---|
| Message-ID | <[email protected]> |
Sorry, forgot to attach the patches..
0001-Change-widths-of-East-Asian-Neutral-characters-to-1.patch
(text/x-diff, 1.8 KB)
From ace2aed26a39db49f7ad7982d1f4b45905216f7d Mon Sep 17 00:00:00 2001 From: Kai Ma <[email protected]> Date: Wed, 5 Aug 2026 00:56:18 +0200 Subject: [PATCH 1/4] 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.2 KB)
From 68d6ed74b3fb6c1c5769a1705382839cb5cfeab6 Mon Sep 17 00:00:00 2001 From: Kai Ma <[email protected]> Date: Sat, 8 Aug 2026 18:46:09 +0200 Subject: [PATCH 2/4] 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 | 29 ++++++++++++++++++++++++----- 3 files changed, 44 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..512bd0bdd7d 100644 --- 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))))) (put 'auto-composition-mode 'permanent-local t) @@ -936,6 +943,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.4 KB)
From 564a059d72e5b9910baacacf61f39ef82f58779f Mon Sep 17 00:00:00 2001 From: Kai Ma <[email protected]> Date: Thu, 30 Jul 2026 16:24:28 +0200 Subject: [PATCH 3/4] 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 | 160 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 118 insertions(+), 42 deletions(-) diff --git a/src/term.c b/src/term.c index 52ae4f61814..b574149d4f0 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); } @@ -5266,6 +5324,24 @@ syms_of_term (void) non-nil to enable this optimization. */); tty_cursor_movement_use_TAB_BS = 0; + 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. + +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. + +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 +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.8 KB)
From cdd561b30ab1b1b6278f902189ab54bc573d70ad Mon Sep 17 00:00:00 2001 From: Kai Ma <[email protected]> Date: Sat, 8 Aug 2026 19:42:31 +0200 Subject: [PATCH 4/4] 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 | 13 ++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lisp/composite.el b/lisp/composite.el index 512bd0bdd7d..8ac29bb71aa 100644 --- a/lisp/composite.el +++ b/lisp/composite.el @@ -952,7 +952,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..d3fd3bdb944 100644 --- a/src/character.c +++ b/src/character.c @@ -353,14 +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; Lisp_Object default_font, frame_font; -#endif eassert (precision <= 0 || (nchars && nbytes)); @@ -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; -- 2.53.0