diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index d7c2905ec7347da..945be3b51998c34 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -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 ` (equivalent to the matching + ``A_*`` constants), and *pair* a color pair number. Unlike the packed + :class:`chtype ` 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 --------- diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 5123671db0c07ea..c0b6a650b7b887f 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -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 ---- diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 1cf766ddb382dd2..87dde5e062bd724 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -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)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 017d62e002fdff9..43286fa36bbb05c 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -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) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 75273243ef6df06..116dbf84f790124 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -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), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 164d9d412ef5e95..f7197b39d9bd3ab 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -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)); diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 75e6d2bd62e8871..bc3cc5cb3835177 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -345,6 +345,36 @@ def test_wide_characters(self): # 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 @@ def test_in_wstr(self): 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 index 000000000000000..f7fc3df6064ade8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-25-22-41-49.gh-issue-152233.pEhm3q.rst @@ -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`. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 0306c4af3288dc0..96e1fdc3bfe5a5d 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -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 @@ PyCurses_ConvertToChtype(PyCursesWindowObject *win, PyObject *obj, chtype *ch) - 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); diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index d4d6e4eeef01584..4a87ec3e215e995 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -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 @@ _curses_window_addnstr(PyObject *self, PyObject *args) } 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 @@ _curses_window_derwin(PyObject *self, PyObject *args) } 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 @@ _curses_window_get_wch(PyObject *self, PyObject *args) #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 @@ _curses_window_hline(PyObject *self, PyObject *args) } 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 @@ _curses_window_touchline(PyObject *self, PyObject *args) } 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]*/