From c19389a694d417eb1f6ac5df9e7bbfc4d3f3023a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 25 Jul 2026 23:29:11 +0300 Subject: [PATCH 1/2] gh-153862: Fix spurious color pair in curses window.inch() on a wide build winch() returns the whole code point, so inch() replacing only its low 8 bits left the high bits in the color field. Rebuild from getcchar()'s attributes and color pair. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/test/test_curses.py | 2 +- Modules/_cursesmodule.c | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 17837c0219f88e..ad5893e6754f68 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -974,7 +974,7 @@ def test_read_from_window(self): with self.subTest(ch=ch): stdscr.addstr(2, 0, ch) self.assertEqual(stdscr.instr(2, 0, 1), b) - self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT, b[0]) + self.assertEqual(stdscr.inch(2, 0), b[0]) def test_coordinate_errors(self): # Addressing a cell outside the window raises curses.error. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index f893c2080fc3f3..68e847364744da 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -776,10 +776,10 @@ curses_getcchar(const cchar_t *wcval, wchar_t *wstr, attr_t *attrs, int *pair) return rtn; } -/* winch() returns the low 8 bits of the character's code point with no locale - conversion, unlike instr(), so recover the locale byte from the wide cell - when the character maps to exactly one byte, keeping the attribute and color - bits in RTN. A character with no single-byte form is left to winch(). */ +/* winch() does not convert the character to its locale byte, and its bits above + the character may be code-point bits rather than attributes, so rebuild the + value from the locale byte plus getcchar()'s attributes and color pair. A + character with no single-byte form is left to winch(). */ static chtype curses_cell_locale_byte(chtype rtn, const cchar_t *cell) { @@ -795,7 +795,7 @@ curses_cell_locale_byte(chtype rtn, const cchar_t *cell) when the character has none in this locale. */ int byte = wctob(wstr[0]); if (byte != EOF) { - rtn = (rtn & ~(chtype)A_CHARTEXT) | (unsigned char)byte; + rtn = (unsigned char)byte | (attrs & ~(attr_t)A_COLOR) | COLOR_PAIR(pair); } return rtn; } From 25a680edb4a908388654d3cfb088b57da10ae969 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 26 Jul 2026 09:41:29 +0300 Subject: [PATCH 2/2] gh-153862: Rebuild curses window.inch() from the wide cell On a wide build inch() derived its result from winch(), whose character is the whole code point rather than a locale byte, so a code point past 0xff overflowed into the color field and past 0xffff into the attribute bits. Read the cell with win_wch() and build the chtype from the locale byte plus the attributes and color pair reported by getcchar(). The character is 0 when it has no single-byte form; win_wch() and getcchar() failures now raise. Co-Authored-By: Claude Opus 4.8 (1M context) --- Modules/_cursesmodule.c | 66 +++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 68e847364744da..b2d745332317a3 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -776,30 +776,6 @@ curses_getcchar(const cchar_t *wcval, wchar_t *wstr, attr_t *attrs, int *pair) return rtn; } -/* winch() does not convert the character to its locale byte, and its bits above - the character may be code-point bits rather than attributes, so rebuild the - value from the locale byte plus getcchar()'s attributes and color pair. A - character with no single-byte form is left to winch(). */ -static chtype -curses_cell_locale_byte(chtype rtn, const cchar_t *cell) -{ - wchar_t wstr[CCHARW_MAX + 1]; - attr_t attrs; - int pair; - if (curses_getcchar(cell, wstr, &attrs, &pair) == ERR - || wstr[0] == L'\0' || wstr[1] != L'\0') - { - return rtn; - } - /* wctob() mirrors ncurses' own _nc_to_char(): the single-byte form, or EOF - when the character has none in this locale. */ - int byte = wctob(wstr[0]); - if (byte != EOF) { - rtn = (unsigned char)byte | (attrs & ~(attr_t)A_COLOR) | COLOR_PAIR(pair); - } - return rtn; -} - /* Hash one cell by value (text, attributes, pair) -- consistent with the equality comparison, not the raw cchar_t whose padding and unused text tail it ignores. Zero the key first so those bytes are deterministic, then @@ -3650,7 +3626,40 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, { chtype rtn; const char *funcname; - +#ifdef HAVE_NCURSESW + /* ncursesw's winch() returns the character's whole code point instead of + its locale byte, overflowing the chtype's 8-bit character field into the + color and attribute bits; read the wide cell and rebuild it instead. */ + cchar_t cell = {0}; + int rc; + if (!group_right_1) { + rc = win_wch(self->win, &cell); + funcname = "win_wch"; + } + else { + rc = mvwin_wch(self->win, y, x, &cell); + funcname = "mvwin_wch"; + } + if (rc == ERR) { + curses_window_set_error(self, funcname, "inch"); + return NULL; + } + wchar_t wstr[CCHARW_MAX + 1]; + attr_t attrs; + int pair; + if (curses_getcchar(&cell, wstr, &attrs, &pair) == ERR) { + curses_window_set_error(self, "getcchar", "inch"); + return NULL; + } + int byte = 0; + if (wstr[0] != L'\0' && wstr[1] == L'\0') { + byte = wctob(wstr[0]); + if (byte == EOF) { + byte = 0; + } + } + rtn = (chtype)byte | (attrs & ~(attr_t)A_COLOR) | COLOR_PAIR(pair); +#else if (!group_right_1) { rtn = winch(self->win); funcname = "winch"; @@ -3663,13 +3672,6 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, curses_window_set_error(self, funcname, "inch"); return NULL; } -#ifdef HAVE_NCURSESW - curses_cell_t cell = {0}; - if ((group_right_1 ? mvwin_wch(self->win, y, x, &cell) - : win_wch(self->win, &cell)) != ERR) - { - rtn = curses_cell_locale_byte(rtn, &cell); - } #endif return PyLong_FromUnsignedLong(rtn); }