Skip to content

Commit 5be0125

Browse files
[3.15] gh-153862: Fix spurious color pair in curses window.inch() on a wide build (GH-154703) (GH-154720)
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. (cherry picked from commit b618874) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * gh-153862: Suggest instr() for non-encodable characters read by inch() A character that does not fit in a single byte has a character byte of 0 in the value returned by inch(); document reading such characters with instr(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3a33ccd commit 5be0125

3 files changed

Lines changed: 40 additions & 37 deletions

File tree

Doc/library/curses.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,9 @@ Window objects
10811081
and the color pair with :func:`pair_number`.
10821082
The character byte is the locale-encoded byte of the cell's character,
10831083
consistent with :meth:`instr`.
1084+
On a wide-character build, a character that does not fit in a single byte
1085+
in the current locale has a character byte of ``0``;
1086+
use :meth:`instr` to read such characters.
10841087

10851088

10861089
.. method:: window.insch(ch[, attr])

Lib/test/test_curses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ def test_read_from_window(self):
520520
with self.subTest(ch=ch):
521521
stdscr.addstr(2, 0, ch)
522522
self.assertEqual(stdscr.instr(2, 0, 1), b)
523-
self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT, b[0])
523+
self.assertEqual(stdscr.inch(2, 0), b[0])
524524

525525
def test_coordinate_errors(self):
526526
# Addressing a cell outside the window raises curses.error.

Modules/_cursesmodule.c

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,34 +2102,6 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1,
21022102
return curses_window_check_err(self, rtn, funcname, "insch");
21032103
}
21042104

2105-
#ifdef HAVE_NCURSESW
2106-
/* winch() returns the low 8 bits of the character's code point with no locale
2107-
conversion, unlike instr(), so recover the locale byte from the wide cell
2108-
when the character maps to exactly one byte, keeping the attribute and color
2109-
bits in RTN. A character with no single-byte form is left to winch(). */
2110-
static chtype
2111-
curses_cell_locale_byte(chtype rtn, const cchar_t *cell)
2112-
{
2113-
wchar_t wstr[CCHARW_MAX + 1];
2114-
attr_t attrs;
2115-
short pair;
2116-
/* getcchar() is not guaranteed to write the text of an empty cell. */
2117-
wstr[0] = L'\0';
2118-
if (getcchar(cell, wstr, &attrs, &pair, NULL) == ERR
2119-
|| wstr[0] == L'\0' || wstr[1] != L'\0')
2120-
{
2121-
return rtn;
2122-
}
2123-
/* wctob() mirrors ncurses' own _nc_to_char(): the single-byte form, or EOF
2124-
when the character has none in this locale. */
2125-
int byte = wctob(wstr[0]);
2126-
if (byte != EOF) {
2127-
rtn = (rtn & ~(chtype)A_CHARTEXT) | (unsigned char)byte;
2128-
}
2129-
return rtn;
2130-
}
2131-
#endif
2132-
21332105
/*[clinic input]
21342106
_curses.window.inch
21352107
@@ -2154,7 +2126,42 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1,
21542126
{
21552127
chtype rtn;
21562128
const char *funcname;
2157-
2129+
#ifdef HAVE_NCURSESW
2130+
/* ncursesw's winch() returns the character's whole code point instead of
2131+
its locale byte, overflowing the chtype's 8-bit character field into the
2132+
color and attribute bits; read the wide cell and rebuild it instead. */
2133+
cchar_t cell = {0};
2134+
int rc;
2135+
if (!group_right_1) {
2136+
rc = win_wch(self->win, &cell);
2137+
funcname = "win_wch";
2138+
}
2139+
else {
2140+
rc = mvwin_wch(self->win, y, x, &cell);
2141+
funcname = "mvwin_wch";
2142+
}
2143+
if (rc == ERR) {
2144+
curses_window_set_error(self, funcname, "inch");
2145+
return NULL;
2146+
}
2147+
wchar_t wstr[CCHARW_MAX + 1];
2148+
attr_t attrs;
2149+
short pair;
2150+
/* getcchar() is not guaranteed to write the text of an empty cell. */
2151+
wstr[0] = L'\0';
2152+
if (getcchar(&cell, wstr, &attrs, &pair, NULL) == ERR) {
2153+
curses_window_set_error(self, "getcchar", "inch");
2154+
return NULL;
2155+
}
2156+
int byte = 0;
2157+
if (wstr[0] != L'\0' && wstr[1] == L'\0') {
2158+
byte = wctob(wstr[0]);
2159+
if (byte == EOF) {
2160+
byte = 0;
2161+
}
2162+
}
2163+
rtn = (chtype)byte | (attrs & ~(attr_t)A_COLOR) | COLOR_PAIR(pair);
2164+
#else
21582165
if (!group_right_1) {
21592166
rtn = winch(self->win);
21602167
funcname = "winch";
@@ -2167,13 +2174,6 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1,
21672174
curses_window_set_error(self, funcname, "inch");
21682175
return NULL;
21692176
}
2170-
#ifdef HAVE_NCURSESW
2171-
cchar_t cell = {0};
2172-
if ((group_right_1 ? mvwin_wch(self->win, y, x, &cell)
2173-
: win_wch(self->win, &cell)) != ERR)
2174-
{
2175-
rtn = curses_cell_locale_byte(rtn, &cell);
2176-
}
21772177
#endif
21782178
return PyLong_FromUnsignedLong(rtn);
21792179
}

0 commit comments

Comments
 (0)