]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-90092: Support multiple terminals in the curses module (GH-151748)
authorSerhiy Storchaka <storchaka@gmail.com>
Wed, 24 Jun 2026 11:33:02 +0000 (14:33 +0300)
committerGitHub <noreply@github.com>
Wed, 24 Jun 2026 11:33:02 +0000 (11:33 +0000)
Add the X/Open Curses SCREEN API for driving more than one terminal:
newterm() and set_term(), plus the ncurses extension new_prescr().

A new screen object wraps the C SCREEN.  It exposes the terminal's
standard window as screen.stdscr.  Each window keeps a reference to its
screen (like a subwindow does to its parent window), so the screen is
deleted automatically once it and all of its windows are unreferenced.

The ncurses use_screen()/use_window() locking helpers are exposed as
the screen.use() and window.use() methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Doc/library/curses.rst
Doc/whatsnew/3.16.rst
Include/py_curses.h
Lib/curses/__init__.py
Lib/test/test_curses.py
Misc/NEWS.d/next/Library/2026-06-19-12-19-30.gh-issue-90092.yBVc0C.rst [new file with mode: 0644]
Modules/_cursesmodule.c
Modules/clinic/_cursesmodule.c.h
configure
configure.ac
pyconfig.h.in

index 3c9a27aa85e6a0913182e958a3d661b4064fef33..758dca976c228dad05708537e18c5ac3c4a63a12 100644 (file)
@@ -217,8 +217,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()``.
 
@@ -462,6 +463,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 <curses-screen-objects>` 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 <curses-screen-objects>`
+   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,
@@ -606,6 +637,17 @@ The module :mod:`!curses` defines the following functions:
 
    .. versionadded:: 3.9
 
+
+.. function:: set_term(screen, /)
+
+   Make *screen*, a :ref:`screen <curses-screen-objects>` 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
@@ -1469,6 +1511,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])
 
@@ -1479,6 +1533,60 @@ Window objects
       Wide and combining characters are now accepted.
 
 
+.. _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 <curses-window-objects>` 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
 ---------
 
index f9e54cde10afe0337080e2e6874583f93065199d..7f35c0e3559a9efd9e95f77415ac620a631682cc 100644 (file)
@@ -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 <curses-screen-objects>` object,
+  and the :meth:`window.use() <curses.window.use>` method.
+  (Contributed by Serhiy Storchaka in :gh:`90092`.)
+
 * The :mod:`curses` character-cell window methods now accept a full character
   cell --- a spacing character optionally followed by combining characters ---
   in addition to a single integer or byte character.  This affects
index 0948aabedd499398c26a2612e48d6a7f7bbd6a2f..3d2ca278f809cb207ae98ec1fe6d0ec3db7af89c 100644 (file)
@@ -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"
 
 
index 605d5fcbec5499d3369c09fc409eb0971bdc24a0..e150c7f932385ebeefb79dd8a44aeada8bed51e5 100644 (file)
@@ -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.
index f3d8179abae5624c2a0c4a0c8d13e080126c5a12..fe53ce49ab63b02fcb81376b6d67cb81b47e2dfe 100644 (file)
@@ -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 requires_colors(test):
 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 @@ class TestCurses(unittest.TestCase):
             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 @@ class TestCurses(unittest.TestCase):
 
     @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):
@@ -1203,6 +1227,22 @@ class TestCurses(unittest.TestCase):
             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):
@@ -1269,6 +1309,10 @@ class TestCurses(unittest.TestCase):
     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)
@@ -1501,9 +1545,11 @@ class TestCurses(unittest.TestCase):
             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):
@@ -1523,9 +1569,11 @@ class TestCurses(unittest.TestCase):
             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')
@@ -1831,5 +1879,141 @@ class TextboxTest(unittest.TestCase):
         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 (file)
index 0000000..41e0ce8
--- /dev/null
@@ -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
+<curses-screen-objects>` object, and the :meth:`curses.window.use` method.
index cf42a96b5ca3ae7370e39850877eb5a7edb3aada..fc71efff22d931046346a524a0ff21c84e0a6c6a 100644 (file)
   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;
@@ -939,7 +944,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)
@@ -967,14 +972,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;
 }
@@ -995,6 +1000,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);
 }
@@ -1005,6 +1011,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;
 }
 
@@ -1768,7 +1775,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]
@@ -3066,7 +3073,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]
@@ -3239,6 +3246,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
@@ -3413,6 +3498,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 */
 };
 
@@ -3452,6 +3541,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
+};
+
 /* -------------------------------------------------------*/
 
 /*
@@ -3560,14 +3825,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() */
@@ -3808,7 +4073,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
@@ -4023,7 +4299,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);
@@ -4197,50 +4473,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() */
@@ -4248,12 +4490,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)
 
@@ -4327,9 +4569,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;
     }
@@ -4399,6 +4688,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.<stdname> 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
 
@@ -4746,7 +5235,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]
@@ -4786,7 +5275,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]
@@ -5796,7 +6285,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
@@ -5820,6 +6313,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
@@ -5944,6 +6438,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;
 }
 
@@ -5953,6 +6449,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;
 }
 
@@ -5985,6 +6483,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);
index 62dab279f5a97c0bea9e4b5008e196afd597e53c..46c6ebedcbbd9eec226bd6f19f1e24e8a0fa78e1 100644 (file)
@@ -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__},
@@ -2840,6 +2840,112 @@ exit:
     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__,
@@ -4517,6 +4623,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) */
@@ -4588,4 +4698,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=0bce70b538541c9e input=a9049054013a1b77]*/
+/*[clinic end generated code: output=8188ebf7404d028a input=a9049054013a1b77]*/
index 12fd0d15698ac1d471cd626f6d12d81eaf19db75..a978b613514f124d30dd65a73a0ca6bd6d4d80ea 100755 (executable)
--- a/configure
+++ b/configure
 
 
 
+
+
+  { 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 <ncursesw/ncurses.h>
+#elif defined(HAVE_NCURSESW_CURSES_H)
+#  include <ncursesw/curses.h>
+#elif defined(HAVE_NCURSES_NCURSES_H)
+#  include <ncurses/ncurses.h>
+#elif defined(HAVE_NCURSES_CURSES_H)
+#  include <ncurses/curses.h>
+#elif defined(HAVE_NCURSES_H)
+#  include <ncurses.h>
+#elif defined(HAVE_CURSES_H)
+#  include <curses.h>
+#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 <ncursesw/ncurses.h>
+#elif defined(HAVE_NCURSESW_CURSES_H)
+#  include <ncursesw/curses.h>
+#elif defined(HAVE_NCURSES_NCURSES_H)
+#  include <ncurses/ncurses.h>
+#elif defined(HAVE_NCURSES_CURSES_H)
+#  include <ncurses/curses.h>
+#elif defined(HAVE_NCURSES_H)
+#  include <ncurses.h>
+#elif defined(HAVE_CURSES_H)
+#  include <curses.h>
+#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 <ncursesw/ncurses.h>
+#elif defined(HAVE_NCURSESW_CURSES_H)
+#  include <ncursesw/curses.h>
+#elif defined(HAVE_NCURSES_NCURSES_H)
+#  include <ncurses/ncurses.h>
+#elif defined(HAVE_NCURSES_CURSES_H)
+#  include <ncurses/curses.h>
+#elif defined(HAVE_NCURSES_H)
+#  include <ncurses.h>
+#elif defined(HAVE_CURSES_H)
+#  include <curses.h>
+#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
index 3b42fdfe40385dcadb7cb15730174be572fed2f3..a2729c22386a951dc4176abed13e9406af0d3b39 100644 (file)
@@ -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
index 2bef8d38497c547cacd03b473adaa002a1b9e6d5..a84b74299e159b104e651d9064d6d43e8940d5d8 100644 (file)
 /* 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
 
 /* 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