https://github.com/python/cpython/commit/8f4abfc39256b4081d9f08125780143036900aaf
commit: 8f4abfc39256b4081d9f08125780143036900aaf
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-27T01:59:24+03:00
summary:

gh-152334: Add curses key-management functions (GH-152338)

Add define_key(), key_defined() and keyok(), the ncurses extensions for
managing how control strings are recognized as key codes, beyond the
predefined terminfo keys and the all-or-nothing window.keypad().

Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-26-22-50-00.gh-issue-152334.Mt7vQx.rst
M Doc/library/curses.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_curses.py
M Modules/_cursesmodule.c
M Modules/clinic/_cursesmodule.c.h

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 a16279e5f39411..a2f33abc658ffe 100644
--- a/Lib/test/test_curses.py
+++ b/Lib/test/test_curses.py
@@ -1321,6 +1321,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]*/

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to