mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Nicolas Pitre <nico@fluxnic.net>
To: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Jiri Slaby <jirislaby@kernel.org>,
	linux-serial@vger.kernel.org, linux-kernel@vger.kernel.org,
	Nicolas Pitre <npitre@baylibre.com>
Subject: [PATCH 2/3] vt: add DEC private mode 2027 to control VS16 widening
Date: Mon, 19 Oct 2026 17:16:16 -0400	[thread overview]
Message-ID: <76b7fb6634652a8670a4f03aa397f21ea57aa367.1792443833.git.npitre@baylibre.com> (raw)
In-Reply-To: <cover.1792443833.git.npitre@baylibre.com>

From: Nicolas Pitre <npitre@baylibre.com>

VS16 (U+FE0F) after a single-width character currently turns it into a
double-width one, giving emoji presentation sequences such as U+203C
U+FE0F (‼️) a square aspect.

There is no consensus on this among terminals. According to the
ucs-detect survey [1], these widen such sequences to 2 cells, as the
Linux console does:

  kitty, foot, Ghostty, Contour, Konsole, iTerm2, Windows Terminal,
  mintty, Rio, tmux (3.4)

while these keep them at 1 cell:

  xterm, VTE based terminals (GNOME Terminal, XFCE Terminal, ...),
  Alacritty, st, urxvt, Apple Terminal, GNU Screen, libvterm,
  xterm.js (WezTerm only partially widens)

On the application side, glibc's wcwidth() is per code point and
returns 0 for VS16, and so do the tools relying on it. Even vim, which
has its own emoji width table, counts U+203C U+FE0F as 1 column.

Applications that track the cursor position themselves therefore get
out of sync with a widening terminal. For example, mosh writes full
width rows and moves with relative CR/LF. A row containing ‼️ is one
column wider on the console than in mosh's model, so its last cell
wraps to the next line and the whole screen ends up scrolled by one.
Others are reconsidering widening by default too: tmux 3.6 added the
"variation-selector-always-wide" option to turn it off, and xterm's
emoji width handling is configurable.

DEC private mode 2027, from the Contour terminal's Unicode core
proposal [2], is meant to give applications this control. With it set,
VS16 promotes a grapheme cluster to emoji presentation and a width of 2
cells. With it reset, the behavior is left to the terminal. Contour,
Ghostty, foot and WezTerm implement it.

Make VS16 widening follow that mode. Applications, or the user before
starting them, can opt out with CSI ? 2027 l, in which case VS16 is
zero-width as wcwidth() says. Only the VS16 part of the proposal is
implemented here.

The initial and reset state of the mode comes from the new
vt.default_vs16_wide parameter, similar to vt.default_utf8. It
defaults to 1, preserving the existing behavior, and can be set to 0
on systems where applications relying on wcwidth() are the norm.

[1] https://ucs-detect.readthedocs.io/results.html
[2] https://github.com/contour-terminal/terminal-unicode-core

Assisted-by: LLM
Signed-off-by: Nicolas Pitre <npitre@baylibre.com>
---
 Documentation/admin-guide/kernel-parameters.txt | 10 ++++++++++
 drivers/tty/vt/vt.c                             | 15 ++++++++++++++-
 include/linux/console_struct.h                  |  1 +
 3 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index e75344f4e0cd..983afb6c531e 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -8451,6 +8451,16 @@ Kernel parameters
 			Default is 1, i.e. UTF-8 mode is enabled for all
 			newly opened terminals.
 
+	vt.default_vs16_wide=
+			[VT]
+			Format=<0|1>
+			Set system-wide default for whether VS16 (U+FE0F)
+			turns the preceding single-width character into a
+			double-width one, as in emoji presentation sequences.
+			This is the initial and reset state of DEC private
+			mode 2027 on all tty's, which applications can also
+			change with CSI ? 2027 h/l. Default is 1.
+
 	vt.global_cursor_default=
 			[VT]
 			Format=<-1|0|1>
diff --git a/drivers/tty/vt/vt.c b/drivers/tty/vt/vt.c
index 335253aec549..4d5c9cabd058 100644
--- a/drivers/tty/vt/vt.c
+++ b/drivers/tty/vt/vt.c
@@ -166,6 +166,9 @@ EXPORT_SYMBOL(global_cursor_default);
 static int cur_default = CUR_UNDERLINE;
 module_param(cur_default, int, S_IRUGO | S_IWUSR);
 
+static bool default_vs16_wide = true;
+module_param(default_vs16_wide, bool, 0644);
+
 /*
  * ignore_poke: don't unblank the screen when things are typed.  This is
  * mainly for the privacy of braille terminal users.
@@ -1984,6 +1987,7 @@ enum {
 	CSI_DEC_hl_MOUSE_VT200	= 1000,
 	CSI_DEC_hl_ALT_SCREEN	= 1049,
 	CSI_DEC_hl_BRACKETED_PASTE = 2004,
+	CSI_DEC_hl_GRAPHEME_WIDTH = 2027,	/* VS16 widening */
 };
 
 /* console_lock is held */
@@ -2039,6 +2043,9 @@ static void csi_DEC_hl(struct vc_data *vc, bool on_off)
 		case CSI_DEC_hl_BRACKETED_PASTE:
 			vc->vc_bracketed_paste = on_off;
 			break;
+		case CSI_DEC_hl_GRAPHEME_WIDTH:
+			vc->vc_vs16_wide = on_off;
+			break;
 		case CSI_DEC_hl_ALT_SCREEN:
 			if (on_off)
 				enter_alt_screen(vc);
@@ -2284,6 +2291,7 @@ static void reset_terminal(struct vc_data *vc, int do_clear)
 	vc->vc_decawm		= 1;
 	vc->vc_deccm		= global_cursor_default;
 	vc->vc_decim		= 0;
+	vc->vc_vs16_wide	= default_vs16_wide;
 
 	if (vc->vc_saved_screen != NULL) {
 		kfree(vc->vc_saved_screen);
@@ -3139,12 +3147,17 @@ static int vc_process_ucs(struct vc_data *vc, int *c, int *tc)
 
 	prev_c = vc_uniscr_getc(vc, -1);
 
-	if (curr_c == UCS_VS16 && prev_c != 0) {
+	if (curr_c == UCS_VS16 && prev_c != 0 && vc->vc_vs16_wide) {
 		/*
 		 * VS16 (U+FE0F) is special. It typically turns the preceding
 		 * single-width character into a double-width one. Let it
 		 * have a width of 1 effectively making the combination with
 		 * the preceding character double-width.
+		 *
+		 * This can be disabled with DEC private mode 2027 for
+		 * applications that compute widths per code point with
+		 * wcwidth(), where VS16 is always zero-width. Otherwise
+		 * they lose track of the cursor position.
 		 */
 		*tc = ' ';
 		return 1;
diff --git a/include/linux/console_struct.h b/include/linux/console_struct.h
index fe915afdece5..8a1031cd097e 100644
--- a/include/linux/console_struct.h
+++ b/include/linux/console_struct.h
@@ -167,6 +167,7 @@ struct vc_data {
 	unsigned int	vc_decawm	: 1;	/* Autowrap Mode */
 	unsigned int	vc_deccm	: 1;	/* Cursor Visible */
 	unsigned int	vc_decim	: 1;	/* Insert Mode */
+	unsigned int	vc_vs16_wide	: 1;	/* VS16 makes preceding char wide */
 	/* misc */
 	unsigned int	vc_priv		: 3;
 	unsigned int	vc_need_wrap	: 1;
-- 
2.55.0


  parent reply	other threads:[~2026-10-19 21:16 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19 21:16 [PATCH 0/3] vt: make VS16 emoji widening controllable Nicolas Pitre
2026-10-19 21:16 ` [PATCH 1/3] vt: don't make VS16 widening depend on earlier screen contents Nicolas Pitre
2026-10-19 21:16 ` Nicolas Pitre [this message]
2026-10-19 21:16 ` [PATCH 3/3] vt: add DECRQM support to report mode states Nicolas Pitre

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=76b7fb6634652a8670a4f03aa397f21ea57aa367.1792443833.git.npitre@baylibre.com \
    --to=nico@fluxnic.net \
    --cc=gregkh@linuxfoundation.org \
    --cc=jirislaby@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-serial@vger.kernel.org \
    --cc=npitre@baylibre.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®