Skip to content

Commit 9081e16

Browse files
gh-151774: Add curses dynamic color-pair functions
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 aa5b164 commit 9081e16

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
@@ -215,6 +229,19 @@ The module :mod:`!curses` defines the following functions:
215229
.. versionadded:: next
216230

217231

232+
.. function:: find_pair(fg, bg)
233+
234+
Return the number of a color pair for foreground color *fg* and background
235+
color *bg*, or ``-1`` if no color pair for this combination of colors has
236+
been allocated.
237+
238+
This function is only available if Python was built against a wide-character
239+
version of the underlying curses library with extended-color support (see
240+
:func:`has_extended_color_support`).
241+
242+
.. versionadded:: next
243+
244+
218245
.. function:: flash()
219246

220247
Flash the screen. That is, change it to reverse-video and then change it back
@@ -228,6 +255,18 @@ The module :mod:`!curses` defines the following functions:
228255
by the user and has not yet been processed by the program.
229256

230257

258+
.. function:: free_pair(pair_number)
259+
260+
Free the color pair *pair_number*, which must have been allocated by
261+
:func:`alloc_pair`. The pair must not be in use.
262+
263+
This function is only available if Python was built against a wide-character
264+
version of the underlying curses library with extended-color support (see
265+
:func:`has_extended_color_support`).
266+
267+
.. versionadded:: next
268+
269+
231270
.. function:: getmouse()
232271

233272
After :meth:`~window.getch` returns :const:`KEY_MOUSE` to signal a mouse event, this
@@ -519,6 +558,18 @@ The module :mod:`!curses` defines the following functions:
519558
presented to curses input functions one by one.
520559

521560

561+
.. function:: reset_color_pairs()
562+
563+
Discard all color-pair definitions, releasing the color pairs allocated by
564+
:func:`init_pair` and :func:`alloc_pair`.
565+
566+
This function is only available if Python was built against a wide-character
567+
version of the underlying curses library with extended-color support (see
568+
:func:`has_extended_color_support`).
569+
570+
.. versionadded:: next
571+
572+
522573
.. function:: reset_prog_mode()
523574

524575
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
@@ -92,6 +92,13 @@ curses
9292
* Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`.
9393
(Contributed by Serhiy Storchaka in :gh:`151744`.)
9494

95+
* Add the :mod:`curses` functions :func:`curses.alloc_pair`,
96+
:func:`curses.find_pair`, :func:`curses.free_pair` and
97+
:func:`curses.reset_color_pairs` for dynamic color-pair management,
98+
available when built against a wide-character ncurses with extended-color
99+
support.
100+
(Contributed by Serhiy Storchaka in :gh:`151774`.)
101+
95102
gzip
96103
----
97104

Lib/test/test_curses.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,54 @@ def test_init_pair(self):
10711071
self.assertRaises(ValueError, curses.init_pair, 1, color, 0)
10721072
self.assertRaises(ValueError, curses.init_pair, 1, 0, color)
10731073

1074+
@requires_curses_func('alloc_pair')
1075+
@requires_colors
1076+
def test_dynamic_color_pairs(self):
1077+
# alloc_pair()/find_pair()/free_pair() (extended-color extension).
1078+
fg = bg = curses.COLORS - 1
1079+
pair = curses.alloc_pair(fg, bg)
1080+
self.assertGreater(pair, 0)
1081+
self.assertEqual(curses.pair_content(pair), (fg, bg))
1082+
# The same combination of colors reuses the same pair.
1083+
self.assertEqual(curses.alloc_pair(fg, bg), pair)
1084+
self.assertEqual(curses.find_pair(fg, bg), pair)
1085+
# Once freed, the pair is no longer found.
1086+
self.assertIsNone(curses.free_pair(pair))
1087+
self.assertEqual(curses.find_pair(fg, bg), -1)
1088+
1089+
# Error paths.
1090+
for color in self.bad_colors2():
1091+
self.assertRaises(ValueError, curses.alloc_pair, color, 0)
1092+
self.assertRaises(ValueError, curses.alloc_pair, 0, color)
1093+
self.assertRaises(ValueError, curses.find_pair, color, 0)
1094+
self.assertRaises(ValueError, curses.find_pair, 0, color)
1095+
for pair in self.bad_pairs():
1096+
self.assertRaises(ValueError, curses.free_pair, pair)
1097+
# Color pair 0 is reserved and cannot be freed.
1098+
self.assertRaises(curses.error, curses.free_pair, 0)
1099+
1100+
# Invalid number or type of arguments.
1101+
self.assertRaises(TypeError, curses.alloc_pair)
1102+
self.assertRaises(TypeError, curses.alloc_pair, 0)
1103+
self.assertRaises(TypeError, curses.alloc_pair, 0, 0, 0)
1104+
self.assertRaises(TypeError, curses.alloc_pair, 'red', 0)
1105+
self.assertRaises(TypeError, curses.alloc_pair, 0, 'red')
1106+
self.assertRaises(TypeError, curses.alloc_pair, fg=0, bg=0)
1107+
self.assertRaises(TypeError, curses.find_pair)
1108+
self.assertRaises(TypeError, curses.find_pair, 0)
1109+
self.assertRaises(TypeError, curses.find_pair, 0, 0, 0)
1110+
self.assertRaises(TypeError, curses.find_pair, 'red', 0)
1111+
self.assertRaises(TypeError, curses.find_pair, 0, 'red')
1112+
self.assertRaises(TypeError, curses.free_pair)
1113+
self.assertRaises(TypeError, curses.free_pair, 1, 2)
1114+
self.assertRaises(TypeError, curses.free_pair, 'red')
1115+
1116+
@requires_curses_func('reset_color_pairs')
1117+
@requires_colors
1118+
def test_reset_color_pairs(self):
1119+
self.assertIsNone(curses.reset_color_pairs())
1120+
self.assertRaises(TypeError, curses.reset_color_pairs, 0)
1121+
10741122
@requires_colors
10751123
def test_color_attrs(self):
10761124
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
@@ -3825,6 +3825,100 @@ _curses_init_pair_impl(PyObject *module, int pair_number, int fg, int bg)
38253825
Py_RETURN_NONE;
38263826
}
38273827

3828+
#if _NCURSES_EXTENDED_COLOR_FUNCS
3829+
/*[clinic input]
3830+
_curses.alloc_pair
3831+
3832+
fg: color_allow_default
3833+
Foreground color number.
3834+
bg: color_allow_default
3835+
Background color number.
3836+
/
3837+
3838+
Allocate a color pair for the given foreground and background colors.
3839+
3840+
If a color pair for the same colors already exists, return its number.
3841+
Otherwise allocate a new color pair and return its number.
3842+
[clinic start generated code]*/
3843+
3844+
static PyObject *
3845+
_curses_alloc_pair_impl(PyObject *module, int fg, int bg)
3846+
/*[clinic end generated code: output=6eb08cb643d4b5a2 input=b29bafd7b360fa35]*/
3847+
{
3848+
PyCursesStatefulInitialised(module);
3849+
PyCursesStatefulInitialisedColor(module);
3850+
3851+
int pair = alloc_pair(fg, bg);
3852+
if (pair < 0) {
3853+
curses_set_error(module, "alloc_pair", NULL);
3854+
return NULL;
3855+
}
3856+
return PyLong_FromLong(pair);
3857+
}
3858+
3859+
/*[clinic input]
3860+
_curses.find_pair
3861+
3862+
fg: color_allow_default
3863+
Foreground color number.
3864+
bg: color_allow_default
3865+
Background color number.
3866+
/
3867+
3868+
Return the number of a color pair for the given colors, or -1.
3869+
3870+
Return -1 if no color pair for this combination of foreground and
3871+
background colors has been allocated.
3872+
[clinic start generated code]*/
3873+
3874+
static PyObject *
3875+
_curses_find_pair_impl(PyObject *module, int fg, int bg)
3876+
/*[clinic end generated code: output=376026c2a3ac4a9b input=930feac14892c251]*/
3877+
{
3878+
PyCursesStatefulInitialised(module);
3879+
PyCursesStatefulInitialisedColor(module);
3880+
3881+
return PyLong_FromLong(find_pair(fg, bg));
3882+
}
3883+
3884+
/*[clinic input]
3885+
_curses.free_pair
3886+
3887+
pair: pair
3888+
The number of the color pair to free.
3889+
/
3890+
3891+
Free a color pair allocated by alloc_pair().
3892+
[clinic start generated code]*/
3893+
3894+
static PyObject *
3895+
_curses_free_pair_impl(PyObject *module, int pair)
3896+
/*[clinic end generated code: output=61be0fb2e4bb4e4a input=d24df62feb4161c6]*/
3897+
{
3898+
PyCursesStatefulInitialised(module);
3899+
PyCursesStatefulInitialisedColor(module);
3900+
3901+
return curses_check_err(module, free_pair(pair), "free_pair", NULL);
3902+
}
3903+
3904+
/*[clinic input]
3905+
_curses.reset_color_pairs
3906+
3907+
Discard all color-pair definitions.
3908+
[clinic start generated code]*/
3909+
3910+
static PyObject *
3911+
_curses_reset_color_pairs_impl(PyObject *module)
3912+
/*[clinic end generated code: output=117e68c6614e1d06 input=57c1cf7e5447e1ac]*/
3913+
{
3914+
PyCursesStatefulInitialised(module);
3915+
PyCursesStatefulInitialisedColor(module);
3916+
3917+
reset_color_pairs();
3918+
Py_RETURN_NONE;
3919+
}
3920+
#endif /* _NCURSES_EXTENDED_COLOR_FUNCS */
3921+
38283922
/* Refresh the private copy of the screen encoding from a freshly created
38293923
stdscr window object. Returns 0 on success, -1 with an exception set. */
38303924
static int
@@ -5328,6 +5422,7 @@ _curses_has_extended_color_support_impl(PyObject *module)
53285422
/* List of functions defined in the module */
53295423

53305424
static PyMethodDef cursesmodule_methods[] = {
5425+
_CURSES_ALLOC_PAIR_METHODDEF
53315426
_CURSES_BAUDRATE_METHODDEF
53325427
_CURSES_BEEP_METHODDEF
53335428
_CURSES_CAN_CHANGE_COLOR_METHODDEF
@@ -5344,8 +5439,10 @@ static PyMethodDef cursesmodule_methods[] = {
53445439
_CURSES_ERASECHAR_METHODDEF
53455440
_CURSES_FILTER_METHODDEF
53465441
_CURSES_NOFILTER_METHODDEF
5442+
_CURSES_FIND_PAIR_METHODDEF
53475443
_CURSES_FLASH_METHODDEF
53485444
_CURSES_FLUSHINP_METHODDEF
5445+
_CURSES_FREE_PAIR_METHODDEF
53495446
_CURSES_GETMOUSE_METHODDEF
53505447
_CURSES_UNGETMOUSE_METHODDEF
53515448
_CURSES_GETSYX_METHODDEF
@@ -5382,6 +5479,7 @@ static PyMethodDef cursesmodule_methods[] = {
53825479
_CURSES_PUTP_METHODDEF
53835480
_CURSES_QIFLUSH_METHODDEF
53845481
_CURSES_RAW_METHODDEF
5482+
_CURSES_RESET_COLOR_PAIRS_METHODDEF
53855483
_CURSES_RESET_PROG_MODE_METHODDEF
53865484
_CURSES_RESET_SHELL_MODE_METHODDEF
53875485
_CURSES_RESETTY_METHODDEF

0 commit comments

Comments
 (0)