Skip to content

gh-153862: Fix curses window.inch() for non-ASCII characters on a wide build#153863

Open
serhiy-storchaka wants to merge 2 commits into
python:mainfrom
serhiy-storchaka:curses-inch-locale-byte
Open

gh-153862: Fix curses window.inch() for non-ASCII characters on a wide build#153863
serhiy-storchaka wants to merge 2 commits into
python:mainfrom
serhiy-storchaka:curses-inch-locale-byte

Conversation

@serhiy-storchaka

@serhiy-storchaka serhiy-storchaka commented Jul 17, 2026

Copy link
Copy Markdown
Member

On a wide (ncursesw) build, winch() returns the low 8 bits of the cell character's code point with no locale conversion, so window.inch() and window.mvinch() disagreed with instr() for a non-ASCII character representable in an 8-bit locale — e.g. under ISO-8859-15 gave 0xAC instead of the locale byte 0xA4.

The fix re-encodes the cell to its locale byte via wctob() when the character has a single-byte form, keeping winch()'s value otherwise (combining cells and multibyte characters, which in_wch() reads in full). This mirrors the conversion ncurses itself performs for getbkgd() (_nc_to_char()), which is unaffected.

The underlying ncurses behavior was reported upstream and is considered documented, so it is worked around on the CPython side.

… a wide build

On a wide build, winch() returns the low 8 bits of the character's code
point with no locale conversion, so inch()/mvinch() disagreed with instr()
for a non-ASCII character of an 8-bit locale ('€' under ISO-8859-15 gave
0xAC instead of 0xA4).  Re-encode the cell to its locale byte via wctob(),
as ncurses does for getbkgd().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@read-the-docs-community

read-the-docs-community Bot commented Jul 17, 2026

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #33640269 | 📁 Comparing 5ebe0c2 against main (1736526)

  🔍 Preview build  

2 files changed
± library/curses.html
± whatsnew/changelog.html

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread Modules/_cursesmodule.c
if (curses_getcchar(cell, wstr, &attrs, &pair) == ERR
|| wstr[0] == L'\0' || wstr[1] != L'\0')
{
return rtn;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may report the error to the caller. Something like:

Details
diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index 1e874d65bfc..e9d8bbbc5d0 100644
--- a/Modules/_cursesmodule.c
+++ b/Modules/_cursesmodule.c
@@ -779,7 +779,8 @@ curses_getcchar(const cchar_t *wcval, wchar_t *wstr, attr_t *attrs, int *pair)
    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(). */
 static chtype
-curses_cell_locale_byte(chtype rtn, const cchar_t *cell)
+curses_cell_locale_byte(PyCursesWindowObject *self, chtype rtn,
+                        const cchar_t *cell, const char *funcname)
 {
     wchar_t wstr[CCHARW_MAX + 1];
     attr_t attrs;
@@ -787,13 +788,15 @@ curses_cell_locale_byte(chtype rtn, const cchar_t *cell)
     if (curses_getcchar(cell, wstr, &attrs, &pair) == ERR
         || wstr[0] == L'\0' || wstr[1] != L'\0')
     {
-        return rtn;
+        curses_window_set_error(self, "curses_getcchar", funcname);
+        return (chtype)ERR;
     }
     /* 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 = (rtn & ~(chtype)A_CHARTEXT) | (unsigned char)byte;
+        assert(rtn != (chtype)ERR);
     }
     return rtn;
 }
@@ -3638,7 +3641,10 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1,
     if ((group_right_1 ? mvwin_wch(self->win, y, x, &cell)
                        : win_wch(self->win, &cell)) != ERR)
     {
-        rtn = curses_cell_locale_byte(rtn, &cell);
+        rtn = curses_cell_locale_byte(self, rtn, &cell, "inch");
+        if (rtn == (chtype)ERR) {
+            return NULL;
+        }
     }
 #endif
     return PyLong_FromUnsignedLong(rtn);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not know what is the good solution here. You still get correct attributes and color pair. getbkgd() returns default background (space) if background character cannot be encoded, but there is not good fallback for inch().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants