https://github.com/python/cpython/commit/306d7a7649061d4c88d90deb6838e7e49d03269e commit: 306d7a7649061d4c88d90deb6838e7e49d03269e branch: main author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-06-28T14:09:03Z summary:
gh-152325: Add curses.has_mouse() and curses.window.mouse_trafo() (GH-152484) has_mouse() reports whether the mouse driver was successfully initialized. window.mouse_trafo(y, x, to_screen) converts a coordinate pair between window-relative and screen-relative coordinates, returning the (y, x) pair or None if it lies outside the window. Together these complete the curses mouse interface. Co-Authored-By: Claude Opus 4.8 <[email protected]> files: A Misc/NEWS.d/next/Library/2026-06-26-21-30-00.gh-issue-152325.Km9xQr.rst M Doc/library/curses.rst M Doc/whatsnew/3.16.rst M Lib/test/test_curses.py M Modules/_cursesmodule.c M Modules/clinic/_cursesmodule.c.h diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index f87ab7a8ee063d..86be23c931019e 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -346,6 +346,13 @@ The module :mod:`!curses` defines the following functions: a key with that value. +.. function:: has_mouse() + + Return ``True`` if the mouse driver has been successfully initialized. + + .. versionadded:: next + + .. function:: define_key(definition, keycode) Define an escape sequence *definition*, a string, as a key that generates @@ -1309,6 +1316,18 @@ Window objects Previously it returned ``1`` or ``0`` instead of ``True`` or ``False``. +.. method:: window.mouse_trafo(y, x, to_screen) + + Convert between window-relative and screen-relative (``stdscr``-relative) character-cell coordinates. + If *to_screen* is true, convert the window-relative coordinates *y*, *x* to screen-relative coordinates; + otherwise convert in the opposite direction. + The two coordinate systems differ when lines are reserved on the screen, for example for soft labels. + + Return the converted coordinates as a ``(y, x)`` tuple, or ``None`` if they lie outside the window. + + .. versionadded:: next + + .. attribute:: window.encoding Encoding used to encode method arguments (Unicode strings and characters). diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 23b88c5d279c2d..cde44221e05749 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -192,6 +192,11 @@ curses against an ncurses with ``NCURSES_EXT_FUNCS``. (Contributed by Serhiy Storchaka in :gh:`152334`.) +* Add the :func:`curses.has_mouse` function and the + :meth:`curses.window.mouse_trafo` method, completing the :mod:`curses` + mouse interface. + (Contributed by Serhiy Storchaka in :gh:`152325`.) + * :class:`curses.textpad.Textbox` now supports entering and reading back the full Unicode range, including combining characters, when curses is built with wide-character support. diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 20b1441d98584c..37caee79837888 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1305,6 +1305,22 @@ def test_enclose(self): self.assertIs(win.enclose(7, 19), False) self.assertIs(win.enclose(6, 20), False) + @requires_curses_window_meth('mouse_trafo') + def test_mouse_trafo(self): + win = curses.newwin(5, 15, 2, 5) + # to_screen=True: window-relative -> stdscr-relative. + self.assertEqual(win.mouse_trafo(0, 0, True), (2, 5)) + self.assertEqual(win.mouse_trafo(3, 10, True), (5, 15)) + self.assertEqual(win.mouse_trafo(4, 14, True), (6, 19)) + # A coordinate outside the window has no counterpart. + self.assertIsNone(win.mouse_trafo(5, 0, True)) + self.assertIsNone(win.mouse_trafo(0, 15, True)) + # to_screen=False is the inverse: stdscr-relative -> window-relative. + self.assertEqual(win.mouse_trafo(2, 5, False), (0, 0)) + self.assertEqual(win.mouse_trafo(6, 19, False), (4, 14)) + self.assertIsNone(win.mouse_trafo(1, 5, False)) + self.assertIsNone(win.mouse_trafo(7, 19, False)) + def test_putwin(self): win = curses.newwin(5, 12, 1, 2) win.addstr(2, 1, 'Lorem ipsum') @@ -1824,6 +1840,11 @@ def test_has_colors(self): self.assertIsInstance(curses.has_colors(), bool) self.assertIsInstance(curses.can_change_color(), bool) + @requires_curses_func('has_mouse') + def test_has_mouse(self): + # Whether a mouse is available depends on the terminal. + self.assertIsInstance(curses.has_mouse(), bool) + def test_start_color(self): if not curses.has_colors(): self.skipTest('requires colors support') diff --git a/Misc/NEWS.d/next/Library/2026-06-26-21-30-00.gh-issue-152325.Km9xQr.rst b/Misc/NEWS.d/next/Library/2026-06-26-21-30-00.gh-issue-152325.Km9xQr.rst new file mode 100644 index 00000000000000..655f61cbb530b1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-21-30-00.gh-issue-152325.Km9xQr.rst @@ -0,0 +1,2 @@ +Add the :func:`curses.has_mouse` function and the +:meth:`curses.window.mouse_trafo` method. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 46c453ac850c7e..4c183ce9a11db7 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -43,7 +43,7 @@ del_curterm mcprint mvcur restartterm ripoffline set_curterm setterm tgetent tgetflag tgetnum tgetstr tgoto tputs - vidattr vidputs wmouse_trafo + vidattr vidputs Low-priority: slk_attr slk_attr_off slk_attr_on slk_attr_set slk_attroff @@ -3007,6 +3007,36 @@ _curses_window_enclose_impl(PyCursesWindowObject *self, int y, int x) { return PyBool_FromLong(wenclose(self->win, y, x)); } + +/*[clinic input] +_curses.window.mouse_trafo + + y: int + Y-coordinate. + x: int + X-coordinate. + to_screen: bool + If True, convert window-relative coordinates to + stdscr-relative ones; otherwise convert the other way. + / + +Convert coordinates between window-relative and screen-relative. + +Return the converted (y, x) coordinates, or None if they are +outside the window. +[clinic start generated code]*/ + +static PyObject * +_curses_window_mouse_trafo_impl(PyCursesWindowObject *self, int y, int x, + int to_screen) +/*[clinic end generated code: output=b21572fa3524c15d input=c51fd793af7f6965]*/ +{ + int ry = y, rx = x; + if (!wmouse_trafo(self->win, &ry, &rx, to_screen)) { + Py_RETURN_NONE; + } + return Py_BuildValue("(ii)", ry, rx); +} #endif /*[clinic input] @@ -4836,6 +4866,7 @@ static PyMethodDef PyCursesWindow_methods[] = { _CURSES_WINDOW_DUPWIN_METHODDEF _CURSES_WINDOW_ECHOCHAR_METHODDEF _CURSES_WINDOW_ENCLOSE_METHODDEF + _CURSES_WINDOW_MOUSE_TRAFO_METHODDEF {"erase", PyCursesWindow_werase, METH_NOARGS, "erase($self, /)\n--\n\n" "Clear the window."}, @@ -6995,6 +7026,21 @@ _curses_meta_impl(PyObject *module, int yes) } #ifdef NCURSES_MOUSE_VERSION +/*[clinic input] +_curses.has_mouse + +Return True if the mouse driver has been successfully initialized. +[clinic start generated code]*/ + +static PyObject * +_curses_has_mouse_impl(PyObject *module) +/*[clinic end generated code: output=7901cc34069e4f57 input=94682101a11c4f30]*/ +{ + PyCursesStatefulInitialised(module); + + return PyBool_FromLong(has_mouse()); +} + /*[clinic input] _curses.mouseinterval @@ -8204,6 +8250,7 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_HAS_IC_METHODDEF _CURSES_HAS_IL_METHODDEF _CURSES_HAS_KEY_METHODDEF + _CURSES_HAS_MOUSE_METHODDEF _CURSES_DEFINE_KEY_METHODDEF _CURSES_KEY_DEFINED_METHODDEF _CURSES_KEYOK_METHODDEF diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index 8e0f922ad0444f..7b09b65d359d03 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -1093,6 +1093,63 @@ _curses_window_enclose(PyObject *self, PyObject *const *args, Py_ssize_t nargs) #endif /* defined(NCURSES_MOUSE_VERSION) */ +#if defined(NCURSES_MOUSE_VERSION) + +PyDoc_STRVAR(_curses_window_mouse_trafo__doc__, +"mouse_trafo($self, y, x, to_screen, /)\n" +"--\n" +"\n" +"Convert coordinates between window-relative and screen-relative.\n" +"\n" +" y\n" +" Y-coordinate.\n" +" x\n" +" X-coordinate.\n" +" to_screen\n" +" If True, convert window-relative coordinates to\n" +" stdscr-relative ones; otherwise convert the other way.\n" +"\n" +"Return the converted (y, x) coordinates, or None if they are\n" +"outside the window."); + +#define _CURSES_WINDOW_MOUSE_TRAFO_METHODDEF \ + {"mouse_trafo", _PyCFunction_CAST(_curses_window_mouse_trafo), METH_FASTCALL, _curses_window_mouse_trafo__doc__}, + +static PyObject * +_curses_window_mouse_trafo_impl(PyCursesWindowObject *self, int y, int x, + int to_screen); + +static PyObject * +_curses_window_mouse_trafo(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int y; + int x; + int to_screen; + + if (!_PyArg_CheckPositional("mouse_trafo", nargs, 3, 3)) { + goto exit; + } + y = PyLong_AsInt(args[0]); + if (y == -1 && PyErr_Occurred()) { + goto exit; + } + x = PyLong_AsInt(args[1]); + if (x == -1 && PyErr_Occurred()) { + goto exit; + } + to_screen = PyObject_IsTrue(args[2]); + if (to_screen < 0) { + goto exit; + } + return_value = _curses_window_mouse_trafo_impl((PyCursesWindowObject *)self, y, x, to_screen); + +exit: + return return_value; +} + +#endif /* defined(NCURSES_MOUSE_VERSION) */ + PyDoc_STRVAR(_curses_window_getbkgd__doc__, "getbkgd($self, /)\n" "--\n" @@ -4161,6 +4218,28 @@ _curses_meta(PyObject *module, PyObject *arg) #if defined(NCURSES_MOUSE_VERSION) +PyDoc_STRVAR(_curses_has_mouse__doc__, +"has_mouse($module, /)\n" +"--\n" +"\n" +"Return True if the mouse driver has been successfully initialized."); + +#define _CURSES_HAS_MOUSE_METHODDEF \ + {"has_mouse", (PyCFunction)_curses_has_mouse, METH_NOARGS, _curses_has_mouse__doc__}, + +static PyObject * +_curses_has_mouse_impl(PyObject *module); + +static PyObject * +_curses_has_mouse(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_has_mouse_impl(module); +} + +#endif /* defined(NCURSES_MOUSE_VERSION) */ + +#if defined(NCURSES_MOUSE_VERSION) + PyDoc_STRVAR(_curses_mouseinterval__doc__, "mouseinterval($module, interval, /)\n" "--\n" @@ -5467,6 +5546,10 @@ _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_MOUSE_TRAFO_METHODDEF + #define _CURSES_WINDOW_MOUSE_TRAFO_METHODDEF +#endif /* !defined(_CURSES_WINDOW_MOUSE_TRAFO_METHODDEF) */ + #ifndef _CURSES_WINDOW_NOUTREFRESH_METHODDEF #define _CURSES_WINDOW_NOUTREFRESH_METHODDEF #endif /* !defined(_CURSES_WINDOW_NOUTREFRESH_METHODDEF) */ @@ -5563,6 +5646,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_HAS_MOUSE_METHODDEF + #define _CURSES_HAS_MOUSE_METHODDEF +#endif /* !defined(_CURSES_HAS_MOUSE_METHODDEF) */ + #ifndef _CURSES_MOUSEINTERVAL_METHODDEF #define _CURSES_MOUSEINTERVAL_METHODDEF #endif /* !defined(_CURSES_MOUSEINTERVAL_METHODDEF) */ @@ -5602,4 +5689,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=f48f8e3554b30b86 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=09d21a41a5bd86dc input=a9049054013a1b77]*/ _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
