diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index d7873054d6b9154..3c9a27aa85e6a09 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -194,6 +194,16 @@ The module :mod:`!curses` defines the following functions: the curses library itself. +.. function:: erasewchar() + + Return the user's current erase character as a one-character string. + This is the wide-character variant of :func:`erasechar`. Availability + depends on building Python against a wide-character-aware version of the + underlying curses library. + + .. versionadded:: next + + .. function:: filter() The :func:`.filter` routine, if used, must be called before :func:`initscr` is @@ -379,6 +389,16 @@ The module :mod:`!curses` defines the following functions: by the curses library itself. +.. function:: killwchar() + + Return the user's current line kill character as a one-character string. + This is the wide-character variant of :func:`killchar`. Availability + depends on building Python against a wide-character-aware version of the + underlying curses library. + + .. versionadded:: next + + .. function:: longname() Return a bytes object containing the terminfo long name field describing the current @@ -690,6 +710,18 @@ The module :mod:`!curses` defines the following functions: example as ``b'^C'``. Printing characters are left as they are. +.. function:: wunctrl(ch) + + Return a string which is a printable representation of the wide character *ch*. + Control characters are represented as a caret followed by the character, for + example as ``'^C'``. Printing characters are left as they are. This is the + wide-character variant of :func:`unctrl`, returning a :class:`str` rather than + :class:`bytes`. Availability depends on building Python against a + wide-character-aware version of the underlying curses library. + + .. versionadded:: next + + .. function:: ungetch(ch) Push *ch* so the next :meth:`~window.getch` will return it. @@ -770,12 +802,19 @@ Window objects character previously painted at that location. By default, the character position and attributes are the current settings for the window object. + *ch* may be a single character, optionally followed by combining + characters, that together occupy one character cell. + .. note:: Writing outside the window, subwindow, or pad raises a :exc:`curses.error`. Attempting to write to the lower-right corner of a window, subwindow, or pad will cause an exception to be raised after the character is printed. + .. 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. + .. method:: window.addnstr(str, n[, attr]) window.addnstr(y, x, str, n[, attr]) @@ -834,6 +873,9 @@ Window objects * Wherever the former background character appears, it is changed to the new background character. + .. versionchanged:: next + Wide and combining characters are now accepted. + .. method:: window.bkgdset(ch[, attr]) @@ -844,6 +886,9 @@ Window objects characters. The background becomes a property of the character and moves with the character through any scrolling and insert/delete line/character operations. + .. versionchanged:: next + Wide and combining characters are now accepted. + .. method:: window.border([ls[, rs[, ts[, bs[, tl[, tr[, bl[, br]]]]]]]]) @@ -877,12 +922,20 @@ Window objects | *br* | Bottom-right corner | :const:`ACS_LRCORNER` | +-----------+---------------------+-----------------------+ + .. versionchanged:: next + Wide and combining characters are now accepted. A single call cannot mix + them with integer or byte characters. + .. method:: window.box([vertch, horch]) Similar to :meth:`border`, but both *ls* and *rs* are *vertch* and both *ts* and *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 + them with integer or byte characters. + .. method:: window.chgat(attr) window.chgat(num, attr) @@ -951,6 +1004,9 @@ Window objects Add character *ch* with attribute *attr*, and immediately call :meth:`refresh` on the window. + .. versionchanged:: next + Wide and combining characters are now accepted. + .. method:: window.enclose(y, x) @@ -1038,6 +1094,20 @@ Window objects The maximum value for *n* was increased from 1023 to 2047. +.. method:: window.get_wstr() + window.get_wstr(n) + window.get_wstr(y, x) + window.get_wstr(y, x, n) + + Read a string from the user, with primitive line editing capacity. + This is the wide-character variant of :meth:`getstr`: it returns a + :class:`str` rather than a :class:`bytes` object, so it can return + characters that are not representable in the window's encoding. + At most *n* characters are read; *n* defaults to and cannot exceed 2047. + + .. versionadded:: next + + .. method:: window.getyx() Return a tuple ``(y, x)`` of current cursor position relative to the window's @@ -1051,6 +1121,9 @@ Window objects the character *ch* with attributes *attr*. The line stops at the right edge of the window if fewer than *n* cells are available. + .. versionchanged:: next + Wide and combining characters are now accepted. + .. method:: window.idcok(flag) @@ -1088,6 +1161,9 @@ Window objects cursor are shifted one position right, with the rightmost character on the line being lost. The cursor position does not change. + .. versionchanged:: next + Wide and combining characters are now accepted. + .. method:: window.insdelln(nlines) @@ -1137,6 +1213,19 @@ Window objects The maximum value for *n* was increased from 1023 to 2047. +.. method:: window.in_wstr([n]) + window.in_wstr(y, x[, n]) + + Return a string of characters, extracted from the window starting at the + current cursor position, or at *y*, *x* if specified. This is the + wide-character variant of :meth:`instr`: it returns a :class:`str` rather + than a :class:`bytes` object, so it can return characters that are not + representable in the window's encoding. Attributes and color information + are stripped from the characters. The maximum value for *n* is 2047. + + .. versionadded:: next + + .. method:: window.is_linetouched(line) Return ``True`` if the specified line was modified since the last call to @@ -1386,6 +1475,9 @@ Window objects Display a vertical line starting at ``(y, x)`` with length *n* consisting of the character *ch* with attributes *attr*. + .. versionchanged:: next + Wide and combining characters are now accepted. + Constants --------- diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 0a110795f371eb7..496a71bcdb7a4a7 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -89,6 +89,25 @@ Improved modules curses ------ +* The :mod:`curses` character-cell window methods now accept a full character + cell --- a spacing character optionally followed by combining characters --- + in addition to a single integer or byte character. This affects + :meth:`~curses.window.addch`, :meth:`~curses.window.bkgd`, + :meth:`~curses.window.bkgdset`, :meth:`~curses.window.border`, + :meth:`~curses.window.box`, :meth:`~curses.window.echochar`, + :meth:`~curses.window.hline`, :meth:`~curses.window.insch` and + :meth:`~curses.window.vline`. + Also add the wide-character read methods :meth:`~curses.window.get_wstr` and + :meth:`~curses.window.in_wstr`, the counterparts of + :meth:`~curses.window.getstr` and :meth:`~curses.window.instr` that return a + :class:`str` rather than :class:`bytes`, + and the module functions :func:`curses.erasewchar`, :func:`curses.killwchar` + and :func:`curses.wunctrl`, the wide-character counterparts of + :func:`curses.erasechar`, :func:`curses.killchar` and :func:`curses.unctrl`. + These features are only available when built against the wide-character + ncursesw library. + (Contributed by Serhiy Storchaka in :gh:`151757`.) + * Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`. (Contributed by Serhiy Storchaka in :gh:`151744`.) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 98f1a7c8a0a2c5c..3427883dc0ffa2a 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -253,6 +253,69 @@ def test_refresh_control(self): self.assertIs(win.is_wintouched(), syncok) self.assertIs(stdscr.is_wintouched(), syncok) + @requires_curses_window_meth('get_wch') + def test_addch_combining(self): + # A character cell may hold a spacing char plus combining marks. + stdscr = self.stdscr + stdscr.move(0, 0) + stdscr.addch('e\u0301') # 'e' + COMBINING ACUTE ACCENT + stdscr.addch(1, 0, 'a\u0323\u0300') # base plus two combining marks + # Too many code points to fit in a single character cell. + self.assertRaises(TypeError, stdscr.addch, 'e' + '\u0301' * 10) + # Only the first code point may be a spacing character. + self.assertRaises(ValueError, stdscr.addch, 'ab') + self.assertRaises(ValueError, stdscr.addch, 'a\u0301b') + # A lone control character is allowed (like addch(ord('\n'))), but it + # cannot be combined with other characters, as base or otherwise. + stdscr.addch('\n') + self.assertRaises(ValueError, stdscr.addch, 'a\n') + self.assertRaises(ValueError, stdscr.addch, '\n\u0301') + self.assertRaises(ValueError, stdscr.addch, '\ne\u0301') + + @requires_curses_window_meth('get_wch') + def test_addch_emoji(self): + # curses has no grapheme-cluster support: a cell holds one spacing + # character plus zero-width combining characters. A lone emoji fits, + # as does an emoji with a zero-width variation selector. + stdscr = self.stdscr + stdscr.addch(0, 0, '\U0001f600') # single emoji + stdscr.addch(1, 0, '\u263a\ufe0f') # WHITE SMILING FACE + VS-16 + # An emoji ZWJ sequence or an emoji with a modifier is more than one + # spacing character and cannot share a single cell. + self.assertRaises(ValueError, stdscr.addch, + '\U0001f44d\U0001f3fd') # thumbs up + skin tone + self.assertRaises(ValueError, stdscr.addch, + '\U0001f468\u200d\U0001f469') # man ZWJ woman + + @requires_curses_window_meth('get_wch') + def test_wide_characters(self): + # Wide and combining characters in the character-cell methods. + stdscr = self.stdscr + combining = 'e\u0301' # 'e' + COMBINING ACUTE ACCENT + vline, hline = '\u2502', '\u2500' # box-drawing vertical/horizontal + stdscr.move(0, 0) + stdscr.echochar(combining) + stdscr.insch(1, 0, combining) + stdscr.hline(2, 0, hline, 5) + stdscr.vline(3, 0, vline, 3) + stdscr.bkgdset(combining) + stdscr.bkgd(combining) + stdscr.border(vline, vline, hline, hline) + stdscr.box(vline, hline) + # border() and box() cannot mix integer and wide-string characters. + self.assertRaises(TypeError, stdscr.box, vline, ord('-')) + + + @requires_curses_window_meth('in_wstr') + def test_in_wstr(self): + # The wide-character window read returns a str (instr returns bytes). + stdscr = self.stdscr + s = 'a\u00e9\u2502z' # 'a', 'e'+acute (precomposed), box vline, 'z' + stdscr.addstr(0, 0, s) + self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s) + self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes) + + def test_output_character(self): stdscr = self.stdscr encoding = stdscr.encoding @@ -281,13 +344,16 @@ def test_output_character(self): stdscr.echochar('A') stdscr.echochar(b'A') stdscr.echochar(65) - with self.assertRaises((UnicodeEncodeError, OverflowError)): - # Unicode is not fully supported yet, but at least it does - # not crash. - # It is supposed to fail because either the character is - # not encodable with the current encoding, or it is encoded to - # a multibyte sequence. - stdscr.echochar('\u0114') + c = '\u0114' + try: + stdscr.echochar(c) + except UnicodeEncodeError: + # The character is not encodable with the current encoding. + self.assertRaises(UnicodeEncodeError, c.encode, encoding) + except OverflowError: + # The character is encoded to a multibyte sequence. + encoded = c.encode(encoding) + self.assertNotEqual(len(encoded), 1, repr(encoded)) stdscr.echochar('A', curses.A_BOLD) self.assertIs(stdscr.is_wintouched(), False) @@ -742,7 +808,6 @@ def test_borders_and_lines(self): self.assertEqual(win.inch(3, 1), b'a'[0]) def test_unctrl(self): - # TODO: wunctrl() self.assertEqual(curses.unctrl(b'A'), b'A') self.assertEqual(curses.unctrl('A'), b'A') self.assertEqual(curses.unctrl(65), b'A') @@ -753,6 +818,21 @@ def test_unctrl(self): self.assertRaises(TypeError, curses.unctrl, b'AB') self.assertRaises(TypeError, curses.unctrl, '') self.assertRaises(TypeError, curses.unctrl, 'AB') + + @requires_curses_func('wunctrl') + def test_wunctrl(self): + # The wide-character variant of unctrl() returns a str. + self.assertEqual(curses.wunctrl(b'A'), 'A') + self.assertEqual(curses.wunctrl('A'), 'A') + self.assertEqual(curses.wunctrl(65), 'A') + self.assertEqual(curses.wunctrl('\n'), '^J') + self.assertEqual(curses.wunctrl(10), '^J') + self.assertEqual(curses.wunctrl('é'), 'é') # printable + self.assertRaises(TypeError, curses.wunctrl, b'') + self.assertRaises(TypeError, curses.wunctrl, b'AB') + self.assertRaises(TypeError, curses.wunctrl, '') + # More than one spacing character is not a single cell. + self.assertRaises(ValueError, curses.wunctrl, 'AB') self.assertRaises(OverflowError, curses.unctrl, 2**64) def test_endwin(self): @@ -800,7 +880,7 @@ def test_misc_module_funcs(self): curses.newpad(50, 50) def test_env_queries(self): - # TODO: term_attrs(), erasewchar(), killwchar() + # TODO: term_attrs() self.assertIsInstance(curses.termname(), bytes) self.assertIsInstance(curses.longname(), bytes) self.assertIsInstance(curses.baudrate(), int) @@ -815,6 +895,24 @@ def test_env_queries(self): self.assertIsInstance(c, bytes) self.assertEqual(len(c), 1) + # The erase and kill characters are a property of the controlling + # terminal: the wide variants report ERR (raising curses.error) without + # one, while the narrow variants above return an unspecified byte. + try: + tty_fd = os.open(os.ctermid(), os.O_RDONLY) + except OSError: + tty_fd = None + if tty_fd is not None: + os.close(tty_fd) + if hasattr(curses, 'erasewchar'): + c = curses.erasewchar() + self.assertIsInstance(c, str) + self.assertEqual(len(c), 1) + if hasattr(curses, 'killwchar'): + c = curses.killwchar() + self.assertIsInstance(c, str) + self.assertEqual(len(c), 1) + def test_output_options(self): stdscr = self.stdscr diff --git a/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst b/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst new file mode 100644 index 000000000000000..34db9698738c2b4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst @@ -0,0 +1,7 @@ +The :mod:`curses` character-cell window methods now accept a full character +cell -- a spacing character optionally followed by combining characters -- in +addition to a single integer or byte character. Add the wide-character read +methods :meth:`curses.window.get_wstr` and :meth:`curses.window.in_wstr`, and +the functions :func:`curses.erasewchar`, :func:`curses.killwchar` and +:func:`curses.wunctrl`. These features are only available when built against +the wide-character ncursesw library. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index e60cba3ef87ead1..f0541b82ae070d7 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -500,7 +500,8 @@ PyCurses_ConvertToChtype(PyCursesWindowObject *win, PyObject *obj, chtype *ch) - int - bytes of length 1 - - str of length 1 + - str of length 1, or a spacing character followed by up to + CCHARW_MAX - 1 combining characters Return: @@ -516,20 +517,43 @@ PyCurses_ConvertToCchar_t(PyCursesWindowObject *win, PyObject *obj, ) { long value; -#ifdef HAVE_NCURSESW - wchar_t buffer[2]; -#endif if (PyUnicode_Check(obj)) { #ifdef HAVE_NCURSESW - if (PyUnicode_AsWideChar(obj, buffer, 2) != 1) { + /* 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 str of length 1, " - "got a str of length %zi", - PyUnicode_GET_LENGTH(obj)); + "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)); return 0; } - *wch = buffer[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); @@ -617,6 +641,33 @@ PyCurses_ConvertToString(PyCursesWindowObject *win, PyObject *obj, return 0; } +#ifdef HAVE_NCURSESW +/* 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. + + This lets a method use the wide *_set functions (which accept combining + characters) for string arguments while still accepting integer chtype + values. */ +static int +PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, long attr, + const char *funcname, chtype *pch, cchar_t *pwc) +{ + wchar_t wstr[CCHARW_MAX + 1]; + int type = PyCurses_ConvertToCchar_t(win, obj, pch, wstr); + if (type == 2) { + if (setcchar(pwc, wstr, (attr_t)attr, PAIR_NUMBER(attr), NULL) == ERR) { + curses_window_set_error(win, "setcchar", funcname); + return 0; + } + } + return type; +} +#endif + static int color_allow_default_converter(PyObject *arg, void *ptr) { @@ -997,7 +1048,7 @@ _curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1, int type; chtype cch = 0; #ifdef HAVE_NCURSESW - wchar_t wstr[2]; + wchar_t wstr[CCHARW_MAX + 1]; cchar_t wcval; #endif const char *funcname; @@ -1005,7 +1056,6 @@ _curses_window_addch_impl(PyCursesWindowObject *self, int group_left_1, #ifdef HAVE_NCURSESW type = PyCurses_ConvertToCchar_t(self, ch, &cch, wstr); if (type == 2) { - wstr[1] = L'\0'; rtn = setcchar(&wcval, wstr, attr, PAIR_NUMBER(attr), NULL); if (rtn == ERR) { curses_window_set_error(self, "setcchar", "addch"); @@ -1277,11 +1327,22 @@ _curses_window_bkgd_impl(PyCursesWindowObject *self, PyObject *ch, long attr) /*[clinic end generated code: output=058290afb2cf4034 input=634015bcb339283d]*/ { chtype bkgd; - +#ifdef HAVE_NCURSESW + cchar_t wch; + int type = PyCurses_ConvertToCell(self, ch, attr, "bkgd", &bkgd, &wch); + if (type == 0) { + return NULL; + } + if (type == 2) { + int rtn = wbkgrnd(self->win, &wch); + return curses_window_check_err(self, rtn, "wbkgrnd", "bkgd"); + } +#else if (!PyCurses_ConvertToChtype(self, ch, &bkgd)) return NULL; +#endif - int rtn = wbkgd(self->win, bkgd | attr); + int rtn = wbkgd(self->win, bkgd | (attr_t)attr); return curses_window_check_err(self, rtn, "wbkgd", "bkgd"); } @@ -1354,11 +1415,22 @@ _curses_window_bkgdset_impl(PyCursesWindowObject *self, PyObject *ch, /*[clinic end generated code: output=8cb994fc4d7e2496 input=e09c682425c9e45b]*/ { chtype bkgd; - +#ifdef HAVE_NCURSESW + cchar_t wch; + int type = PyCurses_ConvertToCell(self, ch, attr, "bkgdset", &bkgd, &wch); + if (type == 0) { + return NULL; + } + if (type == 2) { + wbkgrndset(self->win, &wch); + Py_RETURN_NONE; + } +#else if (!PyCurses_ConvertToChtype(self, ch, &bkgd)) return NULL; +#endif - wbkgdset(self->win, bkgd | attr); + wbkgdset(self->win, bkgd | (attr_t)attr); Py_RETURN_NONE; } @@ -1400,25 +1472,56 @@ _curses_window_border_impl(PyCursesWindowObject *self, PyObject *ls, { chtype ch[8]; int i, rtn; + PyObject *objs[8] = {ls, rs, ts, bs, tl, tr, bl, br}; /* Clear the array of parameters */ - for(i=0; i<8; i++) + for (i = 0; i < 8; i++) ch[i] = 0; -#define CONVERTTOCHTYPE(obj, i) \ - if ((obj) != NULL && !PyCurses_ConvertToChtype(self, (obj), &ch[(i)])) \ - return NULL; - - CONVERTTOCHTYPE(ls, 0); - CONVERTTOCHTYPE(rs, 1); - CONVERTTOCHTYPE(ts, 2); - CONVERTTOCHTYPE(bs, 3); - CONVERTTOCHTYPE(tl, 4); - CONVERTTOCHTYPE(tr, 5); - CONVERTTOCHTYPE(bl, 6); - CONVERTTOCHTYPE(br, 7); - -#undef CONVERTTOCHTYPE +#ifdef HAVE_NCURSESW + cchar_t wch[8]; + const cchar_t *wch_p[8]; + int use_wide = 0; + int types[8]; + for (i = 0; i < 8; i++) { + types[i] = 0; + if (objs[i] != NULL) { + types[i] = PyCurses_ConvertToCell(self, objs[i], A_NORMAL, + "border", &ch[i], &wch[i]); + if (types[i] == 0) { + return NULL; + } + if (types[i] == 2) { + use_wide = 1; + } + } + } + if (use_wide) { + for (i = 0; i < 8; i++) { + if (objs[i] == NULL) { + wch_p[i] = NULL; /* use the default character */ + } + else if (types[i] == 2) { + wch_p[i] = &wch[i]; + } + else { + PyErr_SetString(PyExc_TypeError, + "border() cannot mix integer or bytes " + "characters with wide string characters"); + return NULL; + } + } + rtn = wborder_set(self->win, + wch_p[0], wch_p[1], wch_p[2], wch_p[3], + wch_p[4], wch_p[5], wch_p[6], wch_p[7]); + return curses_window_check_err(self, rtn, "wborder_set", "border"); + } +#else + for (i = 0; i < 8; i++) { + if (objs[i] != NULL && !PyCurses_ConvertToChtype(self, objs[i], &ch[i])) + return NULL; + } +#endif rtn = wborder(self->win, ch[0], ch[1], ch[2], ch[3], @@ -1450,6 +1553,31 @@ _curses_window_box_impl(PyCursesWindowObject *self, int group_right_1, /*[clinic end generated code: output=f3fcb038bb287192 input=e11acb7dbf6790b6]*/ { chtype ch1 = 0, ch2 = 0; +#ifdef HAVE_NCURSESW + cchar_t wch1, wch2; + int t1 = 0, t2 = 0; + if (group_right_1) { + t1 = PyCurses_ConvertToCell(self, verch, A_NORMAL, "box", &ch1, &wch1); + if (t1 == 0) { + return NULL; + } + t2 = PyCurses_ConvertToCell(self, horch, A_NORMAL, "box", &ch2, &wch2); + if (t2 == 0) { + return NULL; + } + } + if (t1 == 2 || t2 == 2) { + if (t1 != 2 || t2 != 2) { + PyErr_SetString(PyExc_TypeError, + "box() cannot mix integer or bytes characters " + "with wide string characters"); + return NULL; + } + int rtn = wborder_set(self->win, &wch1, &wch1, &wch2, &wch2, + NULL, NULL, NULL, NULL); + return curses_window_check_err(self, rtn, "wborder_set", "box"); + } +#else if (group_right_1) { if (!PyCurses_ConvertToChtype(self, verch, &ch1)) { return NULL; @@ -1458,6 +1586,7 @@ _curses_window_box_impl(PyCursesWindowObject *self, int group_right_1, return NULL; } } +#endif return curses_window_check_err(self, box(self->win, ch1, ch2), "box", NULL); } @@ -1661,9 +1790,32 @@ _curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch, /*[clinic end generated code: output=13e7dd875d4b9642 input=e7f34b964e92b156]*/ { chtype ch_; - +#ifdef HAVE_NCURSESW + cchar_t wch; + int type = PyCurses_ConvertToCell(self, ch, attr, "echochar", &ch_, &wch); + if (type == 0) { + return NULL; + } + if (type == 2) { + int rtn; + const char *funcname; +#ifdef py_is_pad + if (py_is_pad(self->win)) { + rtn = pecho_wchar(self->win, &wch); + funcname = "pecho_wchar"; + } + else +#endif + { + rtn = wecho_wchar(self->win, &wch); + funcname = "wecho_wchar"; + } + return curses_window_check_err(self, rtn, funcname, "echochar"); + } +#else if (!PyCurses_ConvertToChtype(self, ch, &ch_)) return NULL; +#endif int rtn; const char *funcname; @@ -2013,15 +2165,28 @@ _curses_window_hline_impl(PyCursesWindowObject *self, int group_left_1, /*[clinic end generated code: output=c00d489d61fc9eef input=81a4dea47268163e]*/ { chtype ch_; - +#ifdef HAVE_NCURSESW + cchar_t wch; + int type = PyCurses_ConvertToCell(self, ch, attr, "hline", &ch_, &wch); + if (type == 0) { + return NULL; + } +#else if (!PyCurses_ConvertToChtype(self, ch, &ch_)) return NULL; +#endif if (group_left_1) { if (wmove(self->win, y, x) == ERR) { curses_window_set_error(self, "wmove", "hline"); return NULL; } } +#ifdef HAVE_NCURSESW + if (type == 2) { + int rtn = whline_set(self->win, &wch, n); + return curses_window_check_err(self, rtn, "whline_set", "hline"); + } +#endif int rtn = whline(self->win, ch_ | (attr_t)attr, n); return curses_window_check_err(self, rtn, "whline", "hline"); } @@ -2059,11 +2224,29 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1, { int rtn; chtype ch_ = 0; - + const char *funcname; +#ifdef HAVE_NCURSESW + cchar_t wch; + int type = PyCurses_ConvertToCell(self, ch, attr, "insch", &ch_, &wch); + if (type == 0) { + return NULL; + } + if (type == 2) { + if (!group_left_1) { + rtn = wins_wch(self->win, &wch); + funcname = "wins_wch"; + } + else { + rtn = mvwins_wch(self->win, y, x, &wch); + funcname = "mvwins_wch"; + } + return curses_window_check_err(self, rtn, funcname, "insch"); + } +#else if (!PyCurses_ConvertToChtype(self, ch, &ch_)) return NULL; +#endif - const char *funcname; if (!group_left_1) { rtn = winsch(self->win, ch_ | (attr_t)attr); funcname = "winsch"; @@ -2169,6 +2352,125 @@ PyCursesWindow_instr(PyObject *op, PyObject *args) return PyBytesWriter_FinishWithSize(writer, strlen(buf)); } +#ifdef HAVE_NCURSESW +PyDoc_STRVAR(_curses_window_get_wstr__doc__, +"get_wstr([[y, x,] n=2047])\n" +"Read a string from the user, with primitive line editing capacity.\n" +"\n" +" y\n" +" Y-coordinate.\n" +" x\n" +" X-coordinate.\n" +" n\n" +" Maximal number of characters.\n" +"\n" +"This is the wide-character variant of getstr(); it returns a str."); + +static PyObject * +PyCursesWindow_get_wstr(PyObject *op, PyObject *args) +{ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + int rtn, use_xy = 0, y = 0, x = 0; + unsigned int max_buf_size = 2048; + unsigned int n = max_buf_size - 1; + + if (!curses_clinic_parse_optional_xy_n(args, &y, &x, &n, &use_xy, + "_curses.window.get_wstr")) + { + return NULL; + } + + n = Py_MIN(n, max_buf_size - 1); + wint_t *buf = PyMem_New(wint_t, n + 1); + if (buf == NULL) { + return PyErr_NoMemory(); + } + + if (use_xy) { + Py_BEGIN_ALLOW_THREADS + rtn = mvwgetn_wstr(self->win, y, x, buf, n); + Py_END_ALLOW_THREADS + } + else { + Py_BEGIN_ALLOW_THREADS + rtn = wgetn_wstr(self->win, buf, n); + Py_END_ALLOW_THREADS + } + + if (rtn == ERR) { + PyMem_Free(buf); + return Py_GetConstant(Py_CONSTANT_EMPTY_STR); + } + + /* wgetn_wstr() fills a wint_t buffer; copy it to a wchar_t buffer. */ + Py_ssize_t len = 0; + while (buf[len]) { + len++; + } + wchar_t *wbuf = PyMem_New(wchar_t, len + 1); + if (wbuf == NULL) { + PyMem_Free(buf); + return PyErr_NoMemory(); + } + for (Py_ssize_t i = 0; i < len; i++) { + wbuf[i] = (wchar_t)buf[i]; + } + PyObject *res = PyUnicode_FromWideChar(wbuf, len); + PyMem_Free(wbuf); + PyMem_Free(buf); + return res; +} + +PyDoc_STRVAR(_curses_window_in_wstr__doc__, +"in_wstr([y, x,] n=2047)\n" +"Return a string of characters, extracted from the window.\n" +"\n" +" y\n" +" Y-coordinate.\n" +" x\n" +" X-coordinate.\n" +" n\n" +" Maximal number of characters.\n" +"\n" +"This is the wide-character variant of instr(); it returns a str."); + +static PyObject * +PyCursesWindow_in_wstr(PyObject *op, PyObject *args) +{ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + int rtn, use_xy = 0, y = 0, x = 0; + unsigned int max_buf_size = 2048; + unsigned int n = max_buf_size - 1; + + if (!curses_clinic_parse_optional_xy_n(args, &y, &x, &n, &use_xy, + "_curses.window.in_wstr")) + { + return NULL; + } + + n = Py_MIN(n, max_buf_size - 1); + wchar_t *buf = PyMem_New(wchar_t, n + 1); + if (buf == NULL) { + return PyErr_NoMemory(); + } + + if (use_xy) { + rtn = mvwinnwstr(self->win, y, x, buf, n); + } + else { + rtn = winnwstr(self->win, buf, n); + } + + if (rtn == ERR) { + PyMem_Free(buf); + return Py_GetConstant(Py_CONSTANT_EMPTY_STR); + } + PyObject *res = PyUnicode_FromWideChar(buf, -1); + PyMem_Free(buf); + return res; +} +#endif /* HAVE_NCURSESW */ + /*[clinic input] _curses.window.insstr @@ -2866,15 +3168,28 @@ _curses_window_vline_impl(PyCursesWindowObject *self, int group_left_1, /*[clinic end generated code: output=287ad1cc8982217f input=a6f2dc86a4648b32]*/ { chtype ch_; - +#ifdef HAVE_NCURSESW + cchar_t wch; + int type = PyCurses_ConvertToCell(self, ch, attr, "vline", &ch_, &wch); + if (type == 0) { + return NULL; + } +#else if (!PyCurses_ConvertToChtype(self, ch, &ch_)) return NULL; +#endif if (group_left_1) { if (wmove(self->win, y, x) == ERR) { curses_window_set_error(self, "wmove", "vline"); return NULL; } } +#ifdef HAVE_NCURSESW + if (type == 2) { + int rtn = wvline_set(self->win, &wch, n); + return curses_window_check_err(self, rtn, "wvline_set", "vline"); + } +#endif int rtn = wvline(self->win, ch_ | (attr_t)attr, n); return curses_window_check_err(self, rtn, "wvline", "vline"); } @@ -2983,6 +3298,12 @@ static PyMethodDef PyCursesWindow_methods[] = { "getstr", PyCursesWindow_getstr, METH_VARARGS, _curses_window_getstr__doc__ }, +#ifdef HAVE_NCURSESW + { + "get_wstr", PyCursesWindow_get_wstr, METH_VARARGS, + _curses_window_get_wstr__doc__ + }, +#endif {"getyx", PyCursesWindow_getyx, METH_NOARGS, "getyx($self, /)\n--\n\n" "Return a tuple (y, x) of the current cursor position."}, @@ -3012,6 +3333,12 @@ static PyMethodDef PyCursesWindow_methods[] = { "instr", PyCursesWindow_instr, METH_VARARGS, _curses_window_instr__doc__ }, +#ifdef HAVE_NCURSESW + { + "in_wstr", PyCursesWindow_in_wstr, METH_VARARGS, + _curses_window_in_wstr__doc__ + }, +#endif _CURSES_WINDOW_IS_LINETOUCHED_METHODDEF {"is_wintouched", PyCursesWindow_is_wintouched, METH_NOARGS, "is_wintouched($self, /)\n--\n\n" @@ -3495,6 +3822,29 @@ _curses_erasechar_impl(PyObject *module) return PyBytes_FromStringAndSize(&ch, 1); } +#ifdef HAVE_NCURSESW +/*[clinic input] +_curses.erasewchar + +Return the user's current wide-character erase character. +[clinic start generated code]*/ + +static PyObject * +_curses_erasewchar_impl(PyObject *module) +/*[clinic end generated code: output=7f3bd8c9097ac456 input=f7e9a3893b4df2f8]*/ +{ + wchar_t ch; + + PyCursesStatefulInitialised(module); + + if (erasewchar(&ch) == ERR) { + curses_set_error(module, "erasewchar", NULL); + return NULL; + } + return PyUnicode_FromWideChar(&ch, 1); +} +#endif /* HAVE_NCURSESW */ + /*[clinic input] _curses.flash @@ -4227,6 +4577,27 @@ _curses_killchar_impl(PyObject *module) return PyBytes_FromStringAndSize(&ch, 1); } +#ifdef HAVE_NCURSESW +/*[clinic input] +_curses.killwchar + +Return the user's current wide-character line kill character. +[clinic start generated code]*/ + +static PyObject * +_curses_killwchar_impl(PyObject *module) +/*[clinic end generated code: output=eac1fd72a0c88d42 input=5c2d7d1ab2f24eb7]*/ +{ + wchar_t ch; + + if (killwchar(&ch) == ERR) { + curses_set_error(module, "killwchar", NULL); + return NULL; + } + return PyUnicode_FromWideChar(&ch, 1); +} +#endif /* HAVE_NCURSESW */ + /*[clinic input] _curses.longname @@ -5081,6 +5452,52 @@ _curses_unctrl(PyObject *module, PyObject *ch) return PyBytes_FromString(res); } +#ifdef HAVE_NCURSESW +/*[clinic input] +_curses.wunctrl + + ch: object + / + +Return a printable representation of the wide character ch. + +Control characters are displayed as a caret followed by the character, +for example as ^C. Printing characters are left as they are. +[clinic start generated code]*/ + +static PyObject * +_curses_wunctrl(PyObject *module, PyObject *ch) +/*[clinic end generated code: output=7b16d5534ff05728 input=9ceb6749118bd07c]*/ +{ + chtype ch_; + wchar_t wstr[CCHARW_MAX + 1]; + cchar_t wcval; + + PyCursesStatefulInitialised(module); + + int type = PyCurses_ConvertToCchar_t(NULL, ch, &ch_, wstr); + if (type == 0) { + return NULL; + } + if (type == 1) { + /* A narrow character is the spacing character of the cell. */ + wstr[0] = (wchar_t)(ch_ & A_CHARTEXT); + wstr[1] = L'\0'; + } + if (setcchar(&wcval, wstr, A_NORMAL, 0, NULL) == ERR) { + curses_set_error(module, "setcchar", "wunctrl"); + return NULL; + } + + wchar_t *res = wunctrl(&wcval); + if (res == NULL) { + curses_set_null_error(module, "wunctrl", NULL); + return NULL; + } + return PyUnicode_FromWideChar(res, -1); +} +#endif /* HAVE_NCURSESW */ + /*[clinic input] _curses.ungetch @@ -5342,6 +5759,7 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_ECHO_METHODDEF _CURSES_ENDWIN_METHODDEF _CURSES_ERASECHAR_METHODDEF + _CURSES_ERASEWCHAR_METHODDEF _CURSES_FILTER_METHODDEF _CURSES_NOFILTER_METHODDEF _CURSES_FLASH_METHODDEF @@ -5364,6 +5782,7 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_IS_TERM_RESIZED_METHODDEF _CURSES_KEYNAME_METHODDEF _CURSES_KILLCHAR_METHODDEF + _CURSES_KILLWCHAR_METHODDEF _CURSES_LONGNAME_METHODDEF _CURSES_META_METHODDEF _CURSES_MOUSEINTERVAL_METHODDEF @@ -5405,6 +5824,7 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_TPARM_METHODDEF _CURSES_TYPEAHEAD_METHODDEF _CURSES_UNCTRL_METHODDEF + _CURSES_WUNCTRL_METHODDEF _CURSES_UNGETCH_METHODDEF _CURSES_UPDATE_LINES_COLS_METHODDEF _CURSES_UNGET_WCH_METHODDEF diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index f577368680ef572..62dab279f5a97c0 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -2267,6 +2267,28 @@ _curses_erasechar(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_erasechar_impl(module); } +#if defined(HAVE_NCURSESW) + +PyDoc_STRVAR(_curses_erasewchar__doc__, +"erasewchar($module, /)\n" +"--\n" +"\n" +"Return the user\'s current wide-character erase character."); + +#define _CURSES_ERASEWCHAR_METHODDEF \ + {"erasewchar", (PyCFunction)_curses_erasewchar, METH_NOARGS, _curses_erasewchar__doc__}, + +static PyObject * +_curses_erasewchar_impl(PyObject *module); + +static PyObject * +_curses_erasewchar(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_erasewchar_impl(module); +} + +#endif /* defined(HAVE_NCURSESW) */ + PyDoc_STRVAR(_curses_flash__doc__, "flash($module, /)\n" "--\n" @@ -3090,6 +3112,28 @@ _curses_killchar(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_killchar_impl(module); } +#if defined(HAVE_NCURSESW) + +PyDoc_STRVAR(_curses_killwchar__doc__, +"killwchar($module, /)\n" +"--\n" +"\n" +"Return the user\'s current wide-character line kill character."); + +#define _CURSES_KILLWCHAR_METHODDEF \ + {"killwchar", (PyCFunction)_curses_killwchar, METH_NOARGS, _curses_killwchar__doc__}, + +static PyObject * +_curses_killwchar_impl(PyObject *module); + +static PyObject * +_curses_killwchar(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_killwchar_impl(module); +} + +#endif /* defined(HAVE_NCURSESW) */ + PyDoc_STRVAR(_curses_longname__doc__, "longname($module, /)\n" "--\n" @@ -4266,6 +4310,22 @@ PyDoc_STRVAR(_curses_unctrl__doc__, #define _CURSES_UNCTRL_METHODDEF \ {"unctrl", (PyCFunction)_curses_unctrl, METH_O, _curses_unctrl__doc__}, +#if defined(HAVE_NCURSESW) + +PyDoc_STRVAR(_curses_wunctrl__doc__, +"wunctrl($module, ch, /)\n" +"--\n" +"\n" +"Return a printable representation of the wide character ch.\n" +"\n" +"Control characters are displayed as a caret followed by the character,\n" +"for example as ^C. Printing characters are left as they are."); + +#define _CURSES_WUNCTRL_METHODDEF \ + {"wunctrl", (PyCFunction)_curses_wunctrl, METH_O, _curses_wunctrl__doc__}, + +#endif /* defined(HAVE_NCURSESW) */ + PyDoc_STRVAR(_curses_ungetch__doc__, "ungetch($module, ch, /)\n" "--\n" @@ -4437,6 +4497,10 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_NOFILTER_METHODDEF #endif /* !defined(_CURSES_NOFILTER_METHODDEF) */ +#ifndef _CURSES_ERASEWCHAR_METHODDEF + #define _CURSES_ERASEWCHAR_METHODDEF +#endif /* !defined(_CURSES_ERASEWCHAR_METHODDEF) */ + #ifndef _CURSES_GETSYX_METHODDEF #define _CURSES_GETSYX_METHODDEF #endif /* !defined(_CURSES_GETSYX_METHODDEF) */ @@ -4473,6 +4537,10 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_IS_TERM_RESIZED_METHODDEF #endif /* !defined(_CURSES_IS_TERM_RESIZED_METHODDEF) */ +#ifndef _CURSES_KILLWCHAR_METHODDEF + #define _CURSES_KILLWCHAR_METHODDEF +#endif /* !defined(_CURSES_KILLWCHAR_METHODDEF) */ + #ifndef _CURSES_MOUSEINTERVAL_METHODDEF #define _CURSES_MOUSEINTERVAL_METHODDEF #endif /* !defined(_CURSES_MOUSEINTERVAL_METHODDEF) */ @@ -4501,6 +4569,10 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_TYPEAHEAD_METHODDEF #endif /* !defined(_CURSES_TYPEAHEAD_METHODDEF) */ +#ifndef _CURSES_WUNCTRL_METHODDEF + #define _CURSES_WUNCTRL_METHODDEF +#endif /* !defined(_CURSES_WUNCTRL_METHODDEF) */ + #ifndef _CURSES_UNGET_WCH_METHODDEF #define _CURSES_UNGET_WCH_METHODDEF #endif /* !defined(_CURSES_UNGET_WCH_METHODDEF) */ @@ -4516,4 +4588,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=7494804bf2c4d1f5 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=0bce70b538541c9e input=a9049054013a1b77]*/