Index | Thread | Search

From:
Kirill A. Korinsky <kirill@korins.ky>
Subject:
Re: bin/ksh: fix emacs/vi line editor getting wrong terminal width
To:
Helg <helg-openbsd@gmx.de>
Cc:
tech@openbsd.org
Date:
Wed, 21 Jan 2026 17:59:35 +0100

Download raw body.

Thread
On Tue, 20 Jan 2026 22:16:28 +0100,
Helg <helg-openbsd@gmx.de> wrote:
> 
> It works in both emacs and vi mode.
> 
> However, in testing this, I've found another bug.
> 
> I've never looked too closely at this but here's how I understand
> it should work. There are 3 possibilities when an indicator should
> be displayed because some text is not visible.
> 
> "> " There is more text to the right
> "< " There is more text to the left
> "* " There is more text in both directions (vi mode shows + instead)
> 
> The "> " does not appear at the right edge of the window but about
> 10 characters from the edge. This sometime results in the > appearing
> in the middle of text. It should be output in the same location as
> < and *.
> 
> Interestingly, vi mode always displays the indicator 10 characters
> from the right edge of the window.  This is too far indented for my
> taste but, either way, it should be consistent in all cases.
> 
> Any chance you can fix this too?
> 

Thanks for reports.

I had started to dig into it, and it leads to a pandora's box.

So, here I've attached tow patches:

1. fix for terminal width
2. dances around string length and UTF-8

(2) should be applied on (1).

I've tested both patched in vi and emacs mode, and it looks works correctly.

But code of ksh is tricky and I defently need more testing.

Thus, if it commited, I think that it should be in two stages in some delay
between (1) and (2).

-- 
wbr, Kirill
From 85a0ff1f68dc3c7909d27362bb55d8aea1e9350b 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/2] bin/ksh: fix emacs/vi line editor getting wrong terminal
 width

Xterm and other terminal emulators resize the pty after starting the
shell. x_init() queries terminal size at startup before the resize
occurs, so x_cols is kept at default 80 colums.

SIGWINCH arrives but check_sigwinch() wasn't called until after command
entry. This causes the line editor thinks that screen is 80 columns,
than leads to show '<' and truncate input before reaching the actual
terminal edge for large window, and wired behaiour for small one.

Here, I call check_sigwinch() when entering edit mode and on EINTR in
x_getc(). Callback lets emacs/vi recalculate display width and redraw.
---
 bin/ksh/edit.c  | 14 +++++++++++---
 bin/ksh/edit.h  |  2 ++
 bin/ksh/emacs.c | 42 +++++++++++++++++++++++++++++++++++++++++-
 bin/ksh/vi.c    | 46 +++++++++++++++++++++++++++++++++++++++++-----
 4 files changed, 95 insertions(+), 9 deletions(-)

diff --git a/bin/ksh/edit.c b/bin/ksh/edit.c
index 3ed69e5375d..83bab8ffbad 100644
--- a/bin/ksh/edit.c
+++ b/bin/ksh/edit.c
@@ -25,7 +25,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 ***);
 static int	x_command_glob(int, const char *, int, char ***);
@@ -58,7 +59,7 @@ x_sigwinch(int sig)
 	got_sigwinch = 1;
 }
 
-static void
+void
 check_sigwinch(void)
 {
 	if (got_sigwinch) {
@@ -75,11 +76,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);
@@ -120,12 +126,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 a/bin/ksh/edit.h b/bin/ksh/edit.h
index 1d573e1f6fb..abcecc667e0 100644
--- a/bin/ksh/edit.h
+++ b/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 a/bin/ksh/emacs.c b/bin/ksh/emacs.c
index 3c9b0074d81..fa18ecd3a14 100644
--- a/bin/ksh/emacs.c
+++ b/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,8 @@ x_emacs(char *buf, size_t len)
 	xmp = NULL;
 	x_histp = histptr + 1;
 
+	x_resize_cb = x_emacs_resize;
+	check_sigwinch();
 	xx_cols = x_cols;
 	x_col = promptlen(prompt, &p);
 	prompt_skip = p - prompt;
@@ -316,8 +319,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 +384,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;
@@ -2162,4 +2169,37 @@ x_lastcp(void)
 	return (xlp);
 }
 
+static void
+x_emacs_resize(void)
+{
+	/* clear old content before resizing */
+	x_putc('\r');
+	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 a/bin/ksh/vi.c b/bin/ksh/vi.c
index 28be809036d..def7aeeae24 100644
--- a/bin/ksh/vi.c
+++ b/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.) */
@@ -201,6 +203,8 @@ x_vi(char *buf, size_t len)
 	int	c;
 
 	vi_reset(buf, len > LINE ? LINE : len);
+	x_resize_cb = x_vi_resize;
+	check_sigwinch();
 	vi_pprompt(1);
 	x_flush();
 	while (1) {
@@ -242,6 +246,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)
@@ -1424,8 +1429,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;
@@ -1436,6 +1439,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) {
@@ -1452,9 +1466,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;
 }
 
 /*
@@ -2300,4 +2311,29 @@ isu8cont(unsigned char c)
 {
 	return !Flag(FVISHOW8) && (c & (0x80 | 0x40)) == 0x80;
 }
+
+static void
+x_vi_resize(void)
+{
+	int cur = 0, col = 0;
+
+	/* clear old content before resizing */
+	x_putc('\r');
+	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 */
-- 
2.52.0

From a4c841e6bb2aa3b85150b9882cacd48f3da84304 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/2] bin/ksh: fix emacs/vi mode logic with woring UTF-8
 characters

---
 bin/ksh/emacs.c | 83 +++++++++++++++++++++++++++++++++++++++----------
 bin/ksh/vi.c    | 17 +++++++---
 2 files changed, 79 insertions(+), 21 deletions(-)

diff --git a/bin/ksh/emacs.c b/bin/ksh/emacs.c
index fa18ecd3a14..b1b384389ae 100644
--- a/bin/ksh/emacs.c
+++ b/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 *);
@@ -642,16 +643,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++);
 	}
 }
 
@@ -670,14 +672,14 @@ x_size_str(char *cp)
 {
 	int size = 0;
 	while (*cp)
-		size += x_size(*cp++);
+		size += x_size((unsigned char)*cp++);
 	return size;
 }
 
 static int
 x_size(int c)
 {
-	if (c=='\t')
+	if (c == '\t')
 		return 4;	/* Kludge, tabs are always four spaces. */
 	if (iscntrl(c))		/* control char */
 		return 2;
@@ -698,7 +700,7 @@ x_zots(char *str)
 	}
 	x_lastcp();
 	while (*str && str < xlp && adj == x_adj_done)
-		x_zotc(*str++);
+		x_zotcp(str++);
 }
 
 static void
@@ -714,6 +716,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)
 {
@@ -1033,7 +1068,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;
@@ -1073,10 +1108,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(' ');
@@ -1133,8 +1170,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;
@@ -1143,8 +1180,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;
@@ -1805,12 +1842,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();
@@ -2164,6 +2211,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 a/bin/ksh/vi.c b/bin/ksh/vi.c
index def7aeeae24..3b98856158e 100644
--- a/bin/ksh/vi.c
+++ b/bin/ksh/vi.c
@@ -1880,6 +1880,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;
 
@@ -1891,8 +1892,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') {
@@ -1908,7 +1912,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) {
@@ -1916,6 +1921,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++;
@@ -2008,8 +2015,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;
-- 
2.52.0