diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index 3ac45eafa9c21e3..ba4fe55e4a22c64 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -696,6 +696,40 @@ The module :mod:`!curses` defines the following functions: Save the current state of the terminal modes in a buffer, usable by :func:`resetty`. +.. function:: scr_dump(filename) + + Write the current contents of the virtual screen to *filename*, which may be + a string or a :term:`path-like object`. The file can later be read by + :func:`scr_restore`, :func:`scr_init` or :func:`scr_set`. This is the + whole-screen counterpart of :meth:`window.putwin`. + + .. versionadded:: next + +.. function:: scr_restore(filename) + + Set the virtual screen to the contents of *filename*, which must have been + written by :func:`scr_dump`. The next call to :func:`doupdate` or + :meth:`window.refresh` restores the screen to those contents. + + .. versionadded:: next + +.. function:: scr_init(filename) + + Initialize the assumed contents of the terminal from *filename*, which must + have been written by :func:`scr_dump`. Use it when the terminal already + displays those contents, for example after another program has drawn the + screen, so that curses does not redraw what is already there. + + .. versionadded:: next + +.. function:: scr_set(filename) + + Use *filename*, which must have been written by :func:`scr_dump`, as both + the virtual screen and the assumed terminal contents. This combines the + effects of :func:`scr_restore` and :func:`scr_init`. + + .. versionadded:: next + .. function:: get_escdelay() Retrieves the value set by :func:`set_escdelay`. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index ca80b0a1227588f..8dfd3bf81695584 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -160,6 +160,11 @@ curses returns a new window that is an independent duplicate of an existing one. (Contributed by Serhiy Storchaka in :gh:`152258`.) +* Add the :mod:`curses` functions :func:`~curses.scr_dump`, + :func:`~curses.scr_restore`, :func:`~curses.scr_init` and + :func:`~curses.scr_set`, which dump the whole screen to a file and restore it. + (Contributed by Serhiy Storchaka in :gh:`152260`.) + gzip ---- diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index ba259ae0d1ce36a..7157896d8cbccda 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1116,6 +1116,43 @@ def test_putwin(self): self.assertEqual(win.getmaxyx(), (5, 12)) self.assertEqual(win.instr(2, 0), b' Lorem ipsum') + def test_scr_dump(self): + # Test scr_dump(), scr_restore(), scr_init() and scr_set(). + # scr_dump() writes the virtual screen to a named file; the other three + # functions load it back. The dumped image is internal curses state, + # not a window, so the round-trip is checked by comparing dump files + # rather than reading cells. + stdscr = self.stdscr + stdscr.erase() + stdscr.addstr(0, 0, 'screen dump test') + stdscr.refresh() + with tempfile.TemporaryDirectory() as d: + dump = os.path.join(d, 'dump') + self.assertIsNone(curses.scr_dump(dump)) + # Dumping the same screen again is deterministic. + dump2 = os.path.join(d, 'dump2') + curses.scr_dump(dump2) + with open(dump, 'rb') as f1, open(dump2, 'rb') as f2: + self.assertEqual(f1.read(), f2.read()) + # scr_restore() reloads that virtual screen, so dumping it again + # reproduces the original file even after the screen has changed. + stdscr.erase() + stdscr.addstr(0, 0, 'something else') + stdscr.refresh() + self.assertIsNone(curses.scr_restore(dump)) + restored = os.path.join(d, 'restored') + curses.scr_dump(restored) + with open(dump, 'rb') as f1, open(restored, 'rb') as f2: + self.assertEqual(f1.read(), f2.read()) + # scr_init() and scr_set() accept a dump file and return None. + self.assertIsNone(curses.scr_init(dump)) + self.assertIsNone(curses.scr_set(dump)) + # A bytes (path-like) filename is accepted too. + curses.scr_dump(os.fsencode(dump)) + # Restoring from a missing file is an error. + self.assertRaises(curses.error, + curses.scr_restore, os.path.join(d, 'nope')) + def test_borders_and_lines(self): win = curses.newwin(5, 10, 5, 2) win.border('|', '!', '-', '_', diff --git a/Misc/NEWS.d/next/Library/2026-06-26-15-10-00.gh-issue-152260.Rk4nP9.rst b/Misc/NEWS.d/next/Library/2026-06-26-15-10-00.gh-issue-152260.Rk4nP9.rst new file mode 100644 index 000000000000000..8c396009815399d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-15-10-00.gh-issue-152260.Rk4nP9.rst @@ -0,0 +1,3 @@ +Add the :mod:`curses` functions :func:`~curses.scr_dump`, +:func:`~curses.scr_restore`, :func:`~curses.scr_init` and +:func:`~curses.scr_set`, which dump the whole screen to a file and restore it. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 537a8c4e913f56b..3d6748340930ee8 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -45,8 +45,8 @@ mcprint mvaddchnstr mvaddchstr mvcur mvinchnstr mvinchstr mvinnstr mmvwaddchnstr mvwaddchstr mvwinchnstr mvwinchstr mvwinnstr - restartterm ripoffline scr_dump - scr_init scr_restore scr_set scrl set_curterm setterm + restartterm ripoffline + scrl set_curterm setterm tgetent tgetflag tgetnum tgetstr tgoto timeout tputs vidattr vidputs waddchnstr waddchstr wcolor_set winchnstr winchstr winnstr wmouse_trafo wscrl @@ -5039,6 +5039,22 @@ static PyType_Spec PyCursesScreen_Type_spec = { Py_RETURN_NONE; \ } +/* + * Function body for a module function that dumps or restores the whole screen + * through a file named by a single filesystem-path argument (filename). + */ +#define ScreenDumpFunctionBody(X) \ +{ \ + PyCursesStatefulInitialised(module); \ + PyObject *bytes; \ + if (!PyUnicode_FSConverter(filename, &bytes)) { \ + return NULL; \ + } \ + int rtn = X(PyBytes_AS_STRING(bytes)); \ + Py_DECREF(bytes); \ + return curses_check_err(module, rtn, # X, NULL); \ +} + /********************************************************************* Global Functions **********************************************************************/ @@ -5612,6 +5628,77 @@ _curses_getwin(PyObject *module, PyObject *file) return res; } +/*[clinic input] +_curses.scr_dump + + filename: object + The file to write to. + / + +Write the current contents of the virtual screen to a file. + +The file can later be used to restore the screen with scr_restore(), +scr_init() or scr_set(). +[clinic start generated code]*/ + +static PyObject * +_curses_scr_dump(PyObject *module, PyObject *filename) +/*[clinic end generated code: output=4425cfa505ac9577 input=358db4b370975345]*/ +ScreenDumpFunctionBody(scr_dump) + +/*[clinic input] +_curses.scr_restore + + filename: object + The file to read from. + / + +Set the virtual screen to the contents of a file made by scr_dump(). + +The next call to doupdate() or refresh() restores the screen to those +contents. +[clinic start generated code]*/ + +static PyObject * +_curses_scr_restore(PyObject *module, PyObject *filename) +/*[clinic end generated code: output=71d669fb560fa57b input=30b1d6b2c328dd55]*/ +ScreenDumpFunctionBody(scr_restore) + +/*[clinic input] +_curses.scr_init + + filename: object + The file to read from. + / + +Initialize the assumed terminal contents from a scr_dump() file. + +Use it as what the terminal currently displays, for example after +another program has drawn the screen. +[clinic start generated code]*/ + +static PyObject * +_curses_scr_init(PyObject *module, PyObject *filename) +/*[clinic end generated code: output=2e861d381d710419 input=81c45e4702124ef6]*/ +ScreenDumpFunctionBody(scr_init) + +/*[clinic input] +_curses.scr_set + + filename: object + The file to read from. + / + +Use a scr_dump() file as both the virtual screen and the terminal. + +This combines the effects of scr_restore() and scr_init(). +[clinic start generated code]*/ + +static PyObject * +_curses_scr_set(PyObject *module, PyObject *filename) +/*[clinic end generated code: output=6056fdec12c5935f input=d248c20543cc289b]*/ +ScreenDumpFunctionBody(scr_set) + /*[clinic input] _curses.halfdelay @@ -7715,6 +7802,10 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_RESIZETERM_METHODDEF _CURSES_RESIZE_TERM_METHODDEF _CURSES_SAVETTY_METHODDEF + _CURSES_SCR_DUMP_METHODDEF + _CURSES_SCR_INIT_METHODDEF + _CURSES_SCR_RESTORE_METHODDEF + _CURSES_SCR_SET_METHODDEF #if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20081102 _CURSES_GET_ESCDELAY_METHODDEF _CURSES_SET_ESCDELAY_METHODDEF diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index 4f4fda094434bef..8fbcf1d99bbbeda 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -3017,6 +3017,65 @@ PyDoc_STRVAR(_curses_getwin__doc__, #define _CURSES_GETWIN_METHODDEF \ {"getwin", (PyCFunction)_curses_getwin, METH_O, _curses_getwin__doc__}, +PyDoc_STRVAR(_curses_scr_dump__doc__, +"scr_dump($module, filename, /)\n" +"--\n" +"\n" +"Write the current contents of the virtual screen to a file.\n" +"\n" +" filename\n" +" The file to write to.\n" +"\n" +"The file can later be used to restore the screen with scr_restore(),\n" +"scr_init() or scr_set()."); + +#define _CURSES_SCR_DUMP_METHODDEF \ + {"scr_dump", (PyCFunction)_curses_scr_dump, METH_O, _curses_scr_dump__doc__}, + +PyDoc_STRVAR(_curses_scr_restore__doc__, +"scr_restore($module, filename, /)\n" +"--\n" +"\n" +"Set the virtual screen to the contents of a file made by scr_dump().\n" +"\n" +" filename\n" +" The file to read from.\n" +"\n" +"The next call to doupdate() or refresh() restores the screen to those\n" +"contents."); + +#define _CURSES_SCR_RESTORE_METHODDEF \ + {"scr_restore", (PyCFunction)_curses_scr_restore, METH_O, _curses_scr_restore__doc__}, + +PyDoc_STRVAR(_curses_scr_init__doc__, +"scr_init($module, filename, /)\n" +"--\n" +"\n" +"Initialize the assumed terminal contents from a scr_dump() file.\n" +"\n" +" filename\n" +" The file to read from.\n" +"\n" +"Use it as what the terminal currently displays, for example after\n" +"another program has drawn the screen."); + +#define _CURSES_SCR_INIT_METHODDEF \ + {"scr_init", (PyCFunction)_curses_scr_init, METH_O, _curses_scr_init__doc__}, + +PyDoc_STRVAR(_curses_scr_set__doc__, +"scr_set($module, filename, /)\n" +"--\n" +"\n" +"Use a scr_dump() file as both the virtual screen and the terminal.\n" +"\n" +" filename\n" +" The file to read from.\n" +"\n" +"This combines the effects of scr_restore() and scr_init()."); + +#define _CURSES_SCR_SET_METHODDEF \ + {"scr_set", (PyCFunction)_curses_scr_set, METH_O, _curses_scr_set__doc__}, + PyDoc_STRVAR(_curses_halfdelay__doc__, "halfdelay($module, tenths, /)\n" "--\n" @@ -5422,4 +5481,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=9d7ca194927796d8 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=864fa5c0f22fcad3 input=a9049054013a1b77]*/