diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index 71eadfd5ca2ad7..d2f11949c04c3f 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -1333,9 +1333,6 @@ Window objects and the color pair is not limited to the value that fits in a :func:`color_pair`. - This method is only available if Python was built against a wide-character - version of the underlying curses library. - .. versionadded:: next @@ -1483,9 +1480,6 @@ Window objects followed by combining characters) together with its attributes and color pair, none of which :meth:`inch` can represent. - This method is only available if Python was built against a wide-character - version of the underlying curses library. - .. versionadded:: next @@ -1585,9 +1579,6 @@ Window objects The result can be written back unchanged with :meth:`addstr` (a read and a re-write is a round-trip that preserves every cell's rendition). - This method is only available if Python was built against a wide-character - version of the underlying curses library. - .. versionadded:: next @@ -2004,7 +1995,7 @@ Complex character objects .. class:: complexchar(text, /, attr=0, pair=0) A *complex character* (or *complexchar*) is an immutable styled - wide-character cell: a spacing character optionally followed by combining + character cell: a spacing character optionally followed by combining characters, together with a set of attributes and a color pair. *text* is the cell's text, *attr* a combination of the @@ -2025,8 +2016,10 @@ Complex character objects :func:`str` returns the cell's text; two complex characters are equal when their text, attributes and color pair all match. - This type is only available if Python was built against a wide-character - version of the underlying curses library. + The same code works on both wide- and narrow-character builds. On a narrow + build a cell holds a single character (no combining marks) that must encode to + one byte in the window's encoding (8-bit locales only), and *pair* is limited + to the value that fits in a :func:`color_pair`. .. attribute:: attr @@ -2042,7 +2035,7 @@ Complex character objects .. class:: complexstr(cells[, attr[, pair]]) A *complex character string* (or *complexstr*) is an immutable sequence of - styled wide-character cells -- the string counterpart of + styled character cells -- the string counterpart of :class:`complexchar` (as :class:`str` is to a single character). If *cells* is a string, it is split into character cells (each a spacing @@ -2070,8 +2063,8 @@ Complex character objects :class:`complexchar` (or strings); a :class:`!complexstr` is the immutable form returned by a read. - This type is only available if Python was built against a wide-character - version of the underlying curses library. + Like :class:`complexchar`, this type works on both wide- and narrow-character + builds, with the same per-cell limitations on a narrow build. .. versionadded:: next diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 90ab3ee630b165..a87d708e3fcdc2 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -111,8 +111,12 @@ curses and the module functions :func:`curses.erasewchar`, :func:`curses.killwchar` and :func:`curses.wunctrl`, the wide-character counterparts of :func:`curses.erasechar`, :func:`curses.killchar` and :func:`curses.unctrl`. - These features are only available when built against the wide-character - ncursesw library. + On a narrow (non-ncursesw) build the character cell holds a single character + without combining marks, representable as one byte in the window's encoding, + and :meth:`~curses.window.in_wstr` returns its decoded text; + :meth:`~curses.window.get_wstr` and the :func:`curses.erasewchar`, + :func:`curses.killwchar` and :func:`curses.wunctrl` functions require the + wide-character ncursesw library. (Contributed by Serhiy Storchaka in :gh:`151757`.) * Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`. @@ -139,13 +143,16 @@ curses (Contributed by Serhiy Storchaka in :gh:`152219`.) * Add the :class:`curses.complexchar` type, representing a styled - wide-character cell (its text, attributes and color pair), and the window + character cell (its text, attributes and color pair), and the window methods :meth:`~curses.window.in_wch` and :meth:`~curses.window.getbkgrnd` - that return one --- the wide-character counterparts of + that return one --- the counterparts of :meth:`~curses.window.inch` and :meth:`~curses.window.getbkgd`. The character-cell methods, such as :meth:`~curses.window.addch` and :meth:`~curses.window.border`, now also accept a - :class:`~curses.complexchar`. + :class:`~curses.complexchar`. These work whether or not Python was built + against a wide-character-aware curses library; on a narrow build a cell holds a + single character representable as one byte in the window's encoding (so only + 8-bit locales are supported). (Contributed by Serhiy Storchaka in :gh:`152233`.) * Add the :class:`curses.complexstr` type, an immutable run of styled cells @@ -153,7 +160,9 @@ curses method :meth:`~curses.window.in_wchstr` that returns one. The string-cell methods :meth:`~curses.window.addstr`, :meth:`~curses.window.addnstr`, :meth:`~curses.window.insstr` and :meth:`~curses.window.insnstr` now also - accept a :class:`~curses.complexstr`. + accept a :class:`~curses.complexstr`. Like :class:`~curses.complexchar`, it + works whether or not Python was built against a wide-character-aware curses + library. (Contributed by Serhiy Storchaka in :gh:`152233`.) * Add the :mod:`curses` window method :meth:`~curses.window.dupwin`, which diff --git a/Lib/curses/textpad.py b/Lib/curses/textpad.py index 57b2f4a523c95b..c58a7174d194cf 100644 --- a/Lib/curses/textpad.py +++ b/Lib/curses/textpad.py @@ -56,13 +56,42 @@ def _update_max_yx(self): self.maxy = maxy - 1 self.maxx = maxx - 1 + def _decode(self, ch): + # The text of a chtype cell or input byte, decoded with the window's + # encoding. A_CHARTEXT keeps the character byte, dropping the attributes. + return bytes([ch & curses.A_CHARTEXT]).decode(self.win.encoding, 'replace') + + def _char_at(self, *yx): + # The text of the cell at the given position (default: the cursor). + # instr() re-encodes it to the window's encoding; inch() cannot + # represent a non-ASCII 8-bit-locale character on a wide build. + return self.win.instr(*yx, 1).decode(self.win.encoding, 'replace') + + def _cell_at(self, *yx): + # The cell at the given position (default: the cursor) as a chtype + # addch() can write back with its rendition. inch() mangles a non-ASCII + # character on a wide build, so take the byte from instr() and the + # attributes from inch(). + return self.win.instr(*yx, 1)[0] | self.win.inch(*yx) & curses.A_ATTRIBUTES + + def _isprint(self, cell): + # Whether a chtype cell holds a printable character; _decode() drops the + # attribute bits. + return self._decode(cell).isprintable() + + def _printable_key(self, ch): + # Whether the integer keystroke is a printable character, not a key + # code. 0..255 are character bytes (decoded with the window's encoding); + # larger values are function and navigation keys. + return ch <= 0xff and self._decode(ch).isprintable() + def _end_of_line(self, y): """Go to the location of the first blank on the given line, returning the index of the last non-blank character.""" self._update_max_yx() last = self.maxx while True: - if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP: + if self._char_at(y, last) != ' ': last = min(self.maxx, last+1) break elif last == 0: @@ -76,15 +105,16 @@ def _insert_printable_char(self, ch): backyx = None while True: if self.insert_mode: - oldch = self.win.inch() + oldch = self._cell_at() 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) + # a scrollable window. Pass it as text: insch() does not decode + # an int byte through the locale on a wide build. + self.win.insch(self._decode(ch), ch & curses.A_ATTRIBUTES) break self.win.addch(ch) - if not self.insert_mode or not curses.ascii.isprint(oldch): + if not self.insert_mode or not self._isprint(oldch): break ch = oldch (y, x) = self.win.getyx() @@ -100,7 +130,7 @@ def do_command(self, ch): self._update_max_yx() (y, x) = self.win.getyx() self.lastcmd = ch - if curses.ascii.isprint(ch): + if self._printable_key(ch): self._insert_printable_char(ch) elif ch == curses.ascii.SOH: # ^a self.win.move(y, 0) @@ -174,7 +204,7 @@ def gather(self): for x in range(self.maxx+1): if self.stripspaces and x > stop: break - result = result + chr(curses.ascii.ascii(self.win.inch(y, x))) + result = result + self._char_at(y, x) if self.maxy > 0: result = result + "\n" return result diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 5f01ac6546a0ba..7611771aa83e0c 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -313,6 +313,8 @@ def test_refresh_control(self): # 'é' common to the Latin encodings # '¤'/'€'/'є' byte 0xA4 in ISO-8859-1 / ISO-8859-15 / KOI8-U # Precomposed characters are used so a round-trip does not depend on the form. + # On a narrow (non-wide) build a cell holds one byte, so cases that need a + # combining sequence or a multibyte character are guarded with _storable(). def _encodable(self, s): # Wide characters are only supported in a locale that can encode them. @@ -322,6 +324,18 @@ def _encodable(self, s): return False return True + def _storable(self, s): + # Text the current build can place in character cells. A wide build + # stores any locale-encodable text (combining sequences and multibyte + # characters included). A narrow build has no wide-character cells, so + # each character must occupy a single cell -- that is, encode to exactly + # one byte. + if not self._encodable(s): + return False + if hasattr(self.stdscr, 'get_wch'): # wide build + return True + return len(s.encode(self.stdscr.encoding)) == len(s) + def _read_char(self, y, x): # The character written to a cell, read back for output checks. inch() # is unusable here: on a wide build it returns the low 8 bits of the @@ -435,7 +449,7 @@ def test_in_wstr(self): 'na\u00efve \u00a4', # ISO-8859-1 'soup\u00e7on \u20ac', # ISO-8859-15 '\u0434\u044f\u043a']: # KOI8-U - if self._encodable(s): + if self._storable(s): with self.subTest(s=s): stdscr.addstr(0, 0, s) self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s) @@ -450,7 +464,7 @@ def test_complexchar(self): self.assertTrue(cc.attr & curses.A_BOLD) self.assertEqual(cc.pair, 0) # A spacing character optionally followed by combining characters. - if self._encodable('e\u0301'): + if self._storable('e\u0301'): self.assertEqual(str(curses.complexchar('e\u0301')), 'e\u0301') # Defaults: no attributes, color pair 0. cc = curses.complexchar('z') @@ -496,7 +510,7 @@ def test_in_wch(self): self.assertTrue(cc.attr & curses.A_UNDERLINE) # A character round-trips through the cell. See _encodable for the set. for ch in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): - if self._encodable(ch): + if self._storable(ch): with self.subTest(ch=ch): stdscr.addch(3, 0, curses.complexchar(ch)) self.assertEqual(str(stdscr.in_wch(3, 0)), ch) @@ -530,7 +544,7 @@ def test_getbkgrnd(self): self.assertTrue(cc.attr & curses.A_BOLD) # A non-ASCII background round-trips as a complexchar. See _encodable. for ch in ('é', '¤', '€', 'є'): - if self._encodable(ch): + if self._storable(ch): with self.subTest(ch=ch): stdscr.bkgd(curses.complexchar(ch)) self.assertEqual(str(stdscr.getbkgrnd()), ch) @@ -569,7 +583,7 @@ def test_complexstr(self): self.assertNotEqual(s, curses.complexstr([cc('A'), 'b', cc('c')])) self.assertNotEqual(s, curses.complexstr([cc('A', B), 'b'])) # A spacing character optionally followed by combining characters. - if self._encodable('é'): + if self._storable('é'): self.assertEqual(str(curses.complexstr(['é', 'x'])), 'éx') # cells is positional-only. @@ -586,7 +600,9 @@ def test_complexstr(self): self.assertEqual(str(curses.complexstr('abc')), 'abc') self.assertEqual(len(curses.complexstr('')), 0) base = 'é' # 'e' + combining acute: two code points, one cell - if self._encodable(base): + # Combining sequences need wide-character cells (a narrow build stores + # one byte per cell). + if hasattr(curses.window, 'get_wch') and self._encodable(base): self.assertEqual(len(curses.complexstr(base)), 1) self.assertEqual(curses.complexstr(base)[0], cc(base)) self.assertEqual(len(curses.complexstr('a' + base + 'b')), 3) @@ -734,7 +750,7 @@ def test_output_character(self): # str is stored as a wide-character cell on a wide build, so every # encodable character round-trips, insch() included. A multibyte # character does not fit a cell on a narrow build and is skipped. - wide = hasattr(stdscr, 'in_wch') + wide = hasattr(stdscr, 'get_wch') for c in ('é', '¤', '€', 'є'): if not self._encodable(c): continue @@ -2219,6 +2235,68 @@ def test_textbox_fill_last_cell_scrollok(self): self._type(box, 'def') self.assertEqual(box.gather(), 'abc\ndef\n') + def test_textbox_8bit(self): + # A character of an 8-bit locale encoding is entered and read back + # through the byte API. The byte path also runs on a wide build, so the + # test is not skipped there. Run the suite under an 8-bit locale + # (ISO-8859-1, ISO-8859-15 or KOI8-U) to reach the non-ASCII cases; each + # string is used only if the encoding maps it to single bytes. 'abc' is + # ASCII, 'café' is common to the Latin encodings, and the rest are + # distinctive (byte 0xA4 is '¤'/'€'/'є' in ISO-8859-1/-15/KOI8-U). + encoding = self.stdscr.encoding + for text in ['abc', 'café', 'naïve ¤¦', 'café €Šž', 'дякую єі']: + try: + data = text.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != len(text): + continue # a multibyte encoding is not the 8-bit byte path + with self.subTest(text=text): + box, win = self._make_textbox(1, 16) + for byte in data: + box.do_command(byte) + self.assertEqual(box.gather(), text + ' ') + + def test_textbox_8bit_insert(self): + # Insert mode shifts the rest of the line right by reading each cell back + # and rewriting it; a non-ASCII 8-bit-locale character must survive the + # shift, even on a wide build where inch() mangles it. See + # test_textbox_8bit for the character choices. + encoding = self.stdscr.encoding + for ch in ['é', '¤', '€', 'є']: + try: + data = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != 1: + continue + with self.subTest(ch=ch): + box, win = self._make_textbox(1, 10, insert_mode=True) + for byte in ('a' + ch + 'c').encode(encoding): + box.do_command(byte) + win.move(0, 1) + box.do_command(ord('b')) # insert 'b', shifting ch and 'c' right + self.assertEqual(box.gather(), 'ab' + ch + 'c ') + + def test_textbox_8bit_fill_last_cell(self): + # A non-ASCII 8-bit-locale character must survive being written to the + # lower-right cell, which uses insch() rather than addch(). See + # test_textbox_8bit for the character choices. + encoding = self.stdscr.encoding + for ch in ['é', '¤', '€', 'є']: + try: + data = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(data) != 1: + continue + with self.subTest(ch=ch): + text = 'ab' + ch # the last character fills the corner + box, win = self._make_textbox(1, len(text), stripspaces=0) + for byte in text.encode(encoding): + box.do_command(byte) + self.assertEqual(box.gather(), text) + def test_textbox_movement(self): box, win = self._make_textbox(3, 10) self._type(box, 'abc') @@ -2596,6 +2674,11 @@ def setUp(self): self.mock_win = MagicMock(spec=curses.window) self.mock_win.getyx.return_value = (1, 1) self.mock_win.getmaxyx.return_value = (10, 20) + self.mock_win.encoding = 'utf-8' + # A non-blank cell so that _end_of_line() reports a full line: instr() + # backs the text reads, inch() the insert-mode shift. + self.mock_win.instr.return_value = b'x' + self.mock_win.inch.return_value = ord('x') self.textbox = curses.textpad.Textbox(self.mock_win) def test_init(self): diff --git a/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst b/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst index 34db9698738c2b..421c58298823c0 100644 --- a/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst +++ b/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst @@ -3,5 +3,9 @@ cell -- a spacing character optionally followed by combining characters -- in addition to a single integer or byte character. Add the wide-character read methods :meth:`curses.window.get_wstr` and :meth:`curses.window.in_wstr`, and the functions :func:`curses.erasewchar`, :func:`curses.killwchar` and -:func:`curses.wunctrl`. These features are only available when built against -the wide-character ncursesw library. +:func:`curses.wunctrl`. On a narrow (non-ncursesw) build the character cell +holds a single character without combining marks, representable as one byte in +the window's encoding, and :meth:`curses.window.in_wstr` returns its decoded +text; :meth:`curses.window.get_wstr` and the :func:`curses.erasewchar`, +:func:`curses.killwchar` and :func:`curses.wunctrl` functions require the +wide-character ncursesw library. diff --git a/Misc/NEWS.d/next/Library/2026-06-25-22-41-49.gh-issue-152233.pEhm3q.rst b/Misc/NEWS.d/next/Library/2026-06-25-22-41-49.gh-issue-152233.pEhm3q.rst index f7fc3df6064ade..43895aafe6ce13 100644 --- a/Misc/NEWS.d/next/Library/2026-06-25-22-41-49.gh-issue-152233.pEhm3q.rst +++ b/Misc/NEWS.d/next/Library/2026-06-25-22-41-49.gh-issue-152233.pEhm3q.rst @@ -1,7 +1,9 @@ -Add the :class:`curses.complexchar` type, representing a styled wide-character +Add the :class:`curses.complexchar` type, representing a styled character cell (text, attributes and color pair), and the :mod:`curses` window methods :meth:`~curses.window.in_wch` and :meth:`~curses.window.getbkgrnd` that return one. The character-cell methods (:meth:`~curses.window.addch`, :meth:`~curses.window.bkgd`, :meth:`~curses.window.border`, :meth:`~curses.window.hline` and others) now also accept a -:class:`~curses.complexchar`. +:class:`~curses.complexchar`. This works whether or not Python was built +against a wide-character-aware curses library; on a narrow build a cell holds a +single character representable as one byte in the window's encoding. diff --git a/Misc/NEWS.d/next/Library/2026-06-26-11-20-00.gh-issue-152233.Kp7mQ2.rst b/Misc/NEWS.d/next/Library/2026-06-26-11-20-00.gh-issue-152233.Kp7mQ2.rst index da9cf22a0dd50d..b905b512a8a12a 100644 --- a/Misc/NEWS.d/next/Library/2026-06-26-11-20-00.gh-issue-152233.Kp7mQ2.rst +++ b/Misc/NEWS.d/next/Library/2026-06-26-11-20-00.gh-issue-152233.Kp7mQ2.rst @@ -1,6 +1,8 @@ Add the :class:`curses.complexstr` type, an immutable string of styled -wide-character cells (the counterpart of :class:`curses.complexchar`), and the +character cells (the counterpart of :class:`curses.complexchar`), and the :mod:`curses` window method :meth:`~curses.window.in_wchstr` that returns one. The string-cell methods :meth:`~curses.window.addstr`, :meth:`~curses.window.addnstr`, :meth:`~curses.window.insstr` and :meth:`~curses.window.insnstr` now also accept a :class:`~curses.complexstr`. +Like :class:`curses.complexchar`, it works whether or not Python was built +against a wide-character-aware curses library. diff --git a/Misc/NEWS.d/next/Library/2026-06-27-12-30-00.gh-issue-133031.Na8Bit.rst b/Misc/NEWS.d/next/Library/2026-06-27-12-30-00.gh-issue-133031.Na8Bit.rst new file mode 100644 index 00000000000000..96e9efe20e42f0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-27-12-30-00.gh-issue-133031.Na8Bit.rst @@ -0,0 +1,3 @@ +:class:`curses.textpad.Textbox` now enters and reads back the non-ASCII +characters of an 8-bit locale encoding, instead of mangling them with a 7-bit +mask. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 5e50f9b1e0b238..7b9de46cf22ab4 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -166,10 +166,8 @@ typedef struct { PyObject *error; // curses exception type PyTypeObject *window_type; // exposed by PyCursesWindow_Type PyTypeObject *screen_type; // _curses.screen -#ifdef HAVE_NCURSESW PyTypeObject *complexchar_type; // _curses.complexchar PyTypeObject *complexstr_type; // _curses.complexstr -#endif PyObject *topscreen; // owned ref to the current screen object, // or NULL for the initscr() screen } cursesmodule_state; @@ -664,37 +662,50 @@ PyCurses_ConvertToString(PyCursesWindowObject *win, PyObject *obj, return 0; } +/* A styled character cell: a cchar_t on a wide build, a chtype on a narrow one + (a single locale character ORed with its attributes and color pair). + complexchar wraps one cell, complexstr an array. */ #ifdef HAVE_NCURSESW +typedef cchar_t curses_cell_t; +#else +typedef chtype curses_cell_t; +#endif + typedef struct { PyObject_HEAD - cchar_t cval; + curses_cell_t cval; } PyCursesComplexCharObject; #define _PyCursesComplexCharObject_CAST(op) ((PyCursesComplexCharObject *)(op)) -/* An immutable packed array of cchar_t cells -- the "complex character - string" counterpart of complexchar (as str is to a single character). - It owns the contiguous buffer that win_wchnstr() fills directly, so a read - and a re-write is a zero-copy round-trip. */ +/* An immutable inline array of cells -- the string counterpart of complexchar. + The read functions fill the buffer directly, so read-then-rewrite is a + zero-copy round-trip. */ typedef struct { PyObject_VAR_HEAD - cchar_t cells[1]; // ob_size cells, stored inline (variable-size object) + curses_cell_t cells[1]; // ob_size cells, stored inline (variable-size object) } PyCursesComplexStrObject; #define _PyCursesComplexStrObject_CAST(op) ((PyCursesComplexStrObject *)(op)) /* Build a single character cell from obj. - Return 1 and store a chtype in *pch for an int or bytes, 2 and store a - cchar_t (with *attr* applied) in *pwc for a str (a spacing character - optionally followed by combining characters), or 0 with an exception set. + On a wide build, return 1 and store a chtype in *pch for an int or bytes, or + 2 and store a cchar_t (with *attr* applied) in *pwc for a str (a spacing + character optionally followed by combining characters). On a narrow build + there is no cchar_t, so always return 1 with a chtype in *pch. Return 0 + with an exception set on error. obj may also be a complexchar, whose cell is used directly; it carries its own rendition, so supplying *attr* too (attr_given) is rejected. */ static int PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, attr_t attr, int attr_given, const char *funcname, - chtype *pch, cchar_t *pwc) + chtype *pch +#ifdef HAVE_NCURSESW + , cchar_t *pwc +#endif + ) { cursesmodule_state *state = get_cursesmodule_state_by_win(win); if (Py_IS_TYPE(obj, state->complexchar_type)) { @@ -704,9 +715,15 @@ PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, attr_t attr, "a complexchar", funcname); return 0; } +#ifdef HAVE_NCURSESW *pwc = _PyCursesComplexCharObject_CAST(obj)->cval; return 2; +#else + *pch = _PyCursesComplexCharObject_CAST(obj)->cval; + return 1; +#endif } +#ifdef HAVE_NCURSESW wchar_t wstr[CCHARW_MAX + 1]; int type = PyCurses_ConvertToCchar_t(win, obj, pch, wstr); if (type == 2) { @@ -716,8 +733,13 @@ PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, attr_t attr, } } return type; +#else + return PyCurses_ConvertToCchar_t(win, obj, pch); +#endif } +#ifdef HAVE_NCURSESW + /* Pack a wide-character cell, routing the color pair through the extended-color opts slot so it is not limited to a short (unlike the chtype COLOR_PAIR field). Without that slot the pair must fit in the @@ -782,6 +804,129 @@ curses_cchar_hash(cursesmodule_state *state, const cchar_t *cell) } #endif +/* ---- Build-agnostic cell helpers over curses_cell_t -------------------- */ + +/* Pack a cell's text (a str), attributes and color pair into *cell. On a + narrow build the text must be one character that encodes to a single byte. + Return 0, or -1 with an exception set. */ +static int +curses_cell_pack(cursesmodule_state *state, curses_cell_t *cell, + PyObject *text, attr_t attr, int pair, const char *funcname) +{ +#ifdef HAVE_NCURSESW + wchar_t wstr[CCHARW_MAX + 1]; + if (PyCurses_ConvertToWideCell(text, wstr) < 0) { + return -1; + } + if (curses_setcchar(cell, wstr, attr, pair) == ERR) { + if (!PyErr_Occurred()) { + PyErr_SetString(state->error, "setcchar() returned ERR"); + } + return -1; + } + return 0; +#else + /* A narrow cell holds one character; reject a longer string with the same + ValueError as the wide build, so the exception type is build-independent. */ + if (PyUnicode_GET_LENGTH(text) != 1) { + PyErr_SetString(PyExc_ValueError, + "a character cell must be a single character"); + return -1; + } + chtype ch; + if (!PyCurses_ConvertToChtype(NULL, text, &ch)) { + return -1; + } + /* Reject a pair that COLOR_PAIR()/PAIR_NUMBER() cannot round-trip (assumes + only that they are inverses). A wide build, or color_set(), can use + larger pairs. */ + chtype color = COLOR_PAIR(pair); + if (pair < 0 || PAIR_NUMBER(color) != pair) { + PyErr_Format(PyExc_OverflowError, + "%s(): color pair %d does not fit in a chtype " + "(color_pair() can encode only pairs 0 to %d)", + funcname, pair, (int)PAIR_NUMBER(A_COLOR)); + return -1; + } + *cell = (ch & A_CHARTEXT) | (attr & ~(attr_t)A_COLOR) | color; + return 0; +#endif +} + +/* Read a cell's attributes and color pair. Return 0, or -1 if wide getcchar() + fails (practically impossible). */ +static int +curses_cell_attr_pair(cursesmodule_state *state, const curses_cell_t *cell, + attr_t *attr, int *pair) +{ +#ifdef HAVE_NCURSESW + wchar_t wstr[CCHARW_MAX + 1]; + if (curses_getcchar(cell, wstr, attr, pair) == ERR) { + PyErr_SetString(state->error, "getcchar() returned ERR"); + return -1; + } + return 0; +#else + *attr = *cell & A_ATTRIBUTES & ~(attr_t)A_COLOR; + *pair = PAIR_NUMBER(*cell & A_COLOR); + return 0; +#endif +} + +/* Build a str from a cell's text (decoding the stored byte on a narrow build). + Return a new reference, or NULL with an exception set. */ +static PyObject * +curses_cell_text(cursesmodule_state *state, const curses_cell_t *cell) +{ +#ifdef HAVE_NCURSESW + wchar_t wstr[CCHARW_MAX + 1]; + attr_t attr; + int pair; + if (curses_getcchar(cell, wstr, &attr, &pair) == ERR) { + PyErr_SetString(state->error, "getcchar() returned ERR"); + return NULL; + } + return PyUnicode_FromWideChar(wstr, -1); +#else + char ch = (char)(*cell & A_CHARTEXT); + return PyUnicode_Decode(&ch, 1, curses_screen_encoding, NULL); +#endif +} + +/* Compare two cells by value (text, attributes, pair). Return 1, 0, or -1 if + wide getcchar() fails (practically impossible). */ +static int +curses_cell_equal(cursesmodule_state *state, const curses_cell_t *a, + const curses_cell_t *b) +{ +#ifdef HAVE_NCURSESW + wchar_t wa[CCHARW_MAX + 1], wb[CCHARW_MAX + 1]; + attr_t aa, ab; + int pa, pb; + if (curses_getcchar(a, wa, &aa, &pa) == ERR || + curses_getcchar(b, wb, &ab, &pb) == ERR) + { + PyErr_SetString(state->error, "getcchar() returned ERR"); + return -1; + } + return (aa == ab && pa == pb && wcscmp(wa, wb) == 0); +#else + return *a == *b; +#endif +} + +/* Hash a cell by value, consistent with curses_cell_equal. */ +static Py_hash_t +curses_cell_hash(cursesmodule_state *state, const curses_cell_t *cell) +{ +#ifdef HAVE_NCURSESW + return curses_cchar_hash(state, cell); +#else + Py_hash_t h = Py_HashBuffer(cell, sizeof(*cell)); + return h == -1 ? -2 : h; +#endif +} + static int color_allow_default_converter(PyObject *arg, void *ptr) { @@ -935,15 +1080,14 @@ class attr_converter(CConverter): [python start generated code]*/ /*[python end generated code: output=da39a3ee5e6b4b0d input=57b994c97cbd5e80]*/ -#ifdef HAVE_NCURSESW /* -------------------------------------------------------*/ -/* Complex character objects (styled wide-character cells) */ +/* Complex character objects (styled character cells) */ /* -------------------------------------------------------*/ -/* Wrap a cchar_t in a new complexchar object (the read side: in_wch, - getbkgrnd, ...). The object simply owns a copy of the cell. */ +/* Wrap a cell in a new complexchar (the read side: in_wch, getbkgrnd). The + object owns a copy of the cell. */ static PyObject * -PyCursesComplexChar_New(cursesmodule_state *state, const cchar_t *wcval) +PyCursesComplexChar_New(cursesmodule_state *state, const curses_cell_t *wcval) { PyCursesComplexCharObject *cc = PyObject_New(PyCursesComplexCharObject, state->complexchar_type); @@ -954,21 +1098,6 @@ PyCursesComplexChar_New(cursesmodule_state *state, const cchar_t *wcval) return (PyObject *)cc; } -/* Decode the cell, raising curses.error on the (practically impossible) - getcchar() failure. */ -static int -complexchar_unpack(PyObject *self, wchar_t *wstr, attr_t *attrs, int *pair) -{ - cchar_t *cval = &_PyCursesComplexCharObject_CAST(self)->cval; - if (curses_getcchar(cval, wstr, attrs, pair) == ERR) { - cursesmodule_state *state = - get_cursesmodule_state_by_cls(Py_TYPE(self)); - PyErr_SetString(state->error, "getcchar() returned ERR"); - return -1; - } - return 0; -} - /*[clinic input] @classmethod _curses.complexchar.__new__ as complexchar_new @@ -998,16 +1127,9 @@ complexchar_new_impl(PyTypeObject *type, PyObject *text, attr_t attr, PyErr_SetString(PyExc_ValueError, "color pair is less than 0"); return NULL; } - wchar_t wstr[CCHARW_MAX + 1]; - if (PyCurses_ConvertToWideCell(text, wstr) < 0) { - return NULL; - } - cchar_t cval; - if (curses_setcchar(&cval, wstr, attr, pair) == ERR) { - if (!PyErr_Occurred()) { - cursesmodule_state *state = get_cursesmodule_state_by_cls(type); - PyErr_SetString(state->error, "setcchar() returned ERR"); - } + cursesmodule_state *state = get_cursesmodule_state_by_cls(type); + curses_cell_t cval; + if (curses_cell_pack(state, &cval, text, attr, pair, "complexchar") < 0) { return NULL; } PyCursesComplexCharObject *cc = @@ -1030,25 +1152,21 @@ complexchar_dealloc(PyObject *self) static PyObject * complexchar_str(PyObject *self) { - wchar_t wstr[CCHARW_MAX + 1]; - attr_t attrs; - int pair; - if (complexchar_unpack(self, wstr, &attrs, &pair) < 0) { - return NULL; - } - return PyUnicode_FromWideChar(wstr, -1); + cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); + return curses_cell_text(state, &_PyCursesComplexCharObject_CAST(self)->cval); } static PyObject * complexchar_repr(PyObject *self) { - wchar_t wstr[CCHARW_MAX + 1]; + cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); + const curses_cell_t *cval = &_PyCursesComplexCharObject_CAST(self)->cval; attr_t attrs; int pair; - if (complexchar_unpack(self, wstr, &attrs, &pair) < 0) { + if (curses_cell_attr_pair(state, cval, &attrs, &pair) < 0) { return NULL; } - PyObject *text = PyUnicode_FromWideChar(wstr, -1); + PyObject *text = curses_cell_text(state, cval); if (text == NULL) { return NULL; } @@ -1075,10 +1193,11 @@ complexchar_repr(PyObject *self) static PyObject * complexchar_get_attr(PyObject *self, void *Py_UNUSED(closure)) { - wchar_t wstr[CCHARW_MAX + 1]; + cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); attr_t attrs; int pair; - if (complexchar_unpack(self, wstr, &attrs, &pair) < 0) { + if (curses_cell_attr_pair(state, + &_PyCursesComplexCharObject_CAST(self)->cval, &attrs, &pair) < 0) { return NULL; } return PyLong_FromUnsignedLong((unsigned long)attrs); @@ -1087,10 +1206,11 @@ complexchar_get_attr(PyObject *self, void *Py_UNUSED(closure)) static PyObject * complexchar_get_pair(PyObject *self, void *Py_UNUSED(closure)) { - wchar_t wstr[CCHARW_MAX + 1]; + cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); attr_t attrs; int pair; - if (complexchar_unpack(self, wstr, &attrs, &pair) < 0) { + if (curses_cell_attr_pair(state, + &_PyCursesComplexCharObject_CAST(self)->cval, &attrs, &pair) < 0) { return NULL; } return PyLong_FromLong(pair); @@ -1104,16 +1224,13 @@ complexchar_richcompare(PyObject *self, PyObject *other, int op) { Py_RETURN_NOTIMPLEMENTED; } - wchar_t wstr1[CCHARW_MAX + 1], wstr2[CCHARW_MAX + 1]; - attr_t attrs1, attrs2; - int pair1, pair2; - if (complexchar_unpack(self, wstr1, &attrs1, &pair1) < 0 || - complexchar_unpack(other, wstr2, &attrs2, &pair2) < 0) - { + cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); + int equal = curses_cell_equal(state, + &_PyCursesComplexCharObject_CAST(self)->cval, + &_PyCursesComplexCharObject_CAST(other)->cval); + if (equal < 0) { return NULL; } - int equal = (attrs1 == attrs2 && pair1 == pair2 && - wcscmp(wstr1, wstr2) == 0); return PyBool_FromLong(equal == (op == Py_EQ)); } @@ -1121,7 +1238,7 @@ static Py_hash_t complexchar_hash(PyObject *self) { cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); - return curses_cchar_hash(state, &_PyCursesComplexCharObject_CAST(self)->cval); + return curses_cell_hash(state, &_PyCursesComplexCharObject_CAST(self)->cval); } static PyGetSetDef complexchar_getsets[] = { @@ -1141,22 +1258,14 @@ static PyGetSetDef complexchar_getsets[] = { and color pair 0) -- into *out. Return 0 on success, -1 with an exception set otherwise. */ static int -curses_pack_cell(cursesmodule_state *state, PyObject *item, cchar_t *out) +curses_pack_cell(cursesmodule_state *state, PyObject *item, curses_cell_t *out) { if (Py_IS_TYPE(item, state->complexchar_type)) { *out = _PyCursesComplexCharObject_CAST(item)->cval; return 0; } if (PyUnicode_Check(item)) { - wchar_t wstr[CCHARW_MAX + 1]; - if (PyCurses_ConvertToWideCell(item, wstr) < 0) { - return -1; - } - if (curses_setcchar(out, wstr, A_NORMAL, 0) == ERR) { - PyErr_SetString(state->error, "setcchar() returned ERR"); - return -1; - } - return 0; + return curses_cell_pack(state, out, item, A_NORMAL, 0, "complexstr"); } PyErr_Format(PyExc_TypeError, "complexstr cell must be a complexchar or a str, not %T", @@ -1167,14 +1276,14 @@ curses_pack_cell(cursesmodule_state *state, PyObject *item, cchar_t *out) /* Wrap a buffer of len cells in a new complexstr, copying them in. tp_alloc sizes the variable-size object for len cells and sets ob_size. */ static PyObject * -PyCursesComplexStr_New(cursesmodule_state *state, const cchar_t *cells, +PyCursesComplexStr_New(cursesmodule_state *state, const curses_cell_t *cells, Py_ssize_t len) { PyTypeObject *type = state->complexstr_type; PyObject *res = type->tp_alloc(type, len); if (res != NULL && len > 0) { memcpy(_PyCursesComplexStrObject_CAST(res)->cells, cells, - (size_t)len * sizeof(cchar_t)); + (size_t)len * sizeof(curses_cell_t)); } return res; } @@ -1187,12 +1296,13 @@ static PyObject * complexstr_from_string(cursesmodule_state *state, PyObject *str, attr_t attr, int pair) { +#ifdef HAVE_NCURSESW Py_ssize_t n; wchar_t *wbuf = PyUnicode_AsWideCharString(str, &n); if (wbuf == NULL) { return NULL; } - cchar_t *cells = n > 0 ? PyMem_New(cchar_t, n) : NULL; + curses_cell_t *cells = n > 0 ? PyMem_New(curses_cell_t, n) : NULL; if (n > 0 && cells == NULL) { PyMem_Free(wbuf); return PyErr_NoMemory(); @@ -1234,6 +1344,46 @@ complexstr_from_string(cursesmodule_state *state, PyObject *str, PyMem_Free(cells); PyMem_Free(wbuf); return res; +#else + /* Validate the pair once (it is the same for every cell); see + curses_cell_pack() for the round-trip rationale. */ + chtype color = COLOR_PAIR(pair); + if (pair < 0 || PAIR_NUMBER(color) != pair) { + PyErr_Format(PyExc_OverflowError, + "complexstr(): color pair %d does not fit in a chtype " + "(color_pair() can encode only pairs 0 to %d)", + pair, (int)PAIR_NUMBER(A_COLOR)); + return NULL; + } + /* Encode the whole string at once; equal byte and character counts confirm + every character is a single byte (one per cell). */ + PyObject *bytes = PyUnicode_AsEncodedString(str, curses_screen_encoding, + NULL); + if (bytes == NULL) { + return NULL; + } + Py_ssize_t n = PyBytes_GET_SIZE(bytes); + if (n != PyUnicode_GET_LENGTH(str)) { + PyErr_SetString(PyExc_OverflowError, + "a character cell does not fit in a single byte"); + Py_DECREF(bytes); + return NULL; + } + curses_cell_t *cells = n > 0 ? PyMem_New(curses_cell_t, n) : NULL; + if (n > 0 && cells == NULL) { + Py_DECREF(bytes); + return PyErr_NoMemory(); + } + const unsigned char *buf = (const unsigned char *)PyBytes_AS_STRING(bytes); + attr_t cattr = attr & ~(attr_t)A_COLOR; + for (Py_ssize_t i = 0; i < n; i++) { + cells[i] = ((chtype)buf[i] & A_CHARTEXT) | cattr | color; + } + PyObject *res = PyCursesComplexStr_New(state, cells, n); + PyMem_Free(cells); + Py_DECREF(bytes); + return res; +#endif } /*[clinic input] @@ -1303,7 +1453,7 @@ complexstr_new_impl(PyTypeObject *type, PyObject *cells, PyObject *attr, Py_DECREF(seq); return NULL; } - cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells; + curses_cell_t *out = _PyCursesComplexStrObject_CAST(res)->cells; for (Py_ssize_t i = 0; i < n; i++) { PyObject *item = PySequence_Fast_GET_ITEM(seq, i); // borrowed if (curses_pack_cell(state, item, &out[i]) < 0) { @@ -1335,7 +1485,7 @@ static PyObject * complexstr_getcell(PyObject *self, Py_ssize_t i) { cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); - cchar_t *cells = _PyCursesComplexStrObject_CAST(self)->cells; + curses_cell_t *cells = _PyCursesComplexStrObject_CAST(self)->cells; return PyCursesComplexChar_New(state, &cells[i]); } @@ -1377,7 +1527,7 @@ complexstr_subscript(PyObject *self, PyObject *key) if (res == NULL) { return NULL; } - cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells; + curses_cell_t *out = _PyCursesComplexStrObject_CAST(res)->cells; for (Py_ssize_t i = 0, idx = start; i < slicelen; i++, idx += step) { out[i] = s->cells[idx]; } @@ -1403,12 +1553,12 @@ complexstr_concat(PyObject *a, PyObject *b) if (res == NULL) { return NULL; } - cchar_t *out = _PyCursesComplexStrObject_CAST(res)->cells; + curses_cell_t *out = _PyCursesComplexStrObject_CAST(res)->cells; if (Py_SIZE(sa)) { - memcpy(out, sa->cells, (size_t)Py_SIZE(sa) * sizeof(cchar_t)); + memcpy(out, sa->cells, (size_t)Py_SIZE(sa) * sizeof(curses_cell_t)); } if (Py_SIZE(sb)) { - memcpy(out + Py_SIZE(sa), sb->cells, (size_t)Py_SIZE(sb) * sizeof(cchar_t)); + memcpy(out + Py_SIZE(sa), sb->cells, (size_t)Py_SIZE(sb) * sizeof(curses_cell_t)); } return res; } @@ -1417,15 +1567,17 @@ static PyObject * complexstr_str(PyObject *self) { PyCursesComplexStrObject *s = _PyCursesComplexStrObject_CAST(self); - if (Py_SIZE(s) == 0) { + Py_ssize_t len = Py_SIZE(s); + if (len == 0) { return Py_GetConstant(Py_CONSTANT_EMPTY_STR); } - wchar_t *buf = PyMem_New(wchar_t, Py_SIZE(s) * CCHARW_MAX + 1); +#ifdef HAVE_NCURSESW + wchar_t *buf = PyMem_New(wchar_t, len * CCHARW_MAX + 1); if (buf == NULL) { return PyErr_NoMemory(); } Py_ssize_t pos = 0; - for (Py_ssize_t i = 0; i < Py_SIZE(s); i++) { + for (Py_ssize_t i = 0; i < len; i++) { attr_t attrs; int pair; /* getcchar() writes the cell's text (and a terminator) at buf + pos; @@ -1442,6 +1594,19 @@ complexstr_str(PyObject *self) PyObject *res = PyUnicode_FromWideChar(buf, pos); PyMem_Free(buf); return res; +#else + /* Each cell stores a single locale-encoded byte; decode them all at once. */ + char *buf = PyMem_Malloc(len); + if (buf == NULL) { + return PyErr_NoMemory(); + } + for (Py_ssize_t i = 0; i < len; i++) { + buf[i] = (char)(s->cells[i] & A_CHARTEXT); + } + PyObject *res = PyUnicode_Decode(buf, len, curses_screen_encoding, NULL); + PyMem_Free(buf); + return res; +#endif } static PyObject * @@ -1464,7 +1629,7 @@ complexstr_hash(PyObject *self) /* Combine the per-cell hashes like a tuple. */ Py_uhash_t acc = _PyTuple_HASH_XXPRIME_5; for (Py_ssize_t i = 0; i < Py_SIZE(s); i++) { - Py_hash_t lane = curses_cchar_hash(state, &s->cells[i]); + Py_hash_t lane = curses_cell_hash(state, &s->cells[i]); if (lane == -1) { return -1; } @@ -1487,20 +1652,14 @@ complexstr_richcompare(PyObject *self, PyObject *other, int op) } PyCursesComplexStrObject *a = _PyCursesComplexStrObject_CAST(self); PyCursesComplexStrObject *b = _PyCursesComplexStrObject_CAST(other); + cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); int equal = (Py_SIZE(a) == Py_SIZE(b)); for (Py_ssize_t i = 0; equal && i < Py_SIZE(a); i++) { - wchar_t wa[CCHARW_MAX + 1], wb[CCHARW_MAX + 1]; - attr_t aa, ab; - int pa, pb; - if (curses_getcchar(&a->cells[i], wa, &aa, &pa) == ERR || - curses_getcchar(&b->cells[i], wb, &ab, &pb) == ERR) - { - cursesmodule_state *state = - get_cursesmodule_state_by_cls(Py_TYPE(self)); - PyErr_SetString(state->error, "getcchar() returned ERR"); + int eq = curses_cell_equal(state, &a->cells[i], &b->cells[i]); + if (eq < 0) { return NULL; } - equal = (aa == ab && pa == pb && wcscmp(wa, wb) == 0); + equal = eq; } return PyBool_FromLong(equal == (op == Py_EQ)); } @@ -1513,7 +1672,7 @@ curses_window_put_cells(PyCursesWindowObject *self, PyObject *obj, int use_xy, int y, int x, int n_limit, int insert, const char *funcname) { - const cchar_t *cells = _PyCursesComplexStrObject_CAST(obj)->cells; + const curses_cell_t *cells = _PyCursesComplexStrObject_CAST(obj)->cells; Py_ssize_t count = Py_SIZE(obj); if (n_limit >= 0 && count > n_limit) { @@ -1530,13 +1689,14 @@ curses_window_put_cells(PyCursesWindowObject *self, PyObject *obj, int rtn; const char *cfuncname; if (insert) { - /* There is no batch cchar_t insert; insert the cells right-to-left at - the position so they end up in order. */ + /* No batch cell insert exists; insert the cells right-to-left so they + end up in order. */ if (use_xy && wmove(self->win, y, x) == ERR) { curses_window_set_error(self, "wmove", funcname); return NULL; } rtn = OK; +#ifdef HAVE_NCURSESW cfuncname = "wins_wch"; for (Py_ssize_t i = count - 1; i >= 0; i--) { rtn = wins_wch(self->win, &cells[i]); @@ -1544,20 +1704,37 @@ curses_window_put_cells(PyCursesWindowObject *self, PyObject *obj, break; } } +#else + cfuncname = "winsch"; + for (Py_ssize_t i = count - 1; i >= 0; i--) { + rtn = winsch(self->win, cells[i]); + if (rtn == ERR) { + break; + } + } +#endif } else if (use_xy) { +#ifdef HAVE_NCURSESW rtn = mvwadd_wchnstr(self->win, y, x, cells, (int)count); cfuncname = "mvwadd_wchnstr"; +#else + rtn = mvwaddchnstr(self->win, y, x, cells, (int)count); + cfuncname = "mvwaddchnstr"; +#endif } else { +#ifdef HAVE_NCURSESW rtn = wadd_wchnstr(self->win, cells, (int)count); cfuncname = "wadd_wchnstr"; +#else + rtn = waddchnstr(self->win, cells, (int)count); + cfuncname = "waddchnstr"; +#endif } return curses_window_check_err(self, rtn, cfuncname, funcname); } -#endif - /***************************************************************************** The Window Object ******************************************************************************/ @@ -1879,7 +2056,7 @@ _curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1, } else #else - type = PyCurses_ConvertToCchar_t(self, ch, &cch); + type = PyCurses_ConvertToCell(self, ch, attr, group_right_1, "addch", &cch); #endif if (type == 1) { if (coordinates_group) { @@ -1961,7 +2138,6 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1, int use_xy = group_left_1, use_attr = group_right_1; const char *funcname; -#ifdef HAVE_NCURSESW { cursesmodule_state *state = get_cursesmodule_state_by_win(self); if (Py_IS_TYPE(str, state->complexstr_type)) { @@ -1974,6 +2150,7 @@ _curses_window_addstr_impl(PyCursesWindowObject *self, int group_left_1, -1, 0, "addstr"); } } +#ifdef HAVE_NCURSESW strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr); #else strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL); @@ -2076,7 +2253,6 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1, int use_xy = group_left_1, use_attr = group_right_1; const char *funcname; -#ifdef HAVE_NCURSESW { cursesmodule_state *state = get_cursesmodule_state_by_win(self); if (Py_IS_TYPE(str, state->complexstr_type)) { @@ -2089,6 +2265,7 @@ _curses_window_addnstr_impl(PyCursesWindowObject *self, int group_left_1, n, 0, "addnstr"); } } +#ifdef HAVE_NCURSESW strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr); #else strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL); @@ -2173,7 +2350,7 @@ _curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch, return curses_window_check_err(self, rtn, "wbkgrnd", "bkgd"); } #else - if (!PyCurses_ConvertToChtype(self, ch, &bkgd)) + if (!PyCurses_ConvertToCell(self, ch, attr, group_right_1, "bkgd", &bkgd)) return NULL; #endif @@ -2383,7 +2560,7 @@ _curses_window_bkgdset_impl(PyCursesWindowObject *self, PyObject *ch, Py_RETURN_NONE; } #else - if (!PyCurses_ConvertToChtype(self, ch, &bkgd)) + if (!PyCurses_ConvertToCell(self, ch, attr, group_right_1, "bkgdset", &bkgd)) return NULL; #endif @@ -2475,7 +2652,8 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls, } #else for (i = 0; i < 8; i++) { - if (objs[i] != NULL && !PyCurses_ConvertToChtype(self, objs[i], &ch[i])) + if (objs[i] != NULL && + !PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0, "border", &ch[i])) return NULL; } #endif @@ -2536,10 +2714,10 @@ _curses_window_box_impl(PyCursesWindowObject *self, int group_right_1, } #else if (group_right_1) { - if (!PyCurses_ConvertToChtype(self, verch, &ch1)) { + if (!PyCurses_ConvertToCell(self, verch, A_NORMAL, 0, "box", &ch1)) { return NULL; } - if (!PyCurses_ConvertToChtype(self, horch, &ch2)) { + if (!PyCurses_ConvertToCell(self, horch, A_NORMAL, 0, "box", &ch2)) { return NULL; } } @@ -2795,7 +2973,7 @@ _curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch, return curses_window_check_err(self, rtn, funcname, "echochar"); } #else - if (!PyCurses_ConvertToChtype(self, ch, &ch_)) + if (!PyCurses_ConvertToCell(self, ch, attr, group_right_1, "echochar", &ch_)) return NULL; #endif @@ -2855,7 +3033,6 @@ _curses_window_getbkgd_impl(PyCursesWindowObject *self) return PyLong_FromLong(rtn); } -#ifdef HAVE_NCURSESW /*[clinic input] _curses.window.in_wch @@ -2878,7 +3055,9 @@ _curses_window_in_wch_impl(PyCursesWindowObject *self, int group_right_1, int y, int x) /*[clinic end generated code: output=846ca8a82f2ecab4 input=a55dd215367dfbb1]*/ { - cchar_t wcval; + curses_cell_t wcval; + cursesmodule_state *state = get_cursesmodule_state_by_win(self); +#ifdef HAVE_NCURSESW int rtn; const char *funcname; if (group_right_1) { @@ -2893,7 +3072,23 @@ _curses_window_in_wch_impl(PyCursesWindowObject *self, int group_right_1, curses_window_set_error(self, funcname, "in_wch"); return NULL; } - cursesmodule_state *state = get_cursesmodule_state_by_win(self); +#else + chtype c; + const char *funcname; + if (group_right_1) { + c = mvwinch(self->win, y, x); + funcname = "mvwinch"; + } + else { + c = winch(self->win); + funcname = "winch"; + } + if (c == (chtype)ERR) { + curses_window_set_error(self, funcname, "in_wch"); + return NULL; + } + wcval = c; +#endif return PyCursesComplexChar_New(state, &wcval); } @@ -2907,17 +3102,24 @@ static PyObject * _curses_window_getbkgrnd_impl(PyCursesWindowObject *self) /*[clinic end generated code: output=afec19cad00eff71 input=e06bf3d6bf90d2ec]*/ { - cchar_t wcval; + curses_cell_t wcval; + cursesmodule_state *state = get_cursesmodule_state_by_win(self); +#ifdef HAVE_NCURSESW if (wgetbkgrnd(self->win, &wcval) == ERR) { curses_window_set_error(self, "wgetbkgrnd", "getbkgrnd"); return NULL; } - cursesmodule_state *state = get_cursesmodule_state_by_win(self); +#else + chtype c = getbkgd(self->win); + if (c == (chtype)ERR) { + curses_window_set_error(self, "getbkgd", "getbkgrnd"); + return NULL; + } + wcval = c; +#endif return PyCursesComplexChar_New(state, &wcval); } -#endif /* HAVE_NCURSESW */ - static PyObject * curses_check_signals_on_input_error(PyCursesWindowObject *self, const char *curses_funcname, @@ -3218,7 +3420,7 @@ _curses_window_hline_impl(PyCursesWindowObject *self, int group_left_1, return NULL; } #else - if (!PyCurses_ConvertToChtype(self, ch, &ch_)) + if (!PyCurses_ConvertToCell(self, ch, attr, group_right_1, "hline", &ch_)) return NULL; #endif if (group_left_1) { @@ -3290,7 +3492,7 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1, return curses_window_check_err(self, rtn, funcname, "insch"); } #else - if (!PyCurses_ConvertToChtype(self, ch, &ch_)) + if (!PyCurses_ConvertToCell(self, ch, attr, group_right_1, "insch", &ch_)) return NULL; #endif @@ -3467,6 +3669,7 @@ PyCursesWindow_get_wstr(PyObject *op, PyObject *args) PyMem_Free(buf); return res; } +#endif /* HAVE_NCURSESW */ PyDoc_STRVAR(_curses_window_in_wstr__doc__, "in_wstr([y, x,] n=2047)\n" @@ -3496,6 +3699,7 @@ PyCursesWindow_in_wstr(PyObject *op, PyObject *args) } n = Py_MIN(n, max_buf_size - 1); +#ifdef HAVE_NCURSESW wchar_t *buf = PyMem_New(wchar_t, n + 1); if (buf == NULL) { return PyErr_NoMemory(); @@ -3515,6 +3719,29 @@ PyCursesWindow_in_wstr(PyObject *op, PyObject *args) PyObject *res = PyUnicode_FromWideChar(buf, -1); PyMem_Free(buf); return res; +#else + /* Without the wide library, read the locale-encoded bytes and decode them + with the window's encoding. */ + char *buf = PyMem_New(char, n + 1); + if (buf == NULL) { + return PyErr_NoMemory(); + } + + if (use_xy) { + rtn = mvwinnstr(self->win, y, x, buf, n); + } + else { + rtn = winnstr(self->win, buf, n); + } + + if (rtn == ERR) { + PyMem_Free(buf); + return Py_GetConstant(Py_CONSTANT_EMPTY_STR); + } + PyObject *res = PyUnicode_Decode(buf, strlen(buf), self->encoding, NULL); + PyMem_Free(buf); + return res; +#endif } PyDoc_STRVAR(_curses_window_in_wchstr__doc__, @@ -3546,11 +3773,13 @@ PyCursesWindow_in_wchstr(PyObject *op, PyObject *args) } n = Py_MIN(n, max_buf_size - 1); - cchar_t *buf = PyMem_New(cchar_t, n + 1); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); + curses_cell_t *buf = PyMem_New(curses_cell_t, n + 1); if (buf == NULL) { return PyErr_NoMemory(); } +#ifdef HAVE_NCURSESW if (use_xy) { rtn = mvwin_wchnstr(self->win, y, x, buf, n); } @@ -3558,7 +3787,6 @@ PyCursesWindow_in_wchstr(PyObject *op, PyObject *args) rtn = win_wchnstr(self->win, buf, n); } - cursesmodule_state *state = get_cursesmodule_state_by_win(self); if (rtn == ERR) { PyMem_Free(buf); return PyCursesComplexStr_New(state, NULL, 0); @@ -3579,11 +3807,32 @@ PyCursesWindow_in_wchstr(PyObject *op, PyObject *args) } count++; } +#else + /* winchnstr() is not guaranteed (SVr4) to terminate the array, so pre-zero + it and stop at the first empty cell; a painted cell always holds at least + a space, never 0. */ + memset(buf, 0, ((size_t)n + 1) * sizeof(curses_cell_t)); + if (use_xy) { + rtn = mvwinchnstr(self->win, y, x, buf, n); + } + else { + rtn = winchnstr(self->win, buf, n); + } + + if (rtn == ERR) { + PyMem_Free(buf); + return PyCursesComplexStr_New(state, NULL, 0); + } + + Py_ssize_t count = 0; + while (count < (Py_ssize_t)n && buf[count] != 0) { + count++; + } +#endif PyObject *res = PyCursesComplexStr_New(state, buf, count); PyMem_Free(buf); return res; } -#endif /* HAVE_NCURSESW */ /*[clinic input] _curses.window.insstr @@ -3629,7 +3878,6 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1, int use_xy = group_left_1, use_attr = group_right_1; const char *funcname; -#ifdef HAVE_NCURSESW { cursesmodule_state *state = get_cursesmodule_state_by_win(self); if (Py_IS_TYPE(str, state->complexstr_type)) { @@ -3642,6 +3890,7 @@ _curses_window_insstr_impl(PyCursesWindowObject *self, int group_left_1, -1, 1, "insstr"); } } +#ifdef HAVE_NCURSESW strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr); #else strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL); @@ -3742,7 +3991,6 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1, int use_xy = group_left_1, use_attr = group_right_1; const char *funcname; -#ifdef HAVE_NCURSESW { cursesmodule_state *state = get_cursesmodule_state_by_win(self); if (Py_IS_TYPE(str, state->complexstr_type)) { @@ -3755,6 +4003,7 @@ _curses_window_insnstr_impl(PyCursesWindowObject *self, int group_left_1, n, 1, "insnstr"); } } +#ifdef HAVE_NCURSESW strtype = PyCurses_ConvertToString(self, str, &bytesobj, &wstr); #else strtype = PyCurses_ConvertToString(self, str, &bytesobj, NULL); @@ -4314,7 +4563,7 @@ _curses_window_vline_impl(PyCursesWindowObject *self, int group_left_1, return NULL; } #else - if (!PyCurses_ConvertToChtype(self, ch, &ch_)) + if (!PyCurses_ConvertToCell(self, ch, attr, group_right_1, "vline", &ch_)) return NULL; #endif if (group_left_1) { @@ -4377,7 +4626,6 @@ PyCursesWindow_set_encoding(PyObject *op, PyObject *value, void *Py_UNUSED(ignor #define clinic_state() (get_cursesmodule_state_by_cls(Py_TYPE(self))) #include "clinic/_cursesmodule.c.h" -#ifdef HAVE_NCURSESW static PyType_Slot PyCursesComplexChar_Type_slots[] = { {Py_tp_doc, (void *)complexchar_new__doc__}, {Py_tp_new, complexchar_new}, @@ -4418,13 +4666,12 @@ static PyType_Slot PyCursesComplexStr_Type_slots[] = { static PyType_Spec PyCursesComplexStr_Type_spec = { .name = "curses.complexstr", .basicsize = offsetof(PyCursesComplexStrObject, cells), - .itemsize = sizeof(cchar_t), + .itemsize = sizeof(curses_cell_t), .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_HEAPTYPE, .slots = PyCursesComplexStr_Type_slots }; -#endif #undef clinic_state #if defined(HAVE_CURSES_USE_SCREEN) || defined(HAVE_CURSES_USE_WINDOW) @@ -4624,7 +4871,6 @@ static PyMethodDef PyCursesWindow_methods[] = { "instr", PyCursesWindow_instr, METH_VARARGS, _curses_window_instr__doc__ }, -#ifdef HAVE_NCURSESW { "in_wstr", PyCursesWindow_in_wstr, METH_VARARGS, _curses_window_in_wstr__doc__ @@ -4633,7 +4879,6 @@ static PyMethodDef PyCursesWindow_methods[] = { "in_wchstr", PyCursesWindow_in_wchstr, METH_VARARGS, _curses_window_in_wchstr__doc__ }, -#endif _CURSES_WINDOW_IS_LINETOUCHED_METHODDEF {"is_wintouched", PyCursesWindow_is_wintouched, METH_NOARGS, "is_wintouched($self, /)\n--\n\n" @@ -8045,10 +8290,8 @@ cursesmodule_traverse(PyObject *mod, visitproc visit, void *arg) Py_VISIT(state->error); Py_VISIT(state->window_type); Py_VISIT(state->screen_type); -#ifdef HAVE_NCURSESW Py_VISIT(state->complexchar_type); Py_VISIT(state->complexstr_type); -#endif Py_VISIT(state->topscreen); return 0; } @@ -8060,10 +8303,8 @@ cursesmodule_clear(PyObject *mod) Py_CLEAR(state->error); Py_CLEAR(state->window_type); Py_CLEAR(state->screen_type); -#ifdef HAVE_NCURSESW Py_CLEAR(state->complexchar_type); Py_CLEAR(state->complexstr_type); -#endif Py_CLEAR(state->topscreen); return 0; } @@ -8105,7 +8346,6 @@ cursesmodule_exec(PyObject *module) if (PyModule_AddType(module, state->screen_type) < 0) { return -1; } -#ifdef HAVE_NCURSESW state->complexchar_type = (PyTypeObject *)PyType_FromModuleAndSpec( module, &PyCursesComplexChar_Type_spec, NULL); if (state->complexchar_type == NULL) { @@ -8122,7 +8362,6 @@ cursesmodule_exec(PyObject *module) if (PyModule_AddType(module, state->complexstr_type) < 0) { return -1; } -#endif /* Add some symbolic constants to the module */ PyObject *module_dict = PyModule_GetDict(module); diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index 1c05a125fd1cc6..f895090bef845f 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -8,8 +8,6 @@ preserve #endif #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(complexchar_new__doc__, "complexchar(text, /, attr=0, pair=0)\n" "--\n" @@ -103,10 +101,6 @@ complexchar_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } -#endif /* defined(HAVE_NCURSESW) */ - -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(complexstr_new__doc__, "complexstr(cells, /, attr=, pair=)\n" "--\n" @@ -194,8 +188,6 @@ complexstr_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } -#endif /* defined(HAVE_NCURSESW) */ - PyDoc_STRVAR(_curses_window_addch__doc__, "addch([y, x,] ch, [attr])\n" "Paint the character.\n" @@ -1119,8 +1111,6 @@ _curses_window_getbkgd(PyObject *self, PyObject *Py_UNUSED(ignored)) return _curses_window_getbkgd_impl((PyCursesWindowObject *)self); } -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(_curses_window_in_wch__doc__, "in_wch([y, x])\n" "Return the complex character at the given position in the window.\n" @@ -1167,10 +1157,6 @@ _curses_window_in_wch(PyObject *self, PyObject *args) return return_value; } -#endif /* defined(HAVE_NCURSESW) */ - -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(_curses_window_getbkgrnd__doc__, "getbkgrnd($self, /)\n" "--\n" @@ -1189,8 +1175,6 @@ _curses_window_getbkgrnd(PyObject *self, PyObject *Py_UNUSED(ignored)) return _curses_window_getbkgrnd_impl((PyCursesWindowObject *)self); } -#endif /* defined(HAVE_NCURSESW) */ - PyDoc_STRVAR(_curses_window_getch__doc__, "getch([y, x])\n" "Get a character code from terminal keyboard.\n" @@ -5503,14 +5487,6 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_WINDOW_ENCLOSE_METHODDEF #endif /* !defined(_CURSES_WINDOW_ENCLOSE_METHODDEF) */ -#ifndef _CURSES_WINDOW_IN_WCH_METHODDEF - #define _CURSES_WINDOW_IN_WCH_METHODDEF -#endif /* !defined(_CURSES_WINDOW_IN_WCH_METHODDEF) */ - -#ifndef _CURSES_WINDOW_GETBKGRND_METHODDEF - #define _CURSES_WINDOW_GETBKGRND_METHODDEF -#endif /* !defined(_CURSES_WINDOW_GETBKGRND_METHODDEF) */ - #ifndef _CURSES_WINDOW_GET_WCH_METHODDEF #define _CURSES_WINDOW_GET_WCH_METHODDEF #endif /* !defined(_CURSES_WINDOW_GET_WCH_METHODDEF) */ @@ -5666,4 +5642,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=976a6629bfe58a3d input=a9049054013a1b77]*/ +/*[clinic end generated code: output=bbf6d77a5813b1e1 input=a9049054013a1b77]*/