https://github.com/python/cpython/commit/95bfaffd522675287dc176d75a4d23e708435e6d
commit: 95bfaffd522675287dc176d75a4d23e708435e6d
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-28T05:22:43Z
summary:

gh-152233: Make the curses cell API work without ncursesw (ПР-152466)

Back complexchar, complexstr and the cell read methods (in_wch,
in_wchstr, in_wstr, getbkgrnd) with a chtype instead of a cchar_t when
ncursesw is absent, so the same code works on both builds.  A narrow
build is limited to one character per cell, encodable as a single byte in
the window's encoding (8-bit locales), with the color pair limited to the
color_pair() range.

Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
M Doc/library/curses.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_curses.py
M Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst
M Misc/NEWS.d/next/Library/2026-06-25-22-41-49.gh-issue-152233.pEhm3q.rst
M Misc/NEWS.d/next/Library/2026-06-26-11-20-00.gh-issue-152233.Kp7mQ2.rst
M Modules/_cursesmodule.c
M Modules/clinic/_cursesmodule.c.h

diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst
index 71eadfd5ca2ad70..d2f11949c04c3f8 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 90ab3ee630b165e..a87d708e3fcdc2f 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/test/test_curses.py b/Lib/test/test_curses.py
index efa0240f4e3f88a..7611771aa83e0cc 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
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 34db9698738c2b4..421c58298823c05 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 f7fc3df6064ade8..43895aafe6ce136 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 da9cf22a0dd50df..b905b512a8a12af 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/Modules/_cursesmodule.c b/Modules/_cursesmodule.c
index 5e50f9b1e0b2384..7b9de46cf22ab44 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 1c05a125fd1cc6c..f895090bef845ff 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=<unrepresentable>, pair=<unrepresentable>)\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]*/

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to