Skip to content

Commit 089e6f6

Browse files
gh-152263: Add curses soft-label-key functions (GH-152264)
Wrap the X/Open Curses soft-label-key functions: slk_init, slk_set, slk_label, slk_refresh, slk_noutrefresh, slk_clear, slk_restore, slk_touch, the chtype attribute functions slk_attron, slk_attroff, slk_attrset and slk_attr, and the attr_t functions slk_attr_on, slk_attr_off, slk_attr_set and slk_color. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c5043dc commit 089e6f6

6 files changed

Lines changed: 1015 additions & 16 deletions

File tree

Doc/library/curses.rst

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,131 @@ Windows and pads
653653
is to be displayed.
654654

655655

656+
Soft labels
657+
~~~~~~~~~~~
658+
659+
.. _curses-slk:
660+
661+
The following functions manage *soft-label keys*, a row of labels displayed
662+
along the bottom line of the screen, typically used to label a row of function
663+
keys. :func:`slk_init` must be called before :func:`initscr` or
664+
:func:`newterm`; it takes one screen line away from the standard window for the
665+
labels.
666+
667+
668+
.. function:: slk_init(fmt=0)
669+
670+
Reserve a screen line for the soft labels and choose their layout. *fmt*
671+
selects the arrangement: ``0`` for 3-2-3 (eight labels), ``1`` for 4-4
672+
(eight labels). Where the underlying curses library supports them, ``2``
673+
gives 4-4-4 (twelve labels) and ``3`` gives 4-4-4 with an index line.
674+
675+
Must be called before :func:`initscr` or :func:`newterm`.
676+
677+
.. versionadded:: next
678+
679+
680+
.. function:: slk_set(labnum, label, justify)
681+
682+
Set the text of soft label number *labnum*, in the range ``1`` through ``8``
683+
(or ``12`` in a twelve-label layout). *justify* controls how *label* is
684+
placed within the label: ``0`` for left, ``1`` for centered, ``2`` for right.
685+
686+
.. versionadded:: next
687+
688+
689+
.. function:: slk_label(labnum)
690+
691+
Return the current text of soft label number *labnum*, justified as it was
692+
set, or an empty string if it has no label.
693+
694+
.. versionadded:: next
695+
696+
697+
.. function:: slk_refresh()
698+
699+
Update the soft labels on the physical screen, like
700+
:meth:`~curses.window.refresh` for a window.
701+
702+
.. versionadded:: next
703+
704+
705+
.. function:: slk_noutrefresh()
706+
707+
Update the soft labels on the virtual screen, like
708+
:meth:`window.noutrefresh`. Use it together with :func:`doupdate` to batch
709+
screen updates.
710+
711+
.. versionadded:: next
712+
713+
714+
.. function:: slk_clear()
715+
716+
Remove the soft labels from the screen.
717+
718+
.. versionadded:: next
719+
720+
721+
.. function:: slk_restore()
722+
723+
Restore the soft labels to the screen after a :func:`slk_clear`.
724+
725+
.. versionadded:: next
726+
727+
728+
.. function:: slk_touch()
729+
730+
Force all the soft labels to be redrawn by the next :func:`slk_refresh` or
731+
:func:`slk_noutrefresh`.
732+
733+
.. versionadded:: next
734+
735+
736+
.. function:: slk_attron(attr)
737+
slk_attroff(attr)
738+
slk_attrset(attr)
739+
740+
Add, remove, or set the attributes used to display the soft labels, given as
741+
packed ``A_*`` attributes.
742+
743+
.. versionadded:: next
744+
745+
746+
.. function:: slk_attr()
747+
748+
Return the current attributes of the soft labels as packed ``A_*``
749+
attributes. Availability depends on the underlying curses library.
750+
751+
.. versionadded:: next
752+
753+
754+
.. function:: slk_attr_on(attr)
755+
slk_attr_off(attr)
756+
757+
Turn the given attributes on or off without affecting any others. Like the
758+
``attr_*`` window methods, these work with the
759+
:ref:`WA_* attributes <curses-wa-constants>` rather than packed ``A_*``
760+
attributes.
761+
762+
.. versionadded:: next
763+
764+
765+
.. function:: slk_attr_set(attr, pair=0)
766+
767+
Set the attributes and color pair of the soft labels. *attr* is given as
768+
:ref:`WA_* attributes <curses-wa-constants>` and *pair* as a color pair
769+
number.
770+
771+
.. versionadded:: next
772+
773+
774+
.. function:: slk_color(pair)
775+
776+
Set the color pair of the soft labels to color pair number *pair*.
777+
778+
.. versionadded:: next
779+
780+
656781
Saving and restoring
657782
~~~~~~~~~~~~~~~~~~~~
658783

Doc/whatsnew/3.16.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,18 @@ curses
182182
:func:`~curses.scr_set`, which dump the whole screen to a file and restore it.
183183
(Contributed by Serhiy Storchaka in :gh:`152260`.)
184184

185+
* Add the soft-label-key functions to the :mod:`curses` module, which manage a
186+
row of labels along the bottom line of the screen:
187+
:func:`~curses.slk_init`, :func:`~curses.slk_set`, :func:`~curses.slk_label`,
188+
:func:`~curses.slk_refresh`, :func:`~curses.slk_noutrefresh`,
189+
:func:`~curses.slk_clear`, :func:`~curses.slk_restore`,
190+
:func:`~curses.slk_touch`, the attribute functions
191+
:func:`~curses.slk_attron`, :func:`~curses.slk_attroff`,
192+
:func:`~curses.slk_attrset`, :func:`~curses.slk_attr`,
193+
:func:`~curses.slk_attr_on`, :func:`~curses.slk_attr_off`,
194+
:func:`~curses.slk_attr_set`, and :func:`~curses.slk_color`.
195+
(Contributed by Serhiy Storchaka in :gh:`152263`.)
196+
185197
* Add the :func:`curses.term_attrs` function, which returns the supported
186198
video attributes as :ref:`WA_* <curses-wa-constants>` values, the
187199
counterpart of :func:`curses.termattrs`.

Lib/test/test_curses.py

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2843,16 +2843,12 @@ def test_move_down(self):
28432843
self.mock_win.reset_mock()
28442844

28452845

2846-
@unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()')
2847-
@unittest.skipIf(not term or term == 'unknown',
2848-
"$TERM=%r, newterm() may not work" % term)
2849-
@unittest.skipIf(sys.platform == "cygwin",
2850-
"cygwin's curses mostly just hangs")
2851-
class ScreenTests(unittest.TestCase):
2852-
# newterm()/set_term() mutate global curses state, but each test drives its
2853-
# own pseudo-terminal(s) and never touches the screen shared by TestCurses,
2854-
# whose setUp() makes that screen current again. So these can run in this
2855-
# process, without a real terminal and without a subprocess.
2846+
class NewtermTestBase(unittest.TestCase):
2847+
# Shared plumbing for tests that drive newterm() over their own
2848+
# pseudo-terminal(s). newterm()/set_term() mutate global curses state, but
2849+
# each test never touches the screen shared by TestCurses, whose setUp()
2850+
# makes that screen current again. So these can run in this process,
2851+
# without a real terminal and without a subprocess.
28562852

28572853
def setUp(self):
28582854
# newterm() may install signal handlers; restore them afterwards.
@@ -2906,6 +2902,14 @@ def stop_reader():
29062902
self.addCleanup(stop_reader)
29072903
return slave
29082904

2905+
2906+
@unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()')
2907+
@unittest.skipIf(not term or term == 'unknown',
2908+
f"$TERM={term!r}, newterm() may not work")
2909+
@unittest.skipIf(sys.platform == "cygwin",
2910+
"cygwin's curses mostly just hangs")
2911+
class ScreenTests(NewtermTestBase):
2912+
29092913
def test_newterm(self):
29102914
s = self.make_pty()
29112915
screen = curses.newterm('xterm', s, s)
@@ -2979,5 +2983,100 @@ def test_disallow_instantiation(self):
29792983
check_disallow_instantiation(self, curses.screen)
29802984

29812985

2986+
@unittest.skipUnless(hasattr(curses, 'slk_init'), 'requires curses.slk_init()')
2987+
@unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()')
2988+
@unittest.skipIf(not term or term == 'unknown',
2989+
f"$TERM={term!r}, newterm() may not work")
2990+
@unittest.skipIf(sys.platform == "cygwin",
2991+
"cygwin's curses mostly just hangs")
2992+
class SLKTests(NewtermTestBase):
2993+
# Soft-label keys reserve the bottom screen line for a row of labels.
2994+
# slk_init() must run before newterm()/initscr(), so each test sets up its
2995+
# own screen rather than reusing the one TestCurses builds in setUp().
2996+
2997+
def make_slk_screen(self, fmt=0):
2998+
s = self.make_pty()
2999+
curses.slk_init(fmt)
3000+
return curses.newterm('xterm', s, s)
3001+
3002+
def test_init_reserves_a_line(self):
3003+
# Every layout takes the bottom line for the labels; the index-line
3004+
# layout (3) takes a second line for the index. Layouts 0 and 1 are
3005+
# standard; 2 and 3 are ncurses extensions that other curses
3006+
# implementations reject (slk_init() then returns an error).
3007+
ncurses = hasattr(curses, 'ncurses_version')
3008+
for fmt, lines in [(0, 23), (1, 23), (2, 23), (3, 22)]:
3009+
with self.subTest(fmt=fmt):
3010+
try:
3011+
screen = self.make_slk_screen(fmt)
3012+
except curses.error:
3013+
if ncurses or fmt < 2:
3014+
raise
3015+
continue
3016+
self.assertEqual(screen.stdscr.getmaxyx()[0], lines)
3017+
curses.endwin()
3018+
3019+
def test_init_bad_format(self):
3020+
for fmt in (-1, 4):
3021+
self.assertRaises(ValueError, curses.slk_init, fmt)
3022+
3023+
def test_set_and_label(self):
3024+
self.make_slk_screen()
3025+
curses.slk_set(1, 'Help', 0)
3026+
curses.slk_set(2, 'Save', 1)
3027+
curses.slk_set(3, 'Quit', 2)
3028+
self.assertEqual(curses.slk_label(1), 'Help')
3029+
self.assertEqual(curses.slk_label(2), 'Save')
3030+
self.assertEqual(curses.slk_label(3), 'Quit')
3031+
3032+
def test_set_wide(self):
3033+
screen = self.make_slk_screen()
3034+
label = 'Ångström'
3035+
try:
3036+
label.encode(screen.stdscr.encoding)
3037+
except UnicodeEncodeError:
3038+
self.skipTest('the locale cannot encode %r' % label)
3039+
curses.slk_set(1, label, 0)
3040+
self.assertEqual(curses.slk_label(1), label)
3041+
3042+
def test_set_bad_justify(self):
3043+
self.make_slk_screen()
3044+
for justify in (-1, 3):
3045+
self.assertRaises(ValueError, curses.slk_set, 1, 'x', justify)
3046+
3047+
def test_refresh(self):
3048+
self.make_slk_screen()
3049+
curses.slk_set(1, 'Help', 0)
3050+
curses.slk_noutrefresh()
3051+
curses.slk_refresh()
3052+
curses.slk_clear()
3053+
curses.slk_restore()
3054+
curses.slk_touch()
3055+
3056+
def test_attributes(self):
3057+
self.make_slk_screen()
3058+
curses.slk_attron(curses.A_BOLD)
3059+
curses.slk_attrset(curses.A_UNDERLINE)
3060+
curses.slk_attroff(curses.A_BOLD)
3061+
if hasattr(curses, 'slk_attr'):
3062+
self.assertIsInstance(curses.slk_attr(), int)
3063+
3064+
def test_attr_on_off(self):
3065+
self.make_slk_screen()
3066+
curses.slk_attr_on(curses.A_BOLD)
3067+
curses.slk_attr_off(curses.A_BOLD)
3068+
3069+
def test_color(self):
3070+
# slk_attr_set() and slk_color() act on a color pair, so the color
3071+
# subsystem must be started first.
3072+
self.make_slk_screen()
3073+
if not curses.has_colors():
3074+
self.skipTest('requires colors support')
3075+
curses.start_color()
3076+
curses.slk_attr_set(curses.A_BOLD)
3077+
curses.slk_attr_set(curses.A_BOLD, 0)
3078+
curses.slk_color(0)
3079+
3080+
29823081
if __name__ == '__main__':
29833082
unittest.main()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Add the soft-label-key functions to the :mod:`curses` module:
2+
:func:`~curses.slk_init`, :func:`~curses.slk_set`, :func:`~curses.slk_label`,
3+
:func:`~curses.slk_refresh`, :func:`~curses.slk_noutrefresh`,
4+
:func:`~curses.slk_clear`, :func:`~curses.slk_restore`,
5+
:func:`~curses.slk_touch`, :func:`~curses.slk_attron`,
6+
:func:`~curses.slk_attroff`, :func:`~curses.slk_attrset`,
7+
:func:`~curses.slk_attr`, :func:`~curses.slk_attr_on`,
8+
:func:`~curses.slk_attr_off`, :func:`~curses.slk_attr_set` and
9+
:func:`~curses.slk_color`.

0 commit comments

Comments
 (0)