]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-152233: Add curses complexchar type and wide-character cell reads (GH-152250)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 26 Jun 2026 10:49:17 +0000 (13:49 +0300)
committerGitHub <noreply@github.com>
Fri, 26 Jun 2026 10:49:17 +0000 (13:49 +0300)
Add the immutable `curses.complexchar` type: a styled wide-character cell — a spacing character optionally followed by combining characters, plus attributes and a color pair. The color pair is stored separately rather than packed into a `chtype` via `COLOR_PAIR()`, so it is not limited to the values that fit alongside the attribute bits. `str(cc)` returns the text; the read-only `attr` and `pair` attributes return its rendition.

Add the window methods `in_wch()` and `getbkgrnd()` — the wide-character counterparts of `inch()` and `getbkgd()` — which return a `complexchar`. (`inch()`/`getbkgd()` can only return a packed `chtype`, so these fill a real gap; this resolves the long-standing gh-83395 request for `in_wch`.)

The existing character-cell methods (`addch`, `insch`, `echochar`, `bkgd`, `bkgdset`, `border`, `box`, `hline`, `vline`) now also accept a `complexchar`. A `complexchar` already carries its own rendition, so passing one together with an explicit `attr` argument raises `TypeError`.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Doc/library/curses.rst
Doc/whatsnew/3.16.rst
Include/internal/pycore_global_objects_fini_generated.h
Include/internal/pycore_global_strings.h
Include/internal/pycore_runtime_init_generated.h
Include/internal/pycore_unicodeobject_generated.h
Lib/test/test_curses.py
Misc/NEWS.d/next/Library/2026-06-25-22-41-49.gh-issue-152233.pEhm3q.rst [new file with mode: 0644]
Modules/_cursesmodule.c
Modules/clinic/_cursesmodule.c.h

index d7c2905ec7347da0d4b130223d2a639eaf72485f..945be3b51998c3460176969d2da54e4743e2f2d8 100644 (file)
@@ -941,7 +941,8 @@ Window objects
 
    .. versionchanged:: next
       A character may now be given as a string of a base character followed
-      by combining characters, instead of only a single character.
+      by combining characters, instead of only a single character, or as a
+      :class:`complexchar` cell.
 
 
 .. method:: window.addnstr(str, n[, attr])
@@ -1051,7 +1052,8 @@ Window objects
      background character.
 
    .. versionchanged:: next
-      Wide and combining characters are now accepted.
+      Wide and combining characters, and :class:`complexchar` cells, are now
+      accepted.
 
 
 .. method:: window.bkgdset(ch[, attr])
@@ -1064,7 +1066,8 @@ Window objects
    the character through any scrolling and insert/delete line/character operations.
 
    .. versionchanged:: next
-      Wide and combining characters are now accepted.
+      Wide and combining characters, and :class:`complexchar` cells, are now
+      accepted.
 
 
 .. method:: window.border([ls[, rs[, ts[, bs[, tl[, tr[, bl[, br]]]]]]]])
@@ -1100,7 +1103,8 @@ Window objects
    +-----------+---------------------+-----------------------+
 
    .. versionchanged:: next
-      Wide and combining characters are now accepted.  A single call cannot mix
+      Wide and combining characters, and :class:`complexchar` cells, are now
+      accepted.  A single call cannot mix
       them with integer or byte characters.
 
 
@@ -1110,7 +1114,8 @@ Window objects
    *bs* are *horch*.  The default corner characters are always used by this function.
 
    .. versionchanged:: next
-      Wide and combining characters are now accepted.  A single call cannot mix
+      Wide and combining characters, and :class:`complexchar` cells, are now
+      accepted.  A single call cannot mix
       them with integer or byte characters.
 
 
@@ -1182,7 +1187,8 @@ Window objects
    on the window.
 
    .. versionchanged:: next
-      Wide and combining characters are now accepted.
+      Wide and combining characters, and :class:`complexchar` cells, are now
+      accepted.
 
 
 .. method:: window.enclose(y, x)
@@ -1221,6 +1227,20 @@ Window objects
    Return the given window's current background character/attribute pair.
 
 
+.. method:: window.getbkgrnd()
+
+   Return the given window's current background as a :class:`complexchar`.
+   This is the wide-character variant of :meth:`getbkgd`: the returned object
+   carries the background character together with its attributes and color pair,
+   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
+
+
 .. method:: window.getch([y, x])
 
    Get a character. Note that the integer returned does *not* have to be in ASCII
@@ -1325,7 +1345,8 @@ Window objects
    of the window if fewer than *n* cells are available.
 
    .. versionchanged:: next
-      Wide and combining characters are now accepted.
+      Wide and combining characters, and :class:`complexchar` cells, are now
+      accepted.
 
 
 .. method:: window.idcok(flag)
@@ -1356,6 +1377,20 @@ Window objects
    the character proper, and upper bits are the attributes.
 
 
+.. method:: window.in_wch([y, x])
+
+   Return the complex character at the given position in the window as a
+   :class:`complexchar`.  This is the wide-character variant of :meth:`inch`:
+   the returned object carries the cell's text (a spacing character optionally
+   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
+
+
 .. method:: window.insch(ch[, attr])
             window.insch(y, x, ch[, attr])
 
@@ -1365,7 +1400,8 @@ Window objects
    line being lost.  The cursor position does not change.
 
    .. versionchanged:: next
-      Wide and combining characters are now accepted.
+      Wide and combining characters, and :class:`complexchar` cells, are now
+      accepted.
 
 
 .. method:: window.insdelln(nlines)
@@ -1776,7 +1812,8 @@ Window objects
    character *ch* with attributes *attr*.
 
    .. versionchanged:: next
-      Wide and combining characters are now accepted.
+      Wide and combining characters, and :class:`complexchar` cells, are now
+      accepted.
 
 
 .. _curses-screen-objects:
@@ -1833,6 +1870,49 @@ Screen objects
    .. versionadded:: next
 
 
+.. _curses-complexchar-objects:
+
+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
+   characters, together with a set of attributes and a color pair.
+
+   *text* is the cell's text, *attr* a combination of the
+   :ref:`WA_* attributes <curses-wa-constants>` (equivalent to the matching
+   ``A_*`` constants), and *pair* a color pair number.  Unlike the packed
+   :class:`chtype <int>` used by :meth:`~window.inch` and the ``A_*`` methods,
+   the color pair is stored separately and is not limited to the value that
+   fits in a :func:`color_pair`.
+
+   Complex characters are returned by :meth:`window.in_wch` and
+   :meth:`window.getbkgrnd`, and are accepted (along with an integer, a byte
+   or a string) by the character-cell methods such as :meth:`window.addch`,
+   :meth:`window.insch`, :meth:`window.bkgd`, :meth:`window.border`,
+   :meth:`window.hline` and :meth:`window.vline`.  A complex character already
+   carries its own rendition, so it cannot be combined with an explicit *attr*
+   argument.
+
+   :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.
+
+   .. attribute:: attr
+
+      The attributes of the character cell (read-only).
+
+   .. attribute:: pair
+
+      The color pair number of the character cell (read-only).
+
+   .. versionadded:: next
+
+
 Constants
 ---------
 
index 5123671db0c07eaf930f28bc23e461bb55ee44ba..c0b6a650b7b887f7fee80d214774e622b81df0ab 100644 (file)
@@ -138,6 +138,16 @@ curses
   attribute value, and the corresponding ``WA_*`` attribute constants.
   (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
+  methods :meth:`~curses.window.in_wch` and :meth:`~curses.window.getbkgrnd`
+  that return one --- the wide-character 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`.
+  (Contributed by Serhiy Storchaka in :gh:`152233`.)
+
 gzip
 ----
 
index 1cf766ddb382dd2120fed726e49fa13b20c8ed7a..87dde5e062bd7248626b5aa91020c407f8e2a548 100644 (file)
@@ -1597,6 +1597,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(asend));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ast));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(athrow));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(attr));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(attribute));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(autocommit));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(backtick));
@@ -1979,6 +1980,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pad));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(padded));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pages));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pair));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(parameter));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(parent));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(password));
index 017d62e002fdff94af2feafc7ff4c65e49ee195c..43286fa36bbb05cb559cab31fe1943c570a4441d 100644 (file)
@@ -320,6 +320,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(asend)
         STRUCT_FOR_ID(ast)
         STRUCT_FOR_ID(athrow)
+        STRUCT_FOR_ID(attr)
         STRUCT_FOR_ID(attribute)
         STRUCT_FOR_ID(autocommit)
         STRUCT_FOR_ID(backtick)
@@ -702,6 +703,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(pad)
         STRUCT_FOR_ID(padded)
         STRUCT_FOR_ID(pages)
+        STRUCT_FOR_ID(pair)
         STRUCT_FOR_ID(parameter)
         STRUCT_FOR_ID(parent)
         STRUCT_FOR_ID(password)
index 75273243ef6df061bdf01d79068f840f35e91c4a..116dbf84f790124f477ce6b48797589814e5d908 100644 (file)
@@ -1595,6 +1595,7 @@ extern "C" {
     INIT_ID(asend), \
     INIT_ID(ast), \
     INIT_ID(athrow), \
+    INIT_ID(attr), \
     INIT_ID(attribute), \
     INIT_ID(autocommit), \
     INIT_ID(backtick), \
@@ -1977,6 +1978,7 @@ extern "C" {
     INIT_ID(pad), \
     INIT_ID(padded), \
     INIT_ID(pages), \
+    INIT_ID(pair), \
     INIT_ID(parameter), \
     INIT_ID(parent), \
     INIT_ID(password), \
index 164d9d412ef5e95a738415368b3852d90ca6c2f4..f7197b39d9bd3ab958347d9bfae39b91d9436cb8 100644 (file)
@@ -1060,6 +1060,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(attr);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(attribute);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
@@ -2588,6 +2592,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(pair);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(parameter);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
index 75e6d2bd62e88719b4dce5b720764610fb8e17ef..bc3cc5cb3835177d23355e6327f848c47d850e16 100644 (file)
@@ -345,6 +345,36 @@ class TestCurses(unittest.TestCase):
         # border() and box() cannot mix integer and wide-string characters.
         self.assertRaises(TypeError, stdscr.box, vline, ord('-'))
 
+    @requires_curses_func('complexchar')
+    def test_complexchar_in_cell_methods(self):
+        # Every single-character-cell method also accepts a complexchar, whose
+        # attributes and color pair come from the cell itself.
+        stdscr = self.stdscr
+        cc = curses.complexchar('A', curses.A_BOLD)
+        v = curses.complexchar('|')
+        h = curses.complexchar('-')
+        stdscr.move(0, 0)
+        stdscr.addch(0, 0, cc)
+        self.assertEqual(str(stdscr.in_wch(0, 0)), 'A')
+        self.assertTrue(stdscr.in_wch(0, 0).attr & curses.A_BOLD)
+        stdscr.insch(1, 0, cc)
+        stdscr.echochar(cc)
+        stdscr.bkgdset(cc)
+        stdscr.bkgd(cc)
+        stdscr.hline(2, 0, h, 3)
+        stdscr.vline(3, 0, v, 3)
+        stdscr.border(v, v, h, h)
+        stdscr.box(v, h)
+        # A complexchar already carries its rendition, so combining it with an
+        # explicit attr argument is rejected.
+        self.assertRaises(TypeError, stdscr.addch, cc, curses.A_BOLD)
+        self.assertRaises(TypeError, stdscr.addch, 0, 0, cc, curses.A_BOLD)
+        self.assertRaises(TypeError, stdscr.insch, cc, curses.A_BOLD)
+        self.assertRaises(TypeError, stdscr.echochar, cc, curses.A_BOLD)
+        self.assertRaises(TypeError, stdscr.bkgd, cc, curses.A_BOLD)
+        self.assertRaises(TypeError, stdscr.bkgdset, cc, curses.A_BOLD)
+        self.assertRaises(TypeError, stdscr.hline, h, 3, curses.A_BOLD)
+        self.assertRaises(TypeError, stdscr.vline, v, 3, curses.A_BOLD)
 
     @requires_curses_window_meth('in_wstr')
     def test_in_wstr(self):
@@ -355,6 +385,90 @@ class TestCurses(unittest.TestCase):
         self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s)
         self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes)
 
+    @requires_curses_func('complexchar')
+    def test_complexchar(self):
+        # A complexchar is a styled wide-character cell: str() is its text,
+        # and the attr and pair attributes are its rendition.
+        cc = curses.complexchar('A', curses.A_BOLD)
+        self.assertEqual(str(cc), 'A')
+        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'):
+            self.assertEqual(str(curses.complexchar('e\u0301')), 'e\u0301')
+        # Defaults: no attributes, color pair 0.
+        cc = curses.complexchar('z')
+        self.assertEqual(str(cc), 'z')
+        self.assertEqual(cc.attr, 0)
+        self.assertEqual(cc.pair, 0)
+        # Immutable rendition.
+        self.assertRaises(AttributeError, setattr, cc, 'attr', 1)
+        self.assertRaises(AttributeError, setattr, cc, 'pair', 1)
+        # Equality and hashing compare text, attributes and color pair.
+        self.assertEqual(curses.complexchar('A', curses.A_BOLD),
+                         curses.complexchar('A', curses.A_BOLD))
+        self.assertEqual(hash(curses.complexchar('A', curses.A_BOLD)),
+                         hash(curses.complexchar('A', curses.A_BOLD)))
+        self.assertNotEqual(curses.complexchar('A'),
+                            curses.complexchar('A', curses.A_BOLD))
+        self.assertNotEqual(curses.complexchar('A'), curses.complexchar('B'))
+        # repr() shows only a non-default attr/pair, and is a constructor call.
+        ns = {'_curses': sys.modules[type(cc).__module__]}
+        self.assertNotIn('attr=', repr(curses.complexchar('z')))
+        self.assertNotIn('pair=', repr(curses.complexchar('z')))
+        r = repr(curses.complexchar('A', curses.A_BOLD))
+        self.assertIn('attr=', r)
+        self.assertNotIn('pair=', r)
+        self.assertEqual(eval(r, ns), curses.complexchar('A', curses.A_BOLD))
+        # Invalid arguments.
+        self.assertRaises(TypeError, curses.complexchar, 65)
+        self.assertRaises(TypeError, curses.complexchar, 'A', 'bold')
+        self.assertRaises(OverflowError, curses.complexchar, 'A', -1)
+        self.assertRaises(OverflowError, curses.complexchar, 'A', 1 << 64)
+        self.assertRaises(ValueError, curses.complexchar, 'A', 0, -1)
+        self.assertRaises(ValueError, curses.complexchar, 'ab')
+
+    @requires_curses_window_meth('in_wch')
+    def test_in_wch(self):
+        # in_wch() returns the styled wide cell as a complexchar -- something
+        # inch() (a packed chtype) cannot represent.
+        stdscr = self.stdscr
+        stdscr.addch(0, 0, curses.complexchar('A', curses.A_UNDERLINE))
+        cc = stdscr.in_wch(0, 0)
+        self.assertEqual(str(cc), 'A')
+        self.assertTrue(cc.attr & curses.A_UNDERLINE)
+        if self._encodable('\u00e9'):  # precomposed, for a portable round-trip
+            stdscr.addch(3, 0, curses.complexchar('\u00e9'))
+            self.assertEqual(str(stdscr.in_wch(3, 0)), '\u00e9')
+        # in_wch() without coordinates reads at the cursor position.
+        stdscr.move(0, 0)
+        self.assertEqual(str(stdscr.in_wch()), 'A')
+
+    @requires_curses_window_meth('in_wch')
+    @requires_colors
+    def test_in_wch_color(self):
+        # Unlike the chtype methods (which pack the pair into the value via
+        # COLOR_PAIR), a complex character carries its color pair separately.
+        stdscr = self.stdscr
+        curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
+        stdscr.addch(0, 0, curses.complexchar('A', curses.A_BOLD, 1))
+        cc = stdscr.in_wch(0, 0)
+        self.assertEqual(str(cc), 'A')
+        self.assertTrue(cc.attr & curses.A_BOLD)
+        self.assertEqual(cc.pair, 1)
+        self.assertEqual(curses.complexchar('A', 0, 1).pair, 1)
+
+    @requires_curses_window_meth('getbkgrnd')
+    def test_getbkgrnd(self):
+        # getbkgrnd() returns the background as a complexchar (getbkgd() can
+        # only return a packed chtype).
+        stdscr = self.stdscr
+        stdscr.bkgdset(curses.complexchar(' ', curses.A_DIM))
+        stdscr.bkgd(curses.complexchar(' ', curses.A_BOLD))
+        cc = stdscr.getbkgrnd()
+        self.assertEqual(str(cc), ' ')
+        self.assertTrue(cc.attr & curses.A_BOLD)
+
 
     def test_output_character(self):
         stdscr = self.stdscr
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
new file mode 100644 (file)
index 0000000..f7fc3df
--- /dev/null
@@ -0,0 +1,7 @@
+Add the :class:`curses.complexchar` type, representing a styled wide-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`.
index 0306c4af3288dc00cb6f53f277a59b08209d2496..96e1fdc3bfe5a5d4d3cabe559130191a4db0315d 100644 (file)
@@ -165,6 +165,9 @@ 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
+#endif
     PyObject *topscreen;            // owned ref to the current screen object,
                                     // or NULL for the initscr() screen
 } cursesmodule_state;
@@ -198,8 +201,9 @@ get_cursesmodule_state_by_win(PyCursesWindowObject *win)
 module _curses
 class _curses.window "PyCursesWindowObject *" "clinic_state()->window_type"
 class _curses.screen "PyCursesScreenObject *" "clinic_state()->screen_type"
+class _curses.complexchar "PyCursesComplexCharObject *" "clinic_state()->complexchar_type"
 [clinic start generated code]*/
-/*[clinic end generated code: output=da39a3ee5e6b4b0d input=4b027ab105ab94e1]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=211a02287a60aed0]*/
 
 /* Indicate whether the module has already been loaded or not. */
 static int curses_module_loaded = 0;
@@ -513,6 +517,48 @@ overflow:
     - 2 if obj is a character (written into *wch)
     - 1 if obj is a byte (written into *ch)
     - 0 on error: raise an exception */
+#ifdef HAVE_NCURSESW
+/* Convert a str -- a spacing character optionally followed by up to
+   CCHARW_MAX - 1 combining characters -- into a wide-character cell.
+   wch must point to a buffer of at least CCHARW_MAX + 1 wide characters.
+   Return 0 on success, -1 with an exception set on error. */
+static int
+PyCurses_ConvertToWideCell(PyObject *obj, wchar_t *wch)
+{
+    assert(PyUnicode_Check(obj));
+    Py_ssize_t nch = PyUnicode_AsWideChar(obj, wch, CCHARW_MAX + 1);
+    if (nch < 0) {
+        return -1;
+    }
+    if (nch == 0 || nch > CCHARW_MAX) {
+        PyErr_Format(PyExc_TypeError,
+                     "expect a string of 1 to %d characters, "
+                     "got a str of length %zi",
+                     (int)CCHARW_MAX, PyUnicode_GET_LENGTH(obj));
+        return -1;
+    }
+    /* A lone control character is allowed (like addch(ord('\n'))), but in a
+       multi-character cell the base must be a printable spacing character and
+       the rest zero-width combining characters.  Check explicitly: otherwise
+       setcchar() would silently drop a trailing spacing character, or fail
+       with a generic error for a control-character base. */
+    if (nch > 1) {
+        int bad = wcwidth(wch[0]) < 0;
+        for (Py_ssize_t i = 1; !bad && i < nch; i++) {
+            bad = wcwidth(wch[i]) != 0;
+        }
+        if (bad) {
+            PyErr_Format(PyExc_ValueError,
+                         "a character cell must be a single spacing character "
+                         "optionally followed by up to %d combining characters",
+                         (int)(CCHARW_MAX - 1));
+            return -1;
+        }
+    }
+    return 0;
+}
+#endif
+
 static int
 PyCurses_ConvertToCchar_t(PyCursesWindowObject *win, PyObject *obj,
                           chtype *ch
@@ -525,40 +571,9 @@ PyCurses_ConvertToCchar_t(PyCursesWindowObject *win, PyObject *obj,
 
     if (PyUnicode_Check(obj)) {
 #ifdef HAVE_NCURSESW
-        /* A character cell may hold a spacing character plus up to
-           CCHARW_MAX - 1 combining characters; wch must point to a buffer
-           of at least CCHARW_MAX + 1 wide characters. */
-        Py_ssize_t nch = PyUnicode_AsWideChar(obj, wch, CCHARW_MAX + 1);
-        if (nch < 0) {
-            return 0;
-        }
-        if (nch == 0 || nch > CCHARW_MAX) {
-            PyErr_Format(PyExc_TypeError,
-                         "expect int or bytes or a string of 1 to %d "
-                         "characters, got a str of length %zi",
-                         (int)CCHARW_MAX, PyUnicode_GET_LENGTH(obj));
+        if (PyCurses_ConvertToWideCell(obj, wch) < 0) {
             return 0;
         }
-        /* A character cell is a single spacing character optionally followed
-           by combining characters.  A lone control character is still allowed
-           (like addch(ord('\n'))), but in a multi-character cell the base must
-           be a printable character and the rest must be zero-width combining
-           characters.  Validate this explicitly: otherwise setcchar() would
-           silently drop a trailing spacing character, or fail with a generic
-           error for a control character used as the base. */
-        if (nch > 1) {
-            int bad = wcwidth(wch[0]) < 0;
-            for (Py_ssize_t i = 1; !bad && i < nch; i++) {
-                bad = wcwidth(wch[i]) != 0;
-            }
-            if (bad) {
-                PyErr_SetString(PyExc_ValueError,
-                                "a character cell must be a single spacing "
-                                "character optionally followed by combining "
-                                "characters");
-                return 0;
-            }
-        }
         return 2;
 #else
         return PyCurses_ConvertToChtype(win, obj, ch);
@@ -647,20 +662,37 @@ PyCurses_ConvertToString(PyCursesWindowObject *win, PyObject *obj,
 }
 
 #ifdef HAVE_NCURSESW
+typedef struct {
+    PyObject_HEAD
+    cchar_t cval;
+} PyCursesComplexCharObject;
+
+#define _PyCursesComplexCharObject_CAST(op)  ((PyCursesComplexCharObject *)(op))
+
 /* Build a single character cell from obj.
 
-   On success return 1 and store the raw chtype (without *attr*) in *pch when
-   obj is an int or bytes, or return 2 and store a cchar_t (with *attr*
-   applied) in *pwc when obj is a str -- a spacing character optionally
-   followed by combining characters.  Return 0 and set an exception on error.
+   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.
 
-   This lets a method use the wide *_set functions (which accept combining
-   characters) for string arguments while still accepting integer chtype
-   values. */
+   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, long attr,
-                       const char *funcname, chtype *pch, cchar_t *pwc)
+                       int attr_given, const char *funcname,
+                       chtype *pch, cchar_t *pwc)
 {
+    cursesmodule_state *state = get_cursesmodule_state_by_win(win);
+    if (Py_IS_TYPE(obj, state->complexchar_type)) {
+        if (attr_given) {
+            PyErr_Format(PyExc_TypeError,
+                         "%s(): attr cannot be specified together with "
+                         "a complexchar", funcname);
+            return 0;
+        }
+        *pwc = _PyCursesComplexCharObject_CAST(obj)->cval;
+        return 2;
+    }
     wchar_t wstr[CCHARW_MAX + 1];
     int type = PyCurses_ConvertToCchar_t(win, obj, pch, wstr);
     if (type == 2) {
@@ -671,6 +703,69 @@ PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, long attr,
     }
     return type;
 }
+
+/* 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
+   short that setcchar() takes; raise OverflowError instead of silently
+   truncating a larger one. */
+static int
+curses_setcchar(cchar_t *wcval, const wchar_t *wstr, attr_t attrs, int pair)
+{
+#if _NCURSES_EXTENDED_COLOR_FUNCS
+    /* The pair passed through the opts slot is authoritative and may exceed
+       a short; ncurses then ignores the short argument, but clamp it into
+       range so the int-to-short narrowing stays well-defined. */
+    short spair = pair <= SHRT_MAX ? (short)pair : SHRT_MAX;
+    return setcchar(wcval, wstr, attrs, spair, &pair);
+#else
+    if (pair > SHRT_MAX) {
+        PyErr_Format(PyExc_OverflowError,
+                     "color pair %d does not fit in a short", pair);
+        return ERR;
+    }
+    return setcchar(wcval, wstr, attrs, (short)pair, NULL);
+#endif
+}
+
+/* Unpack a wide-character cell into its text, attributes and color pair.
+   The pair is read through the extended-color opts slot when available, so
+   values above SHRT_MAX are preserved. */
+static int
+curses_getcchar(const cchar_t *wcval, wchar_t *wstr, attr_t *attrs, int *pair)
+{
+    short spair = 0;
+#if _NCURSES_EXTENDED_COLOR_FUNCS
+    int rtn = getcchar(wcval, wstr, attrs, &spair, pair);
+#else
+    int rtn = getcchar(wcval, wstr, attrs, &spair, NULL);
+    if (rtn != ERR) {
+        *pair = spair;
+    }
+#endif
+    return rtn;
+}
+
+/* Hash one cell by value (text, attributes, pair) -- consistent with the
+   equality comparison, not the raw cchar_t whose padding and unused text tail
+   it ignores.  Zero the key first so those bytes are deterministic, then
+   unpack into it.  Returns -1 with an exception set on the (practically
+   impossible) getcchar() failure. */
+static Py_hash_t
+curses_cchar_hash(cursesmodule_state *state, const cchar_t *cell)
+{
+    struct {
+        attr_t attrs;
+        int pair;
+        wchar_t wstr[CCHARW_MAX + 1];
+    } key;
+    memset(&key, 0, sizeof(key));
+    if (curses_getcchar(cell, key.wstr, &key.attrs, &key.pair) == ERR) {
+        PyErr_SetString(state->error, "getcchar() returned ERR");
+        return -1;
+    }
+    return Py_HashBuffer(&key, sizeof(key));
+}
 #endif
 
 static int
@@ -825,6 +920,205 @@ class attr_converter(CConverter):
 [python start generated code]*/
 /*[python end generated code: output=da39a3ee5e6b4b0d input=6132d3d99d3ec25a]*/
 
+#ifdef HAVE_NCURSESW
+/* -------------------------------------------------------*/
+/* Complex character objects (styled wide-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. */
+static PyObject *
+PyCursesComplexChar_New(cursesmodule_state *state, const cchar_t *wcval)
+{
+    PyCursesComplexCharObject *cc =
+        PyObject_New(PyCursesComplexCharObject, state->complexchar_type);
+    if (cc == NULL) {
+        return NULL;
+    }
+    cc->cval = *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
+
+    text: unicode
+        A spacing character optionally followed by combining characters.
+    /
+    attr: attr = 0
+        The attributes of the character cell.
+    pair: int = 0
+        The color pair number of the character cell.
+
+A styled wide-character cell.
+
+text is a spacing character optionally followed by combining
+characters.  attr is a set of attributes (the WA_* constants) and pair
+is a color pair number.  The object is immutable; str(cc) returns its
+text, and the attr and pair attributes return its rendition.
+[clinic start generated code]*/
+
+static PyObject *
+complexchar_new_impl(PyTypeObject *type, PyObject *text, attr_t attr,
+                     int pair)
+/*[clinic end generated code: output=5d8173048826b946 input=c3f3466a2656a196]*/
+{
+    if (pair < 0) {
+        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");
+        }
+        return NULL;
+    }
+    PyCursesComplexCharObject *cc =
+        (PyCursesComplexCharObject *)type->tp_alloc(type, 0);
+    if (cc == NULL) {
+        return NULL;
+    }
+    cc->cval = cval;
+    return (PyObject *)cc;
+}
+
+static void
+complexchar_dealloc(PyObject *self)
+{
+    PyTypeObject *tp = Py_TYPE(self);
+    tp->tp_free(self);
+    Py_DECREF(tp);
+}
+
+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);
+}
+
+static PyObject *
+complexchar_repr(PyObject *self)
+{
+    wchar_t wstr[CCHARW_MAX + 1];
+    attr_t attrs;
+    int pair;
+    if (complexchar_unpack(self, wstr, &attrs, &pair) < 0) {
+        return NULL;
+    }
+    PyObject *text = PyUnicode_FromWideChar(wstr, -1);
+    if (text == NULL) {
+        return NULL;
+    }
+    /* Show attr and pair only when not at their defaults. */
+    PyObject *res;
+    if (attrs == 0 && pair == 0) {
+        res = PyUnicode_FromFormat("%T(%R)", self, text);
+    }
+    else if (pair == 0) {
+        res = PyUnicode_FromFormat("%T(%R, attr=%lu)", self, text,
+                                   (unsigned long)attrs);
+    }
+    else if (attrs == 0) {
+        res = PyUnicode_FromFormat("%T(%R, pair=%d)", self, text, pair);
+    }
+    else {
+        res = PyUnicode_FromFormat("%T(%R, attr=%lu, pair=%d)", self, text,
+                                   (unsigned long)attrs, pair);
+    }
+    Py_DECREF(text);
+    return res;
+}
+
+static PyObject *
+complexchar_get_attr(PyObject *self, void *Py_UNUSED(closure))
+{
+    wchar_t wstr[CCHARW_MAX + 1];
+    attr_t attrs;
+    int pair;
+    if (complexchar_unpack(self, wstr, &attrs, &pair) < 0) {
+        return NULL;
+    }
+    return PyLong_FromUnsignedLong((unsigned long)attrs);
+}
+
+static PyObject *
+complexchar_get_pair(PyObject *self, void *Py_UNUSED(closure))
+{
+    wchar_t wstr[CCHARW_MAX + 1];
+    attr_t attrs;
+    int pair;
+    if (complexchar_unpack(self, wstr, &attrs, &pair) < 0) {
+        return NULL;
+    }
+    return PyLong_FromLong(pair);
+}
+
+static PyObject *
+complexchar_richcompare(PyObject *self, PyObject *other, int op)
+{
+    if ((op != Py_EQ && op != Py_NE) ||
+        !Py_IS_TYPE(other, Py_TYPE(self)))
+    {
+        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)
+    {
+        return NULL;
+    }
+    int equal = (attrs1 == attrs2 && pair1 == pair2 &&
+                 wcscmp(wstr1, wstr2) == 0);
+    return PyBool_FromLong(equal == (op == Py_EQ));
+}
+
+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);
+}
+
+static PyGetSetDef complexchar_getsets[] = {
+    {"attr", complexchar_get_attr, NULL,
+     PyDoc_STR("the attributes of the character cell"), NULL},
+    {"pair", complexchar_get_pair, NULL,
+     PyDoc_STR("the color pair of the character cell"), NULL},
+    {NULL}
+};
+
+#endif
+
 /*****************************************************************************
  The Window Object
 ******************************************************************************/
@@ -1103,7 +1397,7 @@ _curses.window.addch
         Character to add.
 
     [
-    attr: long(c_default="A_NORMAL") = _curses.A_NORMAL
+    attr: long
         Attributes for the character.
     ]
     /
@@ -1120,26 +1414,21 @@ static PyObject *
 _curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1,
                           int y, int x, PyObject *ch, int group_right_1,
                           long attr)
-/*[clinic end generated code: output=00f4c37af3378f45 input=95ce131578458196]*/
+/*[clinic end generated code: output=00f4c37af3378f45 input=ab196a1dac3d354c]*/
 {
     int coordinates_group = group_left_1;
     int rtn;
     int type;
     chtype cch = 0;
 #ifdef HAVE_NCURSESW
-    wchar_t wstr[CCHARW_MAX + 1];
     cchar_t wcval;
 #endif
     const char *funcname;
 
 #ifdef HAVE_NCURSESW
-    type = PyCurses_ConvertToCchar_t(self, ch, &cch, wstr);
+    type = PyCurses_ConvertToCell(self, ch, attr, group_right_1, "addch",
+                                  &cch, &wcval);
     if (type == 2) {
-        rtn = setcchar(&wcval, wstr, attr, PAIR_NUMBER(attr), NULL);
-        if (rtn == ERR) {
-            curses_window_set_error(self, "setcchar", "addch");
-            return NULL;
-        }
         if (coordinates_group) {
             rtn = mvwadd_wch(self->win,y,x, &wcval);
             funcname = "mvwadd_wch";
@@ -1394,21 +1683,25 @@ _curses.window.bkgd
 
     ch: object
         Background character.
-    attr: long(c_default="A_NORMAL") = _curses.A_NORMAL
+    [
+    attr: long
         Background attributes.
+    ]
     /
 
 Set the background property of the window.
 [clinic start generated code]*/
 
 static PyObject *
-_curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch, long attr)
-/*[clinic end generated code: output=058290afb2cf4034 input=634015bcb339283d]*/
+_curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch,
+                         int group_right_1, long attr)
+/*[clinic end generated code: output=73cb11ecca59612f input=a2129c1b709db432]*/
 {
     chtype bkgd;
 #ifdef HAVE_NCURSESW
     cchar_t wch;
-    int type = PyCurses_ConvertToCell(self, ch, attr, "bkgd", &bkgd, &wch);
+    int type = PyCurses_ConvertToCell(self, ch, attr, group_right_1,
+                                      "bkgd", &bkgd, &wch);
     if (type == 0) {
         return NULL;
     }
@@ -1600,8 +1893,10 @@ _curses.window.bkgdset
 
     ch: object
         Background character.
-    attr: long(c_default="A_NORMAL") = _curses.A_NORMAL
+    [
+    attr: long
         Background attributes.
+    ]
     /
 
 Set the window's background.
@@ -1609,13 +1904,14 @@ Set the window's background.
 
 static PyObject *
 _curses_window_bkgdset_impl(PyCursesWindowObject *self, PyObject *ch,
-                            long attr)
-/*[clinic end generated code: output=8cb994fc4d7e2496 input=e09c682425c9e45b]*/
+                            int group_right_1, long attr)
+/*[clinic end generated code: output=3c32f2de5685a482 input=1f0811b24af821ca]*/
 {
     chtype bkgd;
 #ifdef HAVE_NCURSESW
     cchar_t wch;
-    int type = PyCurses_ConvertToCell(self, ch, attr, "bkgdset", &bkgd, &wch);
+    int type = PyCurses_ConvertToCell(self, ch, attr, group_right_1,
+                                      "bkgdset", &bkgd, &wch);
     if (type == 0) {
         return NULL;
     }
@@ -1684,7 +1980,7 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls,
     for (i = 0; i < 8; i++) {
         types[i] = 0;
         if (objs[i] != NULL) {
-            types[i] = PyCurses_ConvertToCell(self, objs[i], A_NORMAL,
+            types[i] = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, 0,
                                               "border", &ch[i], &wch[i]);
             if (types[i] == 0) {
                 return NULL;
@@ -1755,11 +2051,11 @@ _curses_window_box_impl(PyCursesWindowObject *self, int group_right_1,
     cchar_t wch1, wch2;
     int t1 = 0, t2 = 0;
     if (group_right_1) {
-        t1 = PyCurses_ConvertToCell(self, verch, A_NORMAL, "box", &ch1, &wch1);
+        t1 = PyCurses_ConvertToCell(self, verch, A_NORMAL, 0, "box", &ch1, &wch1);
         if (t1 == 0) {
             return NULL;
         }
-        t2 = PyCurses_ConvertToCell(self, horch, A_NORMAL, "box", &ch2, &wch2);
+        t2 = PyCurses_ConvertToCell(self, horch, A_NORMAL, 0, "box", &ch2, &wch2);
         if (t2 == 0) {
             return NULL;
         }
@@ -1970,8 +2266,10 @@ _curses.window.echochar
     ch: object
         Character to add.
 
-    attr: long(c_default="A_NORMAL") = _curses.A_NORMAL
+    [
+    attr: long
         Attributes for the character.
+    ]
     /
 
 Add character ch with attribute attr, and refresh.
@@ -1979,13 +2277,14 @@ Add character ch with attribute attr, and refresh.
 
 static PyObject *
 _curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch,
-                             long attr)
-/*[clinic end generated code: output=13e7dd875d4b9642 input=e7f34b964e92b156]*/
+                             int group_right_1, long attr)
+/*[clinic end generated code: output=f42da9e200c935e5 input=26e16855ec1b0e78]*/
 {
     chtype ch_;
 #ifdef HAVE_NCURSESW
     cchar_t wch;
-    int type = PyCurses_ConvertToCell(self, ch, attr, "echochar", &ch_, &wch);
+    int type = PyCurses_ConvertToCell(self, ch, attr, group_right_1,
+                                      "echochar", &ch_, &wch);
     if (type == 0) {
         return NULL;
     }
@@ -2066,6 +2365,69 @@ _curses_window_getbkgd_impl(PyCursesWindowObject *self)
     return PyLong_FromLong(rtn);
 }
 
+#ifdef HAVE_NCURSESW
+/*[clinic input]
+_curses.window.in_wch
+
+    [
+    y: int
+        Y-coordinate.
+    x: int
+        X-coordinate.
+    ]
+    /
+
+Return the complex character at the given position in the window.
+
+The returned object is a complexchar carrying the cell's text,
+attributes and color pair.
+[clinic start generated code]*/
+
+static PyObject *
+_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;
+    int rtn;
+    const char *funcname;
+    if (group_right_1) {
+        rtn = mvwin_wch(self->win, y, x, &wcval);
+        funcname = "mvwin_wch";
+    }
+    else {
+        rtn = win_wch(self->win, &wcval);
+        funcname = "win_wch";
+    }
+    if (rtn == ERR) {
+        curses_window_set_error(self, funcname, "in_wch");
+        return NULL;
+    }
+    cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+    return PyCursesComplexChar_New(state, &wcval);
+}
+
+/*[clinic input]
+_curses.window.getbkgrnd
+
+Return the window's current background complex character.
+[clinic start generated code]*/
+
+static PyObject *
+_curses_window_getbkgrnd_impl(PyCursesWindowObject *self)
+/*[clinic end generated code: output=afec19cad00eff71 input=e06bf3d6bf90d2ec]*/
+{
+    cchar_t wcval;
+    if (wgetbkgrnd(self->win, &wcval) == ERR) {
+        curses_window_set_error(self, "wgetbkgrnd", "getbkgrnd");
+        return NULL;
+    }
+    cursesmodule_state *state = get_cursesmodule_state_by_win(self);
+    return PyCursesComplexChar_New(state, &wcval);
+}
+
+#endif /* HAVE_NCURSESW */
+
 static PyObject *
 curses_check_signals_on_input_error(PyCursesWindowObject *self,
                                     const char *curses_funcname,
@@ -2343,7 +2705,7 @@ _curses.window.hline
         Line length.
 
     [
-    attr: long(c_default="A_NORMAL") = _curses.A_NORMAL
+    attr: long
         Attributes for the characters.
     ]
     /
@@ -2355,12 +2717,13 @@ static PyObject *
 _curses_window_hline_impl(PyCursesWindowObject *self, int group_left_1,
                           int y, int x, PyObject *ch, int n,
                           int group_right_1, long attr)
-/*[clinic end generated code: output=c00d489d61fc9eef input=81a4dea47268163e]*/
+/*[clinic end generated code: output=c00d489d61fc9eef input=924f8c28521bc2ec]*/
 {
     chtype ch_;
 #ifdef HAVE_NCURSESW
     cchar_t wch;
-    int type = PyCurses_ConvertToCell(self, ch, attr, "hline", &ch_, &wch);
+    int type = PyCurses_ConvertToCell(self, ch, attr, group_right_1, "hline",
+                                      &ch_, &wch);
     if (type == 0) {
         return NULL;
     }
@@ -2398,7 +2761,7 @@ _curses.window.insch
         Character to insert.
 
     [
-    attr: long(c_default="A_NORMAL") = _curses.A_NORMAL
+    attr: long
         Attributes for the character.
     ]
     /
@@ -2413,14 +2776,15 @@ static PyObject *
 _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1,
                           int y, int x, PyObject *ch, int group_right_1,
                           long attr)
-/*[clinic end generated code: output=ade8cfe3a3bf3e34 input=d662a0f96f33e15a]*/
+/*[clinic end generated code: output=ade8cfe3a3bf3e34 input=47d2989159ae6ca7]*/
 {
     int rtn;
     chtype ch_ = 0;
     const char *funcname;
 #ifdef HAVE_NCURSESW
     cchar_t wch;
-    int type = PyCurses_ConvertToCell(self, ch, attr, "insch", &ch_, &wch);
+    int type = PyCurses_ConvertToCell(self, ch, attr, group_right_1, "insch",
+                                      &ch_, &wch);
     if (type == 0) {
         return NULL;
     }
@@ -3346,7 +3710,7 @@ _curses.window.vline
         Line length.
 
     [
-    attr: long(c_default="A_NORMAL") = _curses.A_NORMAL
+    attr: long
         Attributes for the character.
     ]
     /
@@ -3358,12 +3722,13 @@ static PyObject *
 _curses_window_vline_impl(PyCursesWindowObject *self, int group_left_1,
                           int y, int x, PyObject *ch, int n,
                           int group_right_1, long attr)
-/*[clinic end generated code: output=287ad1cc8982217f input=a6f2dc86a4648b32]*/
+/*[clinic end generated code: output=287ad1cc8982217f input=1d4aa27ff0309bbc]*/
 {
     chtype ch_;
 #ifdef HAVE_NCURSESW
     cchar_t wch;
-    int type = PyCurses_ConvertToCell(self, ch, attr, "vline", &ch_, &wch);
+    int type = PyCurses_ConvertToCell(self, ch, attr, group_right_1, "vline",
+                                      &ch_, &wch);
     if (type == 0) {
         return NULL;
     }
@@ -3430,6 +3795,29 @@ 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},
+    {Py_tp_dealloc, complexchar_dealloc},
+    {Py_tp_repr, complexchar_repr},
+    {Py_tp_str, complexchar_str},
+    {Py_tp_richcompare, complexchar_richcompare},
+    {Py_tp_hash, complexchar_hash},
+    {Py_tp_getset, complexchar_getsets},
+    {0, NULL}
+};
+
+static PyType_Spec PyCursesComplexChar_Type_spec = {
+    .name = "_curses.complexchar",
+    .basicsize = sizeof(PyCursesComplexCharObject),
+    .flags = Py_TPFLAGS_DEFAULT
+        | Py_TPFLAGS_IMMUTABLETYPE
+        | Py_TPFLAGS_HEAPTYPE,
+    .slots = PyCursesComplexChar_Type_slots
+};
+#endif
 #undef clinic_state
 
 #if defined(HAVE_CURSES_USE_SCREEN) || defined(HAVE_CURSES_USE_WINDOW)
@@ -3562,6 +3950,7 @@ static PyMethodDef PyCursesWindow_methods[] = {
      "getbegyx($self, /)\n--\n\n"
      "Return a tuple (y, x) of the upper-left corner coordinates."},
     _CURSES_WINDOW_GETBKGD_METHODDEF
+    _CURSES_WINDOW_GETBKGRND_METHODDEF
     _CURSES_WINDOW_GETCH_METHODDEF
     _CURSES_WINDOW_GETKEY_METHODDEF
     _CURSES_WINDOW_GET_WCH_METHODDEF
@@ -3613,6 +4002,7 @@ static PyMethodDef PyCursesWindow_methods[] = {
      "If flag is true, refresh the window on every change to it."},
 #endif
     _CURSES_WINDOW_INCH_METHODDEF
+    _CURSES_WINDOW_IN_WCH_METHODDEF
     _CURSES_WINDOW_INSCH_METHODDEF
     {"insdelln", PyCursesWindow_winsdelln, METH_VARARGS,
      "insdelln($self, nlines, /)\n--\n\n"
@@ -6846,6 +7236,9 @@ 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);
+#endif
     Py_VISIT(state->topscreen);
     return 0;
 }
@@ -6857,6 +7250,9 @@ 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);
+#endif
     Py_CLEAR(state->topscreen);
     return 0;
 }
@@ -6898,6 +7294,16 @@ 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) {
+        return -1;
+    }
+    if (PyModule_AddType(module, state->complexchar_type) < 0) {
+        return -1;
+    }
+#endif
 
     /* Add some symbolic constants to the module */
     PyObject *module_dict = PyModule_GetDict(module);
index d4d6e4eeef01584df1168e796f2445071624787d..4a87ec3e215e995ccfacd35487f70c1ccb641200 100644 (file)
@@ -6,10 +6,107 @@ preserve
 #  include "pycore_gc.h"          // PyGC_Head
 #  include "pycore_runtime.h"     // _Py_ID()
 #endif
-#include "pycore_modsupport.h"    // _PyArg_CheckPositional()
+#include "pycore_modsupport.h"    // _PyArg_UnpackKeywords()
+
+#if defined(HAVE_NCURSESW)
+
+PyDoc_STRVAR(complexchar_new__doc__,
+"complexchar(text, /, attr=0, pair=0)\n"
+"--\n"
+"\n"
+"A styled wide-character cell.\n"
+"\n"
+"  text\n"
+"    A spacing character optionally followed by combining characters.\n"
+"  attr\n"
+"    The attributes of the character cell.\n"
+"  pair\n"
+"    The color pair number of the character cell.\n"
+"\n"
+"text is a spacing character optionally followed by combining\n"
+"characters.  attr is a set of attributes (the WA_* constants) and pair\n"
+"is a color pair number.  The object is immutable; str(cc) returns its\n"
+"text, and the attr and pair attributes return its rendition.");
+
+static PyObject *
+complexchar_new_impl(PyTypeObject *type, PyObject *text, attr_t attr,
+                     int pair);
+
+static PyObject *
+complexchar_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 2
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { &_Py_ID(attr), &_Py_ID(pair), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"", "attr", "pair", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "complexchar",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[3];
+    PyObject * const *fastargs;
+    Py_ssize_t nargs = PyTuple_GET_SIZE(args);
+    Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1;
+    PyObject *text;
+    attr_t attr = 0;
+    int pair = 0;
+
+    fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser,
+            /*minpos*/ 1, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!fastargs) {
+        goto exit;
+    }
+    if (!PyUnicode_Check(fastargs[0])) {
+        _PyArg_BadArgument("complexchar", "argument 1", "str", fastargs[0]);
+        goto exit;
+    }
+    text = fastargs[0];
+    if (!noptargs) {
+        goto skip_optional_pos;
+    }
+    if (fastargs[1]) {
+        if (!attr_converter(fastargs[1], &attr)) {
+            goto exit;
+        }
+        if (!--noptargs) {
+            goto skip_optional_pos;
+        }
+    }
+    pair = PyLong_AsInt(fastargs[2]);
+    if (pair == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+skip_optional_pos:
+    return_value = complexchar_new_impl(type, text, attr, pair);
+
+exit:
+    return return_value;
+}
+
+#endif /* defined(HAVE_NCURSESW) */
 
 PyDoc_STRVAR(_curses_window_addch__doc__,
-"addch([y, x,] ch, [attr=_curses.A_NORMAL])\n"
+"addch([y, x,] ch, [attr])\n"
 "Paint the character.\n"
 "\n"
 "  y\n"
@@ -43,7 +140,7 @@ _curses_window_addch(PyObject *self, PyObject *args)
     int x = 0;
     PyObject *ch;
     int group_right_1 = 0;
-    long attr = A_NORMAL;
+    long attr = 0;
 
     switch (PyTuple_GET_SIZE(args)) {
         case 1:
@@ -228,9 +325,7 @@ exit:
 }
 
 PyDoc_STRVAR(_curses_window_bkgd__doc__,
-"bkgd($self, ch, attr=_curses.A_NORMAL, /)\n"
-"--\n"
-"\n"
+"bkgd(ch, [attr])\n"
 "Set the background property of the window.\n"
 "\n"
 "  ch\n"
@@ -239,31 +334,37 @@ PyDoc_STRVAR(_curses_window_bkgd__doc__,
 "    Background attributes.");
 
 #define _CURSES_WINDOW_BKGD_METHODDEF    \
-    {"bkgd", _PyCFunction_CAST(_curses_window_bkgd), METH_FASTCALL, _curses_window_bkgd__doc__},
+    {"bkgd", (PyCFunction)_curses_window_bkgd, METH_VARARGS, _curses_window_bkgd__doc__},
 
 static PyObject *
-_curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch, long attr);
+_curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch,
+                         int group_right_1, long attr);
 
 static PyObject *
-_curses_window_bkgd(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
+_curses_window_bkgd(PyObject *self, PyObject *args)
 {
     PyObject *return_value = NULL;
     PyObject *ch;
-    long attr = A_NORMAL;
+    int group_right_1 = 0;
+    long attr = 0;
 
-    if (!_PyArg_CheckPositional("bkgd", nargs, 1, 2)) {
-        goto exit;
-    }
-    ch = args[0];
-    if (nargs < 2) {
-        goto skip_optional;
-    }
-    attr = PyLong_AsLong(args[1]);
-    if (attr == -1 && PyErr_Occurred()) {
-        goto exit;
+    switch (PyTuple_GET_SIZE(args)) {
+        case 1:
+            if (!PyArg_ParseTuple(args, "O:bkgd", &ch)) {
+                goto exit;
+            }
+            break;
+        case 2:
+            if (!PyArg_ParseTuple(args, "Ol:bkgd", &ch, &attr)) {
+                goto exit;
+            }
+            group_right_1 = 1;
+            break;
+        default:
+            PyErr_SetString(PyExc_TypeError, "_curses.window.bkgd requires 1 to 2 arguments");
+            goto exit;
     }
-skip_optional:
-    return_value = _curses_window_bkgd_impl((PyCursesWindowObject *)self, ch, attr);
+    return_value = _curses_window_bkgd_impl((PyCursesWindowObject *)self, ch, group_right_1, attr);
 
 exit:
     return return_value;
@@ -510,9 +611,7 @@ _curses_window_getattrs(PyObject *self, PyObject *Py_UNUSED(ignored))
 }
 
 PyDoc_STRVAR(_curses_window_bkgdset__doc__,
-"bkgdset($self, ch, attr=_curses.A_NORMAL, /)\n"
-"--\n"
-"\n"
+"bkgdset(ch, [attr])\n"
 "Set the window\'s background.\n"
 "\n"
 "  ch\n"
@@ -521,32 +620,37 @@ PyDoc_STRVAR(_curses_window_bkgdset__doc__,
 "    Background attributes.");
 
 #define _CURSES_WINDOW_BKGDSET_METHODDEF    \
-    {"bkgdset", _PyCFunction_CAST(_curses_window_bkgdset), METH_FASTCALL, _curses_window_bkgdset__doc__},
+    {"bkgdset", (PyCFunction)_curses_window_bkgdset, METH_VARARGS, _curses_window_bkgdset__doc__},
 
 static PyObject *
 _curses_window_bkgdset_impl(PyCursesWindowObject *self, PyObject *ch,
-                            long attr);
+                            int group_right_1, long attr);
 
 static PyObject *
-_curses_window_bkgdset(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
+_curses_window_bkgdset(PyObject *self, PyObject *args)
 {
     PyObject *return_value = NULL;
     PyObject *ch;
-    long attr = A_NORMAL;
+    int group_right_1 = 0;
+    long attr = 0;
 
-    if (!_PyArg_CheckPositional("bkgdset", nargs, 1, 2)) {
-        goto exit;
-    }
-    ch = args[0];
-    if (nargs < 2) {
-        goto skip_optional;
-    }
-    attr = PyLong_AsLong(args[1]);
-    if (attr == -1 && PyErr_Occurred()) {
-        goto exit;
+    switch (PyTuple_GET_SIZE(args)) {
+        case 1:
+            if (!PyArg_ParseTuple(args, "O:bkgdset", &ch)) {
+                goto exit;
+            }
+            break;
+        case 2:
+            if (!PyArg_ParseTuple(args, "Ol:bkgdset", &ch, &attr)) {
+                goto exit;
+            }
+            group_right_1 = 1;
+            break;
+        default:
+            PyErr_SetString(PyExc_TypeError, "_curses.window.bkgdset requires 1 to 2 arguments");
+            goto exit;
     }
-skip_optional:
-    return_value = _curses_window_bkgdset_impl((PyCursesWindowObject *)self, ch, attr);
+    return_value = _curses_window_bkgdset_impl((PyCursesWindowObject *)self, ch, group_right_1, attr);
 
 exit:
     return return_value;
@@ -797,9 +901,7 @@ exit:
 }
 
 PyDoc_STRVAR(_curses_window_echochar__doc__,
-"echochar($self, ch, attr=_curses.A_NORMAL, /)\n"
-"--\n"
-"\n"
+"echochar(ch, [attr])\n"
 "Add character ch with attribute attr, and refresh.\n"
 "\n"
 "  ch\n"
@@ -808,32 +910,37 @@ PyDoc_STRVAR(_curses_window_echochar__doc__,
 "    Attributes for the character.");
 
 #define _CURSES_WINDOW_ECHOCHAR_METHODDEF    \
-    {"echochar", _PyCFunction_CAST(_curses_window_echochar), METH_FASTCALL, _curses_window_echochar__doc__},
+    {"echochar", (PyCFunction)_curses_window_echochar, METH_VARARGS, _curses_window_echochar__doc__},
 
 static PyObject *
 _curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch,
-                             long attr);
+                             int group_right_1, long attr);
 
 static PyObject *
-_curses_window_echochar(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
+_curses_window_echochar(PyObject *self, PyObject *args)
 {
     PyObject *return_value = NULL;
     PyObject *ch;
-    long attr = A_NORMAL;
+    int group_right_1 = 0;
+    long attr = 0;
 
-    if (!_PyArg_CheckPositional("echochar", nargs, 1, 2)) {
-        goto exit;
-    }
-    ch = args[0];
-    if (nargs < 2) {
-        goto skip_optional;
-    }
-    attr = PyLong_AsLong(args[1]);
-    if (attr == -1 && PyErr_Occurred()) {
-        goto exit;
+    switch (PyTuple_GET_SIZE(args)) {
+        case 1:
+            if (!PyArg_ParseTuple(args, "O:echochar", &ch)) {
+                goto exit;
+            }
+            break;
+        case 2:
+            if (!PyArg_ParseTuple(args, "Ol:echochar", &ch, &attr)) {
+                goto exit;
+            }
+            group_right_1 = 1;
+            break;
+        default:
+            PyErr_SetString(PyExc_TypeError, "_curses.window.echochar requires 1 to 2 arguments");
+            goto exit;
     }
-skip_optional:
-    return_value = _curses_window_echochar_impl((PyCursesWindowObject *)self, ch, attr);
+    return_value = _curses_window_echochar_impl((PyCursesWindowObject *)self, ch, group_right_1, attr);
 
 exit:
     return return_value;
@@ -902,6 +1009,78 @@ _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"
+"\n"
+"  y\n"
+"    Y-coordinate.\n"
+"  x\n"
+"    X-coordinate.\n"
+"\n"
+"The returned object is a complexchar carrying the cell\'s text,\n"
+"attributes and color pair.");
+
+#define _CURSES_WINDOW_IN_WCH_METHODDEF    \
+    {"in_wch", (PyCFunction)_curses_window_in_wch, METH_VARARGS, _curses_window_in_wch__doc__},
+
+static PyObject *
+_curses_window_in_wch_impl(PyCursesWindowObject *self, int group_right_1,
+                           int y, int x);
+
+static PyObject *
+_curses_window_in_wch(PyObject *self, PyObject *args)
+{
+    PyObject *return_value = NULL;
+    int group_right_1 = 0;
+    int y = 0;
+    int x = 0;
+
+    switch (PyTuple_GET_SIZE(args)) {
+        case 0:
+            break;
+        case 2:
+            if (!PyArg_ParseTuple(args, "ii:in_wch", &y, &x)) {
+                goto exit;
+            }
+            group_right_1 = 1;
+            break;
+        default:
+            PyErr_SetString(PyExc_TypeError, "_curses.window.in_wch requires 0 to 2 arguments");
+            goto exit;
+    }
+    return_value = _curses_window_in_wch_impl((PyCursesWindowObject *)self, group_right_1, y, x);
+
+exit:
+    return return_value;
+}
+
+#endif /* defined(HAVE_NCURSESW) */
+
+#if defined(HAVE_NCURSESW)
+
+PyDoc_STRVAR(_curses_window_getbkgrnd__doc__,
+"getbkgrnd($self, /)\n"
+"--\n"
+"\n"
+"Return the window\'s current background complex character.");
+
+#define _CURSES_WINDOW_GETBKGRND_METHODDEF    \
+    {"getbkgrnd", (PyCFunction)_curses_window_getbkgrnd, METH_NOARGS, _curses_window_getbkgrnd__doc__},
+
+static PyObject *
+_curses_window_getbkgrnd_impl(PyCursesWindowObject *self);
+
+static PyObject *
+_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"
@@ -1049,7 +1228,7 @@ exit:
 #endif /* defined(HAVE_NCURSESW) */
 
 PyDoc_STRVAR(_curses_window_hline__doc__,
-"hline([y, x,] ch, n, [attr=_curses.A_NORMAL])\n"
+"hline([y, x,] ch, n, [attr])\n"
 "Display a horizontal line.\n"
 "\n"
 "  y\n"
@@ -1081,7 +1260,7 @@ _curses_window_hline(PyObject *self, PyObject *args)
     PyObject *ch;
     int n;
     int group_right_1 = 0;
-    long attr = A_NORMAL;
+    long attr = 0;
 
     switch (PyTuple_GET_SIZE(args)) {
         case 2:
@@ -1119,7 +1298,7 @@ exit:
 }
 
 PyDoc_STRVAR(_curses_window_insch__doc__,
-"insch([y, x,] ch, [attr=_curses.A_NORMAL])\n"
+"insch([y, x,] ch, [attr])\n"
 "Insert a character before the current or specified position.\n"
 "\n"
 "  y\n"
@@ -1151,7 +1330,7 @@ _curses_window_insch(PyObject *self, PyObject *args)
     int x = 0;
     PyObject *ch;
     int group_right_1 = 0;
-    long attr = A_NORMAL;
+    long attr = 0;
 
     switch (PyTuple_GET_SIZE(args)) {
         case 1:
@@ -1926,7 +2105,7 @@ exit:
 }
 
 PyDoc_STRVAR(_curses_window_vline__doc__,
-"vline([y, x,] ch, n, [attr=_curses.A_NORMAL])\n"
+"vline([y, x,] ch, n, [attr])\n"
 "Display a vertical line.\n"
 "\n"
 "  y\n"
@@ -1958,7 +2137,7 @@ _curses_window_vline(PyObject *self, PyObject *args)
     PyObject *ch;
     int n;
     int group_right_1 = 0;
-    long attr = A_NORMAL;
+    long attr = 0;
 
     switch (PyTuple_GET_SIZE(args)) {
         case 2:
@@ -4979,6 +5158,14 @@ _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) */
@@ -5122,4 +5309,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=3d8d59f44ded2226 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=081cc398989ca202 input=a9049054013a1b77]*/