Skip to content

Commit 49d484e

Browse files
gh-152258: Add curses.window.dupwin() (GH-152259)
dupwin() returns a new window that is an independent duplicate of an existing one -- same size, position, contents and attributes, but with its own cell buffer, so changes to one do not affect the other. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3ffda34 commit 49d484e

6 files changed

Lines changed: 98 additions & 2 deletions

File tree

Doc/library/curses.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,17 @@ Window objects
11921192
object for the derived window.
11931193

11941194

1195+
.. method:: window.dupwin()
1196+
1197+
Return a new window that is an exact duplicate of the window: it has the same
1198+
size, position, contents and attributes. Unlike a window created by
1199+
:meth:`subwin` or :meth:`derwin`, the duplicate is independent of the
1200+
original -- it has its own cell buffer, so later changes to one do not affect
1201+
the other.
1202+
1203+
.. versionadded:: next
1204+
1205+
11951206
.. method:: window.echochar(ch[, attr])
11961207

11971208
Add character *ch* with attribute *attr*, and immediately call :meth:`refresh`

Doc/whatsnew/3.16.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ curses
156156
accept a :class:`~curses.complexstr`.
157157
(Contributed by Serhiy Storchaka in :gh:`152233`.)
158158

159+
* Add the :mod:`curses` window method :meth:`~curses.window.dupwin`, which
160+
returns a new window that is an independent duplicate of an existing one.
161+
(Contributed by Serhiy Storchaka in :gh:`152258`.)
162+
159163
gzip
160164
----
161165

Lib/test/test_curses.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,35 @@ def test_subwindows_references(self):
218218
del win2
219219
gc_collect()
220220

221+
def test_dupwin(self):
222+
win = curses.newwin(5, 10, 2, 3)
223+
win.addstr(0, 0, 'ABCDE')
224+
win.addstr(1, 0, 'fghij')
225+
dup = win.dupwin()
226+
# Same geometry and contents as the original.
227+
self.assertEqual(dup.getbegyx(), win.getbegyx())
228+
self.assertEqual(dup.getmaxyx(), win.getmaxyx())
229+
self.assertEqual(dup.instr(0, 0, 5), b'ABCDE')
230+
self.assertEqual(dup.instr(1, 0, 5), b'fghij')
231+
# The duplicate is independent, not a subwindow.
232+
if hasattr(dup, 'is_subwin'):
233+
self.assertIs(dup.is_subwin(), False)
234+
self.assertIsNone(dup.getparent())
235+
# Changes to one do not affect the other.
236+
dup.addstr(0, 0, 'xxxxx')
237+
win.addstr(1, 0, 'YYYYY')
238+
self.assertEqual(win.instr(0, 0, 5), b'ABCDE')
239+
self.assertEqual(dup.instr(0, 0, 5), b'xxxxx')
240+
self.assertEqual(dup.instr(1, 0, 5), b'fghij')
241+
self.assertEqual(win.instr(1, 0, 5), b'YYYYY')
242+
# A subwindow can also be duplicated; the duplicate is independent.
243+
sub = win.subwin(3, 5, 2, 3)
244+
subdup = sub.dupwin()
245+
self.assertEqual(subdup.getmaxyx(), sub.getmaxyx())
246+
if hasattr(subdup, 'is_subwin'):
247+
self.assertIs(subdup.is_subwin(), False)
248+
self.assertIsNone(subdup.getparent())
249+
221250
def test_move_cursor(self):
222251
stdscr = self.stdscr
223252
win = stdscr.subwin(10, 15, 2, 5)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add the :mod:`curses` window method :meth:`~curses.window.dupwin`, which
2+
returns a new window that is an independent duplicate of an existing one.

Modules/_cursesmodule.c

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
Here's a list of currently unsupported functions:
4242
4343
addchnstr addchstr color_set define_key
44-
del_curterm dupwin inchnstr inchstr innstr keyok
44+
del_curterm inchnstr inchstr innstr keyok
4545
mcprint mvaddchnstr mvaddchstr mvcur mvinchnstr
4646
mvinchstr mvinnstr mmvwaddchnstr mvwaddchstr
4747
mvwinchnstr mvwinchstr mvwinnstr
@@ -2722,6 +2722,33 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, int group_left_1,
27222722
return PyCursesWindow_New(state, win, NULL, self, self->screen);
27232723
}
27242724

2725+
/*[clinic input]
2726+
_curses.window.dupwin
2727+
2728+
Create an exact duplicate of the window.
2729+
2730+
The new window is independent of the original: it has the same size,
2731+
position, contents and attributes, but its own cell buffer, so later
2732+
changes to one do not affect the other.
2733+
[clinic start generated code]*/
2734+
2735+
static PyObject *
2736+
_curses_window_dupwin_impl(PyCursesWindowObject *self)
2737+
/*[clinic end generated code: output=37d91aa8f88f13d1 input=787301b3799b618e]*/
2738+
{
2739+
WINDOW *win = dupwin(self->win);
2740+
if (win == NULL) {
2741+
curses_window_set_null_error(self, "dupwin", NULL);
2742+
return NULL;
2743+
}
2744+
2745+
/* The duplicate owns an independent cell buffer (unlike a subwindow), so
2746+
it has no parent: pass NULL as orig. Inherit the source encoding and
2747+
screen so it matches the original. */
2748+
cursesmodule_state *state = get_cursesmodule_state_by_win(self);
2749+
return PyCursesWindow_New(state, win, self->encoding, NULL, self->screen);
2750+
}
2751+
27252752
/*[clinic input]
27262753
_curses.window.echochar
27272754
@@ -4520,6 +4547,7 @@ static PyMethodDef PyCursesWindow_methods[] = {
45204547
"deleteln($self, /)\n--\n\n"
45214548
"Delete the line under the cursor; move following lines up by one."},
45224549
_CURSES_WINDOW_DERWIN_METHODDEF
4550+
_CURSES_WINDOW_DUPWIN_METHODDEF
45234551
_CURSES_WINDOW_ECHOCHAR_METHODDEF
45244552
_CURSES_WINDOW_ENCLOSE_METHODDEF
45254553
{"erase", PyCursesWindow_werase, METH_NOARGS,

Modules/clinic/_cursesmodule.c.h

Lines changed: 23 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)