diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index d7873054d6b915..3ab937cad34773 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -348,6 +348,41 @@ The module :mod:`!curses` defines the following functions: no flushing is done. +.. function:: is_cbreak() + + Return ``True`` if cbreak mode (see :func:`cbreak`) is enabled, + ``False`` otherwise. + Availability: ncurses 6.5 or later. + + .. versionadded:: next + + +.. function:: is_echo() + + Return ``True`` if echo mode (see :func:`echo`) is enabled, + ``False`` otherwise. + Availability: ncurses 6.5 or later. + + .. versionadded:: next + + +.. function:: is_nl() + + Return ``True`` if nl mode (see :func:`nl`) is enabled, ``False`` otherwise. + Availability: ncurses 6.5 or later. + + .. versionadded:: next + + +.. function:: is_raw() + + Return ``True`` if raw mode (see :func:`raw`) is enabled, + ``False`` otherwise. + Availability: ncurses 6.5 or later. + + .. versionadded:: next + + .. function:: is_term_resized(nlines, ncols) Return ``True`` if :func:`resize_term` would modify the window structure, @@ -1005,6 +1040,16 @@ Window objects .. versionadded:: 3.3 +.. method:: window.getdelay() + + Return the window's read timeout in milliseconds, + as set by :meth:`nodelay` or :meth:`timeout`: + ``-1`` for blocking, ``0`` for non-blocking, + or a positive number of milliseconds. + + .. versionadded:: next + + .. method:: window.getkey([y, x]) Get a character, returning a string instead of an integer, as :meth:`getch` @@ -1018,6 +1063,14 @@ Window objects Return a tuple ``(y, x)`` of the height and width of the window. +.. method:: window.getparent() + + Return the parent window of this subwindow, + or ``None`` if this window is not a subwindow. + + .. versionadded:: next + + .. method:: window.getparyx() Return the beginning coordinates of this window relative to its parent window @@ -1025,6 +1078,14 @@ Window objects parent. +.. method:: window.getscrreg() + + Return a tuple ``(top, bottom)`` of the window's current scrolling region, + as set by :meth:`setscrreg`. + + .. versionadded:: next + + .. method:: window.getstr() window.getstr(n) window.getstr(y, x) @@ -1137,6 +1198,48 @@ Window objects The maximum value for *n* was increased from 1023 to 2047. +.. method:: window.is_cleared() + + Return the current value set by :meth:`clearok`. + + .. versionadded:: next + + +.. method:: window.is_idcok() + + Return the current value set by :meth:`idcok`. + + .. versionadded:: next + + +.. method:: window.is_idlok() + + Return the current value set by :meth:`idlok`. + + .. versionadded:: next + + +.. method:: window.is_immedok() + + Return the current value set by :meth:`immedok`. + + .. versionadded:: next + + +.. method:: window.is_keypad() + + Return the current value set by :meth:`keypad`. + + .. versionadded:: next + + +.. method:: window.is_leaveok() + + Return the current value set by :meth:`leaveok`. + + .. versionadded:: next + + .. method:: window.is_linetouched(line) Return ``True`` if the specified line was modified since the last call to @@ -1144,6 +1247,49 @@ Window objects exception if *line* is not valid for the given window. +.. method:: window.is_nodelay() + + Return the current value set by :meth:`nodelay`. + + .. versionadded:: next + + +.. method:: window.is_notimeout() + + Return the current value set by :meth:`notimeout`. + + .. versionadded:: next + + +.. method:: window.is_pad() + + Return ``True`` if the window is a pad created by :func:`newpad`. + + .. versionadded:: next + + +.. method:: window.is_scrollok() + + Return the current value set by :meth:`scrollok`. + + .. versionadded:: next + + +.. method:: window.is_subwin() + + Return ``True`` if the window is a subwindow created by :meth:`subwin` + or :meth:`derwin`. + + .. versionadded:: next + + +.. method:: window.is_syncok() + + Return the current value set by :meth:`syncok`. + + .. versionadded:: next + + .. method:: window.is_wintouched() Return ``True`` if the specified window was modified since the last call to diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 2b6f396bdf16dd..be683b1723bae7 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -92,6 +92,12 @@ curses * Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`. (Contributed by Serhiy Storchaka in :gh:`151744`.) +* Add :mod:`curses` functions and window methods that report state which could + previously only be set, such as :meth:`curses.window.is_keypad`, + :meth:`curses.window.getparent` and :func:`curses.is_cbreak`, + available when built against an ncurses with ``NCURSES_EXT_FUNCS``. + (Contributed by Serhiy Storchaka in :gh:`151776`.) + gzip ---- diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 98f1a7c8a0a2c5..0aac76dd720a81 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -890,6 +890,75 @@ def test_input_options(self): stdscr.timeout(0) stdscr.timeout(5) + @requires_curses_window_meth('is_scrollok') + def test_state_getters(self): + stdscr = self.stdscr + # Each is_*() getter returns the value set by the matching setter. + for setter, getter in [ + ('clearok', 'is_cleared'), + ('idcok', 'is_idcok'), + ('idlok', 'is_idlok'), + ('keypad', 'is_keypad'), + ('leaveok', 'is_leaveok'), + ('nodelay', 'is_nodelay'), + ('notimeout', 'is_notimeout'), + ('scrollok', 'is_scrollok'), + ]: + getattr(stdscr, setter)(True) + self.assertIs(getattr(stdscr, getter)(), True) + getattr(stdscr, setter)(False) + self.assertIs(getattr(stdscr, getter)(), False) + if hasattr(stdscr, 'immedok'): + stdscr.immedok(True) + self.assertIs(stdscr.is_immedok(), True) + stdscr.immedok(False) + if hasattr(stdscr, 'syncok'): + stdscr.syncok(True) + self.assertIs(stdscr.is_syncok(), True) + stdscr.syncok(False) + + # getdelay() reflects timeout()/nodelay(). + stdscr.timeout(100) + self.assertEqual(stdscr.getdelay(), 100) + stdscr.nodelay(True) + self.assertEqual(stdscr.getdelay(), 0) + stdscr.timeout(-1) + self.assertEqual(stdscr.getdelay(), -1) + + # getscrreg() reflects setscrreg(). + stdscr.setscrreg(5, 10) + self.assertEqual(stdscr.getscrreg(), (5, 10)) + + # is_pad()/is_subwin()/getparent(). + self.assertIs(stdscr.is_pad(), False) + self.assertIs(stdscr.is_subwin(), False) + self.assertIsNone(stdscr.getparent()) + sub = stdscr.subwin(3, 3, 0, 0) + self.assertIs(sub.is_subwin(), True) + self.assertIs(sub.getparent(), stdscr) + pad = curses.newpad(5, 5) + self.assertIs(pad.is_pad(), True) + + @requires_curses_func('is_cbreak') + def test_global_state_getters(self): + if self.isatty: + curses.cbreak() + self.assertIs(curses.is_cbreak(), True) + curses.nocbreak() + self.assertIs(curses.is_cbreak(), False) + curses.raw() + self.assertIs(curses.is_raw(), True) + curses.noraw() + self.assertIs(curses.is_raw(), False) + curses.echo() + self.assertIs(curses.is_echo(), True) + curses.noecho() + self.assertIs(curses.is_echo(), False) + curses.nl() + self.assertIs(curses.is_nl(), True) + curses.nonl() + self.assertIs(curses.is_nl(), False) + @requires_curses_func('typeahead') def test_typeahead(self): curses.typeahead(sys.__stdin__.fileno()) diff --git a/Misc/NEWS.d/next/Library/2026-06-20-12-10-02.gh-issue-151776.BtsXIF.rst b/Misc/NEWS.d/next/Library/2026-06-20-12-10-02.gh-issue-151776.BtsXIF.rst new file mode 100644 index 00000000000000..3eabc4a6bea82b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-20-12-10-02.gh-issue-151776.BtsXIF.rst @@ -0,0 +1,12 @@ +Add :mod:`curses` functions and window methods that report state which could +previously only be set: the window methods :meth:`~curses.window.is_cleared`, +:meth:`~curses.window.is_idcok`, :meth:`~curses.window.is_idlok`, +:meth:`~curses.window.is_immedok`, :meth:`~curses.window.is_keypad`, +:meth:`~curses.window.is_leaveok`, :meth:`~curses.window.is_nodelay`, +:meth:`~curses.window.is_notimeout`, :meth:`~curses.window.is_pad`, +:meth:`~curses.window.is_scrollok`, :meth:`~curses.window.is_subwin`, +:meth:`~curses.window.is_syncok`, :meth:`~curses.window.getdelay`, +:meth:`~curses.window.getparent` and :meth:`~curses.window.getscrreg`, and the +functions :func:`curses.is_cbreak`, :func:`curses.is_echo`, +:func:`curses.is_nl` and :func:`curses.is_raw`. They are only available when +built against an ncurses with ``NCURSES_EXT_FUNCS``. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index e60cba3ef87ead..9671e505aeded5 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -844,6 +844,52 @@ Window_NoArgNoReturnFunction(wdeleteln) Window_NoArgTrueFalseFunction(is_wintouched) +#if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404 +Window_NoArgTrueFalseFunction(is_cleared) +Window_NoArgTrueFalseFunction(is_idcok) +Window_NoArgTrueFalseFunction(is_idlok) +Window_NoArgTrueFalseFunction(is_immedok) +Window_NoArgTrueFalseFunction(is_keypad) +Window_NoArgTrueFalseFunction(is_leaveok) +Window_NoArgTrueFalseFunction(is_nodelay) +Window_NoArgTrueFalseFunction(is_notimeout) +Window_NoArgTrueFalseFunction(is_pad) +Window_NoArgTrueFalseFunction(is_scrollok) +Window_NoArgTrueFalseFunction(is_subwin) +Window_NoArgTrueFalseFunction(is_syncok) + +static PyObject * +PyCursesWindow_getdelay(PyObject *op, PyObject *Py_UNUSED(ignored)) +{ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + return PyLong_FromLong(wgetdelay(self->win)); +} + +static PyObject * +PyCursesWindow_getscrreg(PyObject *op, PyObject *Py_UNUSED(ignored)) +{ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + int top, bottom; + if (wgetscrreg(self->win, &top, &bottom) == ERR) { + curses_window_set_error(self, "wgetscrreg", "getscrreg"); + return NULL; + } + return Py_BuildValue("(ii)", top, bottom); +} + +static PyObject * +PyCursesWindow_getparent(PyObject *op, PyObject *Py_UNUSED(ignored)) +{ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + /* The standard window has no parent; subwindows keep a reference to the + window they were derived from. */ + if (self->orig == NULL) { + Py_RETURN_NONE; + } + return Py_NewRef((PyObject *)self->orig); +} +#endif /* NCURSES_EXT_FUNCS */ + Window_NoArgNoReturnVoidFunction(wsyncup) Window_NoArgNoReturnVoidFunction(wsyncdown) Window_NoArgNoReturnVoidFunction(wstandend) @@ -2973,12 +3019,28 @@ static PyMethodDef PyCursesWindow_methods[] = { _CURSES_WINDOW_GETCH_METHODDEF _CURSES_WINDOW_GETKEY_METHODDEF _CURSES_WINDOW_GET_WCH_METHODDEF +#if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404 + {"getdelay", PyCursesWindow_getdelay, METH_NOARGS, + "getdelay($self, /)\n--\n\n" + "Return the window's read timeout in milliseconds.\n\n" + "-1 means blocking, 0 means non-blocking; see nodelay() and timeout()."}, +#endif {"getmaxyx", PyCursesWindow_getmaxyx, METH_NOARGS, "getmaxyx($self, /)\n--\n\n" "Return a tuple (y, x) of the window height and width."}, +#if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404 + {"getparent", PyCursesWindow_getparent, METH_NOARGS, + "getparent($self, /)\n--\n\n" + "Return the parent window, or None if this is not a subwindow."}, +#endif {"getparyx", PyCursesWindow_getparyx, METH_NOARGS, "getparyx($self, /)\n--\n\n" "Return (y, x) relative to the parent window, or (-1, -1) if none."}, +#if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404 + {"getscrreg", PyCursesWindow_getscrreg, METH_NOARGS, + "getscrreg($self, /)\n--\n\n" + "Return a tuple (top, bottom) of the current scrolling region."}, +#endif { "getstr", PyCursesWindow_getstr, METH_VARARGS, _curses_window_getstr__doc__ @@ -3016,6 +3078,44 @@ static PyMethodDef PyCursesWindow_methods[] = { {"is_wintouched", PyCursesWindow_is_wintouched, METH_NOARGS, "is_wintouched($self, /)\n--\n\n" "Return True if the window changed since the last refresh()."}, +#if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404 + {"is_cleared", PyCursesWindow_is_cleared, METH_NOARGS, + "is_cleared($self, /)\n--\n\n" + "Return the current value set by clearok()."}, + {"is_idcok", PyCursesWindow_is_idcok, METH_NOARGS, + "is_idcok($self, /)\n--\n\n" + "Return the current value set by idcok()."}, + {"is_idlok", PyCursesWindow_is_idlok, METH_NOARGS, + "is_idlok($self, /)\n--\n\n" + "Return the current value set by idlok()."}, + {"is_immedok", PyCursesWindow_is_immedok, METH_NOARGS, + "is_immedok($self, /)\n--\n\n" + "Return the current value set by immedok()."}, + {"is_keypad", PyCursesWindow_is_keypad, METH_NOARGS, + "is_keypad($self, /)\n--\n\n" + "Return the current value set by keypad()."}, + {"is_leaveok", PyCursesWindow_is_leaveok, METH_NOARGS, + "is_leaveok($self, /)\n--\n\n" + "Return the current value set by leaveok()."}, + {"is_nodelay", PyCursesWindow_is_nodelay, METH_NOARGS, + "is_nodelay($self, /)\n--\n\n" + "Return the current value set by nodelay()."}, + {"is_notimeout", PyCursesWindow_is_notimeout, METH_NOARGS, + "is_notimeout($self, /)\n--\n\n" + "Return the current value set by notimeout()."}, + {"is_pad", PyCursesWindow_is_pad, METH_NOARGS, + "is_pad($self, /)\n--\n\n" + "Return True if the window is a pad."}, + {"is_scrollok", PyCursesWindow_is_scrollok, METH_NOARGS, + "is_scrollok($self, /)\n--\n\n" + "Return the current value set by scrollok()."}, + {"is_subwin", PyCursesWindow_is_subwin, METH_NOARGS, + "is_subwin($self, /)\n--\n\n" + "Return True if the window is a subwindow."}, + {"is_syncok", PyCursesWindow_is_syncok, METH_NOARGS, + "is_syncok($self, /)\n--\n\n" + "Return the current value set by syncok()."}, +#endif {"keypad", PyCursesWindow_keypad, METH_VARARGS, "keypad($self, flag, /)\n--\n\n" "Interpret escape sequences for special keys if flag is true."}, @@ -3298,6 +3398,65 @@ _curses_cbreak_impl(PyObject *module, int flag) /*[clinic end generated code: output=9f9dee9664769751 input=42d81687f11ddbf3]*/ NoArgOrFlagNoReturnFunctionBody(cbreak, flag) +/* is_cbreak()/is_echo()/is_nl()/is_raw() were added in ncurses 6.5. */ +#if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427 +/*[clinic input] +_curses.is_cbreak + +Return True if cbreak mode is enabled, False otherwise. +[clinic start generated code]*/ + +static PyObject * +_curses_is_cbreak_impl(PyObject *module) +/*[clinic end generated code: output=8a1ad7889fb43daf input=99988df6fd2f1c81]*/ +{ + PyCursesStatefulInitialised(module); + return PyBool_FromLong(is_cbreak()); +} + +/*[clinic input] +_curses.is_echo + +Return True if echo mode is enabled, False otherwise. +[clinic start generated code]*/ + +static PyObject * +_curses_is_echo_impl(PyObject *module) +/*[clinic end generated code: output=72692d2aa41591c4 input=f6152cf7c00e47eb]*/ +{ + PyCursesStatefulInitialised(module); + return PyBool_FromLong(is_echo()); +} + +/*[clinic input] +_curses.is_nl + +Return True if nl mode is enabled, False otherwise. +[clinic start generated code]*/ + +static PyObject * +_curses_is_nl_impl(PyObject *module) +/*[clinic end generated code: output=999eb44abc43ce65 input=1e0a2607e45a01e1]*/ +{ + PyCursesStatefulInitialised(module); + return PyBool_FromLong(is_nl()); +} + +/*[clinic input] +_curses.is_raw + +Return True if raw mode is enabled, False otherwise. +[clinic start generated code]*/ + +static PyObject * +_curses_is_raw_impl(PyObject *module) +/*[clinic end generated code: output=dd9816d777561c35 input=a64fa6a251ed3ece]*/ +{ + PyCursesStatefulInitialised(module); + return PyBool_FromLong(is_raw()); +} +#endif /* NCURSES_EXT_FUNCS */ + /*[clinic input] _curses.color_content @@ -5360,6 +5519,10 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_INIT_PAIR_METHODDEF _CURSES_INITSCR_METHODDEF _CURSES_INTRFLUSH_METHODDEF + _CURSES_IS_CBREAK_METHODDEF + _CURSES_IS_ECHO_METHODDEF + _CURSES_IS_NL_METHODDEF + _CURSES_IS_RAW_METHODDEF _CURSES_ISENDWIN_METHODDEF _CURSES_IS_TERM_RESIZED_METHODDEF _CURSES_KEYNAME_METHODDEF diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index f577368680ef57..167043292f0726 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -1991,6 +1991,94 @@ _curses_cbreak(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) + +PyDoc_STRVAR(_curses_is_cbreak__doc__, +"is_cbreak($module, /)\n" +"--\n" +"\n" +"Return True if cbreak mode is enabled, False otherwise."); + +#define _CURSES_IS_CBREAK_METHODDEF \ + {"is_cbreak", (PyCFunction)_curses_is_cbreak, METH_NOARGS, _curses_is_cbreak__doc__}, + +static PyObject * +_curses_is_cbreak_impl(PyObject *module); + +static PyObject * +_curses_is_cbreak(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_is_cbreak_impl(module); +} + +#endif /* (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) */ + +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) + +PyDoc_STRVAR(_curses_is_echo__doc__, +"is_echo($module, /)\n" +"--\n" +"\n" +"Return True if echo mode is enabled, False otherwise."); + +#define _CURSES_IS_ECHO_METHODDEF \ + {"is_echo", (PyCFunction)_curses_is_echo, METH_NOARGS, _curses_is_echo__doc__}, + +static PyObject * +_curses_is_echo_impl(PyObject *module); + +static PyObject * +_curses_is_echo(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_is_echo_impl(module); +} + +#endif /* (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) */ + +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) + +PyDoc_STRVAR(_curses_is_nl__doc__, +"is_nl($module, /)\n" +"--\n" +"\n" +"Return True if nl mode is enabled, False otherwise."); + +#define _CURSES_IS_NL_METHODDEF \ + {"is_nl", (PyCFunction)_curses_is_nl, METH_NOARGS, _curses_is_nl__doc__}, + +static PyObject * +_curses_is_nl_impl(PyObject *module); + +static PyObject * +_curses_is_nl(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_is_nl_impl(module); +} + +#endif /* (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) */ + +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) + +PyDoc_STRVAR(_curses_is_raw__doc__, +"is_raw($module, /)\n" +"--\n" +"\n" +"Return True if raw mode is enabled, False otherwise."); + +#define _CURSES_IS_RAW_METHODDEF \ + {"is_raw", (PyCFunction)_curses_is_raw, METH_NOARGS, _curses_is_raw__doc__}, + +static PyObject * +_curses_is_raw_impl(PyObject *module); + +static PyObject * +_curses_is_raw(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_is_raw_impl(module); +} + +#endif /* (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) */ + PyDoc_STRVAR(_curses_color_content__doc__, "color_content($module, color_number, /)\n" "--\n" @@ -4437,6 +4525,22 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_NOFILTER_METHODDEF #endif /* !defined(_CURSES_NOFILTER_METHODDEF) */ +#ifndef _CURSES_IS_CBREAK_METHODDEF + #define _CURSES_IS_CBREAK_METHODDEF +#endif /* !defined(_CURSES_IS_CBREAK_METHODDEF) */ + +#ifndef _CURSES_IS_ECHO_METHODDEF + #define _CURSES_IS_ECHO_METHODDEF +#endif /* !defined(_CURSES_IS_ECHO_METHODDEF) */ + +#ifndef _CURSES_IS_NL_METHODDEF + #define _CURSES_IS_NL_METHODDEF +#endif /* !defined(_CURSES_IS_NL_METHODDEF) */ + +#ifndef _CURSES_IS_RAW_METHODDEF + #define _CURSES_IS_RAW_METHODDEF +#endif /* !defined(_CURSES_IS_RAW_METHODDEF) */ + #ifndef _CURSES_GETSYX_METHODDEF #define _CURSES_GETSYX_METHODDEF #endif /* !defined(_CURSES_GETSYX_METHODDEF) */ @@ -4516,4 +4620,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=68f33f61fb666127 input=a9049054013a1b77]*/