From 61dffa79eba0bdd713291f00aeca8ad7a5cc365f Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sat, 22 Aug 2026 01:41:04 +0300 Subject: [PATCH 1/3] gh-156207: Fix curses Textbox.gather() for double-width characters gather() read the window one cell at a time, so the second cell of a double-width character was reported as another copy of it. Read each line with in_wchstr() instead: its count is a cell count, and it skips the continuation cell. --- Lib/curses/textpad.py | 6 ++---- Lib/test/test_curses.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Lib/curses/textpad.py b/Lib/curses/textpad.py index f6dfc990901d995..54d13cf72fbcbcd 100644 --- a/Lib/curses/textpad.py +++ b/Lib/curses/textpad.py @@ -204,10 +204,8 @@ def gather(self): stop = self._end_of_line(y) if stop == 0 and self.stripspaces: continue - for x in range(self.maxx+1): - if self.stripspaces and x > stop: - break - result = result + str(self.win.in_wch(y, x)) + count = stop+1 if self.stripspaces else self.maxx+1 + result = result + str(self.win.in_wchstr(y, 0, count)) if self.maxy > 0: result = result + "\n" return result diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index ea2dcd76b585a99..32a39e60aaedfae 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -2660,6 +2660,22 @@ def test_textbox_combining(self): box.do_command(ch) self.assertEqual(box.gather(), text + ' ') + @requires_wide_build + def test_textbox_double_width(self): + # A double-width (East Asian) character occupies two cells. gather() + # reads a whole line at a time so that the second cell, which holds + # the same character, is not reported as another one. + text = '你好' + if self._encodable(text): + box, win = self._make_textbox(1, 12) + for ch in text: + box.do_command(ch) + self.assertEqual(box.gather(), text + ' ') + box, win = self._make_textbox(1, 12, stripspaces=0) + for ch in text: + box.do_command(ch) + self.assertEqual(box.gather(), text + ' ' * 8) + def test_textbox_edit_wide(self): # edit() reads characters through get_wch(). Each character is pushed # with unget_wch(), which on a narrow build requires it to encode to a From 5c8418d31a48225dbde97306da3eea5ec2e9d245 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Mon, 24 Aug 2026 20:10:01 +0300 Subject: [PATCH 2/3] Read the whole line and strip the redundant blanks in_wstr() reads a line to its end, so gather() no longer needs move() + _end_of_line() to work out how much of it to ask for: strip the trailing blanks instead, keeping the one the cursor rests on. --- Lib/curses/textpad.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Lib/curses/textpad.py b/Lib/curses/textpad.py index 54d13cf72fbcbcd..31ad37539ca4a42 100644 --- a/Lib/curses/textpad.py +++ b/Lib/curses/textpad.py @@ -200,12 +200,16 @@ def gather(self): result = "" self._update_max_yx() for y in range(self.maxy+1): - self.win.move(y, 0) - stop = self._end_of_line(y) - if stop == 0 and self.stripspaces: - continue - count = stop+1 if self.stripspaces else self.maxx+1 - result = result + str(self.win.in_wchstr(y, 0, count)) + # The whole line: in_wstr() reads a double-width character once, + # skipping the continuation cell that holds its other half. + line = self.win.in_wstr(y, 0) + if self.stripspaces: + stripped = line.rstrip(' ') + if not stripped: + continue + # Keep the blank the cursor rests on past the last character. + line = stripped + ' ' if len(stripped) < len(line) else stripped + result = result + line if self.maxy > 0: result = result + "\n" return result From d986cd7714dfdf2ff89f1088d4bd80b80c14f2ce Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Tue, 25 Aug 2026 11:00:46 +0300 Subject: [PATCH 3/3] Skip when the locale cannot encode the text; stripspaces is boolean --- Lib/test/test_curses.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 32a39e60aaedfae..c94ddfc734a3df9 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -2666,15 +2666,16 @@ def test_textbox_double_width(self): # reads a whole line at a time so that the second cell, which holds # the same character, is not reported as another one. text = '你好' - if self._encodable(text): - box, win = self._make_textbox(1, 12) - for ch in text: - box.do_command(ch) - self.assertEqual(box.gather(), text + ' ') - box, win = self._make_textbox(1, 12, stripspaces=0) - for ch in text: - box.do_command(ch) - self.assertEqual(box.gather(), text + ' ' * 8) + if not self._encodable(text): + self.skipTest('the locale cannot encode %r' % text) + box, win = self._make_textbox(1, 12) + for ch in text: + box.do_command(ch) + self.assertEqual(box.gather(), text + ' ') + box, win = self._make_textbox(1, 12, stripspaces=False) + for ch in text: + box.do_command(ch) + self.assertEqual(box.gather(), text + ' ' * 8) def test_textbox_edit_wide(self): # edit() reads characters through get_wch(). Each character is pushed