Skip to content

Commit b953155

Browse files
pranavchoudhary-techserhiy-storchakaclaude
authored
gh-155976: Refresh curses LINES, COLS, COLORS and COLOR_PAIRS on screen switch (GH-155994)
set_term() and screen.use() now update curses.LINES, curses.COLS, curses.COLORS and curses.COLOR_PAIRS to the values of the current screen. LINES and COLS are taken from the size of its standard window, since ncurses sets its own LINES and COLS only when a screen is created. Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 97a7135 commit b953155

3 files changed

Lines changed: 111 additions & 5 deletions

File tree

‎Lib/test/test_curses.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
check_disallow_instantiation, MISSING_C_DOCSTRINGS,
1515
gc_collect, SHORT_TIMEOUT)
1616
from test.support.import_helper import import_module
17+
from test.support import os_helper
1718

1819
# Optionally test curses module. This currently requires that the
1920
# 'curses' resource be given on the regrtest command line using the -u
@@ -129,6 +130,9 @@ def _broken_variation_selector_width():
129130

130131
BROKEN_VARIATION_SELECTOR_WIDTH = _broken_variation_selector_width()
131132

133+
class CustomError(Exception):
134+
pass
135+
132136
# newterm() is used when available (it reports errors instead of exiting), but
133137
# initscr() is still the fallback, and an unusable $TERM has no terminal to
134138
# drive either way.
@@ -3353,6 +3357,50 @@ def test_set_term(self):
33533357
self.assertIs(curses.set_term(a), b) # returns the previous one
33543358
self.assertIs(curses.set_term(b), a)
33553359

3360+
def make_screens_of_different_terms(self):
3361+
# Two screens of different sizes and numbers of colors. Return them
3362+
# with the values of LINES, COLS, COLORS and COLOR_PAIRS for each.
3363+
screens = []
3364+
with os_helper.EnvironmentVarGuard() as env:
3365+
for term, lines, cols in [('xterm', 25, 80),
3366+
('xterm-256color', 30, 100)]:
3367+
env['LINES'] = str(lines)
3368+
env['COLUMNS'] = str(cols)
3369+
s = self.make_pty()
3370+
try:
3371+
screen = curses.newterm(term, s, s)
3372+
except curses.error:
3373+
self.skipTest(f'no {term} terminfo entry')
3374+
curses.start_color()
3375+
self.assertEqual(screen.stdscr.getmaxyx(), (lines, cols))
3376+
screens.append((screen, self.get_screen_values()))
3377+
self.assertNotEqual(screens[0][1], screens[1][1])
3378+
return screens
3379+
3380+
def get_screen_values(self):
3381+
return curses.LINES, curses.COLS, curses.COLORS, curses.COLOR_PAIRS
3382+
3383+
def test_set_term_refreshes_lines_cols_colors(self):
3384+
(a, avalues), (b, bvalues) = self.make_screens_of_different_terms()
3385+
self.assertIs(curses.set_term(a), b)
3386+
self.assertEqual(self.get_screen_values(), avalues)
3387+
self.assertIs(curses.set_term(b), a)
3388+
self.assertEqual(self.get_screen_values(), bvalues)
3389+
3390+
@unittest.skipUnless(hasattr(curses.screen, 'use'),
3391+
'requires curses.screen.use()')
3392+
def test_use_screen_refreshes_lines_cols_colors(self):
3393+
(a, avalues), (b, bvalues) = self.make_screens_of_different_terms()
3394+
self.assertEqual(a.use(lambda scr: self.get_screen_values()), avalues)
3395+
self.assertEqual(self.get_screen_values(), bvalues)
3396+
3397+
def raise_in_use(scr):
3398+
self.assertEqual(self.get_screen_values(), avalues)
3399+
raise CustomError
3400+
with self.assertRaises(CustomError):
3401+
a.use(raise_in_use)
3402+
self.assertEqual(self.get_screen_values(), bvalues)
3403+
33563404
def test_window_keeps_screen_alive(self):
33573405
# The standard window keeps its screen alive; dropping every other
33583406
# reference and collecting must not invalidate the window.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`curses.set_term` and :meth:`curses.screen.use` now update
2+
:data:`curses.LINES`, :data:`curses.COLS`, :data:`curses.COLORS`
3+
and :data:`curses.COLOR_PAIRS` for the current screen.

‎Modules/_cursesmodule.c‎

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ static int curses_initscr_called = FALSE;
209209
/* Tells whether start_color() has been called to initialise color usage. */
210210
static int curses_start_color_called = FALSE;
211211

212+
static int update_lines_cols(PyObject *private_module);
213+
212214
/* Encoding of the initial screen, used by module-level functions that have
213215
no window object to take it from (e.g. unctrl(), ungetch()). This is a
214216
private copy: the window object that initscr() returns may be deallocated
@@ -5480,7 +5482,13 @@ static PyGetSetDef PyCursesScreen_getsets[] = {
54805482
static int
54815483
curses_use_screen_cb(SCREEN *Py_UNUSED(sp), void *data)
54825484
{
5483-
curses_use_call((curses_use_data *)data);
5485+
curses_use_data *u = (curses_use_data *)data;
5486+
PyObject *module = PyType_GetModule(Py_TYPE(u->obj));
5487+
if (module == NULL || !update_lines_cols(module)) {
5488+
u->result = NULL;
5489+
return 0;
5490+
}
5491+
curses_use_call(u);
54845492
return 0;
54855493
}
54865494

@@ -5502,12 +5510,27 @@ PyCursesScreen_use(PyObject *self, PyObject *args, PyObject *kwargs)
55025510
if (curses_use_parse(args, &func, &extra) < 0) {
55035511
return NULL;
55045512
}
5513+
PyObject *module = PyType_GetModule(Py_TYPE(self));
5514+
if (module == NULL) {
5515+
Py_DECREF(extra);
5516+
return NULL;
5517+
}
55055518
curses_use_data data = {self, func, extra, kwargs, NULL};
55065519
PyObject *prev = state->topscreen;
55075520
state->topscreen = Py_NewRef(self);
55085521
use_screen(so->screen, curses_use_screen_cb, &data);
55095522
Py_SETREF(state->topscreen, prev);
55105523
Py_DECREF(extra);
5524+
if (data.result == NULL) {
5525+
PyObject *exc = PyErr_GetRaisedException();
5526+
(void)update_lines_cols(module);
5527+
_PyErr_ChainExceptions1(exc);
5528+
}
5529+
else {
5530+
if (!update_lines_cols(module)) {
5531+
Py_CLEAR(data.result);
5532+
}
5533+
}
55115534
return data.result;
55125535
}
55135536
#endif /* HAVE_CURSES_USE_SCREEN */
@@ -7073,8 +7096,6 @@ _curses_setupterm_impl(PyObject *module, const char *term, int fd)
70737096
Py_RETURN_NONE;
70747097
}
70757098

7076-
static int update_lines_cols(PyObject *private_module); /* defined below */
7077-
70787099
/* Return a file descriptor for obj, or, if obj is NULL or None, for the
70797100
sys.<stdname> stream. Returns -1 with an exception set on error. */
70807101
static int
@@ -7930,7 +7951,11 @@ update_lines_cols(PyObject *private_module)
79307951
goto error;
79317952
}
79327953

7933-
o = PyLong_FromLong(LINES);
7954+
int lines = LINES;
7955+
if (stdscr != NULL) {
7956+
lines = getmaxy(stdscr);
7957+
}
7958+
o = PyLong_FromLong(lines);
79347959
if (o == NULL) {
79357960
goto error;
79367961
}
@@ -7942,7 +7967,11 @@ update_lines_cols(PyObject *private_module)
79427967
}
79437968
Py_DECREF(o);
79447969

7945-
o = PyLong_FromLong(COLS);
7970+
int cols = COLS;
7971+
if (stdscr != NULL) {
7972+
cols = getmaxx(stdscr);
7973+
}
7974+
o = PyLong_FromLong(cols);
79467975
if (o == NULL) {
79477976
goto error;
79487977
}
@@ -7953,6 +7982,32 @@ update_lines_cols(PyObject *private_module)
79537982
goto error;
79547983
}
79557984
Py_DECREF(o);
7985+
7986+
if (curses_start_color_called) {
7987+
o = PyLong_FromUnsignedLongLong((unsigned long long)COLORS);
7988+
if (o == NULL) {
7989+
goto error;
7990+
}
7991+
if (PyDict_SetItemString(exposed_module_dict, "COLORS", o) < 0) {
7992+
goto error;
7993+
}
7994+
if (PyDict_SetItemString(private_module_dict, "COLORS", o) < 0) {
7995+
goto error;
7996+
}
7997+
Py_DECREF(o);
7998+
7999+
o = PyLong_FromUnsignedLongLong((unsigned long long)COLOR_PAIRS);
8000+
if (o == NULL) {
8001+
goto error;
8002+
}
8003+
if (PyDict_SetItemString(exposed_module_dict, "COLOR_PAIRS", o) < 0) {
8004+
goto error;
8005+
}
8006+
if (PyDict_SetItemString(private_module_dict, "COLOR_PAIRS", o) < 0) {
8007+
goto error;
8008+
}
8009+
Py_DECREF(o);
8010+
}
79568011
Py_DECREF(exposed_module);
79578012
return 1;
79588013

0 commit comments

Comments
 (0)