From 2b5208a2c676494448d45d74d3e9220300c5a832 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 1 Jul 2026 07:51:00 +0300 Subject: [PATCH] [3.13] gh-152502: Detect the curses mouse interface portably (GH-152705) The mouse interface (getmouse(), the BUTTON* constants, ...) was gated on the ncurses-specific NCURSES_MOUSE_VERSION macro, so it was dropped on other curses implementations that provide it, such as NetBSD curses and PDCurses. Gate it instead on a configure capability probe or the PDCURSES macro. Probe for getmouse() with its X/Open getmouse(MEVENT *) signature, since PDCurses declares an incompatible getmouse(void) unless built for the ncurses mouse API, which PDC_NCMOUSE now always selects. The is_*() state-query methods named in the original change do not exist on 3.15, so only the mouse interface is backported. (cherry picked from commit 7bbea4f4868ef89b07b986d7a0d4b585e8271f27) Co-Authored-By: Claude Opus 4.8 --- Include/py_curses.h | 7 +++ ...-06-30-21-40-00.gh-issue-152502.Kq3Vn7.rst | 5 ++ Modules/_cursesmodule.c | 10 ++-- Modules/clinic/_cursesmodule.c.h | 22 ++++----- configure | 49 +++++++++++++++++++ configure.ac | 12 +++++ pyconfig.h.in | 3 ++ 7 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-30-21-40-00.gh-issue-152502.Kq3Vn7.rst diff --git a/Include/py_curses.h b/Include/py_curses.h index 3e8b16c201f810..6024f5327f31fc 100644 --- a/Include/py_curses.h +++ b/Include/py_curses.h @@ -36,6 +36,13 @@ #define NCURSES_OPAQUE 0 #endif +/* PDCurses exposes its ncurses-compatible mouse API, the one this module uses, + only when this is defined before the curses header is included below. + Ignored by other curses implementations. */ +#ifndef PDC_NCMOUSE +# define PDC_NCMOUSE +#endif + #if defined(HAVE_NCURSESW_NCURSES_H) # include #elif defined(HAVE_NCURSESW_CURSES_H) diff --git a/Misc/NEWS.d/next/Library/2026-06-30-21-40-00.gh-issue-152502.Kq3Vn7.rst b/Misc/NEWS.d/next/Library/2026-06-30-21-40-00.gh-issue-152502.Kq3Vn7.rst new file mode 100644 index 00000000000000..815bc47966c40c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-30-21-40-00.gh-issue-152502.Kq3Vn7.rst @@ -0,0 +1,5 @@ +Detect the :mod:`curses` mouse interface (:func:`~curses.getmouse`, the +``BUTTON*`` constants, and others) with a configure capability probe or library +macros instead of gating it on ncurses-specific macros. It is now also +available with other curses implementations that provide it, such as NetBSD +curses and PDCurses (the latter underpins ``windows-curses``). diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 852aee3fdcbe6c..23a09656a261aa 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1358,7 +1358,7 @@ _curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch, "echochar"); } -#ifdef NCURSES_MOUSE_VERSION +#if defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES) /*[clinic input] _curses.window.enclose @@ -3003,7 +3003,7 @@ _curses_getsyx_impl(PyObject *module) } #endif -#ifdef NCURSES_MOUSE_VERSION +#if defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES) /*[clinic input] _curses.getmouse @@ -3699,7 +3699,7 @@ _curses_meta_impl(PyObject *module, int yes) return PyCursesCheckERR(meta(stdscr, yes), "meta"); } -#ifdef NCURSES_MOUSE_VERSION +#if defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES) /*[clinic input] _curses.mouseinterval @@ -4946,7 +4946,7 @@ PyInit__curses(void) SetDictInt("COLOR_CYAN", COLOR_CYAN); SetDictInt("COLOR_WHITE", COLOR_WHITE); -#ifdef NCURSES_MOUSE_VERSION +#if defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES) /* Mouse-related constants */ SetDictInt("BUTTON1_PRESSED", BUTTON1_PRESSED); SetDictInt("BUTTON1_RELEASED", BUTTON1_RELEASED); @@ -4972,7 +4972,7 @@ PyInit__curses(void) SetDictInt("BUTTON4_DOUBLE_CLICKED", BUTTON4_DOUBLE_CLICKED); SetDictInt("BUTTON4_TRIPLE_CLICKED", BUTTON4_TRIPLE_CLICKED); -#if NCURSES_MOUSE_VERSION > 1 +#ifdef BUTTON5_PRESSED SetDictInt("BUTTON5_PRESSED", BUTTON5_PRESSED); SetDictInt("BUTTON5_RELEASED", BUTTON5_RELEASED); SetDictInt("BUTTON5_CLICKED", BUTTON5_CLICKED); diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index 3d128f6c9497b9..b7a2551ff2028c 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -683,7 +683,7 @@ _curses_window_echochar(PyCursesWindowObject *self, PyObject *const *args, Py_ss return return_value; } -#if defined(NCURSES_MOUSE_VERSION) +#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) PyDoc_STRVAR(_curses_window_enclose__doc__, "enclose($self, y, x, /)\n" @@ -726,7 +726,7 @@ _curses_window_enclose(PyCursesWindowObject *self, PyObject *const *args, Py_ssi return return_value; } -#endif /* defined(NCURSES_MOUSE_VERSION) */ +#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ PyDoc_STRVAR(_curses_window_getbkgd__doc__, "getbkgd($self, /)\n" @@ -2303,7 +2303,7 @@ _curses_getsyx(PyObject *module, PyObject *Py_UNUSED(ignored)) #endif /* defined(getsyx) */ -#if defined(NCURSES_MOUSE_VERSION) +#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) PyDoc_STRVAR(_curses_getmouse__doc__, "getmouse($module, /)\n" @@ -2326,9 +2326,9 @@ _curses_getmouse(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_getmouse_impl(module); } -#endif /* defined(NCURSES_MOUSE_VERSION) */ +#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ -#if defined(NCURSES_MOUSE_VERSION) +#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) PyDoc_STRVAR(_curses_ungetmouse__doc__, "ungetmouse($module, id, x, y, z, bstate, /)\n" @@ -2400,7 +2400,7 @@ _curses_ungetmouse(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -#endif /* defined(NCURSES_MOUSE_VERSION) */ +#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ PyDoc_STRVAR(_curses_getwin__doc__, "getwin($module, file, /)\n" @@ -3089,7 +3089,7 @@ _curses_meta(PyObject *module, PyObject *arg) return return_value; } -#if defined(NCURSES_MOUSE_VERSION) +#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) PyDoc_STRVAR(_curses_mouseinterval__doc__, "mouseinterval($module, interval, /)\n" @@ -3126,9 +3126,9 @@ _curses_mouseinterval(PyObject *module, PyObject *arg) return return_value; } -#endif /* defined(NCURSES_MOUSE_VERSION) */ +#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ -#if defined(NCURSES_MOUSE_VERSION) +#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) PyDoc_STRVAR(_curses_mousemask__doc__, "mousemask($module, newmask, /)\n" @@ -3164,7 +3164,7 @@ _curses_mousemask(PyObject *module, PyObject *arg) return return_value; } -#endif /* defined(NCURSES_MOUSE_VERSION) */ +#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ PyDoc_STRVAR(_curses_napms__doc__, "napms($module, ms, /)\n" @@ -4395,4 +4395,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=ca163c1f0f8293e2 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=28bf3e5445c762b4 input=a9049054013a1b77]*/ diff --git a/configure b/configure index 47129847ac5d5d..476aeed494ed09 100755 --- a/configure +++ b/configure @@ -27836,6 +27836,55 @@ fi + +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for ncurses-style curses function getmouse" >&5 +printf %s "checking for ncurses-style curses function getmouse... " >&6; } +if test ${ac_cv_lib_curses_getmouse+y} +then : + printf %s "(cached) " >&6 +else $as_nop + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#define NCURSES_OPAQUE 0 +#if defined(HAVE_NCURSESW_NCURSES_H) +# include +#elif defined(HAVE_NCURSESW_CURSES_H) +# include +#elif defined(HAVE_NCURSES_NCURSES_H) +# include +#elif defined(HAVE_NCURSES_CURSES_H) +# include +#elif defined(HAVE_NCURSES_H) +# include +#elif defined(HAVE_CURSES_H) +# include +#endif + +int +main (void) +{ +MEVENT event; (void)getmouse(&event); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_getmouse=yes +else $as_nop + ac_cv_lib_curses_getmouse=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_getmouse" >&5 +printf "%s\n" "$ac_cv_lib_curses_getmouse" >&6; } +if test "x$ac_cv_lib_curses_getmouse" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_GETMOUSE 1" >>confdefs.h + +fi CPPFLAGS=$ac_save_cppflags fi diff --git a/configure.ac b/configure.ac index 9ff641bd0d403c..f57d4ff573756e 100644 --- a/configure.ac +++ b/configure.ac @@ -6976,6 +6976,18 @@ PY_CHECK_CURSES_FUNC([set_escdelay]) PY_CHECK_CURSES_FUNC([set_tabsize]) PY_CHECK_CURSES_VAR([ESCDELAY]) PY_CHECK_CURSES_VAR([TABSIZE]) + +dnl Probe for the X/Open getmouse(MEVENT *) signature specifically: PDCurses +dnl declares an incompatible getmouse(void) unless built for the ncurses mouse API. +AC_CACHE_CHECK([for ncurses-style curses function getmouse], + [ac_cv_lib_curses_getmouse], + [AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM(_CURSES_INCLUDES, [MEVENT event; (void)getmouse(&event);])], + [ac_cv_lib_curses_getmouse=yes], + [ac_cv_lib_curses_getmouse=no])]) +AS_VAR_IF([ac_cv_lib_curses_getmouse], [yes], + [AC_DEFINE([HAVE_CURSES_GETMOUSE], [1], + [Define if you have the 'getmouse' function with the X/Open signature.])]) CPPFLAGS=$ac_save_cppflags ])dnl have_curses != no ])dnl save env diff --git a/pyconfig.h.in b/pyconfig.h.in index 7afea416539ecc..42a9f048a1776f 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -189,6 +189,9 @@ /* Define if you have the 'filter' function. */ #undef HAVE_CURSES_FILTER +/* Define if you have the 'getmouse' function with the X/Open signature. */ +#undef HAVE_CURSES_GETMOUSE + /* Define to 1 if you have the header file. */ #undef HAVE_CURSES_H