Skip to content

Commit ac023ea

Browse files
gh-90092: Support multiple terminals in the curses module (GH-151748)
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>
1 parent 793f471 commit ac023ea

11 files changed

Lines changed: 1218 additions & 80 deletions

File tree

Doc/library/curses.rst

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,9 @@ The module :mod:`!curses` defines the following functions:
217217
.. function:: nofilter()
218218

219219
Undo the effect of a previous :func:`.filter` call.
220-
Like :func:`.filter`, it must be called before :func:`initscr` so that the
221-
next initialization uses the full screen again.
220+
Like :func:`.filter`, it must be called before :func:`initscr` (or
221+
:func:`newterm`) so that the next initialization uses the full screen
222+
again.
222223

223224
Availability: if the underlying curses library provides ``nofilter()``.
224225

@@ -462,6 +463,36 @@ The module :mod:`!curses` defines the following functions:
462463
right corner of the screen.
463464

464465

466+
.. function:: newterm(type=None, fd=None, infd=None, /)
467+
468+
Initialize a new terminal in addition to the one initialized by
469+
:func:`initscr`,
470+
and return a :ref:`screen <curses-screen-objects>` for it.
471+
This allows a program to drive more than one terminal.
472+
473+
*type* is the terminal name, as in :func:`setupterm`;
474+
if ``None``, the value of the :envvar:`TERM` environment variable is used.
475+
*fd* and *infd* are the output and input files for the terminal:
476+
either a file object or a file descriptor.
477+
They default to :data:`sys.stdout` and :data:`sys.stdin`.
478+
479+
The new screen becomes the current one.
480+
Use :func:`set_term` to switch between screens.
481+
482+
.. versionadded:: next
483+
484+
485+
.. function:: new_prescr()
486+
487+
Return a new :ref:`screen <curses-screen-objects>`
488+
that can be used to call functions that affect global state
489+
before :func:`initscr` or :func:`newterm` is called.
490+
491+
Availability: if the underlying curses library provides ``new_prescr()``.
492+
493+
.. versionadded:: next
494+
495+
465496
.. function:: nl(flag=True)
466497

467498
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:
606637

607638
.. versionadded:: 3.9
608639

640+
641+
.. function:: set_term(screen, /)
642+
643+
Make *screen*, a :ref:`screen <curses-screen-objects>` returned by
644+
:func:`newterm`, the current terminal,
645+
and return the previously current screen.
646+
Returns ``None`` if the previous screen was the one created by
647+
:func:`initscr`.
648+
649+
.. versionadded:: next
650+
609651
.. function:: setsyx(y, x)
610652

611653
Set the virtual screen cursor to *y*, *x*. If *y* and *x* are both ``-1``, then
@@ -1469,6 +1511,18 @@ Window objects
14691511
:meth:`refresh`.
14701512

14711513

1514+
.. method:: window.use(func, /, *args, **kwargs)
1515+
1516+
Call ``func(window, *args, **kwargs)`` with the lock of the window held,
1517+
and return its result.
1518+
This provides automatic protection for the window
1519+
against concurrent access from another thread.
1520+
1521+
Availability: if the underlying curses library provides ``use_window()``.
1522+
1523+
.. versionadded:: next
1524+
1525+
14721526
.. method:: window.vline(ch, n[, attr])
14731527
window.vline(y, x, ch, n[, attr])
14741528

@@ -1479,6 +1533,60 @@ Window objects
14791533
Wide and combining characters are now accepted.
14801534

14811535

1536+
.. _curses-screen-objects:
1537+
1538+
Screen objects
1539+
--------------
1540+
1541+
.. class:: screen
1542+
1543+
A *screen* object represents a terminal initialized by :func:`newterm`
1544+
(or :func:`new_prescr`),
1545+
in addition to the default screen created by :func:`initscr`.
1546+
Screen objects are returned by those functions;
1547+
they cannot be instantiated directly.
1548+
1549+
A screen is freed automatically once it is no longer referenced,
1550+
either directly or through one of its windows.
1551+
Each window keeps its screen alive,
1552+
so a screen remains valid as long as any of its windows does.
1553+
1554+
.. versionadded:: next
1555+
1556+
1557+
.. method:: screen.close()
1558+
1559+
Detach the screen's standard window,
1560+
breaking the reference cycle between them
1561+
so the screen can be reclaimed promptly instead of waiting for a
1562+
garbage collection.
1563+
Afterwards :attr:`~screen.stdscr` is ``None``
1564+
and the window it returned earlier can no longer be used.
1565+
The screen's resources are released
1566+
once it and all its windows are no longer referenced.
1567+
1568+
.. versionadded:: next
1569+
1570+
1571+
.. attribute:: screen.stdscr
1572+
1573+
The standard :ref:`window <curses-window-objects>` of the screen,
1574+
covering the whole terminal,
1575+
or ``None`` for a screen created by :func:`new_prescr`.
1576+
1577+
1578+
.. method:: screen.use(func, /, *args, **kwargs)
1579+
1580+
Call ``func(screen, *args, **kwargs)`` with the lock of the screen held,
1581+
and return its result.
1582+
This provides automatic protection for the screen
1583+
against concurrent access from another thread.
1584+
1585+
Availability: if the underlying curses library provides ``use_screen()``.
1586+
1587+
.. versionadded:: next
1588+
1589+
14821590
Constants
14831591
---------
14841592

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ Improved modules
8989
curses
9090
------
9191

92+
* Add support for multiple terminals to the :mod:`curses` module:
93+
the new functions :func:`curses.newterm`, :func:`curses.set_term`
94+
and :func:`curses.new_prescr`,
95+
the corresponding :ref:`screen <curses-screen-objects>` object,
96+
and the :meth:`window.use() <curses.window.use>` method.
97+
(Contributed by Serhiy Storchaka in :gh:`90092`.)
98+
9299
* The :mod:`curses` character-cell window methods now accept a full character
93100
cell --- a spacing character optionally followed by combining characters ---
94101
in addition to a single integer or byte character. This affects

Include/py_curses.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,18 @@ typedef struct PyCursesWindowObject {
8080
WINDOW *win;
8181
char *encoding;
8282
struct PyCursesWindowObject *orig;
83+
PyObject *screen; /* the screen the window belongs to, or NULL,
84+
kept alive for the lifetime of the window */
8385
} PyCursesWindowObject;
8486

87+
typedef struct {
88+
PyObject_HEAD
89+
SCREEN *screen; /* NULL after the screen has been deleted */
90+
FILE *outfp; /* owned output stream, or NULL */
91+
FILE *infp; /* owned input stream, or NULL */
92+
PyObject *stdscr; /* the screen's standard window, or NULL */
93+
} PyCursesScreenObject;
94+
8595
#define PyCurses_CAPSULE_NAME "_curses._C_API"
8696

8797

Lib/curses/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ def initscr():
3434
setattr(curses, key, value)
3535
return stdscr
3636

37+
# newterm() is wrapped for the same reason as initscr(): the ACS_* constants
38+
# and LINES/COLS only become available once a terminal is initialized, and are
39+
# then copied to the curses package's dictionary.
40+
41+
try:
42+
newterm
43+
except NameError:
44+
pass
45+
else:
46+
def newterm(type=None, fd=None, infd=None, /):
47+
import _curses, curses
48+
screen = _curses.newterm(type, fd, infd)
49+
for key, value in _curses.__dict__.items():
50+
if key.startswith('ACS_') or key in ('LINES', 'COLS'):
51+
setattr(curses, key, value)
52+
return screen
53+
3754
# This is a similar wrapper for start_color(), which adds the COLORS and
3855
# COLOR_PAIRS variables which are only available after start_color() is
3956
# called.

0 commit comments

Comments
 (0)