diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index d7873054d6b9154..2808b6259497aa3 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -207,8 +207,9 @@ The module :mod:`!curses` defines the following functions: .. function:: nofilter() Undo the effect of a previous :func:`.filter` call. - Like :func:`.filter`, it must be called before :func:`initscr` so that the - next initialization uses the full screen again. + Like :func:`.filter`, it must be called before :func:`initscr` (or + :func:`newterm`) so that the next initialization uses the full screen + again. Availability: if the underlying curses library provides ``nofilter()``. @@ -442,6 +443,36 @@ The module :mod:`!curses` defines the following functions: right corner of the screen. +.. function:: newterm(type=None, fd=None, infd=None, /) + + Initialize a new terminal in addition to the one initialized by + :func:`initscr`, + and return a :ref:`screen ` for it. + This allows a program to drive more than one terminal. + + *type* is the terminal name, as in :func:`setupterm`; + if ``None``, the value of the :envvar:`TERM` environment variable is used. + *fd* and *infd* are the output and input files for the terminal: + either a file object or a file descriptor. + They default to :data:`sys.stdout` and :data:`sys.stdin`. + + The new screen becomes the current one. + Use :func:`set_term` to switch between screens. + + .. versionadded:: next + + +.. function:: new_prescr() + + Return a new :ref:`screen ` + that can be used to call functions that affect global state + before :func:`initscr` or :func:`newterm` is called. + + Availability: if the underlying curses library provides ``new_prescr()``. + + .. versionadded:: next + + .. function:: nl(flag=True) Enter newline mode. This mode translates the return key into newline on input, @@ -586,6 +617,17 @@ The module :mod:`!curses` defines the following functions: .. versionadded:: 3.9 + +.. function:: set_term(screen, /) + + Make *screen*, a :ref:`screen ` returned by + :func:`newterm`, the current terminal, + and return the previously current screen. + Returns ``None`` if the previous screen was the one created by + :func:`initscr`. + + .. versionadded:: next + .. function:: setsyx(y, x) Set the virtual screen cursor to *y*, *x*. If *y* and *x* are both ``-1``, then @@ -1380,6 +1422,18 @@ Window objects :meth:`refresh`. +.. method:: window.use(func, /, *args, **kwargs) + + Call ``func(window, *args, **kwargs)`` with the lock of the window held, + and return its result. + This provides automatic protection for the window + against concurrent access from another thread. + + Availability: if the underlying curses library provides ``use_window()``. + + .. versionadded:: next + + .. method:: window.vline(ch, n[, attr]) window.vline(y, x, ch, n[, attr]) @@ -1387,6 +1441,60 @@ Window objects character *ch* with attributes *attr*. +.. _curses-screen-objects: + +Screen objects +-------------- + +.. class:: screen + + A *screen* object represents a terminal initialized by :func:`newterm` + (or :func:`new_prescr`), + in addition to the default screen created by :func:`initscr`. + Screen objects are returned by those functions; + they cannot be instantiated directly. + + A screen is freed automatically once it is no longer referenced, + either directly or through one of its windows. + Each window keeps its screen alive, + so a screen remains valid as long as any of its windows does. + + .. versionadded:: next + + +.. method:: screen.close() + + Detach the screen's standard window, + breaking the reference cycle between them + so the screen can be reclaimed promptly instead of waiting for a + garbage collection. + Afterwards :attr:`~screen.stdscr` is ``None`` + and the window it returned earlier can no longer be used. + The screen's resources are released + once it and all its windows are no longer referenced. + + .. versionadded:: next + + +.. attribute:: screen.stdscr + + The standard :ref:`window ` of the screen, + covering the whole terminal, + or ``None`` for a screen created by :func:`new_prescr`. + + +.. method:: screen.use(func, /, *args, **kwargs) + + Call ``func(screen, *args, **kwargs)`` with the lock of the screen held, + and return its result. + This provides automatic protection for the screen + against concurrent access from another thread. + + Availability: if the underlying curses library provides ``use_screen()``. + + .. versionadded:: next + + Constants --------- diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 0a110795f371eb7..7baa8bba398a899 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -89,6 +89,13 @@ Improved modules curses ------ +* Add support for multiple terminals to the :mod:`curses` module: + the new functions :func:`curses.newterm`, :func:`curses.set_term` + and :func:`curses.new_prescr`, + the corresponding :ref:`screen ` object, + and the :meth:`window.use` method. + (Contributed by Serhiy Storchaka in :gh:`90092`.) + * Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`. (Contributed by Serhiy Storchaka in :gh:`151744`.) diff --git a/Include/py_curses.h b/Include/py_curses.h index 0948aabedd49939..3d2ca278f809cb2 100644 --- a/Include/py_curses.h +++ b/Include/py_curses.h @@ -80,8 +80,18 @@ typedef struct PyCursesWindowObject { WINDOW *win; char *encoding; struct PyCursesWindowObject *orig; + PyObject *screen; /* the screen the window belongs to, or NULL, + kept alive for the lifetime of the window */ } PyCursesWindowObject; +typedef struct { + PyObject_HEAD + SCREEN *screen; /* NULL after the screen has been deleted */ + FILE *outfp; /* owned output stream, or NULL */ + FILE *infp; /* owned input stream, or NULL */ + PyObject *stdscr; /* the screen's standard window, or NULL */ +} PyCursesScreenObject; + #define PyCurses_CAPSULE_NAME "_curses._C_API" diff --git a/Lib/curses/__init__.py b/Lib/curses/__init__.py index 605d5fcbec5499d..e150c7f932385eb 100644 --- a/Lib/curses/__init__.py +++ b/Lib/curses/__init__.py @@ -34,6 +34,23 @@ def initscr(): setattr(curses, key, value) return stdscr +# newterm() is wrapped for the same reason as initscr(): the ACS_* constants +# and LINES/COLS only become available once a terminal is initialized, and are +# then copied to the curses package's dictionary. + +try: + newterm +except NameError: + pass +else: + def newterm(type=None, fd=None, infd=None, /): + import _curses, curses + screen = _curses.newterm(type, fd, infd) + for key, value in _curses.__dict__.items(): + if key.startswith('ACS_') or key in ('LINES', 'COLS'): + setattr(curses, key, value) + return screen + # This is a similar wrapper for start_color(), which adds the COLORS and # COLOR_PAIRS variables which are only available after start_color() is # called. diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 98f1a7c8a0a2c5c..796dfe1e5168e1b 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1,15 +1,17 @@ import functools import inspect import os +import select import string import sys import tempfile +import threading import unittest from unittest.mock import MagicMock from test.support import (requires, verbose, SaveSignals, cpython_only, check_disallow_instantiation, MISSING_C_DOCSTRINGS, - gc_collect) + gc_collect, SHORT_TIMEOUT) from test.support.import_helper import import_module # Optionally test curses module. This currently requires that the @@ -53,9 +55,11 @@ def wrapped(self, *args, **kwargs): term = os.environ.get('TERM') SHORT_MAX = 0x7fff -# If newterm was supported we could use it instead of initscr and not exit +# newterm() is used when available (it reports errors instead of exiting), but +# initscr() is still the fallback, and an unusable $TERM has no terminal to +# drive either way. @unittest.skipIf(not term or term == 'unknown', - "$TERM=%r, calling initscr() may cause exit" % term) + "$TERM=%r, no usable terminal" % term) @unittest.skipIf(sys.platform == "cygwin", "cygwin's curses mostly just hangs") class TestCurses(unittest.TestCase): @@ -110,7 +114,25 @@ def setUp(self): sys.stderr.flush() sys.stdout.flush() print(file=self.output, flush=True) - self.stdscr = curses.initscr() + if hasattr(curses, 'newterm'): + # Use newterm() rather than initscr(): it reports errors instead of + # exiting, and gives each test a fresh screen, which also lets + # ScreenTests run newterm()/set_term() in the same process. + try: + infd = sys.__stdin__.fileno() + except (AttributeError, ValueError, OSError): + infd = stdout_fd + self.screen = curses.newterm(term, stdout_fd, infd) + self.stdscr = self.screen.stdscr + # Close the screen after the test to break its window<->screen + # reference cycle deterministically, rather than leaving it for the + # cyclic GC to collect during a much later test (where a window's + # delwin() can fail -- an unraisable error on macOS). + self.addCleanup(self.screen.close) + self.addCleanup(setattr, self, 'screen', None) + self.addCleanup(setattr, self, 'stdscr', None) + else: + self.stdscr = curses.initscr() if self.isatty: curses.savetty() self.addCleanup(curses.endwin) @@ -119,10 +141,12 @@ def setUp(self): @requires_curses_func('filter') def test_filter(self): - # TODO: Should be called before initscr() or newterm() are called. + # filter() must be called before initscr()/newterm(); it confines + # curses to a single line. Undo it with nofilter() afterwards so that + # it does not shrink the screens created by later tests. curses.filter() if hasattr(curses, 'nofilter'): - curses.nofilter() + self.addCleanup(curses.nofilter) @requires_curses_func('use_env') def test_use_env(self): @@ -1089,6 +1113,22 @@ def test_use_default_colors(self): self.skipTest('cannot change color (use_default_colors() failed)') self.assertEqual(curses.pair_content(0), (-1, -1)) + @requires_curses_window_meth('use') + def test_use_window(self): + win = self.stdscr + self.assertEqual(win.use(lambda w, a, b: (w is win, a, b), 5, b=6), + (True, 5, 6)) + with self.assertRaises(ZeroDivisionError): + win.use(lambda w: 1 / 0) + + @unittest.skipUnless(hasattr(curses.screen, 'use'), + 'requires screen.use()') + def test_use_screen(self): + screen = self.screen + self.assertEqual( + screen.use(lambda sc, flag: (sc is screen, flag), flag=True), + (True, True)) + @requires_curses_func('assume_default_colors') @requires_colors def test_assume_default_colors(self): @@ -1155,6 +1195,10 @@ def test_userptr_memory_leak(self): def test_userptr_segfault(self): w = curses.newwin(10, 10) panel = curses.panel.new_panel(w) + # set_userptr(A()) makes a panel<->userptr reference cycle (A.__del__ + # closes over panel); clean it up so the panel and its window do not + # linger until a later test collects them. + self.addCleanup(self._delete_panels, panel) class A: def __del__(self): panel.set_userptr(None) @@ -1387,9 +1431,11 @@ def test_resize_term(self): curses.resize_term(35000, 1) with self.assertRaises(OverflowError): curses.resize_term(1, 35000) - # GH-120378: Overflow failure in resize_term() causes refresh to fail - tmp = curses.initscr() - tmp.erase() + # GH-120378: a failed resize can leave refresh broken; restore the + # original size to recover. Avoid initscr(), which would switch away + # from the shared newterm() screen and corrupt later tests. + curses.resize_term(lines, cols) + self.stdscr.erase() @requires_curses_func('resizeterm') def test_resizeterm(self): @@ -1409,9 +1455,11 @@ def test_resizeterm(self): curses.resizeterm(35000, 1) with self.assertRaises(OverflowError): curses.resizeterm(1, 35000) - # GH-120378: Overflow failure in resizeterm() causes refresh to fail - tmp = curses.initscr() - tmp.erase() + # GH-120378: a failed resize can leave refresh broken; restore the + # original size to recover. Avoid initscr(), which would switch away + # from the shared newterm() screen and corrupt later tests. + curses.resizeterm(lines, cols) + self.stdscr.erase() def test_ungetch(self): curses.ungetch(b'A') @@ -1717,5 +1765,141 @@ def test_move_down(self): self.mock_win.reset_mock() +@unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') +@unittest.skipIf(not term or term == 'unknown', + "$TERM=%r, newterm() may not work" % term) +@unittest.skipIf(sys.platform == "cygwin", + "cygwin's curses mostly just hangs") +class ScreenTests(unittest.TestCase): + # newterm()/set_term() mutate global curses state, but each test drives its + # own pseudo-terminal(s) and never touches the screen shared by TestCurses, + # whose setUp() makes that screen current again. So these can run in this + # process, without a real terminal and without a subprocess. + + def setUp(self): + # newterm() may install signal handlers; restore them afterwards. + self.save_signals = SaveSignals() + self.save_signals.save() + self.addCleanup(self.save_signals.restore) + + def tearDown(self): + # Leave visual mode and reclaim the test's screens while their + # pseudo-terminals are still open (make_pty() closes them later). + try: + curses.endwin() + except curses.error: + pass + gc_collect() + + @staticmethod + def _drain_pty(master, stop): + # Read and discard whatever curses writes to the screen, until asked to + # stop and nothing more is pending. poll() rather than a blocking + # read() so we can stop without closing the fd (closing it while this + # thread is blocked in read() hangs on macOS). + poller = select.poll() + poller.register(master, select.POLLIN) + while True: + if poller.poll(100): + try: + if not os.read(master, 1024): + break # EOF + except OSError: + break + elif stop.is_set(): + break + + def make_pty(self): + master, slave = os.openpty() + # Nothing reads the master end, so writing to the slave and the + # tcdrain() in endwin() can block on macOS once the pty buffer fills; + # drain it from a background thread (endwin() releases the GIL). + stop = threading.Event() + reader = threading.Thread(target=self._drain_pty, args=(master, stop), + daemon=True) + reader.start() + # Stop and join the reader before closing the fds: on macOS, closing + # either end while the reader is blocked in read() hangs. + def stop_reader(): + stop.set() + reader.join(SHORT_TIMEOUT) + self.addCleanup(os.close, master) + self.addCleanup(os.close, slave) + self.addCleanup(stop_reader) + return slave + + def test_newterm(self): + s = self.make_pty() + screen = curses.newterm('xterm', s, s) + self.assertIsInstance(screen, curses.screen) + win = screen.stdscr + self.assertIsInstance(win, curses.window) + self.assertEqual(win.getmaxyx(), (24, 80)) + win.addstr(0, 0, 'hello') + win.refresh() + + def test_newterm_file_object(self): + # type=None uses $TERM; the file arguments accept file objects too. + s = self.make_pty() + out = os.fdopen(os.dup(s), 'wb', buffering=0) + self.addCleanup(out.close) + screen = curses.newterm(None, out, s) + self.assertIsInstance(screen, curses.screen) + + def test_set_term(self): + s = self.make_pty() + s2 = self.make_pty() + a = curses.newterm('xterm', s, s) # current screen is a + b = curses.newterm('xterm', s2, s2) # current screen is b + self.assertIs(curses.set_term(a), b) # returns the previous one + self.assertIs(curses.set_term(b), a) + + def test_window_keeps_screen_alive(self): + # The standard window keeps its screen alive; dropping every other + # reference and collecting must not invalidate the window. + s = self.make_pty() + win = curses.newterm('xterm', s, s).stdscr + gc_collect() + win.addstr(0, 0, 'still alive') + win.refresh() + + def test_screen_freed(self): + # Dropping all references to a (non-current) screen and its windows + # frees it without error. + s = self.make_pty() + s2 = self.make_pty() + a = curses.newterm('xterm', s, s) + b = curses.newterm('xterm', s2, s2) # a is no longer current + del a + gc_collect() + + def test_close(self): + s = self.make_pty() + screen = curses.newterm('xterm', s, s) + win = screen.stdscr + self.assertIsInstance(win, curses.window) + screen.close() + # After close() the standard window is detached and unusable, and + # stdscr is None. No reference cycle remains. + self.assertIsNone(screen.stdscr) + self.assertRaises(curses.error, win.addstr, 0, 0, 'x') + # close() is idempotent. + screen.close() + + @unittest.skipUnless(hasattr(curses, 'new_prescr'), + 'requires curses.new_prescr()') + def test_new_prescr(self): + screen = curses.new_prescr() + self.assertIsInstance(screen, curses.screen) + self.assertIsNone(screen.stdscr) + del screen + gc_collect() + + @cpython_only + def test_disallow_instantiation(self): + # The screen type cannot be instantiated directly (bpo-43916). + check_disallow_instantiation(self, curses.screen) + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-06-19-12-19-30.gh-issue-90092.yBVc0C.rst b/Misc/NEWS.d/next/Library/2026-06-19-12-19-30.gh-issue-90092.yBVc0C.rst new file mode 100644 index 000000000000000..6cc9d0c516ea882 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-19-12-19-30.gh-issue-90092.yBVc0C.rst @@ -0,0 +1,4 @@ +Add support for multiple terminals to the :mod:`curses` module: the new +functions :func:`curses.newterm`, :func:`curses.set_term` and +:func:`curses.new_prescr`, the corresponding :ref:`screen +` object, and the :meth:`window.use` method. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index e60cba3ef87ead1..e55b6562294ab2d 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -41,12 +41,12 @@ Here's a list of currently unsupported functions: addchnstr addchstr color_set define_key - del_curterm delscreen dupwin inchnstr inchstr innstr keyok + del_curterm dupwin inchnstr inchstr innstr keyok mcprint mvaddchnstr mvaddchstr mvcur mvinchnstr mvinchstr mvinnstr mmvwaddchnstr mvwaddchstr - mvwinchnstr mvwinchstr mvwinnstr newterm + mvwinchnstr mvwinchstr mvwinnstr restartterm ripoffline scr_dump - scr_init scr_restore scr_set scrl set_curterm set_term setterm + scr_init scr_restore scr_set scrl set_curterm setterm tgetent tgetflag tgetnum tgetstr tgoto timeout tputs vidattr vidputs waddchnstr waddchstr wcolor_set winchnstr winchstr winnstr wmouse_trafo wscrl @@ -108,7 +108,7 @@ static const char PyCursesVersion[] = "2.2"; #include "pycore_capsule.h" // _PyCapsule_SetTraverse() #include "pycore_long.h" // _PyLong_GetZero() #include "pycore_structseq.h" // _PyStructSequence_NewType() -#include "pycore_fileutils.h" // _Py_set_inheritable +#include "pycore_fileutils.h" // _Py_dup(), _Py_set_inheritable() #ifdef __hpux #define STRICT_SYSV_CURSES @@ -164,6 +164,9 @@ typedef chtype attr_t; /* No attr_t type is available */ typedef struct { PyObject *error; // curses exception type PyTypeObject *window_type; // exposed by PyCursesWindow_Type + PyTypeObject *screen_type; // _curses.screen + PyObject *topscreen; // owned ref to the current screen object, + // or NULL for the initscr() screen } cursesmodule_state; static inline cursesmodule_state * @@ -189,12 +192,14 @@ get_cursesmodule_state_by_win(PyCursesWindowObject *win) } #define _PyCursesWindowObject_CAST(op) ((PyCursesWindowObject *)(op)) +#define _PyCursesScreenObject_CAST(op) ((PyCursesScreenObject *)(op)) /*[clinic input] module _curses class _curses.window "PyCursesWindowObject *" "clinic_state()->window_type" +class _curses.screen "PyCursesScreenObject *" "clinic_state()->screen_type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=ae6cb623018f2cbc]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=4b027ab105ab94e1]*/ /* Indicate whether the module has already been loaded or not. */ static int curses_module_loaded = 0; @@ -888,7 +893,7 @@ Window_TwoArgNoReturnFunction(wresize, int, "ii;lines,columns") static PyObject * PyCursesWindow_New(cursesmodule_state *state, WINDOW *win, const char *encoding, - PyCursesWindowObject *orig) + PyCursesWindowObject *orig, PyObject *screen) { if (encoding == NULL) { #if defined(MS_WINDOWS) @@ -916,14 +921,14 @@ PyCursesWindow_New(cursesmodule_state *state, return NULL; } wo->win = win; + wo->orig = (PyCursesWindowObject *)Py_XNewRef((PyObject *)orig); + wo->screen = Py_XNewRef(screen); wo->encoding = _PyMem_Strdup(encoding); if (wo->encoding == NULL) { Py_DECREF(wo); PyErr_NoMemory(); return NULL; } - wo->orig = orig; - Py_XINCREF(orig); PyObject_GC_Track((PyObject *)wo); return (PyObject *)wo; } @@ -944,6 +949,7 @@ PyCursesWindow_dealloc(PyObject *self) PyMem_Free(wo->encoding); } Py_XDECREF(wo->orig); + Py_XDECREF(wo->screen); window_type->tp_free(self); Py_DECREF(window_type); } @@ -954,6 +960,7 @@ PyCursesWindow_traverse(PyObject *self, visitproc visit, void *arg) Py_VISIT(Py_TYPE(self)); PyCursesWindowObject *wo = (PyCursesWindowObject *)self; Py_VISIT(wo->orig); + Py_VISIT(wo->screen); return 0; } @@ -1639,7 +1646,7 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, int group_left_1, } cursesmodule_state *state = get_cursesmodule_state_by_win(self); - return PyCursesWindow_New(state, win, NULL, self); + return PyCursesWindow_New(state, win, NULL, self, self->screen); } /*[clinic input] @@ -2764,7 +2771,7 @@ _curses_window_subwin_impl(PyCursesWindowObject *self, int group_left_1, } cursesmodule_state *state = get_cursesmodule_state_by_win(self); - return PyCursesWindow_New(state, win, self->encoding, self); + return PyCursesWindow_New(state, win, self->encoding, self, self->screen); } /*[clinic input] @@ -2924,6 +2931,84 @@ PyCursesWindow_set_encoding(PyObject *op, PyObject *value, void *Py_UNUSED(ignor #include "clinic/_cursesmodule.c.h" #undef clinic_state +#if defined(HAVE_CURSES_USE_SCREEN) || defined(HAVE_CURSES_USE_WINDOW) +/* Shared trampoline for window.use()/screen.use(): call + func(obj, *extra, **kwargs) and store the result (NULL on exception) in + data->result. */ +typedef struct { + PyObject *obj; /* the window or screen object */ + PyObject *func; /* the callable */ + PyObject *extra; /* extra positional arguments (a tuple) */ + PyObject *kwargs; /* keyword arguments (a dict), or NULL */ + PyObject *result; /* output: the call result, or NULL */ +} curses_use_data; + +static void +curses_use_call(curses_use_data *data) +{ + Py_ssize_t n = PyTuple_GET_SIZE(data->extra); + PyObject *callargs = PyTuple_New(n + 1); + if (callargs == NULL) { + data->result = NULL; + return; + } + PyTuple_SET_ITEM(callargs, 0, Py_NewRef(data->obj)); + for (Py_ssize_t i = 0; i < n; i++) { + PyTuple_SET_ITEM(callargs, i + 1, + Py_NewRef(PyTuple_GET_ITEM(data->extra, i))); + } + data->result = PyObject_Call(data->func, callargs, data->kwargs); + Py_DECREF(callargs); +} + +/* Parse (func, *extra) from a use() method's argument tuple. */ +static int +curses_use_parse(PyObject *args, PyObject **func, PyObject **extra) +{ + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + if (nargs < 1) { + PyErr_SetString(PyExc_TypeError, + "use() missing required argument 'func'"); + return -1; + } + *func = PyTuple_GET_ITEM(args, 0); + if (!PyCallable_Check(*func)) { + PyErr_SetString(PyExc_TypeError, "use(): func must be callable"); + return -1; + } + *extra = PyTuple_GetSlice(args, 1, nargs); + return *extra == NULL ? -1 : 0; +} +#endif + +#ifdef HAVE_CURSES_USE_WINDOW +static int +curses_use_window_cb(WINDOW *Py_UNUSED(win), void *data) +{ + curses_use_call((curses_use_data *)data); + return 0; +} + +PyDoc_STRVAR(PyCursesWindow_use__doc__, +"use($self, func, /, *args, **kwargs)\n--\n\n" +"Call func(win, *args, **kwargs) with the window locked,\n" +"and return its result."); + +static PyObject * +PyCursesWindow_use(PyObject *self, PyObject *args, PyObject *kwargs) +{ + PyCursesWindowObject *wo = _PyCursesWindowObject_CAST(self); + PyObject *func, *extra; + if (curses_use_parse(args, &func, &extra) < 0) { + return NULL; + } + curses_use_data data = {self, func, extra, kwargs, NULL}; + use_window(wo->win, curses_use_window_cb, &data); + Py_DECREF(extra); + return data.result; +} +#endif /* HAVE_CURSES_USE_WINDOW */ + static PyMethodDef PyCursesWindow_methods[] = { _CURSES_WINDOW_ADDCH_METHODDEF _CURSES_WINDOW_ADDNSTR_METHODDEF @@ -3086,6 +3171,10 @@ static PyMethodDef PyCursesWindow_methods[] = { "untouchwin($self, /)\n--\n\n" "Mark all lines in the window as unchanged since last refresh()."}, _CURSES_WINDOW_VLINE_METHODDEF +#ifdef HAVE_CURSES_USE_WINDOW + {"use", _PyCFunction_CAST(PyCursesWindow_use), + METH_VARARGS | METH_KEYWORDS, PyCursesWindow_use__doc__}, +#endif {NULL, NULL} /* sentinel */ }; @@ -3118,6 +3207,182 @@ static PyType_Spec PyCursesWindow_Type_spec = { .slots = PyCursesWindow_Type_slots }; +/* -------------------------------------------------------*/ +/* Screen objects (multiple terminals) */ +/* -------------------------------------------------------*/ + +static PyObject * +PyCursesScreen_New(cursesmodule_state *state, SCREEN *screen, + FILE *outfp, FILE *infp, PyObject *stdscr) +{ + PyCursesScreenObject *so = PyObject_GC_New(PyCursesScreenObject, + state->screen_type); + if (so == NULL) { + return NULL; + } + so->screen = screen; + so->outfp = outfp; + so->infp = infp; + so->stdscr = Py_XNewRef(stdscr); + PyObject_GC_Track((PyObject *)so); + return (PyObject *)so; +} + +/* Free the C SCREEN and the FILE* streams owned by a screen object. + Safe to call more than once. + + This must run by reference counting (from the dealloc), not from tp_clear: + it has to happen only once every window on the screen is gone, and thus + after del_panel() for any panel built on one of those windows. delscreen() + tears down the screen that del_panel() needs, so a panel outliving its + screen would crash. */ +static void +curses_screen_close(PyCursesScreenObject *so) +{ + if (so->screen != NULL) { + delscreen(so->screen); + so->screen = NULL; + } + if (so->outfp != NULL) { + fclose(so->outfp); + so->outfp = NULL; + } + if (so->infp != NULL) { + fclose(so->infp); + so->infp = NULL; + } +} + +static PyObject * +PyCursesScreen_get_stdscr(PyObject *self, void *Py_UNUSED(closure)) +{ + PyCursesScreenObject *so = _PyCursesScreenObject_CAST(self); + if (so->stdscr == NULL) { + Py_RETURN_NONE; + } + return Py_NewRef(so->stdscr); +} + +static int +PyCursesScreen_traverse(PyObject *self, visitproc visit, void *arg) +{ + Py_VISIT(Py_TYPE(self)); + Py_VISIT(_PyCursesScreenObject_CAST(self)->stdscr); + return 0; +} + +static int +PyCursesScreen_clear(PyObject *self) +{ + PyCursesScreenObject *so = _PyCursesScreenObject_CAST(self); + /* Break the reference cycle between a screen and its standard window by + dropping the reference to that window. Do NOT delscreen() here: that is + deferred to the dealloc so it runs after every window (see + curses_screen_close()). delscreen() will free the standard window, so + detach it from its wrapper first: the wrapper must not delwin() a window + that delscreen() frees. Any further use of the wrapper operates on a + NULL window and fails cleanly. */ + if (so->stdscr != NULL) { + ((PyCursesWindowObject *)so->stdscr)->win = NULL; + } + Py_CLEAR(so->stdscr); + return 0; +} + +static void +PyCursesScreen_dealloc(PyObject *self) +{ + PyTypeObject *tp = Py_TYPE(self); + PyObject_GC_UnTrack(self); + (void)PyCursesScreen_clear(self); + curses_screen_close(_PyCursesScreenObject_CAST(self)); + tp->tp_free(self); + Py_DECREF(tp); +} + +static PyGetSetDef PyCursesScreen_getsets[] = { + {"stdscr", PyCursesScreen_get_stdscr, NULL, + "the screen's standard window (stdscr)", NULL}, + {NULL, NULL, NULL, NULL, NULL} /* sentinel */ +}; + +#ifdef HAVE_CURSES_USE_SCREEN +static int +curses_use_screen_cb(SCREEN *Py_UNUSED(sp), void *data) +{ + curses_use_call((curses_use_data *)data); + return 0; +} + +PyDoc_STRVAR(PyCursesScreen_use__doc__, +"use($self, func, /, *args, **kwargs)\n--\n\n" +"Call func(screen, *args, **kwargs) with the screen locked,\n" +"and return its result."); + +static PyObject * +PyCursesScreen_use(PyObject *self, PyObject *args, PyObject *kwargs) +{ + PyCursesScreenObject *so = _PyCursesScreenObject_CAST(self); + if (so->screen == NULL) { + cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self)); + PyErr_SetString(state->error, "the screen has been deleted"); + return NULL; + } + PyObject *func, *extra; + if (curses_use_parse(args, &func, &extra) < 0) { + return NULL; + } + curses_use_data data = {self, func, extra, kwargs, NULL}; + use_screen(so->screen, curses_use_screen_cb, &data); + Py_DECREF(extra); + return data.result; +} +#endif /* HAVE_CURSES_USE_SCREEN */ + +PyDoc_STRVAR(PyCursesScreen_close__doc__, +"close($self, /)\n--\n\n" +"Detach the screen's standard window, breaking their reference cycle.\n\n" +"Afterwards the stdscr attribute is None and the window it returned earlier\n" +"can no longer be used. The screen is released once it and its windows are\n" +"no longer referenced."); + +static PyObject * +PyCursesScreen_close(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + (void)PyCursesScreen_clear(self); + Py_RETURN_NONE; +} + +static PyMethodDef PyCursesScreen_methods[] = { + {"close", PyCursesScreen_close, METH_NOARGS, + PyCursesScreen_close__doc__}, +#ifdef HAVE_CURSES_USE_SCREEN + {"use", _PyCFunction_CAST(PyCursesScreen_use), + METH_VARARGS | METH_KEYWORDS, PyCursesScreen_use__doc__}, +#endif + {NULL, NULL} /* sentinel */ +}; + +static PyType_Slot PyCursesScreen_Type_slots[] = { + {Py_tp_methods, PyCursesScreen_methods}, + {Py_tp_getset, PyCursesScreen_getsets}, + {Py_tp_dealloc, PyCursesScreen_dealloc}, + {Py_tp_traverse, PyCursesScreen_traverse}, + {Py_tp_clear, PyCursesScreen_clear}, + {0, NULL} +}; + +static PyType_Spec PyCursesScreen_Type_spec = { + .name = "_curses.screen", + .basicsize = sizeof(PyCursesScreenObject), + .flags = Py_TPFLAGS_DEFAULT + | Py_TPFLAGS_DISALLOW_INSTANTIATION + | Py_TPFLAGS_IMMUTABLETYPE + | Py_TPFLAGS_HEAPTYPE + | Py_TPFLAGS_HAVE_GC, + .slots = PyCursesScreen_Type_slots +}; + /* -------------------------------------------------------*/ /* @@ -3226,14 +3491,14 @@ _curses.nofilter Undo the effect of a preceding filter() call. -Must be called before initscr(). It restores the normal behaviour -disabled by filter(), so that the next initscr() uses the full screen -rather than a single line. +Must be called before initscr(). It restores the normal behaviour that +filter() disables, so that the next initscr() or newterm() uses the full +screen rather than a single line. [clinic start generated code]*/ static PyObject * _curses_nofilter_impl(PyObject *module) -/*[clinic end generated code: output=d95ca4d48a6bdbdf input=58aea83b1a5c969f]*/ +/*[clinic end generated code: output=d95ca4d48a6bdbdf input=53183055c0901ab7]*/ { /* not checking for PyCursesInitialised here since nofilter() must be called before initscr() */ @@ -3474,7 +3739,18 @@ De-initialize the library, and return terminal to normal status. static PyObject * _curses_endwin_impl(PyObject *module) /*[clinic end generated code: output=c0150cd96d2f4128 input=e172cfa43062f3fa]*/ -NoArgNoReturnFunctionBody(endwin) +{ + PyCursesStatefulInitialised(module); + + /* endwin() writes to the terminal and may call tcdrain(), which can block + (e.g. on a pty whose output is not being read); release the GIL so other + threads -- including one draining that terminal -- can run meanwhile. */ + int code; + Py_BEGIN_ALLOW_THREADS + code = endwin(); + Py_END_ALLOW_THREADS + return curses_check_err(module, code, "endwin", NULL); +} /*[clinic input] _curses.erasechar @@ -3666,7 +3942,7 @@ _curses_getwin(PyObject *module, PyObject *file) goto error; } cursesmodule_state *state = get_cursesmodule_state(module); - res = PyCursesWindow_New(state, win, NULL, NULL); + res = PyCursesWindow_New(state, win, NULL, NULL, state->topscreen); error: fclose(fp); @@ -3840,50 +4116,16 @@ curses_update_screen_encoding(PyObject *winobj) return 0; } -/*[clinic input] -_curses.initscr - -Initialize the library. - -Return a WindowObject which represents the whole screen. -[clinic start generated code]*/ - -static PyObject * -_curses_initscr_impl(PyObject *module) -/*[clinic end generated code: output=619fb68443810b7b input=514f4bce1821f6b5]*/ +/* Populate the module dictionary with the ACS_* line-drawing constants and + LINES/COLS. These are only meaningful once a screen exists (after + initscr() or newterm()), which is why this is not done at module + initialisation. Returns 0 on success, -1 with an exception set. */ +static int +curses_init_dict(PyObject *module) { - WINDOW *win; - - if (curses_initscr_called) { - cursesmodule_state *state = get_cursesmodule_state(module); - int code = wrefresh(stdscr); - if (code == ERR) { - _curses_set_null_error(state, "wrefresh", "initscr"); - return NULL; - } - PyObject *winobj = PyCursesWindow_New(state, stdscr, NULL, NULL); - if (winobj == NULL) { - return NULL; - } - if (curses_update_screen_encoding(winobj) < 0) { - Py_DECREF(winobj); - return NULL; - } - return winobj; - } - - win = initscr(); - - if (win == NULL) { - curses_set_null_error(module, "initscr", NULL); - return NULL; - } - - curses_initscr_called = curses_setupterm_called = TRUE; - PyObject *module_dict = PyModule_GetDict(module); // borrowed if (module_dict == NULL) { - return NULL; + return -1; } /* This was moved from initcurses() because it core dumped on SGI, where they're not defined until you've called initscr() */ @@ -3891,12 +4133,12 @@ _curses_initscr_impl(PyObject *module) do { \ PyObject *value = PyLong_FromLong((long)(VALUE)); \ if (value == NULL) { \ - return NULL; \ + return -1; \ } \ int rc = PyDict_SetItemString(module_dict, (NAME), value); \ Py_DECREF(value); \ if (rc < 0) { \ - return NULL; \ + return -1; \ } \ } while (0) @@ -3970,9 +4212,56 @@ _curses_initscr_impl(PyObject *module) SetDictInt("LINES", LINES); SetDictInt("COLS", COLS); #undef SetDictInt + return 0; +} + +/*[clinic input] +_curses.initscr + +Initialize the library. + +Return a WindowObject which represents the whole screen. +[clinic start generated code]*/ + +static PyObject * +_curses_initscr_impl(PyObject *module) +/*[clinic end generated code: output=619fb68443810b7b input=514f4bce1821f6b5]*/ +{ + WINDOW *win; + + if (curses_initscr_called) { + cursesmodule_state *state = get_cursesmodule_state(module); + int code = wrefresh(stdscr); + if (code == ERR) { + _curses_set_null_error(state, "wrefresh", "initscr"); + return NULL; + } + PyObject *winobj = PyCursesWindow_New(state, stdscr, NULL, NULL, NULL); + if (winobj == NULL) { + return NULL; + } + if (curses_update_screen_encoding(winobj) < 0) { + Py_DECREF(winobj); + return NULL; + } + return winobj; + } + + win = initscr(); + + if (win == NULL) { + curses_set_null_error(module, "initscr", NULL); + return NULL; + } + + curses_initscr_called = curses_setupterm_called = TRUE; + + if (curses_init_dict(module) < 0) { + return NULL; + } cursesmodule_state *state = get_cursesmodule_state(module); - PyObject *winobj = PyCursesWindow_New(state, win, NULL, NULL); + PyObject *winobj = PyCursesWindow_New(state, win, NULL, NULL, NULL); if (winobj == NULL) { return NULL; } @@ -4042,6 +4331,206 @@ _curses_setupterm_impl(PyObject *module, const char *term, int fd) Py_RETURN_NONE; } +static int update_lines_cols(PyObject *private_module); /* defined below */ + +/* Return a file descriptor for obj, or, if obj is NULL or None, for the + sys. stream. Returns -1 with an exception set on error. */ +static int +curses_fileno(PyObject *module, PyObject *obj, const char *stdname) +{ + if (obj != NULL && obj != Py_None) { + return PyObject_AsFileDescriptor(obj); + } + PyObject *stream; + if (PySys_GetOptionalAttrString(stdname, &stream) < 0) { + return -1; + } + if (stream == NULL || stream == Py_None) { + cursesmodule_state *state = get_cursesmodule_state(module); + PyErr_Format(state->error, "lost sys.%s", stdname); + Py_XDECREF(stream); + return -1; + } + int fd = PyObject_AsFileDescriptor(stream); + Py_DECREF(stream); + return fd; +} + +/* Duplicate fd and wrap it in a new (non-inheritable) stdio stream that the + screen object will own. Duplicating means closing the stream later does + not close the caller's fd. Returns NULL with an exception set on error. */ +static FILE * +curses_fdopen_dup(int fd, const char *mode) +{ + /* _Py_dup() duplicates the descriptor and makes the copy non-inheritable + atomically (and sets the error on failure). */ + int dfd = _Py_dup(fd); + if (dfd < 0) { + return NULL; + } + FILE *stream = fdopen(dfd, mode); + if (stream == NULL) { + PyErr_SetFromErrno(PyExc_OSError); + close(dfd); + return NULL; + } + return stream; +} + +/*[clinic input] +_curses.newterm + + type: str(accept={str, NoneType}) = None + Terminal name; if None, the TERM environment variable is used. + fd: object = None + Output file object or descriptor (default: sys.stdout). + infd: object = None + Input file object or descriptor (default: sys.stdin). + / + +Return a new screen for the terminal, in addition to the initial screen. + +This is an alternative to initscr() for programs running on more than +one terminal. Use set_term() to switch between the screens. +[clinic start generated code]*/ + +static PyObject * +_curses_newterm_impl(PyObject *module, const char *type, PyObject *fd, + PyObject *infd) +/*[clinic end generated code: output=62663c31909d796c input=98507fe48c2e93cb]*/ +{ + /* Duplicate each descriptor right after resolving it: resolving the other + one runs arbitrary Python code (e.g. a fileno() method) that could close + this one before it is duplicated. */ + int out_fd = curses_fileno(module, fd, "stdout"); + if (out_fd < 0) { + return NULL; + } + FILE *outfp = curses_fdopen_dup(out_fd, "wb"); + if (outfp == NULL) { + return NULL; + } + + int in_fd = curses_fileno(module, infd, "stdin"); + if (in_fd < 0) { + fclose(outfp); + return NULL; + } + FILE *infp = curses_fdopen_dup(in_fd, "rb"); + if (infp == NULL) { + fclose(outfp); + return NULL; + } + + SCREEN *screen = newterm((char *)type, outfp, infp); + if (screen == NULL) { + curses_set_null_error(module, "newterm", NULL); + fclose(outfp); + fclose(infp); + return NULL; + } + /* newterm() makes the new screen the current one, so stdscr now refers + to its standard window. */ + curses_initscr_called = curses_setupterm_called = TRUE; + + cursesmodule_state *state = get_cursesmodule_state(module); + /* The screen object owns the SCREEN and the streams; deleting it (when it + is no longer referenced) calls delscreen() and closes the streams. */ + PyObject *screenobj = PyCursesScreen_New(state, screen, outfp, infp, NULL); + if (screenobj == NULL) { + delscreen(screen); + fclose(outfp); + fclose(infp); + return NULL; + } + /* The standard window keeps the screen alive for its own lifetime. */ + PyObject *win = PyCursesWindow_New(state, stdscr, NULL, NULL, screenobj); + if (win == NULL || + curses_update_screen_encoding(win) < 0 || + curses_init_dict(module) < 0) + { + Py_XDECREF(win); + Py_DECREF(screenobj); + return NULL; + } + ((PyCursesScreenObject *)screenobj)->stdscr = Py_NewRef(win); + Py_DECREF(win); + Py_XSETREF(state->topscreen, Py_NewRef(screenobj)); + return screenobj; +} + +/* Check that obj is an open screen object; returns it cast, or NULL with + TypeError/curses.error set. */ +static PyCursesScreenObject * +curses_check_screen(PyObject *module, PyObject *obj) +{ + cursesmodule_state *state = get_cursesmodule_state(module); + if (!PyObject_TypeCheck(obj, state->screen_type)) { + PyErr_Format(PyExc_TypeError, + "expected a curses screen, got %T", obj); + return NULL; + } + PyCursesScreenObject *so = _PyCursesScreenObject_CAST(obj); + if (so->screen == NULL) { + PyErr_SetString(state->error, "the screen has been deleted"); + return NULL; + } + return so; +} + +/*[clinic input] +_curses.set_term + + screen: object + / + +Switch to the given screen and return the previously current screen. + +Returns None if the previous screen was the one created by initscr(). +[clinic start generated code]*/ + +static PyObject * +_curses_set_term(PyObject *module, PyObject *screen) +/*[clinic end generated code: output=204cf9c40523bdef input=ed4dba18dd9adf6a]*/ +{ + PyCursesScreenObject *so = curses_check_screen(module, screen); + if (so == NULL) { + return NULL; + } + set_term(so->screen); + if (!update_lines_cols(module)) { + return NULL; + } + cursesmodule_state *state = get_cursesmodule_state(module); + PyObject *prev = state->topscreen; /* steal the owned reference */ + state->topscreen = Py_NewRef(screen); + return prev != NULL ? prev : Py_NewRef(Py_None); +} + +#ifdef HAVE_CURSES_NEW_PRESCR +/*[clinic input] +_curses.new_prescr + +Create a screen and return it, without initializing a terminal. + +The screen can be used to call functions that affect the screen before +calling newterm() or initscr(). +[clinic start generated code]*/ + +static PyObject * +_curses_new_prescr_impl(PyObject *module) +/*[clinic end generated code: output=e7de5031da7511e2 input=1a3a89d630b641c3]*/ +{ + SCREEN *screen = new_prescr(); + if (screen == NULL) { + curses_set_null_error(module, "new_prescr", NULL); + return NULL; + } + cursesmodule_state *state = get_cursesmodule_state(module); + return PyCursesScreen_New(state, screen, NULL, NULL, NULL); +} +#endif /* HAVE_CURSES_NEW_PRESCR */ + #if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20081102 // https://invisible-island.net/ncurses/NEWS.html#index-t20080119 @@ -4368,7 +4857,7 @@ _curses_newpad_impl(PyObject *module, int nlines, int ncols) } cursesmodule_state *state = get_cursesmodule_state(module); - return PyCursesWindow_New(state, win, NULL, NULL); + return PyCursesWindow_New(state, win, NULL, NULL, state->topscreen); } /*[clinic input] @@ -4408,7 +4897,7 @@ _curses_newwin_impl(PyObject *module, int nlines, int ncols, } cursesmodule_state *state = get_cursesmodule_state(module); - return PyCursesWindow_New(state, win, NULL, NULL); + return PyCursesWindow_New(state, win, NULL, NULL, state->topscreen); } /*[clinic input] @@ -5370,7 +5859,11 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_MOUSEMASK_METHODDEF _CURSES_NAPMS_METHODDEF _CURSES_NEWPAD_METHODDEF + _CURSES_NEWTERM_METHODDEF _CURSES_NEWWIN_METHODDEF +#ifdef HAVE_CURSES_NEW_PRESCR + _CURSES_NEW_PRESCR_METHODDEF +#endif _CURSES_NL_METHODDEF _CURSES_NOCBREAK_METHODDEF _CURSES_NOECHO_METHODDEF @@ -5394,6 +5887,7 @@ static PyMethodDef cursesmodule_methods[] = { #endif _CURSES_GET_TABSIZE_METHODDEF _CURSES_SET_TABSIZE_METHODDEF + _CURSES_SET_TERM_METHODDEF _CURSES_SETSYX_METHODDEF _CURSES_SETUPTERM_METHODDEF _CURSES_START_COLOR_METHODDEF @@ -5517,6 +6011,8 @@ cursesmodule_traverse(PyObject *mod, visitproc visit, void *arg) cursesmodule_state *state = get_cursesmodule_state(mod); Py_VISIT(state->error); Py_VISIT(state->window_type); + Py_VISIT(state->screen_type); + Py_VISIT(state->topscreen); return 0; } @@ -5526,6 +6022,8 @@ cursesmodule_clear(PyObject *mod) cursesmodule_state *state = get_cursesmodule_state(mod); Py_CLEAR(state->error); Py_CLEAR(state->window_type); + Py_CLEAR(state->screen_type); + Py_CLEAR(state->topscreen); return 0; } @@ -5558,6 +6056,14 @@ cursesmodule_exec(PyObject *module) if (PyModule_AddType(module, state->window_type) < 0) { return -1; } + state->screen_type = (PyTypeObject *)PyType_FromModuleAndSpec( + module, &PyCursesScreen_Type_spec, NULL); + if (state->screen_type == NULL) { + return -1; + } + if (PyModule_AddType(module, state->screen_type) < 0) { + return -1; + } /* Add some symbolic constants to the module */ PyObject *module_dict = PyModule_GetDict(module); diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index f577368680ef572..fcb0ee0cf61f6af 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -1874,9 +1874,9 @@ PyDoc_STRVAR(_curses_nofilter__doc__, "\n" "Undo the effect of a preceding filter() call.\n" "\n" -"Must be called before initscr(). It restores the normal behaviour\n" -"disabled by filter(), so that the next initscr() uses the full screen\n" -"rather than a single line."); +"Must be called before initscr(). It restores the normal behaviour that\n" +"filter() disables, so that the next initscr() or newterm() uses the full\n" +"screen rather than a single line."); #define _CURSES_NOFILTER_METHODDEF \ {"nofilter", (PyCFunction)_curses_nofilter, METH_NOARGS, _curses_nofilter__doc__}, @@ -2818,6 +2818,112 @@ _curses_setupterm(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyO return return_value; } +PyDoc_STRVAR(_curses_newterm__doc__, +"newterm($module, type=None, fd=None, infd=None, /)\n" +"--\n" +"\n" +"Return a new screen for the terminal, in addition to the initial screen.\n" +"\n" +" type\n" +" Terminal name; if None, the TERM environment variable is used.\n" +" fd\n" +" Output file object or descriptor (default: sys.stdout).\n" +" infd\n" +" Input file object or descriptor (default: sys.stdin).\n" +"\n" +"This is an alternative to initscr() for programs running on more than\n" +"one terminal. Use set_term() to switch between the screens."); + +#define _CURSES_NEWTERM_METHODDEF \ + {"newterm", _PyCFunction_CAST(_curses_newterm), METH_FASTCALL, _curses_newterm__doc__}, + +static PyObject * +_curses_newterm_impl(PyObject *module, const char *type, PyObject *fd, + PyObject *infd); + +static PyObject * +_curses_newterm(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + const char *type = NULL; + PyObject *fd = Py_None; + PyObject *infd = Py_None; + + if (!_PyArg_CheckPositional("newterm", nargs, 0, 3)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional; + } + if (args[0] == Py_None) { + type = NULL; + } + else if (PyUnicode_Check(args[0])) { + Py_ssize_t type_length; + type = PyUnicode_AsUTF8AndSize(args[0], &type_length); + if (type == NULL) { + goto exit; + } + if (strlen(type) != (size_t)type_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + } + else { + _PyArg_BadArgument("newterm", "argument 1", "str or None", args[0]); + goto exit; + } + if (nargs < 2) { + goto skip_optional; + } + fd = args[1]; + if (nargs < 3) { + goto skip_optional; + } + infd = args[2]; +skip_optional: + return_value = _curses_newterm_impl(module, type, fd, infd); + +exit: + return return_value; +} + +PyDoc_STRVAR(_curses_set_term__doc__, +"set_term($module, screen, /)\n" +"--\n" +"\n" +"Switch to the given screen and return the previously current screen.\n" +"\n" +"Returns None if the previous screen was the one created by initscr()."); + +#define _CURSES_SET_TERM_METHODDEF \ + {"set_term", (PyCFunction)_curses_set_term, METH_O, _curses_set_term__doc__}, + +#if defined(HAVE_CURSES_NEW_PRESCR) + +PyDoc_STRVAR(_curses_new_prescr__doc__, +"new_prescr($module, /)\n" +"--\n" +"\n" +"Create a screen and return it, without initializing a terminal.\n" +"\n" +"The screen can be used to call functions that affect the screen before\n" +"calling newterm() or initscr()."); + +#define _CURSES_NEW_PRESCR_METHODDEF \ + {"new_prescr", (PyCFunction)_curses_new_prescr, METH_NOARGS, _curses_new_prescr__doc__}, + +static PyObject * +_curses_new_prescr_impl(PyObject *module); + +static PyObject * +_curses_new_prescr(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_new_prescr_impl(module); +} + +#endif /* defined(HAVE_CURSES_NEW_PRESCR) */ + #if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20081102) PyDoc_STRVAR(_curses_get_escdelay__doc__, @@ -4453,6 +4559,10 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_HAS_KEY_METHODDEF #endif /* !defined(_CURSES_HAS_KEY_METHODDEF) */ +#ifndef _CURSES_NEW_PRESCR_METHODDEF + #define _CURSES_NEW_PRESCR_METHODDEF +#endif /* !defined(_CURSES_NEW_PRESCR_METHODDEF) */ + #ifndef _CURSES_GET_ESCDELAY_METHODDEF #define _CURSES_GET_ESCDELAY_METHODDEF #endif /* !defined(_CURSES_GET_ESCDELAY_METHODDEF) */ @@ -4516,4 +4626,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=222110438e8a55e3 input=a9049054013a1b77]*/ diff --git a/configure b/configure index 12fd0d15698ac1d..a978b613514f124 100755 --- a/configure +++ b/configure @@ -30524,6 +30524,186 @@ fi + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function new_prescr" >&5 +printf %s "checking for curses function new_prescr... " >&6; } +if test ${ac_cv_lib_curses_new_prescr+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) 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) +{ + + #ifndef new_prescr + void *x=new_prescr + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_new_prescr=yes +else case e in #( + e) ac_cv_lib_curses_new_prescr=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_new_prescr" >&5 +printf "%s\n" "$ac_cv_lib_curses_new_prescr" >&6; } + if test "x$ac_cv_lib_curses_new_prescr" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_NEW_PRESCR 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function use_screen" >&5 +printf %s "checking for curses function use_screen... " >&6; } +if test ${ac_cv_lib_curses_use_screen+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) 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) +{ + + #ifndef use_screen + void *x=use_screen + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_use_screen=yes +else case e in #( + e) ac_cv_lib_curses_use_screen=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_use_screen" >&5 +printf "%s\n" "$ac_cv_lib_curses_use_screen" >&6; } + if test "x$ac_cv_lib_curses_use_screen" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_USE_SCREEN 1" >>confdefs.h + +fi + + + + + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for curses function use_window" >&5 +printf %s "checking for curses function use_window... " >&6; } +if test ${ac_cv_lib_curses_use_window+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) 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) +{ + + #ifndef use_window + void *x=use_window + #endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_lib_curses_use_window=yes +else case e in #( + e) ac_cv_lib_curses_use_window=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curses_use_window" >&5 +printf "%s\n" "$ac_cv_lib_curses_use_window" >&6; } + if test "x$ac_cv_lib_curses_use_window" = xyes +then : + +printf "%s\n" "#define HAVE_CURSES_USE_WINDOW 1" >>confdefs.h + +fi + + + CPPFLAGS=$ac_save_cppflags fi diff --git a/configure.ac b/configure.ac index 3b42fdfe40385dc..a2729c22386a951 100644 --- a/configure.ac +++ b/configure.ac @@ -7196,6 +7196,9 @@ PY_CHECK_CURSES_FUNC([nofilter]) PY_CHECK_CURSES_FUNC([has_key]) PY_CHECK_CURSES_FUNC([typeahead]) PY_CHECK_CURSES_FUNC([use_env]) +PY_CHECK_CURSES_FUNC([new_prescr]) +PY_CHECK_CURSES_FUNC([use_screen]) +PY_CHECK_CURSES_FUNC([use_window]) CPPFLAGS=$ac_save_cppflags ])dnl have_curses != no ])dnl save env diff --git a/pyconfig.h.in b/pyconfig.h.in index 2bef8d38497c547..a84b74299e159b1 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -218,6 +218,9 @@ /* Define if you have the 'is_term_resized' function. */ #undef HAVE_CURSES_IS_TERM_RESIZED +/* Define if you have the 'new_prescr' function. */ +#undef HAVE_CURSES_NEW_PRESCR + /* Define if you have the 'nofilter' function. */ #undef HAVE_CURSES_NOFILTER @@ -236,6 +239,12 @@ /* Define if you have the 'use_env' function. */ #undef HAVE_CURSES_USE_ENV +/* Define if you have the 'use_screen' function. */ +#undef HAVE_CURSES_USE_SCREEN + +/* Define if you have the 'use_window' function. */ +#undef HAVE_CURSES_USE_WINDOW + /* Define if you have the 'wchgat' function. */ #undef HAVE_CURSES_WCHGAT