diff --git a/Lib/curses/textpad.py b/Lib/curses/textpad.py index c8dbf9fb614fcb..57b2f4a523c95b 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 c6a762c04e0525..48a0b6a175eaff 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1291,6 +1291,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 00000000000000..5a12f428f2a8b6 --- /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.