]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-152325: Add curses.has_mouse() and curses.window.mouse_trafo() (GH-152484)
authorSerhiy Storchaka <storchaka@gmail.com>
Sun, 28 Jun 2026 14:09:03 +0000 (17:09 +0300)
committerGitHub <noreply@github.com>
Sun, 28 Jun 2026 14:09:03 +0000 (14:09 +0000)
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 <noreply@anthropic.com>
Doc/library/curses.rst
Doc/whatsnew/3.16.rst
Lib/test/test_curses.py
Misc/NEWS.d/next/Library/2026-06-26-21-30-00.gh-issue-152325.Km9xQr.rst [new file with mode: 0644]
Modules/_cursesmodule.c
Modules/clinic/_cursesmodule.c.h

index f87ab7a8ee063ddcf71c0daf5b6051cad5e0542a..86be23c931019eb1fc4c25515eb311504cba8200 100644 (file)
@@ -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).
index 23b88c5d279c2dd7e9c5b16e0fa1f6969e415065..cde44221e05749a71f8883280879ebd2c5fe93fb 100644 (file)
@@ -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.
index 20b1441d98584c530ae413f4766c71e13cea5f97..37caee7983788802e5a7c5760c316c5a5f97649c 100644 (file)
@@ -1305,6 +1305,22 @@ class TestCurses(unittest.TestCase):
         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 @@ class TestCurses(unittest.TestCase):
         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 (file)
index 0000000..655f61c
--- /dev/null
@@ -0,0 +1,2 @@
+Add the :func:`curses.has_mouse` function and the
+:meth:`curses.window.mouse_trafo` method.
index 46c453ac850c7eb8586ca028ea9ba64213e36c2e..4c183ce9a11db73a1c6146be902ace8cff090311 100644 (file)
@@ -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
index 8e0f922ad0444f547a8fa1610264eacbe02ee9fc..7b09b65d359d03157c8deb8610580350a103351f 100644 (file)
@@ -1093,6 +1093,63 @@ exit:
 
 #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 @@ exit:
 
 #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]*/