[PATCH v2 3/3] keymaps: add opt-in CSI overlay and load it when the kernel supports it

Nicolas Pitre <[email protected]> Tue, 23 Jun 2026 18:48:58 -0400
Newsgroups dev.linux.lists.kbd
Message-ID <[email protected]>
The KT_CSI keysyms only work on Linux 7.1 and later. Converting the
shared include files (linux-keys-bare.inc and friends) in place would
silently switch every keymap that includes them to CSI sequences, which
regress the function and navigation keys on older kernels: on a Unicode
console the keys produce nothing, and on a non-Unicode console loadkeys
reports errors. Since kbd is updated independently of the kernel, this
would break existing installs.

Instead, keep the default include files as the traditional (legacy)
bindings and add an opt-in overlay, linux-keys-csi.inc, that rebinds the
function keys (F1-F12) and navigation keys (Home, End, Insert, Delete,
PageUp, PageDown) to the Csi_* keysyms. It carries no "keymaps" line so
loadkeys merges it onto an existing keymap rather than clearing unrelated
tables, and it only sets the specific (modifier, keycode) entries it
needs: the legacy F13-F36 shift/control bindings are overridden, while
Alt/Control+Alt console switching and the Shift+PageUp/PageDown scroll
bindings are left intact.

Two ways to use it:

  - Include it after linux-keys-bare in a custom keymap:
        include "linux-keys-bare"
        include "linux-keys-csi"

  - Let kbd-terminfo-fixup apply it at boot. The script now detects
    whether the running kernel supports KT_CSI (by version, since a
    runtime probe is unreliable on Unicode consoles) and, if so, loads
    the overlay before reconciling terminfo. For the same reason it only
    updates the terminfo F1-F5 entries when the kernel honors KT_CSI;
    Backtab works on any kernel and is always reconciled.
    KBD_FORCE_CSI=1/0 overrides the detection for backported kernels. If
    the script is not installed, or the kernel is older than 7.1, the
    legacy bindings remain in effect.

This makes the migration safe by construction: nothing changes for
systems on pre-7.1 kernels, and the modifier-aware keys are enabled only
where the kernel can honor them.

Signed-off-by: Nicolas Pitre <[email protected]>
---
 contrib/kbd-terminfo-fixup                   | 117 ++++++++++++++++---
 contrib/kbd-terminfo-fixup.service.in        |   2 +-
 data/keymaps/i386/include/linux-keys-csi.inc |  83 +++++++++++++
 3 files changed, 183 insertions(+), 19 deletions(-)
 create mode 100644 data/keymaps/i386/include/linux-keys-csi.inc

diff --git a/contrib/kbd-terminfo-fixup b/contrib/kbd-terminfo-fixup
index 965460d..8cf4188 100755
--- a/contrib/kbd-terminfo-fixup
+++ b/contrib/kbd-terminfo-fixup
@@ -1,21 +1,35 @@
 #!/bin/sh
-# kbd-terminfo-fixup - Update terminfo when new kbd keysym bindings are used
+# kbd-terminfo-fixup - Enable modifier-aware console keys and reconcile terminfo
 #
-# This script handles two terminfo mismatches:
+# This script does two things, in order:
 #
-# 1. The Linux console has a long-standing discrepancy where F1-F5 use
-#    non-standard sequences (\e[[A through \e[[E) while F6 and above use
-#    standard CSI sequences. The Csi_F1 through Csi_F5 keysyms fix this.
+# A. Keymap upgrade (kernel-aware migration).
+#    If the running kernel supports the KT_CSI keysym type (Linux 7.1 and
+#    later), it loads the linux-keys-csi.inc overlay, rebinding the
+#    function and navigation keys to modifier-aware CSI sequences. The
+#    default keymaps ship the traditional (legacy) bindings, so a system
+#    running an older kernel - or one where this script is not installed -
+#    keeps working unchanged. The CSI variant is enabled at runtime only
+#    where the kernel can honor it. Set KBD_FORCE_CSI=1 (or =0) to force
+#    the overlay on (or off) regardless of the kernel version.
 #
-# 2. The Backtab keysym produces the standard \e[Z sequence, but the
-#    linux terminfo entry expects \e^I (ESC + Tab).
+# B. Terminfo reconciliation. Two console sequences do not match the
+#    stock "linux" terminfo entry:
 #
-# This script detects when these keysyms are in use and installs a
-# modified "linux" terminfo entry in /etc/terminfo so applications can
-# correctly recognize them.
+#    1. The Linux console has a long-standing discrepancy where F1-F5 use
+#       non-standard sequences (\e[[A through \e[[E) while F6 and above
+#       use standard CSI sequences. The Csi_F1 through Csi_F5 keysyms fix
+#       this.
 #
-# The override is always rebuilt from the distribution's pristine entry
-# (never from the active search path, which may include a previous
+#    2. The Backtab keysym produces the standard \e[Z sequence, but the
+#       linux terminfo entry expects \e^I (ESC + Tab).
+#
+#    This script detects when these keysyms are in use (after the overlay
+#    above, if any) and installs a modified "linux" terminfo entry in
+#    /etc/terminfo so applications can correctly recognize them.
+#
+# The terminfo override is always rebuilt from the distribution's pristine
+# entry (never from the active search path, which may include a previous
 # override), so fixups that are no longer needed are automatically
 # dropped when the keymap changes. The generated entry is tagged with a
 # "(kbd-terminfo-fixup)" marker in its description; an existing
@@ -29,7 +43,8 @@
 # Usage: kbd-terminfo-fixup [--update | --remove | --check]
 #
 # Options:
-#   --update    Create/update terminfo if CSI keysyms detected (default)
+#   --update    Apply the CSI overlay if supported, then create/update
+#               terminfo if CSI keysyms are detected (default)
 #   --remove    Remove system terminfo override
 #   --check     Check if CSI keysyms are in use (exit 0 if yes)
 
@@ -38,6 +53,50 @@ set -e
 TERMINFO_DIR="${TERMINFO_DIR:-/etc/terminfo}"
 MARKER="kbd-terminfo-fixup"
 
+# Keymap overlay loaded by name from the kbd keymap search path. The
+# ".inc" is given explicitly because loadkeys only auto-appends .map/.kmap.
+OVERLAY_KEYMAP="linux-keys-csi.inc"
+
+# Return success if the running kernel supports the KT_CSI keysym type.
+# KT_CSI and modifier-aware cursor keys were added in Linux 7.1. A runtime
+# probe is unreliable (an old kernel silently accepts unknown keysym types
+# on a Unicode-mode console), so gate on the kernel version, with an
+# override for backports.
+kernel_supports_csi() {
+    case "${KBD_FORCE_CSI:-}" in
+        1|yes|true)  return 0 ;;
+        0|no|false)  return 1 ;;
+    esac
+    kver=$(uname -r)
+    kmaj=${kver%%.*}
+    krest=${kver#*.}
+    kmin=${krest%%.*}
+    # Keep only the leading digits (e.g. "1-rc8" -> "1")
+    kmaj=${kmaj%%[!0-9]*}
+    kmin=${kmin%%[!0-9]*}
+    if [ -z "$kmaj" ]; then return 1; fi
+    if [ -z "$kmin" ]; then kmin=0; fi
+    if [ "$kmaj" -gt 7 ]; then
+        return 0
+    fi
+    if [ "$kmaj" -eq 7 ] && [ "$kmin" -ge 1 ]; then
+        return 0
+    fi
+    return 1
+}
+
+# Apply the modifier-aware CSI keymap overlay if the kernel supports it.
+# Best effort: never fail the boot if the overlay cannot be loaded.
+apply_csi_overlay() {
+    kernel_supports_csi || return 0
+    command -v loadkeys >/dev/null 2>&1 || return 0
+    if loadkeys "$OVERLAY_KEYMAP" >/dev/null 2>&1; then
+        echo "Applied CSI keymap overlay ($OVERLAY_KEYMAP)"
+    else
+        echo "Warning: kernel supports KT_CSI but loading $OVERLAY_KEYMAP failed" >&2
+    fi
+}
+
 # Check which Csi_F1-F5 keysyms are in use
 # (-w prevents e.g. Csi_F11 from matching as Csi_F1)
 detect_csi_keys() {
@@ -91,13 +150,26 @@ do_check() {
 do_update() {
     target="${TERMINFO_DIR}/l/linux"
 
+    # Step A: enable the CSI keymap where the kernel supports it. Done
+    # before detection so the terminfo update below sees the freshly
+    # bound Csi_* keysyms. This is independent of terminfo ownership.
+    apply_csi_overlay
+
     # Never touch an override file we did not create
     if [ -f "$target" ] && ! override_is_ours; then
         echo "$target was not created by this script; leaving it alone"
         return 0
     fi
 
-    csi_keys=$(detect_csi_keys)
+    # The Csi_F* terminfo fix only makes sense when the kernel honors
+    # KT_CSI; otherwise those keys emit nothing and kf1-5 must stay at the
+    # legacy sequences. Backtab is an ordinary string keysym that works on
+    # any kernel, so it is always considered.
+    if kernel_supports_csi; then
+        csi_keys=$(detect_csi_keys)
+    else
+        csi_keys=
+    fi
     backtab=$(detect_backtab)
 
     if [ -z "$csi_keys" ] && [ -z "$backtab" ]; then
@@ -176,15 +248,24 @@ show_usage() {
     cat <<EOF
 Usage: kbd-terminfo-fixup [--update | --remove | --check]
 
-Update system terminfo when new kbd keysym bindings are used.
+Enable modifier-aware console keys and reconcile the terminfo entry.
 
 Options:
-  --update    Create/update terminfo if new keysyms detected, or remove
-              the override if not (default)
+  --update    Apply the CSI keymap overlay if the running kernel supports
+              KT_CSI (Linux 7.1+), then create/update terminfo if new
+              keysyms are detected, or remove the override if not (default)
   --remove    Remove system terminfo override
   --check     Check if new keysyms are in use
 
-This script handles two cases:
+Environment:
+  KBD_FORCE_CSI   Set to 1 to force the CSI keymap overlay on, or 0 to
+                  force it off, regardless of the detected kernel version.
+
+The CSI keymap overlay (linux-keys-csi.inc) rebinds the function and
+navigation keys to modifier-aware CSI sequences. The default keymaps keep
+the traditional bindings, so older kernels are unaffected.
+
+This script also handles two terminfo cases:
 
 1. The Linux console has a long-standing discrepancy where F1-F5 use
    non-standard sequences (\e[[A through \e[[E) while F6 and above use
diff --git a/contrib/kbd-terminfo-fixup.service.in b/contrib/kbd-terminfo-fixup.service.in
index df9591e..6f86e8b 100644
--- a/contrib/kbd-terminfo-fixup.service.in
+++ b/contrib/kbd-terminfo-fixup.service.in
@@ -1,5 +1,5 @@
 [Unit]
-Description=Update terminfo for CSI function keys
+Description=Enable modifier-aware console keys and update terminfo
 Documentation=man:keymaps(5)
 After=systemd-vconsole-setup.service
 ConditionPathExists=/dev/tty0
diff --git a/data/keymaps/i386/include/linux-keys-csi.inc b/data/keymaps/i386/include/linux-keys-csi.inc
new file mode 100644
index 0000000..d600fbe
--- /dev/null
+++ b/data/keymaps/i386/include/linux-keys-csi.inc
@@ -0,0 +1,83 @@
+# Modifier-aware CSI variant of the function and navigation keys.
+#
+# This is an opt-in overlay for the keys defined in linux-keys-bare.inc.
+# It rebinds the function keys (F1-F12) and the navigation keys (Home,
+# End, Insert, Delete, PageUp, PageDown) to the Csi_* keysyms, which
+# produce standard CSI sequences and let the kernel encode the modifier
+# state (Shift, Control, Alt) into the emitted sequence.
+#
+# This requires Linux kernel 7.1 or later (KT_CSI support). On older
+# kernels these keysyms have no effect, so this file is NOT included by
+# the default keymaps. There are two ways to use it:
+#
+#   - Include it after linux-keys-bare in your own keymap, e.g.
+#       include "linux-keys-bare"
+#       include "linux-keys-csi"
+#
+#   - Let the kbd-terminfo-fixup boot service load it automatically; it
+#     does so only when the running kernel supports KT_CSI.
+#
+# Designed to be loaded as an overlay on top of an already loaded keymap:
+# it deliberately has no "keymaps" line so that loadkeys merges it rather
+# than clearing unrelated tables.
+
+#
+# Function keys F1-F12.
+#
+# The plain entry selects the CSI sequence; the kernel adds the modifier
+# parameter at runtime. The shift and control entries are set explicitly
+# to override the legacy F13-F36 bindings from linux-keys-bare.inc (the
+# kernel still encodes the held modifier). Alt and Control+Alt are left
+# untouched so that console switching keeps working.
+#
+plain   keycode  59 = Csi_F1
+shift   keycode  59 = Csi_F1
+control keycode  59 = Csi_F1
+plain   keycode  60 = Csi_F2
+shift   keycode  60 = Csi_F2
+control keycode  60 = Csi_F2
+plain   keycode  61 = Csi_F3
+shift   keycode  61 = Csi_F3
+control keycode  61 = Csi_F3
+plain   keycode  62 = Csi_F4
+shift   keycode  62 = Csi_F4
+control keycode  62 = Csi_F4
+plain   keycode  63 = Csi_F5
+shift   keycode  63 = Csi_F5
+control keycode  63 = Csi_F5
+plain   keycode  64 = Csi_F6
+shift   keycode  64 = Csi_F6
+control keycode  64 = Csi_F6
+plain   keycode  65 = Csi_F7
+shift   keycode  65 = Csi_F7
+control keycode  65 = Csi_F7
+plain   keycode  66 = Csi_F8
+shift   keycode  66 = Csi_F8
+control keycode  66 = Csi_F8
+plain   keycode  67 = Csi_F9
+shift   keycode  67 = Csi_F9
+control keycode  67 = Csi_F9
+plain   keycode  68 = Csi_F10
+shift   keycode  68 = Csi_F10
+control keycode  68 = Csi_F10
+plain   keycode  87 = Csi_F11
+shift   keycode  87 = Csi_F11
+control keycode  87 = Csi_F11
+plain   keycode  88 = Csi_F12
+shift   keycode  88 = Csi_F12
+control keycode  88 = Csi_F12
+
+#
+# Navigation keys.
+#
+# A single plain entry is enough: the kernel falls back to it for any
+# held modifier and encodes the modifier into the sequence. Shift+PageUp
+# and Shift+PageDown are left as the console scroll actions defined in
+# linux-keys-bare.inc.
+#
+plain keycode 110 = Csi_Insert
+plain keycode 102 = Csi_Home
+plain keycode 104 = Csi_PgUp
+plain keycode 111 = Csi_Delete
+plain keycode 107 = Csi_End
+plain keycode 109 = Csi_PgDn
-- 
2.54.0