From 07dbde65b0a742c02f71fa5b9ca12d2411080e15 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 27 Jun 2026 10:08:04 +0300 Subject: [PATCH] gh-71880: Allow editing the last cell in curses.textpad.Textbox (GH-152363) Textbox.edit() ignored typing in the lower-right cell of the window. It is now written with insch(), which fills the cell without moving the cursor out of the window (addch() there raises an error and scrolls a scrollable window). (cherry picked from commit 11b394381f30815a8ad0123afb0a55e7a0369f79) Co-authored-by: Serhiy Storchaka Co-authored-by: Claude Opus 4.8 --- Lib/curses/textpad.py | 19 ++++++------ Lib/test/test_curses.py | 29 +++++++++++++++++++ ...6-06-26-23-56-40.gh-issue-71880.782D31.rst | 5 ++++ 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-26-23-56-40.gh-issue-71880.782D31.rst diff --git a/Lib/curses/textpad.py b/Lib/curses/textpad.py index c8dbf9fb614fcbe..57b2f4a523c95bc 100644 --- a/Lib/curses/textpad.py +++ b/Lib/curses/textpad.py @@ -74,16 +74,16 @@ def _insert_printable_char(self, ch): self._update_max_yx() (y, x) = self.win.getyx() backyx = None - while y < self.maxy or x < self.maxx: + while True: if self.insert_mode: oldch = self.win.inch() - # The try-catch ignores the error we trigger from some curses - # versions by trying to write into the lowest-rightmost spot - # in the window. - try: - self.win.addch(ch) - except curses.error: - pass + if y >= self.maxy and x >= self.maxx: + # Use insch() in the lower-right cell: addch() there would move + # the cursor out of the window, raising an error and scrolling + # a scrollable window. + self.win.insch(ch) + break + self.win.addch(ch) if not self.insert_mode or not curses.ascii.isprint(oldch): break ch = oldch @@ -101,8 +101,7 @@ def do_command(self, ch): (y, x) = self.win.getyx() self.lastcmd = ch if curses.ascii.isprint(ch): - if y < self.maxy or x < self.maxx: - self._insert_printable_char(ch) + self._insert_printable_char(ch) elif ch == curses.ascii.SOH: # ^a self.win.move(y, 0) elif ch in (curses.ascii.STX,curses.KEY_LEFT, diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 2029c1d8a312327..5cbe83b8e868c82 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1287,6 +1287,35 @@ def test_textbox_insert_mode(self): self._type(box, 'b') self.assertEqual(box.gather(), 'abXc ') + def test_textbox_fill_last_cell(self): + # The lower-right cell can be written, even though addch() there + # cannot advance the cursor past the end of the window. + box, win = self._make_textbox(1, 4, stripspaces=0) + self._type(box, 'abcd') + self.assertEqual(box.gather(), 'abcd') + + def test_textbox_fill_last_cell_multiline(self): + box, win = self._make_textbox(2, 3, stripspaces=0) + self._type(box, 'abc') + box.do_command(curses.ascii.NL) # ^j -> start of next line + self._type(box, 'def') # 'f' lands in the lower-right cell + self.assertEqual(box.gather(), 'abc\ndef\n') + + def test_textbox_fill_last_cell_insert_mode(self): + box, win = self._make_textbox(1, 4, insert_mode=True, stripspaces=0) + self._type(box, 'abcd') + self.assertEqual(box.gather(), 'abcd') + + def test_textbox_fill_last_cell_scrollok(self): + # Writing the lower-right cell must not scroll the window even if it + # has scrolling enabled. + box, win = self._make_textbox(2, 3, stripspaces=0) + win.scrollok(True) + self._type(box, 'abc') + box.do_command(curses.ascii.NL) + self._type(box, 'def') + self.assertEqual(box.gather(), 'abc\ndef\n') + def test_textbox_movement(self): box, win = self._make_textbox(3, 10) self._type(box, 'abc') diff --git a/Misc/NEWS.d/next/Library/2026-06-26-23-56-40.gh-issue-71880.782D31.rst b/Misc/NEWS.d/next/Library/2026-06-26-23-56-40.gh-issue-71880.782D31.rst new file mode 100644 index 000000000000000..5a12f428f2a8b6c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-23-56-40.gh-issue-71880.782D31.rst @@ -0,0 +1,5 @@ +:class:`curses.textpad.Textbox` now lets the lower-right cell of the window be +edited. Writing it with :meth:`~curses.window.addch` would move the cursor +past the end of the window, raising an error and scrolling a scrollable window, +so it is now written with :meth:`~curses.window.insch`, which keeps the cursor +in place.