Index | Thread | Search

From:
Kirill A. Korinsky <kirill@korins.ky>
Subject:
bin/ksh: series of patches which fixes wrong terminal width and UTF-8
To:
OpenBSD tech <tech@openbsd.org>
Date:
Tue, 07 Jul 2026 22:42:15 +0200

Download raw body.

Thread
tech@,

sorry to attached pathces, and not inlined, I think it makes it review
simpler.

I use that pathces for last half year in emacs mode and happy with this, but
I think we should fix that bugs and commit it.

It should be applied as sequence.

Anyone willing to test it? Ok it?

-- 
wbr, Kirill
From f2f236782d0f6ecfa15516512c31b7cd006e9091 Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Wed, 21 Jan 2026 01:16:04 +0100
Subject: [PATCH 1/3] bin/ksh: fix emacs/vi mode using wrong terminal width

xterm and other terminal emulators resize the pty after starting the
shell. x_init() queries the terminal size at startup before the resize
occurs, so x_cols stays at the default 80 columns.

SIGWINCH arrives, but check_sigwinch() isn't called until after command
entry. This makes the line editor think the screen is 80 columns, which
makes it show '<' and truncate input before the actual terminal edge on
large windows, and causes weird behavior on small ones.

Call check_sigwinch() when entering edit mode and on EINTR in x_getc().
The callback lets emacs/vi mode recalculate display width and redraw.

Regarding tests, the vi mode resize path is multi stage and can
interleave with pending input, so the byte stream varies even when the
final screen is correct; to make the test deterministic, edit.c waits
for two consecutive quiet poll timeouts to ensure the child is idle
before sending TIOCSWINSZ and SIGWINCH.

---
 bin/ksh/edit.c                | 14 ++++++--
 bin/ksh/edit.h                |  2 ++
 bin/ksh/emacs.c               | 57 ++++++++++++++++++++++++++++++++-
 bin/ksh/vi.c                  | 60 ++++++++++++++++++++++++++++++++---
 regress/bin/ksh/edit/edit.c   | 54 +++++++++++++++++++++++++++++--
 regress/bin/ksh/edit/emacs.sh | 24 ++++++++++++++
 regress/bin/ksh/edit/vi.sh    | 24 ++++++++++++++
 7 files changed, 223 insertions(+), 12 deletions(-)

diff --git bin/ksh/edit.c bin/ksh/edit.c
index a510b5f9915..09254c1e8c4 100644
--- bin/ksh/edit.c
+++ bin/ksh/edit.c
@@ -26,7 +26,8 @@ X_chars edchars;
 
 static void x_sigwinch(int);
 volatile sig_atomic_t got_sigwinch;
-static void check_sigwinch(void);
+void check_sigwinch(void);
+void (*x_resize_cb)(void);	/* callback for resize during edit */
 
 static int	x_file_glob(int, const char *, int, char ***, int);
 static char	*x_tilde_expand(const char *);
@@ -64,7 +65,7 @@ x_sigwinch(int sig)
 	got_sigwinch = 1;
 }
 
-static void
+void
 check_sigwinch(void)
 {
 	if (got_sigwinch) {
@@ -81,11 +82,16 @@ check_sigwinch(void)
 			 * see the change cause the environ doesn't change.
 			 */
 			if (ws.ws_col) {
+				int old_cols = x_cols;
+
 				x_cols = ws.ws_col < MIN_COLS ? MIN_COLS :
 				    ws.ws_col;
 
 				if ((vp = typeset("COLUMNS", 0, 0, 0, 0)))
 					setint(vp, (int64_t) ws.ws_col);
+
+				if (x_cols != old_cols && x_resize_cb)
+					(*x_resize_cb)();
 			}
 			if (ws.ws_row && (vp = typeset("LINES", 0, 0, 0, 0)))
 				setint(vp, (int64_t) ws.ws_row);
@@ -126,12 +132,14 @@ x_getc(void)
 	char c;
 	int n;
 
-	while ((n = blocking_read(STDIN_FILENO, &c, 1)) < 0 && errno == EINTR)
+	while ((n = blocking_read(STDIN_FILENO, &c, 1)) < 0 && errno == EINTR) {
+		check_sigwinch();
 		if (trap) {
 			x_mode(false);
 			runtraps(0);
 			x_mode(true);
 		}
+	}
 	if (n != 1)
 		return -1;
 	return (int) (unsigned char) c;
diff --git bin/ksh/edit.h bin/ksh/edit.h
index e1d200b9c93..e2978f3c7de 100644
--- bin/ksh/edit.h
+++ bin/ksh/edit.h
@@ -39,6 +39,8 @@ extern X_chars edchars;
 #define XCF_COMMAND_FILE (XCF_COMMAND|XCF_FILE)
 
 /* edit.c */
+void	check_sigwinch(void);
+extern void (*x_resize_cb)(void);
 int	x_getc(void);
 void	x_flush(void);
 int	x_putc(int);
diff --git bin/ksh/emacs.c bin/ksh/emacs.c
index 399ef59ce83..e3664df112f 100644
--- bin/ksh/emacs.c
+++ bin/ksh/emacs.c
@@ -105,6 +105,7 @@ static int	xlp_valid;
 /* end from 4.9 edit.h } */
 static	int	x_tty;		/* are we on a tty? */
 static	int	x_bind_quiet;	/* be quiet when binding keys */
+static void	x_emacs_resize(void);
 static int	(*x_last_command)(int);
 
 static	char   **x_histp;	/* history position */
@@ -286,6 +287,9 @@ x_emacs(char *buf, size_t len)
 	xmp = NULL;
 	x_histp = histptr + 1;
 
+	x_resize_cb = NULL;
+	check_sigwinch();
+	x_resize_cb = x_emacs_resize;
 	xx_cols = x_cols;
 	x_col = promptlen(prompt, &p);
 	prompt_skip = p - prompt;
@@ -316,8 +320,11 @@ x_emacs(char *buf, size_t len)
 	x_last_command = NULL;
 	while (1) {
 		x_flush();
-		if ((at = x_e_getu8(line, at)) < 0)
+		if ((at = x_e_getu8(line, at)) < 0) {
+			x_resize_cb = NULL;
 			return 0;
+		}
+
 		ntries++;
 
 		if (x_arg == -1) {
@@ -378,6 +385,7 @@ x_emacs(char *buf, size_t len)
 				x_last_command = NULL;
 			break;
 		case KEOL:
+			x_resize_cb = NULL;
 			ret = xep - xbuf;
 			return (ret);
 			break;
@@ -2166,4 +2174,51 @@ x_lastcp(void)
 	return (xlp);
 }
 
+static void
+x_emacs_resize(void)
+{
+	int cr_done = 0, cleared = 0;
+
+#ifndef SMALL
+	if (cur_term != NULL) {
+		if (carriage_return != NULL &&
+		    tputs(carriage_return, 1, x_putc) != ERR)
+			cr_done = 1;
+		if (clr_eos != NULL &&
+		    tputs(clr_eos, 1, x_putc) != ERR)
+			cleared = 1;
+	}
+#endif
+	if (!cr_done)
+		x_putc('\r');
+	if (!cleared) {
+		x_putc('\033');
+		x_putc('[');
+		x_putc('J');
+	}
+
+	xx_cols = x_cols;
+	x_col = promptlen(prompt, NULL);
+	if (x_col > xx_cols)
+		x_col = x_col - (x_col / xx_cols) * xx_cols;
+	x_displen = xx_cols - 2 - x_col;
+	if (x_displen < 1) {
+		x_col = 0;
+		x_displen = xx_cols - 2;
+		prompt_redraw = 0;
+	} else {
+		xbp = xbuf;
+		xlp_valid = false;
+		if (xcp <= x_lastcp()) {
+			x_redraw(0);
+			x_flush();
+			return;
+		}
+	}
+
+	x_col = 0;
+	x_displen = xx_cols - 2;
+	x_adjust();
+}
+
 #endif /* EMACS */
diff --git bin/ksh/vi.c bin/ksh/vi.c
index e386fa1e65f..9ac55404ade 100644
--- bin/ksh/vi.c
+++ bin/ksh/vi.c
@@ -75,6 +75,8 @@ static void	vi_error(void);
 static void	vi_macro_reset(void);
 static int	x_vi_putbuf(const char *, size_t);
 static int	isu8cont(unsigned char);
+static void	x_vi_resize(void);
+static void	calc_winsize(void);
 
 #define C_	0x1		/* a valid command that isn't a M_, E_, U_ */
 #define M_	0x2		/* movement command (h, l, etc.) */
@@ -200,6 +202,9 @@ x_vi(char *buf, size_t len)
 {
 	int	c;
 
+	x_resize_cb = NULL;
+	check_sigwinch();
+	x_resize_cb = x_vi_resize;
 	vi_reset(buf, len > LINE ? LINE : len);
 	vi_pprompt(1);
 	x_flush();
@@ -242,6 +247,7 @@ x_vi(char *buf, size_t len)
 		x_flush();
 	}
 
+	x_resize_cb = NULL;
 	x_putc('\r'); x_putc('\n'); x_flush();
 
 	if (c == -1 || len <= (size_t)es->linelen)
@@ -1425,8 +1431,6 @@ free_edstate(struct edstate *old)
 static void
 edit_reset(char *buf, size_t len)
 {
-	const char *p;
-
 	es = &ebuf;
 	es->cbuf = buf;
 	es->cbufsize = len;
@@ -1437,6 +1441,17 @@ edit_reset(char *buf, size_t len)
 	es->cursor = undo->cursor = 0;
 	es->winleft = undo->winleft = 0;
 
+	calc_winsize();
+	win = 0;
+	morec = ' ';
+	holdlen = 0;
+}
+
+static void
+calc_winsize(void)
+{
+	const char *p;
+
 	cur_col = pwidth = promptlen(prompt, &p);
 	prompt_skip = p - prompt;
 	if (pwidth > x_cols - 3 - MIN_EDIT_SPACE) {
@@ -1453,9 +1468,6 @@ edit_reset(char *buf, size_t len)
 	(void) memset(wbuf[0], ' ', wbuf_len);
 	(void) memset(wbuf[1], ' ', wbuf_len);
 	winwidth = x_cols - pwidth - 3;
-	win = 0;
-	morec = ' ';
-	holdlen = 0;
 }
 
 /*
@@ -2305,4 +2317,42 @@ isu8cont(unsigned char c)
 {
 	return !Flag(FVISHOW8) && (c & (0x80 | 0x40)) == 0x80;
 }
+
+static void
+x_vi_resize(void)
+{
+	int cur = 0, col = 0;
+	int cr_done = 0, cleared = 0;
+
+#ifndef SMALL
+	if (cur_term != NULL) {
+		if (carriage_return != NULL &&
+		    tputs(carriage_return, 1, x_putc) != ERR)
+			cr_done = 1;
+		if (clr_eos != NULL &&
+		    tputs(clr_eos, 1, x_putc) != ERR)
+			cleared = 1;
+	}
+#endif
+	if (!cr_done)
+		x_putc('\r');
+	if (!cleared) {
+		x_putc('\033');
+		x_putc('[');
+		x_putc('J');
+	}
+
+	calc_winsize();
+
+	while (cur < es->linelen)
+		col = newcol((unsigned char) es->cbuf[cur++], col);
+	if (col <= winwidth)
+		es->winleft = 0;
+	else if (outofwin())
+		rewindow();
+
+	redraw_line(0, 0);
+	refresh_line(insert != 0);
+	x_flush();
+}
 #endif	/* VI */
diff --git regress/bin/ksh/edit/edit.c regress/bin/ksh/edit/edit.c
index 7b49f371ab6..785dc5a251b 100644
--- regress/bin/ksh/edit/edit.c
+++ regress/bin/ksh/edit/edit.c
@@ -16,6 +16,7 @@
  */
 
 #include <sys/wait.h>
+#include <sys/ioctl.h>
 
 #include <err.h>
 #include <errno.h>
@@ -49,6 +50,7 @@ main(int argc, char *argv[])
 	ssize_t		 n;
 	size_t		 nin, nprompt, nread, nwrite;
 	int		 c, nready, ptyfd, readprompt, ret, status, timeout;
+	int		 pending_resize, pending_cols, pending_quiet, pending_post;
 
 	while ((c = getopt(argc, argv, "p:")) != -1) {
 		switch (c) {
@@ -96,14 +98,20 @@ main(int argc, char *argv[])
 	}
 
 	nprompt = nread = nwrite = ret = 0;
+	pending_resize = pending_cols = pending_quiet = pending_post = 0;
 	readprompt = 1;
 	while (!gotsig) {
 		pfd.fd = ptyfd;
-		if (!readprompt && nwrite < nin)
+		if (pending_resize || pending_post) {
+			pfd.events = POLLIN;
+			timeout = WRTIM;
+		} else if (!readprompt && nwrite < nin) {
 			pfd.events = POLLOUT;
-		else
+			timeout = WRTIM;
+		} else {
 			pfd.events = POLLIN;
-		timeout = readprompt ? PRTIM : WRTIM;
+			timeout = readprompt ? PRTIM : WRTIM;
+		}
 		nready = poll(&pfd, 1, timeout);
 		if (nready == -1) {
 			if (errno == EINTR)
@@ -111,6 +119,22 @@ main(int argc, char *argv[])
 			err(1, "poll");
 		}
 		if (nready == 0) {
+			if (pending_resize) {
+				if (++pending_quiet >= 2) {
+					ws.ws_col = pending_cols;
+					if (ioctl(ptyfd, TIOCSWINSZ, &ws) == -1)
+						err(1, "ioctl TIOCSWINSZ");
+					kill(pid, SIGWINCH);
+					pending_resize = pending_cols = 0;
+					pending_quiet = 0;
+					pending_post = 1;
+				}
+				continue;
+			}
+			if (pending_post) {
+				pending_post = 0;
+				continue;
+			}
 			if (timeout == PRTIM) {
 				warnx("timeout waiting from prompt");
 				ret = 1;
@@ -136,6 +160,28 @@ main(int argc, char *argv[])
 				readprompt = 0;
 			}
 		} else if (pfd.revents & POLLOUT) {
+			if (in[nwrite] == '\0' && nwrite + 3 < nin &&
+			    in[nwrite + 1] == 'R' &&
+			    in[nwrite + 2] == 'S' &&
+			    in[nwrite + 3] >= '0' && in[nwrite + 3] <= '9') {
+				size_t i = nwrite + 3;
+				int cols = 0, nd = 0;
+
+				while (i < nin && nd < 3 &&
+				    in[i] >= '0' && in[i] <= '9') {
+					cols = cols * 10 + (in[i] - '0');
+					i++;
+					nd++;
+				}
+				if (nd > 0 && (i == nin ||
+				    in[i] < '0' || in[i] > '9') &&
+				    cols > 0) {
+					pending_resize = 1;
+					pending_cols = cols;
+					nwrite = i;
+					continue;
+				}
+			}
 			if (strchr(linefeed, in[nwrite]) != NULL)
 				readprompt = 1;
 
@@ -143,6 +189,8 @@ main(int argc, char *argv[])
 			if (n == -1)
 				err(1, "write");
 			nwrite += n;
+			if (pending_resize)
+				pending_quiet = 0;
 		}
 	}
 	close(ptyfd);
diff --git regress/bin/ksh/edit/emacs.sh regress/bin/ksh/edit/emacs.sh
index abd664664bb..6ae4184e928 100644
--- regress/bin/ksh/edit/emacs.sh
+++ regress/bin/ksh/edit/emacs.sh
@@ -52,6 +52,30 @@ testseq "z\0002\0363\0277\0277\0277" " # z\b\0363\0277\0277\0277z\b"
 testseq "z\0002\0364\0200\0200\0200" " # z\b\0364\0200\0200\0200z\b"
 testseq "z\0002\0364\0217\0277\0277" " # z\b\0364\0217\0277\0277z\b"
 
+# ASCII resize: cols=1/11/12
+testseq "aaaaaaaaaabbbbbbbbbb\0001\000RS1" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J\r # aaaaaaa>\b\b\b\b\b\b\b\b"
+testseq "aaaaaaaaaabbbbbbbbbb\003310\0002\000RS11" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\r\033[J\raaaaabbbbb*\b\b\b\b\b\b"
+testseq "aaaaaaaaaabbbbbbbbbb\000RS12" \
+	" # aaaaaaaaaabbbbbbbbbb\r\033[J\rbbbbb     <\b\b\b\b\b\b"
+
+# ASCII resize: cols=13, cursor -1/0/+1
+testseq "aaaaaaaaaabbbbbbbbbb\003313\0002\000RS13" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J\r # aaaaaaaa>\b\b"
+testseq "aaaaaaaaaabbbbbbbbbb\003312\0002\000RS13" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J\r # aaaaaaaa>\b"
+testseq "aaaaaaaaaabbbbbbbbbb\003311\0002\000RS13" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\b\r\033[J\raaaaaabbbbb*\b\b\b\b\b\b\b"
+
+# ASCII resize: cols=79/80/81
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\0001\000RS79" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\rbbbbbbbbbbbbccccccccccccccccccccccccc                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J\r # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccc>\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\0001\000RS80" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\rbbbbbbbbbbbbccccccccccccccccccccccccc                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\0001\000RS81" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\rbbbbbbbbbbbbccccccccccccccccccccccccc                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J\r # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
 # insertion of incomplete UTF-8
 testseq "z\0002\0302\0006" " # z\b\0302z\bz"
 testseq "z\0002\0377\0006" " # z\b\0377z\bz"
diff --git regress/bin/ksh/edit/vi.sh regress/bin/ksh/edit/vi.sh
index d9ae7967908..eef5886f999 100644
--- regress/bin/ksh/edit/vi.sh
+++ regress/bin/ksh/edit/vi.sh
@@ -233,3 +233,27 @@ testseq "(x)\00330%hrc" " # (x)\b\b\b(x\bc\b"
 
 # ^R: Redraw.
 testseq "test\0033h\0022" " # test\b\b\r\r\n # test\b\b"
+
+# ASCII resize: cols=1/11/12
+testseq "aaaaaaaaaabbbbbbbbbb\0033hhhhhhhhhhhhhhhhhhh\000RS1" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J# aaaaaaa >\r# "
+testseq "aaaaaaaaaabbbbbbbbbb\0033hhhhhhhhh\000RS11" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\r\033[J# aaabbbb +\b\b\b\b\b\b"
+testseq "aaaaaaaaaabbbbbbbbbb\0033\000RS12" \
+	" # aaaaaaaaaabbbbbbbbbb\b\r\033[J# bbbb    <\b\b\b\b\b\b"
+
+# ASCII resize: cols=13, cursor -1/0/+1
+testseq "aaaaaaaaaabbbbbbbbbb\0033hhhhhhhhhhhhh\000RS13" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J # aaaaaaa >\b\b\b"
+testseq "aaaaaaaaaabbbbbbbbbb\0033hhhhhhhhhhhh\000RS13" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J # aaaaaab +\b\b\b\b\b\b"
+testseq "aaaaaaaaaabbbbbbbbbb\0033hhhhhhhhhhh\000RS13" \
+	" # aaaaaaaaaabbbbbbbbbb\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J # aaaaabb +\b\b\b\b\b\b"
+
+# ASCII resize: cols=79/80/81
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\0033hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\000RS79" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc\r # bbbbbbbbbbbbbcccccccccccccccccccccccc                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bc\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\baaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccc >\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc >\r # "
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\0033hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\000RS80" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc\r # bbbbbbbbbbbbbcccccccccccccccccccccccc                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bc\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\baaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccc >\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\0033hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\000RS81" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc\r # bbbbbbbbbbbbbcccccccccccccccccccccccc                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bc\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\baaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccc >\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc >\r # "
-- 
2.54.0

From 6a24f1c027c1e3937ed3fa91662661f9637dafce Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Wed, 21 Jan 2026 17:05:09 +0100
Subject: [PATCH 2/3] bin/ksh: fix emacs mode UTF-8 display logic

The emacs line editor counts bytes when deciding what fits on screen and
can stop in the middle of a UTF-8 sequence. This leaves partial UTF-8
sequences on output, misplaces the "<" indicator, and can desync cursor
movement after a resize.

Track display columns instead of bytes by skipping UTF-8 continuation
bytes when positioning and redrawing. Add x_zotcp() to emit buffer bytes
without splitting UTF-8 sequences, and use it in cursor motion and
transpose paths.
---
 bin/ksh/emacs.c               | 81 ++++++++++++++++++++++++++++-------
 regress/bin/ksh/edit/emacs.sh | 32 ++++++++++++++
 2 files changed, 97 insertions(+), 16 deletions(-)

diff --git bin/ksh/emacs.c bin/ksh/emacs.c
index e3664df112f..d8b86c2851e 100644
--- bin/ksh/emacs.c
+++ bin/ksh/emacs.c
@@ -130,6 +130,7 @@ static int	x_size_str(char *);
 static int	x_size(int);
 static void	x_zots(char *);
 static void	x_zotc(int);
+static void	x_zotcp(char *);
 static void	x_load_hist(char **);
 static int	x_search(char *, int, int);
 static int	x_match(char *, char *);
@@ -643,16 +644,17 @@ x_fword(void)
 static void
 x_goto(char *cp)
 {
-	if (cp < xbp || cp >= (xbp + x_displen)) {
+	x_lastcp();
+	if (cp < xbp || cp > xlp) {
 		/* we are heading off screen */
 		xcp = cp;
 		x_adjust();
 	} else if (cp < xcp) {		/* move back */
 		while (cp < xcp)
-			x_bs((unsigned char)*--xcp);
+			x_bs(*--xcp);
 	} else if (cp > xcp) {		/* move forward */
 		while (cp > xcp)
-			x_zotc((unsigned char)*xcp++);
+			x_zotcp(xcp++);
 	}
 }
 
@@ -671,7 +673,7 @@ x_size_str(char *cp)
 {
 	int size = 0;
 	while (*cp)
-		size += x_size(*cp++);
+		size += x_size((unsigned char)*cp++);
 	return size;
 }
 
@@ -699,7 +701,7 @@ x_zots(char *str)
 	}
 	x_lastcp();
 	while (*str && str < xlp && adj == x_adj_done)
-		x_zotc(*str++);
+		x_zotcp(str++);
 }
 
 static void
@@ -715,6 +717,39 @@ x_zotc(int c)
 		x_e_putc(c);
 }
 
+static void
+x_zotcp(char *p)
+{
+	unsigned char uc = (unsigned char)*p;
+
+	if (uc == '\t') {
+		/*  Kludge, tabs are always four spaces.  */
+		x_e_puts("    ");
+		return;
+	}
+	if (isu8cont(uc)) {
+		if (x_col <= xx_cols) {
+			x_putc(uc);
+		}
+		if (x_adj_ok && !isu8cont((unsigned char)p[1]) &&
+		    (x_col < 0 || x_col >= (xx_cols - 2)))
+			x_adjust();
+		return;
+	}
+	if (iscntrl(uc)) {
+		x_e_putc('^');
+		x_e_putc(UNCTRL(uc));
+	} else {
+		if (x_col < xx_cols) {
+			x_putc(uc);
+			x_col++;
+		}
+		if (x_adj_ok && !isu8cont((unsigned char)p[1]) &&
+		    (x_col < 0 || x_col >= (xx_cols - 2)))
+			x_adjust();
+	}
+}
+
 static int
 x_mv_back(int c)
 {
@@ -1034,7 +1069,7 @@ x_clear_screen(int c)
 static void
 x_redraw(int limit)
 {
-	int	i, j, truncate = 0;
+	int	i, j, truncate = 0, dcols;
 	char	*cp;
 
 	x_adj_ok = 0;
@@ -1076,10 +1111,12 @@ x_redraw(int limit)
 	if (xbp != xbuf || xep > xlp)
 		limit = xx_cols;
 	if (limit >= 0) {
-		if (xep > xlp)
-			i = 0;			/* we fill the line */
-		else
-			i = limit - (xlp - xbp);
+		dcols = 0;
+		for (cp = xbp; cp < xlp; cp++)
+			dcols += x_size((unsigned char)*cp);
+		i = limit - dcols;
+		if (i < 0)
+			i = 0;
 
 		for (j = 0; j < i && x_col < (xx_cols - 2); j++)
 			x_e_putc(' ');
@@ -1136,8 +1173,8 @@ x_transpose(int c)
 		 */
 		x_bs(xcp[-1]);
 		x_bs(xcp[-2]);
-		x_zotc(xcp[-1]);
-		x_zotc(xcp[-2]);
+		x_zotcp(&xcp[-1]);
+		x_zotcp(&xcp[-2]);
 		tmp = xcp[-1];
 		xcp[-1] = xcp[-2];
 		xcp[-2] = tmp;
@@ -1146,8 +1183,8 @@ x_transpose(int c)
 		 * cursor, move cursor position along one.
 		 */
 		x_bs(xcp[-1]);
-		x_zotc(xcp[0]);
-		x_zotc(xcp[-1]);
+		x_zotcp(&xcp[0]);
+		x_zotcp(&xcp[-1]);
 		tmp = xcp[-1];
 		xcp[-1] = xcp[0];
 		xcp[0] = tmp;
@@ -1810,12 +1847,22 @@ do_complete(int flags,	/* XCF_{COMMAND,FILE,COMMAND_FILE} */
 static void
 x_adjust(void)
 {
+	char *cp;
+	int col;
+
 	x_adj_done++;			/* flag the fact that we were called. */
 	/*
 	 * we had a problem if the prompt length > xx_cols / 2
 	 */
-	if ((xbp = xcp - (x_displen / 2)) < xbuf)
-		xbp = xbuf;
+	col = x_displen / 2;
+	cp = xcp;
+	while (cp > xbuf && col > 0) {
+		cp--;
+		while (cp > xbuf && isu8cont(*cp))
+			cp--;
+		col -= x_size((unsigned char)*cp);
+	}
+	xbp = cp;
 	xlp_valid = false;
 	x_redraw(xx_cols);
 	x_flush();
@@ -2169,6 +2216,8 @@ x_lastcp(void)
 		for (i = 0, rcp = xbp; rcp < xep && i < x_displen; rcp++)
 			i += x_size((unsigned char)*rcp);
 		xlp = rcp;
+		while (xlp < xep && isu8cont(*xlp))
+			xlp++;
 	}
 	xlp_valid = true;
 	return (xlp);
diff --git regress/bin/ksh/edit/emacs.sh regress/bin/ksh/edit/emacs.sh
index 6ae4184e928..736d54f0616 100644
--- regress/bin/ksh/edit/emacs.sh
+++ regress/bin/ksh/edit/emacs.sh
@@ -76,6 +76,38 @@ testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccc
 testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\0001\000RS81" \
 	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\rbbbbbbbbbbbbccccccccccccccccccccccccc                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J\r # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
 
+# ASCII window indicator at left edge
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\rbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# UTF-8 window indicator at left edge
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# ASCII redraw at right edge
+PS1='012345678901234567890123456789012345678901234567890123456789'
+testseq "#aaaaaaaaaaaaaaaaab\0033#" "012345678901234567890123456789012345678901234567890123456789#aaaaaaaaaaaaaaaaa\raaaaaaaaa                                                                     <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bb\r012345678901234567890123456789012345678901234567890123456789aaaaaaaaaaaaaaaaab \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# UTF-8 redraw at right edge
+testseq "#aaaaaaaaaaaaaaaaa\0303\0266\0033#" "012345678901234567890123456789012345678901234567890123456789#aaaaaaaaaaaaaaaaa\raaaaaaaaa                                                                     <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\0303\0266\r012345678901234567890123456789012345678901234567890123456789aaaaaaaaaaaaaaaaa\0303\0266 \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+PS1=' # '
+
+# ASCII window indicator at right edge
+testseq "\003342ab\003397b\0001" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\raaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\rbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                       <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbbbbbbbbbbbbbbbbbbbbbbbb\r # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb>\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# UTF-8 window indicator at right edge
+testseq "\003342a\0303\0266\003397b\0001" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\raaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\rbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                       <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbbbbbbbbbbbbbbbbbbbbbbbb\r # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb>\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# ASCII window indicator on both sides
+testseq "\003342ab\003397b\0001\003379\0006" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\raaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\rbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                       <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbbbbbbbbbbbbbbbbbbbbbbbb\r # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb>\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\rbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb*\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# UTF-8 window indicator on both sides
+testseq "\003342a\0303\0266\003397b\0001\003379\0006" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\raaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                         <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\rbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                       <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbbbbbbbbbbbbbbbbbbbbbbbb\r # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb>\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\ra\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb*\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
 # insertion of incomplete UTF-8
 testseq "z\0002\0302\0006" " # z\b\0302z\bz"
 testseq "z\0002\0377\0006" " # z\b\0377z\bz"
-- 
2.54.0

From 0832ae0c5c91a818cf4a7004cdde8b02ba7a8e9a Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Thu, 22 Jan 2026 16:54:45 +0100
Subject: [PATCH 3/3] bin/ksh: fix vi mode UTF-8 window display

The vi line editor sizes its window buffers by column count and stops
rendering at the right edge. With UTF-8 input, a continuation byte can
land at the edge, producing garbage like "\\xb9<" and misplacing the
indicator; small-window resizes can also overrun the window buffer.

Size the window buffers for UTF-8 and allow trailing continuation bytes
at the right edge. When a trailing UTF-8 byte is present, force the
indicator to print as a space plus the marker in the usual position.
---
 bin/ksh/vi.c               | 26 +++++++++++++++++++-------
 regress/bin/ksh/edit/vi.sh | 32 ++++++++++++++++++++++++++++++++
 2 files changed, 51 insertions(+), 7 deletions(-)

diff --git bin/ksh/vi.c bin/ksh/vi.c
index 9ac55404ade..edea59b40ff 100644
--- bin/ksh/vi.c
+++ bin/ksh/vi.c
@@ -24,6 +24,7 @@
 
 #undef CTRL
 #define	CTRL(x)		((x) & 0x1F)	/* ASCII */
+#define UTF8_MAXBYTES	4
 
 struct edstate {
 	char	*cbuf;		/* main buffer to build the command line */
@@ -1371,7 +1372,7 @@ static int	prompt_trunc;		/* how much of prompt to truncate */
 static int	prompt_skip;		/* how much of prompt to skip */
 static int	winwidth;		/* available column positions */
 static char	*wbuf[2];		/* current & previous window buffer */
-static int	wbuf_len;		/* length of window buffers (x_cols-3)*/
+static int	wbuf_len;		/* length of window buffers */
 static int	win;			/* number of window buffer in use */
 static char	morec;			/* more character at right of window */
 static char	holdbuf[LINE];		/* place to hold last edit buffer */
@@ -1451,6 +1452,7 @@ static void
 calc_winsize(void)
 {
 	const char *p;
+	int newlen;
 
 	cur_col = pwidth = promptlen(prompt, &p);
 	prompt_skip = p - prompt;
@@ -1460,8 +1462,9 @@ calc_winsize(void)
 		pwidth -= prompt_trunc;
 	} else
 		prompt_trunc = 0;
-	if (!wbuf_len || wbuf_len != x_cols - 3) {
-		wbuf_len = x_cols - 3;
+	newlen = (x_cols - 3) * UTF8_MAXBYTES;
+	if (!wbuf_len || wbuf_len != newlen) {
+		wbuf_len = newlen;
 		wbuf[0] = aresize(wbuf[0], wbuf_len, APERM);
 		wbuf[1] = aresize(wbuf[1], wbuf_len, APERM);
 	}
@@ -1884,6 +1887,7 @@ display(char *wb1, char *wb2, int leftside)
 	int	 ncol;	/* display column of the cursor */
 	int	 cnt;	/* remaining display columns to fill */
 	int	 moreright;
+	int	 trailu8;
 	char	 mc;	/* new "more character" at the right of window */
 	unsigned char ch;
 
@@ -1895,8 +1899,11 @@ display(char *wb1, char *wb2, int leftside)
 	ncol = col = 0;
 	cur = es->winleft;
 	moreright = 0;
+	trailu8 = 0;
 	twb1 = wb1;
-	while (col < winwidth && cur < es->linelen) {
+	while (cur < es->linelen &&
+	    (col < winwidth ||
+	    (col == winwidth && isu8cont(es->cbuf[cur])))) {
 		if (cur == es->cursor && leftside)
 			ncol = col + pwidth;
 		if ((ch = es->cbuf[cur]) == '\t') {
@@ -1912,7 +1919,8 @@ display(char *wb1, char *wb2, int leftside)
 				}
 				ch &= 0x7f;
 			}
-			if (col < winwidth) {
+			if (col < winwidth ||
+			    (col == winwidth && isu8cont(ch))) {
 				if (ch < ' ' || ch == 0x7f) {
 					*twb1++ = '^';
 					if (++col < winwidth) {
@@ -1920,6 +1928,8 @@ display(char *wb1, char *wb2, int leftside)
 						col++;
 					}
 				} else {
+					if (col == winwidth && isu8cont(ch))
+						trailu8 = 1;
 					*twb1++ = ch;
 					if (col == 0 || !isu8cont(ch))
 						col++;
@@ -2012,8 +2022,10 @@ display(char *wb1, char *wb2, int leftside)
 		mc = '>';
 	else
 		mc = ' ';
-	if (mc != morec) {
-		ed_mov_opt(pwidth + winwidth + 1, wb1);
+	if (mc != morec || trailu8) {
+		ed_mov_opt(pwidth + winwidth, wb1);
+		x_putc(' ');
+		cur_col++;
 		x_putc(mc);
 		cur_col++;
 		morec = mc;
diff --git regress/bin/ksh/edit/vi.sh regress/bin/ksh/edit/vi.sh
index eef5886f999..dd4b9719ce7 100644
--- regress/bin/ksh/edit/vi.sh
+++ regress/bin/ksh/edit/vi.sh
@@ -257,3 +257,35 @@ testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccc
 	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc\r # bbbbbbbbbbbbbcccccccccccccccccccccccc                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bc\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\baaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccc >\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
 testseq "aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc\0033hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh\000RS81" \
 	" # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc\r # bbbbbbbbbbbbbcccccccccccccccccccccccc                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bc\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\baaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccccccc >\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r\033[J # aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccc >\r # "
+
+# ASCII window indicator at left edge
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbb" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbb\r # aaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbb                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# UTF-8 window indicator at left edge
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbb" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbb\r # aaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbb                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# ASCII window indicator at right edge
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb\00330" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbb\b\baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >\r # "
+
+# UTF-8 window indicator at right edge
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266b\00330" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\0303\0266b\b\baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >\r # "
+
+# ASCII window indicator on both sides
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\003340h" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\baaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r # aaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                    \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbb +\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# UTF-8 window indicator on both sides
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\003340h" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\baaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbb\r # aaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb                                    \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266bbbbbbbbbbbbbbbbbbbbbbbbbb +\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
+
+# ASCII redraw at right edge
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb\00330\0022" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\bbb\b\baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >\r # \r\r\n # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >\r # "
+
+# UTF-8 redraw at right edge
+testseq "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\0303\0266b\00330\0022" \
+	" # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b                                      <\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\0303\0266b\b\baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >\r # \r\r\n # aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >\r # "
-- 
2.54.0