Skip to content

Commit a85e73b

Browse files
gh-152260: Add curses.scr_dump(), scr_restore(), scr_init() and scr_set() (GH-152261)
These module-level functions write the whole virtual screen to a file and load it back -- the screen-wide counterpart of window.putwin()/getwin(). The filename argument accepts a string or a path-like object. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bf61794 commit a85e73b

6 files changed

Lines changed: 232 additions & 3 deletions

File tree

Doc/library/curses.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,40 @@ The module :mod:`!curses` defines the following functions:
696696
Save the current state of the terminal modes in a buffer, usable by
697697
:func:`resetty`.
698698

699+
.. function:: scr_dump(filename)
700+
701+
Write the current contents of the virtual screen to *filename*, which may be
702+
a string or a :term:`path-like object`. The file can later be read by
703+
:func:`scr_restore`, :func:`scr_init` or :func:`scr_set`. This is the
704+
whole-screen counterpart of :meth:`window.putwin`.
705+
706+
.. versionadded:: next
707+
708+
.. function:: scr_restore(filename)
709+
710+
Set the virtual screen to the contents of *filename*, which must have been
711+
written by :func:`scr_dump`. The next call to :func:`doupdate` or
712+
:meth:`window.refresh` restores the screen to those contents.
713+
714+
.. versionadded:: next
715+
716+
.. function:: scr_init(filename)
717+
718+
Initialize the assumed contents of the terminal from *filename*, which must
719+
have been written by :func:`scr_dump`. Use it when the terminal already
720+
displays those contents, for example after another program has drawn the
721+
screen, so that curses does not redraw what is already there.
722+
723+
.. versionadded:: next
724+
725+
.. function:: scr_set(filename)
726+
727+
Use *filename*, which must have been written by :func:`scr_dump`, as both
728+
the virtual screen and the assumed terminal contents. This combines the
729+
effects of :func:`scr_restore` and :func:`scr_init`.
730+
731+
.. versionadded:: next
732+
699733
.. function:: get_escdelay()
700734

701735
Retrieves the value set by :func:`set_escdelay`.

Doc/whatsnew/3.16.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ curses
160160
returns a new window that is an independent duplicate of an existing one.
161161
(Contributed by Serhiy Storchaka in :gh:`152258`.)
162162

163+
* Add the :mod:`curses` functions :func:`~curses.scr_dump`,
164+
:func:`~curses.scr_restore`, :func:`~curses.scr_init` and
165+
:func:`~curses.scr_set`, which dump the whole screen to a file and restore it.
166+
(Contributed by Serhiy Storchaka in :gh:`152260`.)
167+
163168
gzip
164169
----
165170

Lib/test/test_curses.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,43 @@ def test_putwin(self):
11161116
self.assertEqual(win.getmaxyx(), (5, 12))
11171117
self.assertEqual(win.instr(2, 0), b' Lorem ipsum')
11181118

1119+
def test_scr_dump(self):
1120+
# Test scr_dump(), scr_restore(), scr_init() and scr_set().
1121+
# scr_dump() writes the virtual screen to a named file; the other three
1122+
# functions load it back. The dumped image is internal curses state,
1123+
# not a window, so the round-trip is checked by comparing dump files
1124+
# rather than reading cells.
1125+
stdscr = self.stdscr
1126+
stdscr.erase()
1127+
stdscr.addstr(0, 0, 'screen dump test')
1128+
stdscr.refresh()
1129+
with tempfile.TemporaryDirectory() as d:
1130+
dump = os.path.join(d, 'dump')
1131+
self.assertIsNone(curses.scr_dump(dump))
1132+
# Dumping the same screen again is deterministic.
1133+
dump2 = os.path.join(d, 'dump2')
1134+
curses.scr_dump(dump2)
1135+
with open(dump, 'rb') as f1, open(dump2, 'rb') as f2:
1136+
self.assertEqual(f1.read(), f2.read())
1137+
# scr_restore() reloads that virtual screen, so dumping it again
1138+
# reproduces the original file even after the screen has changed.
1139+
stdscr.erase()
1140+
stdscr.addstr(0, 0, 'something else')
1141+
stdscr.refresh()
1142+
self.assertIsNone(curses.scr_restore(dump))
1143+
restored = os.path.join(d, 'restored')
1144+
curses.scr_dump(restored)
1145+
with open(dump, 'rb') as f1, open(restored, 'rb') as f2:
1146+
self.assertEqual(f1.read(), f2.read())
1147+
# scr_init() and scr_set() accept a dump file and return None.
1148+
self.assertIsNone(curses.scr_init(dump))
1149+
self.assertIsNone(curses.scr_set(dump))
1150+
# A bytes (path-like) filename is accepted too.
1151+
curses.scr_dump(os.fsencode(dump))
1152+
# Restoring from a missing file is an error.
1153+
self.assertRaises(curses.error,
1154+
curses.scr_restore, os.path.join(d, 'nope'))
1155+
11191156
def test_borders_and_lines(self):
11201157
win = curses.newwin(5, 10, 5, 2)
11211158
win.border('|', '!', '-', '_',
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add the :mod:`curses` functions :func:`~curses.scr_dump`,
2+
:func:`~curses.scr_restore`, :func:`~curses.scr_init` and
3+
:func:`~curses.scr_set`, which dump the whole screen to a file and restore it.

Modules/_cursesmodule.c

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
mcprint mvaddchnstr mvaddchstr mvcur mvinchnstr
4646
mvinchstr mvinnstr mmvwaddchnstr mvwaddchstr
4747
mvwinchnstr mvwinchstr mvwinnstr
48-
restartterm ripoffline scr_dump
49-
scr_init scr_restore scr_set scrl set_curterm setterm
48+
restartterm ripoffline
49+
scrl set_curterm setterm
5050
tgetent tgetflag tgetnum tgetstr tgoto timeout tputs
5151
vidattr vidputs waddchnstr waddchstr
5252
wcolor_set winchnstr winchstr winnstr wmouse_trafo wscrl
@@ -5039,6 +5039,22 @@ static PyType_Spec PyCursesScreen_Type_spec = {
50395039
Py_RETURN_NONE; \
50405040
}
50415041

5042+
/*
5043+
* Function body for a module function that dumps or restores the whole screen
5044+
* through a file named by a single filesystem-path argument (filename).
5045+
*/
5046+
#define ScreenDumpFunctionBody(X) \
5047+
{ \
5048+
PyCursesStatefulInitialised(module); \
5049+
PyObject *bytes; \
5050+
if (!PyUnicode_FSConverter(filename, &bytes)) { \
5051+
return NULL; \
5052+
} \
5053+
int rtn = X(PyBytes_AS_STRING(bytes)); \
5054+
Py_DECREF(bytes); \
5055+
return curses_check_err(module, rtn, # X, NULL); \
5056+
}
5057+
50425058
/*********************************************************************
50435059
Global Functions
50445060
**********************************************************************/
@@ -5612,6 +5628,77 @@ _curses_getwin(PyObject *module, PyObject *file)
56125628
return res;
56135629
}
56145630

5631+
/*[clinic input]
5632+
_curses.scr_dump
5633+
5634+
filename: object
5635+
The file to write to.
5636+
/
5637+
5638+
Write the current contents of the virtual screen to a file.
5639+
5640+
The file can later be used to restore the screen with scr_restore(),
5641+
scr_init() or scr_set().
5642+
[clinic start generated code]*/
5643+
5644+
static PyObject *
5645+
_curses_scr_dump(PyObject *module, PyObject *filename)
5646+
/*[clinic end generated code: output=4425cfa505ac9577 input=358db4b370975345]*/
5647+
ScreenDumpFunctionBody(scr_dump)
5648+
5649+
/*[clinic input]
5650+
_curses.scr_restore
5651+
5652+
filename: object
5653+
The file to read from.
5654+
/
5655+
5656+
Set the virtual screen to the contents of a file made by scr_dump().
5657+
5658+
The next call to doupdate() or refresh() restores the screen to those
5659+
contents.
5660+
[clinic start generated code]*/
5661+
5662+
static PyObject *
5663+
_curses_scr_restore(PyObject *module, PyObject *filename)
5664+
/*[clinic end generated code: output=71d669fb560fa57b input=30b1d6b2c328dd55]*/
5665+
ScreenDumpFunctionBody(scr_restore)
5666+
5667+
/*[clinic input]
5668+
_curses.scr_init
5669+
5670+
filename: object
5671+
The file to read from.
5672+
/
5673+
5674+
Initialize the assumed terminal contents from a scr_dump() file.
5675+
5676+
Use it as what the terminal currently displays, for example after
5677+
another program has drawn the screen.
5678+
[clinic start generated code]*/
5679+
5680+
static PyObject *
5681+
_curses_scr_init(PyObject *module, PyObject *filename)
5682+
/*[clinic end generated code: output=2e861d381d710419 input=81c45e4702124ef6]*/
5683+
ScreenDumpFunctionBody(scr_init)
5684+
5685+
/*[clinic input]
5686+
_curses.scr_set
5687+
5688+
filename: object
5689+
The file to read from.
5690+
/
5691+
5692+
Use a scr_dump() file as both the virtual screen and the terminal.
5693+
5694+
This combines the effects of scr_restore() and scr_init().
5695+
[clinic start generated code]*/
5696+
5697+
static PyObject *
5698+
_curses_scr_set(PyObject *module, PyObject *filename)
5699+
/*[clinic end generated code: output=6056fdec12c5935f input=d248c20543cc289b]*/
5700+
ScreenDumpFunctionBody(scr_set)
5701+
56155702
/*[clinic input]
56165703
_curses.halfdelay
56175704
@@ -7715,6 +7802,10 @@ static PyMethodDef cursesmodule_methods[] = {
77157802
_CURSES_RESIZETERM_METHODDEF
77167803
_CURSES_RESIZE_TERM_METHODDEF
77177804
_CURSES_SAVETTY_METHODDEF
7805+
_CURSES_SCR_DUMP_METHODDEF
7806+
_CURSES_SCR_INIT_METHODDEF
7807+
_CURSES_SCR_RESTORE_METHODDEF
7808+
_CURSES_SCR_SET_METHODDEF
77187809
#if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20081102
77197810
_CURSES_GET_ESCDELAY_METHODDEF
77207811
_CURSES_SET_ESCDELAY_METHODDEF

Modules/clinic/_cursesmodule.c.h

Lines changed: 60 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)