Skip to content

Commit 3cd4283

Browse files
gh-151774: Add curses dynamic color-pair functions (GH-151775)
Add alloc_pair(), find_pair(), free_pair() and reset_color_pairs(), wrapping the ncurses extended-color dynamic pair management. They are available only when built against a wide-character ncurses with extended-color support. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c7faa69 commit 3cd4283

6 files changed

Lines changed: 374 additions & 1 deletion

File tree

Doc/library/curses.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ The module :mod:`!curses` defines the following functions:
8585
.. versionadded:: 3.14
8686

8787

88+
.. function:: alloc_pair(fg, bg)
89+
90+
Allocate a color pair for foreground color *fg* and background color *bg*,
91+
and return its number. If a color pair for the same combination of colors
92+
already exists, return its number. Otherwise allocate a new color pair and
93+
return its number.
94+
95+
This function is only available if Python was built against a wide-character
96+
version of the underlying curses library with extended-color support (see
97+
:func:`has_extended_color_support`).
98+
99+
.. versionadded:: next
100+
101+
88102
.. function:: baudrate()
89103

90104
Return the output speed of the terminal in bits per second. On software
@@ -226,6 +240,19 @@ The module :mod:`!curses` defines the following functions:
226240
.. versionadded:: next
227241

228242

243+
.. function:: find_pair(fg, bg)
244+
245+
Return the number of a color pair for foreground color *fg* and background
246+
color *bg*, or ``-1`` if no color pair for this combination of colors has
247+
been allocated.
248+
249+
This function is only available if Python was built against a wide-character
250+
version of the underlying curses library with extended-color support (see
251+
:func:`has_extended_color_support`).
252+
253+
.. versionadded:: next
254+
255+
229256
.. function:: flash()
230257

231258
Flash the screen. That is, change it to reverse-video and then change it back
@@ -239,6 +266,18 @@ The module :mod:`!curses` defines the following functions:
239266
by the user and has not yet been processed by the program.
240267

241268

269+
.. function:: free_pair(pair_number)
270+
271+
Free the color pair *pair_number*, which must have been allocated by
272+
:func:`alloc_pair`. The pair must not be in use.
273+
274+
This function is only available if Python was built against a wide-character
275+
version of the underlying curses library with extended-color support (see
276+
:func:`has_extended_color_support`).
277+
278+
.. versionadded:: next
279+
280+
242281
.. function:: getmouse()
243282

244283
After :meth:`~window.getch` returns :const:`KEY_MOUSE` to signal a mouse event, this
@@ -570,6 +609,18 @@ The module :mod:`!curses` defines the following functions:
570609
presented to curses input functions one by one.
571610

572611

612+
.. function:: reset_color_pairs()
613+
614+
Discard all color-pair definitions, releasing the color pairs allocated by
615+
:func:`init_pair` and :func:`alloc_pair`.
616+
617+
This function is only available if Python was built against a wide-character
618+
version of the underlying curses library with extended-color support (see
619+
:func:`has_extended_color_support`).
620+
621+
.. versionadded:: next
622+
623+
573624
.. function:: reset_prog_mode()
574625

575626
Restore the terminal to "program" mode, as previously saved by

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ curses
118118
* Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`.
119119
(Contributed by Serhiy Storchaka in :gh:`151744`.)
120120

121+
* Add the :mod:`curses` functions :func:`curses.alloc_pair`,
122+
:func:`curses.find_pair`, :func:`curses.free_pair` and
123+
:func:`curses.reset_color_pairs` for dynamic color-pair management,
124+
available when built against a wide-character ncurses with extended-color
125+
support.
126+
(Contributed by Serhiy Storchaka in :gh:`151774`.)
127+
121128
gzip
122129
----
123130

Lib/test/test_curses.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,54 @@ def test_init_pair(self):
12091209
self.assertRaises(ValueError, curses.init_pair, 1, color, 0)
12101210
self.assertRaises(ValueError, curses.init_pair, 1, 0, color)
12111211

1212+
@requires_curses_func('alloc_pair')
1213+
@requires_colors
1214+
def test_dynamic_color_pairs(self):
1215+
# alloc_pair()/find_pair()/free_pair() (extended-color extension).
1216+
fg = bg = curses.COLORS - 1
1217+
pair = curses.alloc_pair(fg, bg)
1218+
self.assertGreater(pair, 0)
1219+
self.assertEqual(curses.pair_content(pair), (fg, bg))
1220+
# The same combination of colors reuses the same pair.
1221+
self.assertEqual(curses.alloc_pair(fg, bg), pair)
1222+
self.assertEqual(curses.find_pair(fg, bg), pair)
1223+
# Once freed, the pair is no longer found.
1224+
self.assertIsNone(curses.free_pair(pair))
1225+
self.assertEqual(curses.find_pair(fg, bg), -1)
1226+
1227+
# Error paths.
1228+
for color in self.bad_colors2():
1229+
self.assertRaises(ValueError, curses.alloc_pair, color, 0)
1230+
self.assertRaises(ValueError, curses.alloc_pair, 0, color)
1231+
self.assertRaises(ValueError, curses.find_pair, color, 0)
1232+
self.assertRaises(ValueError, curses.find_pair, 0, color)
1233+
for pair in self.bad_pairs():
1234+
self.assertRaises(ValueError, curses.free_pair, pair)
1235+
# Color pair 0 is reserved and cannot be freed.
1236+
self.assertRaises(curses.error, curses.free_pair, 0)
1237+
1238+
# Invalid number or type of arguments.
1239+
self.assertRaises(TypeError, curses.alloc_pair)
1240+
self.assertRaises(TypeError, curses.alloc_pair, 0)
1241+
self.assertRaises(TypeError, curses.alloc_pair, 0, 0, 0)
1242+
self.assertRaises(TypeError, curses.alloc_pair, 'red', 0)
1243+
self.assertRaises(TypeError, curses.alloc_pair, 0, 'red')
1244+
self.assertRaises(TypeError, curses.alloc_pair, fg=0, bg=0)
1245+
self.assertRaises(TypeError, curses.find_pair)
1246+
self.assertRaises(TypeError, curses.find_pair, 0)
1247+
self.assertRaises(TypeError, curses.find_pair, 0, 0, 0)
1248+
self.assertRaises(TypeError, curses.find_pair, 'red', 0)
1249+
self.assertRaises(TypeError, curses.find_pair, 0, 'red')
1250+
self.assertRaises(TypeError, curses.free_pair)
1251+
self.assertRaises(TypeError, curses.free_pair, 1, 2)
1252+
self.assertRaises(TypeError, curses.free_pair, 'red')
1253+
1254+
@requires_curses_func('reset_color_pairs')
1255+
@requires_colors
1256+
def test_reset_color_pairs(self):
1257+
self.assertIsNone(curses.reset_color_pairs())
1258+
self.assertRaises(TypeError, curses.reset_color_pairs, 0)
1259+
12121260
@requires_colors
12131261
def test_color_attrs(self):
12141262
for pair in 0, 1, 255:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add the :mod:`curses` functions :func:`curses.alloc_pair`,
2+
:func:`curses.find_pair`, :func:`curses.free_pair` and
3+
:func:`curses.reset_color_pairs` for dynamic color-pair management. They are
4+
only available when Python is built against a wide-character version of the
5+
underlying curses library with extended-color support.

Modules/_cursesmodule.c

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4458,6 +4458,100 @@ _curses_init_pair_impl(PyObject *module, int pair_number, int fg, int bg)
44584458
Py_RETURN_NONE;
44594459
}
44604460

4461+
#if _NCURSES_EXTENDED_COLOR_FUNCS
4462+
/*[clinic input]
4463+
_curses.alloc_pair
4464+
4465+
fg: color_allow_default
4466+
Foreground color number.
4467+
bg: color_allow_default
4468+
Background color number.
4469+
/
4470+
4471+
Allocate a color pair for the given foreground and background colors.
4472+
4473+
If a color pair for the same colors already exists, return its number.
4474+
Otherwise allocate a new color pair and return its number.
4475+
[clinic start generated code]*/
4476+
4477+
static PyObject *
4478+
_curses_alloc_pair_impl(PyObject *module, int fg, int bg)
4479+
/*[clinic end generated code: output=6eb08cb643d4b5a2 input=b29bafd7b360fa35]*/
4480+
{
4481+
PyCursesStatefulInitialised(module);
4482+
PyCursesStatefulInitialisedColor(module);
4483+
4484+
int pair = alloc_pair(fg, bg);
4485+
if (pair < 0) {
4486+
curses_set_error(module, "alloc_pair", NULL);
4487+
return NULL;
4488+
}
4489+
return PyLong_FromLong(pair);
4490+
}
4491+
4492+
/*[clinic input]
4493+
_curses.find_pair
4494+
4495+
fg: color_allow_default
4496+
Foreground color number.
4497+
bg: color_allow_default
4498+
Background color number.
4499+
/
4500+
4501+
Return the number of a color pair for the given colors, or -1.
4502+
4503+
Return -1 if no color pair for this combination of foreground and
4504+
background colors has been allocated.
4505+
[clinic start generated code]*/
4506+
4507+
static PyObject *
4508+
_curses_find_pair_impl(PyObject *module, int fg, int bg)
4509+
/*[clinic end generated code: output=376026c2a3ac4a9b input=930feac14892c251]*/
4510+
{
4511+
PyCursesStatefulInitialised(module);
4512+
PyCursesStatefulInitialisedColor(module);
4513+
4514+
return PyLong_FromLong(find_pair(fg, bg));
4515+
}
4516+
4517+
/*[clinic input]
4518+
_curses.free_pair
4519+
4520+
pair: pair
4521+
The number of the color pair to free.
4522+
/
4523+
4524+
Free a color pair allocated by alloc_pair().
4525+
[clinic start generated code]*/
4526+
4527+
static PyObject *
4528+
_curses_free_pair_impl(PyObject *module, int pair)
4529+
/*[clinic end generated code: output=61be0fb2e4bb4e4a input=d24df62feb4161c6]*/
4530+
{
4531+
PyCursesStatefulInitialised(module);
4532+
PyCursesStatefulInitialisedColor(module);
4533+
4534+
return curses_check_err(module, free_pair(pair), "free_pair", NULL);
4535+
}
4536+
4537+
/*[clinic input]
4538+
_curses.reset_color_pairs
4539+
4540+
Discard all color-pair definitions.
4541+
[clinic start generated code]*/
4542+
4543+
static PyObject *
4544+
_curses_reset_color_pairs_impl(PyObject *module)
4545+
/*[clinic end generated code: output=117e68c6614e1d06 input=57c1cf7e5447e1ac]*/
4546+
{
4547+
PyCursesStatefulInitialised(module);
4548+
PyCursesStatefulInitialisedColor(module);
4549+
4550+
reset_color_pairs();
4551+
Py_RETURN_NONE;
4552+
}
4553+
#endif /* _NCURSES_EXTENDED_COLOR_FUNCS */
4554+
44614555
/* Refresh the private copy of the screen encoding from a freshly created
44624556
stdscr window object. Returns 0 on success, -1 with an exception set. */
44634557
static int
@@ -6241,6 +6335,7 @@ _curses_has_extended_color_support_impl(PyObject *module)
62416335
/* List of functions defined in the module */
62426336

62436337
static PyMethodDef cursesmodule_methods[] = {
6338+
_CURSES_ALLOC_PAIR_METHODDEF
62446339
_CURSES_BAUDRATE_METHODDEF
62456340
_CURSES_BEEP_METHODDEF
62466341
_CURSES_CAN_CHANGE_COLOR_METHODDEF
@@ -6258,8 +6353,10 @@ static PyMethodDef cursesmodule_methods[] = {
62586353
_CURSES_ERASEWCHAR_METHODDEF
62596354
_CURSES_FILTER_METHODDEF
62606355
_CURSES_NOFILTER_METHODDEF
6356+
_CURSES_FIND_PAIR_METHODDEF
62616357
_CURSES_FLASH_METHODDEF
62626358
_CURSES_FLUSHINP_METHODDEF
6359+
_CURSES_FREE_PAIR_METHODDEF
62636360
_CURSES_GETMOUSE_METHODDEF
62646361
_CURSES_UNGETMOUSE_METHODDEF
62656362
_CURSES_GETSYX_METHODDEF
@@ -6301,6 +6398,7 @@ static PyMethodDef cursesmodule_methods[] = {
63016398
_CURSES_PUTP_METHODDEF
63026399
_CURSES_QIFLUSH_METHODDEF
63036400
_CURSES_RAW_METHODDEF
6401+
_CURSES_RESET_COLOR_PAIRS_METHODDEF
63046402
_CURSES_RESET_PROG_MODE_METHODDEF
63056403
_CURSES_RESET_SHELL_MODE_METHODDEF
63066404
_CURSES_RESETTY_METHODDEF

0 commit comments

Comments
 (0)