curses inch()/mvinch() mangle non-ASCII characters of an 8-bit locale on a wide build

Serhiy Storchaka <[email protected]> Sat, 27 Jun 2026 19:42:50 +0300
Newsgroups gmane.comp.lib.ncurses.bugs
Message-ID <[email protected]>
On a wide build (linked against ncursesw), window.inch() and 
window.mvinch() return a wrong value for a non-ASCII character that is 
representable in the locale's 8-bit encoding (such as ISO-8859-15 or 
KOI8-U). The character comes back as the low 8 bits of the Unicode code 
point instead of the locale-encoded byte.

This is an ncurses inconsistency, reproducible in plain C. With 
LANG=en_US.iso885915, where '€' is the single byte 0xA4:

#include <curses.h>
#include <locale.h>
#include <stdio.h>

int main(void)
{
     setlocale(LC_ALL, "");
     initscr();
     addch(0xA4);                 /* '€' in ISO-8859-15 */
     chtype c = mvinch(0, 0);
     char buf[80];
     winnstr(stdscr, buf, sizeof(buf) - 1);
     endwin();
     printf("inch:    0x%02lx\n", (unsigned long)(c & A_CHARTEXT));
     printf("winnstr: 0x%02x\n", (unsigned char)buf[0]);
     return 0;
}
Compiled with cc example.c -lncursesw and run under that locale, inch 
prints 0xac and winnstr prints 0xa4. 0xAC is 0x20AC & 0xFF, the code 
point of '€' truncated to 8 bits. The cell is stored correctly; only 
inch() reads it back wrong.

The cause is an inconsistency between two ncurses functions that the 
module wraps. winnstr() (used by instr()) re-encodes each cell to the 
locale encoding: in lib_instr.c it calls getcchar() to get the wchar_t 
and then wcstombs() to encode it. winch() (used by inch()) does not: in 
lib_winch.c it returns the raw wchar_t code point placed into the 
chtype, without wcstombs().

The X/Open note that inch()/winch() are "only guaranteed to operate 
reliably on character sets in which each character fits into a single 
byte" is about the locale's encoding, not the function. In an 8-bit 
locale every character fits in one byte, so these functions are in scope 
and should return the locale byte. The unsupported case is a character 
that needs more than one byte in the current locale (any non-ASCII 
character under UTF-8), which cannot fit a chtype.

The fix is to make inch()/mvinch() re-encode the cell on a wide build, 
mirroring winnstr(), in _curses_window_inch_impl(): read the cell with 
win_wch()/mvwin_wch(), extract the wchar_t with getcchar(), and 
wcstombs() it. If it encodes to exactly one byte, return that byte in 
the A_CHARTEXT field while keeping the attribute bits from winch(). 
Otherwise (a multibyte encoding or an error) fall back to the current 
winch() value. The narrow build is unchanged.

Only inch()/mvinch() are affected. inchstr()/inchnstr() are not exposed 
by the module, and getbkgd() already round-trips. The write side needs 
no change: ncurses stores each cell as a wchar_t on a wide build, so the 
asymmetry is purely on readback. UTF-8 locales remain unsupported for 
inch().


--

Serhiy Storchaka, CPython core developer