]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-152334: Add curses key-management functions (GH-152338)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 26 Jun 2026 22:59:24 +0000 (01:59 +0300)
committerGitHub <noreply@github.com>
Fri, 26 Jun 2026 22:59:24 +0000 (01:59 +0300)
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 <noreply@anthropic.com>
Doc/library/curses.rst
Doc/whatsnew/3.16.rst
Lib/test/test_curses.py
Misc/NEWS.d/next/Library/2026-06-26-22-50-00.gh-issue-152334.Mt7vQx.rst [new file with mode: 0644]
Modules/_cursesmodule.c
Modules/clinic/_cursesmodule.c.h

index ba4fe55e4a22c64d86629d8b3d4e1da22c071c1b..7b61a0ed5fa6bacdbd26665340b95123a5851246 100644 (file)
@@ -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
index 8dfd3bf81695584a4f632a03526de1e6b9503917..e29d6e9b9e25ee9c526502f2b4765dcf61c34c56 100644 (file)
@@ -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
 ----
 
index a16279e5f39411b7734309c4063eef54bf4ac6d7..a2f33abc658ffe34aaf6a3d5eff0623013b1eeb3 100644 (file)
@@ -1321,6 +1321,21 @@ class TestCurses(unittest.TestCase):
                 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 (file)
index 0000000..8d24ebf
--- /dev/null
@@ -0,0 +1,2 @@
+Add the :func:`curses.define_key`, :func:`curses.key_defined` and
+:func:`curses.keyok` key-management functions.
index 3d6748340930ee8ab0ff3208a7e7e40ae694643c..4d37e244eba7d29044c4b444637d369faa1c94bf 100644 (file)
@@ -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
index 8fbcf1d99bbbedae6c11a8e6decd3bdb72e1e86a..a677abe6037edf195c7827997d1d5dab9144228a 100644 (file)
@@ -3213,6 +3213,161 @@ exit:
 
 #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]*/