diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index 91ea6150fb15ba3..70959d60d411434 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -1017,6 +1017,18 @@ the following methods and attributes: Return the character at the given position in the window. The bottom 8 bits are the character proper, and upper bits are the attributes. + .. note:: + + ``inch`` only works for ASCII characters. Use :meth:`in_wch` instead + for unicode support. + +.. method:: window.in_wch([x, y]) + + Return the wide character at the given position in the window. The return + value is a 3-tuple ``(character, attributes, color_pair)``. + + .. versionadded:: 3.14 + .. method:: window.insch(ch[, attr]) window.insch(y, x, ch[, attr]) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 83d10dd85790743..a9f14c4bd6a2ea9 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1107,6 +1107,39 @@ def test_issue6243(self): curses.ungetch(1025) self.stdscr.getkey() + def test_in_wch(self): + stdscr = self.stdscr + + if not hasattr(stdscr, 'in_wch'): + raise unittest.SkipTest('requires curses.window.in_wch') + + if curses.has_colors(): + curses.init_pair(1, curses.COLOR_RED, 0) + expected_pair = 1 + color_attr = curses.color_pair(expected_pair) + else: + expected_pair = 0 + color_attr = 0 + + def _norm(ch, attr, color): + # for some reason, curses returns some color bits here? + return ch, attr & ~curses.A_COLOR, color + + for ch in ('a', '\xe9', '\u20ac', '\U0001f643'): + expected = (ch, curses.A_BOLD, expected_pair) + + stdscr.insstr(0, 0, ch, color_attr | curses.A_BOLD) + self.assertEqual(_norm(*stdscr.in_wch()), expected) + + stdscr.addstr(0, 0, ch, color_attr | curses.A_BOLD) + self.assertEqual(_norm(*stdscr.in_wch(0, 0)), expected) + + # in_wch can return multiple characters in the case of zero width + # curses at max returns 5 characters in a cchar_t + stdscr.addstr(0, 0, 'a' + '\u200b' * 10) + expected = ('a\u200b\u200b\u200b\u200b', 0, 0) + self.assertEqual(stdscr.in_wch(0, 0), expected) + @requires_curses_func('unget_wch') @unittest.skipIf(getattr(curses, 'ncurses_version', (99,)) < (5, 8), "unget_wch is broken in ncurses 5.7 and earlier") diff --git a/Misc/NEWS.d/next/Library/2020-01-04-15-46-15.bpo-39214.z0398o.rst b/Misc/NEWS.d/next/Library/2020-01-04-15-46-15.bpo-39214.z0398o.rst new file mode 100644 index 000000000000000..2da471e355f3971 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-01-04-15-46-15.bpo-39214.z0398o.rst @@ -0,0 +1 @@ +Add :func:`curses.window.in_wch` function - by Anthony Sottile. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index b5854e8c33f28a9..d6f14bae1aeed24 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1736,6 +1736,54 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, return rtn; } +#ifdef HAVE_NCURSESW +/*[clinic input] +_curses.window.in_wch + + [ + y: int + Starting Y-coordinate. + x: int + Starting X-coordinate. + ] + / + +Retrieve the wide character at the gven position in the window. + +The return value is a 3-tuple of (character, attributes, 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=5c20d96b592b0e0b]*/ +{ + int ret; + + cchar_t wcval; + + wchar_t wstr[CCHARW_MAX + 1]; + attr_t attr; + short color_pair; + + if (group_right_1) { + ret = mvwin_wch(self->win, y, x, &wcval); + } else { + ret = win_wch(self->win, &wcval); + } + if (PyCursesCheckERR(ret, "in_wch") == NULL) { + return NULL; + } + + ret = getcchar(&wcval, wstr, &attr, &color_pair, NULL); + if (PyCursesCheckERR(ret, "getcchar") == NULL) { + return NULL; + } + + return Py_BuildValue("ukh", wstr, attr, color_pair); +} +#endif + /*[-clinic input] _curses.window.instr @@ -2530,6 +2578,7 @@ static PyMethodDef PyCursesWindow_Methods[] = { {"immedok", (PyCFunction)PyCursesWindow_immedok, METH_VARARGS}, #endif _CURSES_WINDOW_INCH_METHODDEF + _CURSES_WINDOW_IN_WCH_METHODDEF _CURSES_WINDOW_INSCH_METHODDEF {"insdelln", (PyCFunction)PyCursesWindow_winsdelln, METH_VARARGS}, {"insertln", (PyCFunction)PyCursesWindow_winsertln, METH_NOARGS}, diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index f7e0aaf7b23649f..531785526048aa3 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -1091,6 +1091,55 @@ _curses_window_inch(PyCursesWindowObject *self, PyObject *args) return return_value; } +#if defined(HAVE_NCURSESW) + +PyDoc_STRVAR(_curses_window_in_wch__doc__, +"in_wch([y, x])\n" +"Retrieve the wide character at the gven position in the window.\n" +"\n" +" y\n" +" Starting Y-coordinate.\n" +" x\n" +" Starting X-coordinate.\n" +"\n" +"The return value is a 3-tuple of (character, attributes, 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(PyCursesWindowObject *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(self, group_right_1, y, x); + +exit: + return return_value; +} + +#endif /* defined(HAVE_NCURSESW) */ + PyDoc_STRVAR(_curses_window_insstr__doc__, "insstr([y, x,] str, [attr])\n" "Insert the string before the current or specified position.\n" @@ -4235,6 +4284,10 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_WINDOW_GET_WCH_METHODDEF #endif /* !defined(_CURSES_WINDOW_GET_WCH_METHODDEF) */ +#ifndef _CURSES_WINDOW_IN_WCH_METHODDEF + #define _CURSES_WINDOW_IN_WCH_METHODDEF +#endif /* !defined(_CURSES_WINDOW_IN_WCH_METHODDEF) */ + #ifndef _CURSES_WINDOW_NOUTREFRESH_METHODDEF #define _CURSES_WINDOW_NOUTREFRESH_METHODDEF #endif /* !defined(_CURSES_WINDOW_NOUTREFRESH_METHODDEF) */ @@ -4318,4 +4371,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_USE_DEFAULT_COLORS_METHODDEF #define _CURSES_USE_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_USE_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=96887782374f070a input=a9049054013a1b77]*/ +/*[clinic end generated code: output=88098e997b5b33d9 input=a9049054013a1b77]*/