diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index ba4fe55e4a22c6..7b61a0ed5fa6ba 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -345,6 +345,37 @@ The module :mod:`!curses` defines the following functions: a key with that value. +.. function:: define_key(definition, keycode) + + Define an escape sequence *definition*, a string, as a key that generates + the key code *keycode*, so that :mod:`curses` interprets it like one of the + keys predefined in the terminal database. + + If *definition* is ``None``, any existing binding for *keycode* is removed. + If *keycode* is zero or negative, any existing binding for *definition* is + removed. + + .. versionadded:: next + + +.. function:: key_defined(definition) + + Return the key code bound to the escape sequence *definition*, a string, + ``0`` if no key code is bound to it, or ``-1`` if *definition* is a prefix + of a longer bound sequence (and so is ambiguous). + + .. versionadded:: next + + +.. function:: keyok(keycode, enable) + + Enable (if *enable* is true) or disable (otherwise) interpretation of the + key code *keycode*. Unlike :meth:`window.keypad`, this affects a single + key code rather than all of them. + + .. versionadded:: next + + .. function:: halfdelay(tenths) Used for half-delay mode, which is similar to cbreak mode in that characters diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 8dfd3bf8169558..e29d6e9b9e25ee 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -165,6 +165,11 @@ curses :func:`~curses.scr_set`, which dump the whole screen to a file and restore it. (Contributed by Serhiy Storchaka in :gh:`152260`.) +* Add the :mod:`curses` key-management functions :func:`~curses.define_key`, + :func:`~curses.key_defined` and :func:`~curses.keyok`, available when built + against an ncurses with ``NCURSES_EXT_FUNCS``. + (Contributed by Serhiy Storchaka in :gh:`152334`.) + gzip ---- diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 7157896d8cbccd..ea1560bb5c97e2 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -1315,6 +1315,21 @@ def test_env_queries(self): self.assertIsInstance(c, str) self.assertEqual(len(c), 1) + @requires_curses_func('define_key') + def test_key_management(self): + # Bind a custom escape sequence to a free key code and read it back. + seq = '\x1bspam' + keycode = 0o600 + curses.define_key(seq, keycode) + self.assertEqual(curses.key_defined(seq), keycode) + # keyok enables or disables interpretation of a single key code. + # Use the key code just defined, which is guaranteed to be known. + self.assertIsNone(curses.keyok(keycode, False)) + self.assertIsNone(curses.keyok(keycode, True)) + # Passing None removes the binding for the key code. + curses.define_key(None, keycode) + self.assertEqual(curses.key_defined(seq), 0) + def test_output_options(self): stdscr = self.stdscr diff --git a/Misc/NEWS.d/next/Library/2026-06-26-22-50-00.gh-issue-152334.Mt7vQx.rst b/Misc/NEWS.d/next/Library/2026-06-26-22-50-00.gh-issue-152334.Mt7vQx.rst new file mode 100644 index 00000000000000..8d24ebfde8f1ec --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-22-50-00.gh-issue-152334.Mt7vQx.rst @@ -0,0 +1,2 @@ +Add the :func:`curses.define_key`, :func:`curses.key_defined` and +:func:`curses.keyok` key-management functions. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 3d6748340930ee..4d37e244eba7d2 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -5776,6 +5776,77 @@ _curses_has_key_impl(PyObject *module, int key) } #endif +#if defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS +/*[clinic input] +_curses.define_key + + definition: str(accept={str, NoneType}) + Escape sequence to bind, or None to remove a binding. + keycode: int + Key code to generate. + / + +Define an escape sequence for a key code. + +If definition is None, any existing binding for keycode is removed. +If keycode is zero or negative, the binding for definition is removed. +[clinic start generated code]*/ + +static PyObject * +_curses_define_key_impl(PyObject *module, const char *definition, + int keycode) +/*[clinic end generated code: output=9dc655653bb09062 input=8db9e0d8802c709f]*/ +{ + PyCursesStatefulInitialised(module); + + return curses_check_err(module, define_key(definition, keycode), + "define_key", NULL); +} + +/*[clinic input] +_curses.key_defined + + definition: str + Escape sequence. + / + +Return the key code bound to an escape sequence. + +Return 0 if no key code is bound to the escape sequence, or -1 if the +escape sequence is a prefix of another bound sequence (so ambiguous). +[clinic start generated code]*/ + +static PyObject * +_curses_key_defined_impl(PyObject *module, const char *definition) +/*[clinic end generated code: output=2d357e01fe277c88 input=03749d7bd79d8d2c]*/ +{ + PyCursesStatefulInitialised(module); + + return PyLong_FromLong(key_defined(definition)); +} + +/*[clinic input] +_curses.keyok + + keycode: int + Key code. + enable: bool + Whether the key code is interpreted. + / + +Enable or disable interpretation of an individual key code. +[clinic start generated code]*/ + +static PyObject * +_curses_keyok_impl(PyObject *module, int keycode, int enable) +/*[clinic end generated code: output=43eab0b4d9973e44 input=5bee51d850f481b9]*/ +{ + PyCursesStatefulInitialised(module); + + return curses_check_err(module, keyok(keycode, enable), "keyok", NULL); +} +#endif + /*[clinic input] _curses.init_color @@ -7759,6 +7830,9 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_HAS_IC_METHODDEF _CURSES_HAS_IL_METHODDEF _CURSES_HAS_KEY_METHODDEF + _CURSES_DEFINE_KEY_METHODDEF + _CURSES_KEY_DEFINED_METHODDEF + _CURSES_KEYOK_METHODDEF _CURSES_HALFDELAY_METHODDEF _CURSES_INIT_COLOR_METHODDEF _CURSES_INIT_PAIR_METHODDEF diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index 8fbcf1d99bbbed..a677abe6037edf 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -3213,6 +3213,161 @@ _curses_has_key(PyObject *module, PyObject *arg) #endif /* defined(HAVE_CURSES_HAS_KEY) */ +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS) + +PyDoc_STRVAR(_curses_define_key__doc__, +"define_key($module, definition, keycode, /)\n" +"--\n" +"\n" +"Define an escape sequence for a key code.\n" +"\n" +" definition\n" +" Escape sequence to bind, or None to remove a binding.\n" +" keycode\n" +" Key code to generate.\n" +"\n" +"If definition is None, any existing binding for keycode is removed.\n" +"If keycode is zero or negative, the binding for definition is removed."); + +#define _CURSES_DEFINE_KEY_METHODDEF \ + {"define_key", _PyCFunction_CAST(_curses_define_key), METH_FASTCALL, _curses_define_key__doc__}, + +static PyObject * +_curses_define_key_impl(PyObject *module, const char *definition, + int keycode); + +static PyObject * +_curses_define_key(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + const char *definition; + int keycode; + + if (!_PyArg_CheckPositional("define_key", nargs, 2, 2)) { + goto exit; + } + if (args[0] == Py_None) { + definition = NULL; + } + else if (PyUnicode_Check(args[0])) { + Py_ssize_t definition_length; + definition = PyUnicode_AsUTF8AndSize(args[0], &definition_length); + if (definition == NULL) { + goto exit; + } + if (strlen(definition) != (size_t)definition_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + } + else { + _PyArg_BadArgument("define_key", "argument 1", "str or None", args[0]); + goto exit; + } + keycode = PyLong_AsInt(args[1]); + if (keycode == -1 && PyErr_Occurred()) { + goto exit; + } + return_value = _curses_define_key_impl(module, definition, keycode); + +exit: + return return_value; +} + +#endif /* (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS) */ + +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS) + +PyDoc_STRVAR(_curses_key_defined__doc__, +"key_defined($module, definition, /)\n" +"--\n" +"\n" +"Return the key code bound to an escape sequence.\n" +"\n" +" definition\n" +" Escape sequence.\n" +"\n" +"Return 0 if no key code is bound to the escape sequence, or -1 if the\n" +"escape sequence is a prefix of another bound sequence (so ambiguous)."); + +#define _CURSES_KEY_DEFINED_METHODDEF \ + {"key_defined", (PyCFunction)_curses_key_defined, METH_O, _curses_key_defined__doc__}, + +static PyObject * +_curses_key_defined_impl(PyObject *module, const char *definition); + +static PyObject * +_curses_key_defined(PyObject *module, PyObject *arg) +{ + PyObject *return_value = NULL; + const char *definition; + + if (!PyUnicode_Check(arg)) { + _PyArg_BadArgument("key_defined", "argument", "str", arg); + goto exit; + } + Py_ssize_t definition_length; + definition = PyUnicode_AsUTF8AndSize(arg, &definition_length); + if (definition == NULL) { + goto exit; + } + if (strlen(definition) != (size_t)definition_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + return_value = _curses_key_defined_impl(module, definition); + +exit: + return return_value; +} + +#endif /* (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS) */ + +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS) + +PyDoc_STRVAR(_curses_keyok__doc__, +"keyok($module, keycode, enable, /)\n" +"--\n" +"\n" +"Enable or disable interpretation of an individual key code.\n" +"\n" +" keycode\n" +" Key code.\n" +" enable\n" +" Whether the key code is interpreted."); + +#define _CURSES_KEYOK_METHODDEF \ + {"keyok", _PyCFunction_CAST(_curses_keyok), METH_FASTCALL, _curses_keyok__doc__}, + +static PyObject * +_curses_keyok_impl(PyObject *module, int keycode, int enable); + +static PyObject * +_curses_keyok(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int keycode; + int enable; + + if (!_PyArg_CheckPositional("keyok", nargs, 2, 2)) { + goto exit; + } + keycode = PyLong_AsInt(args[0]); + if (keycode == -1 && PyErr_Occurred()) { + goto exit; + } + enable = PyObject_IsTrue(args[1]); + if (enable < 0) { + goto exit; + } + return_value = _curses_keyok_impl(module, keycode, enable); + +exit: + return return_value; +} + +#endif /* (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS) */ + PyDoc_STRVAR(_curses_init_color__doc__, "init_color($module, color_number, r, g, b, /)\n" "--\n" @@ -5390,6 +5545,18 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_HAS_KEY_METHODDEF #endif /* !defined(_CURSES_HAS_KEY_METHODDEF) */ +#ifndef _CURSES_DEFINE_KEY_METHODDEF + #define _CURSES_DEFINE_KEY_METHODDEF +#endif /* !defined(_CURSES_DEFINE_KEY_METHODDEF) */ + +#ifndef _CURSES_KEY_DEFINED_METHODDEF + #define _CURSES_KEY_DEFINED_METHODDEF +#endif /* !defined(_CURSES_KEY_DEFINED_METHODDEF) */ + +#ifndef _CURSES_KEYOK_METHODDEF + #define _CURSES_KEYOK_METHODDEF +#endif /* !defined(_CURSES_KEYOK_METHODDEF) */ + #ifndef _CURSES_ALLOC_PAIR_METHODDEF #define _CURSES_ALLOC_PAIR_METHODDEF #endif /* !defined(_CURSES_ALLOC_PAIR_METHODDEF) */ @@ -5481,4 +5648,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=864fa5c0f22fcad3 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=db4cb7f72e1dc166 input=a9049054013a1b77]*/