Files
terminal/fbsdkeys.go
T
LixenandClaude Opus 5.5 e9aeed0be0 v0.5.1 console key releases: Linux VT and FreeBSD vt(4), with a guard
* linux console source: releases from the console's own keyboard, typed by its keymap

SetKeyReport with KeySourceConsole, on a Linux virtual console the game runs
on, switches the keyboard to raw keycodes (K_MEDIUMRAW) while releases are
reported. The kernel allows the switch on the caller's controlling tty, so no
root is needed; a console that refuses is not asked again and the protocol is
next. Going back to presses, Fini and EmergencyReset switch it back.

Each keycode is typed as the console types it cooked, after
drivers/tty/vt/keyboard.c, by the console's own tables read at the switch
(KDGKBENT, KDGKBSENT, KDGKBDIACRUC, KDGKBMETA): modifiers and their left and
right groups, Caps and Num Lock with their lights kept in step (KDSKBLED),
sticky and locked modifiers, Unicode entries, dead keys and compose, the
keypad by Num Lock, function strings, Meta as ESC prefix or bit 7, console
switching by Alt+Fn and Alt+arrows to consoles in use, and from kernel 7.1
cursor and CSI keys with their modifiers. Translation starts from the
console's modifiers and locks (TIOCLINUX 6, KDGKBLED). Each key is held by
its keycode; a key typing several keys taps all but its last. Switching to
another console ends every hold Lost. Mouse reports are off while the
keyboard is raw, since the console types them into the same input, and come
back after it.

The guard: before the first switch the package runs the binary again, named
console-guard, holding the console and the read end of a pipe only the game
writes. If the pipe ends while the keyboard is raw (kill -9, the OOM killer, a
Go fatal error), the guard switches the keyboard back, restores the termios
the game found and shows the cursor, without a reset that would clear the
game's last words. A game leading its console's session takes the console
with it as it dies; its guard then leads a session of its own and opens the
console again once the kernel has hung it up, to restore it.

Checked in QEMU on a real VT, kernels 6.8 and 7.2, the game as uid 1000:
the same key strokes read identically raw and cooked (Shift released first,
Ctrl, Alt, AltGr, Caps and Num Lock, the keypad, function keys, modified
cursor keys on 7.2; dead grave and tilde, compose, a euro sign and Cyrillic
from a loaded keymap), Alt+F2 away ended the hold Lost and Alt+F1 came back,
and the keyboard came back to K_UNICODE with ICANON and ECHO after Fini, a
panic with EmergencyReset, kill -9, and kill -9 of a game leading its session
(left raw without the guard's own session).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3zCVjspRDuEuMrDTumVhp

* freebsd console source: releases from vt(4)'s keyboard, typed by its keymap

On FreeBSD's vt(4) console the keyboard switches to keycodes (K_CODE),
which vt allows any process with the console open, no root needed. Each
keycode types what vt types for it cooked: the keyboard's keymap and
accent map (GIO_KEYMAP, GIO_DEADKEYMAP) run through a port of the
kernel's genkbd_keyaction, kbdmux's left Alt keypad codes, and
vt_processkey's xterm sequences, ESC for Alt and console switching. The
locks go back to the keyboard and its lights with KDSKBSTATE. Scroll Lock
is inert while raw; with the console's history mode on (Scroll Lock lit)
the keyboard stays as it is and the ask reports presses.

Shared with the Linux source now: the restore EmergencyReset runs, the
guard, which opens again a console revoked or hung up with its session's
leader, and what a key asks of the console. A console that can't go raw
just now (Scroll Lock on, its guard gone, output not drained) is tried
again at the next ask rather than never; a refusal at prepare still
holds for good.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3zCVjspRDuEuMrDTumVhp

* example-keys: show each key event, for trying key releases

A viewer for the key events a terminal or console reports: F2 asks for
releases from the console or the terminal's protocol, or gives them
back, and the report each ask came to shows above the events.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3zCVjspRDuEuMrDTumVhp

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-21 11:16:27 -04:00

499 lines
14 KiB
Go

package terminal
import "unicode/utf8"
// The FreeBSD console's keymap and its translation of keycodes, after the
// kernel's own (sys/dev/kbd/kbd.c, sys/dev/vt/vt_core.c), so a vt(4)
// keyboard switched to keycodes (K_CODE) types what the console types with it
// cooked: the keyboard's driver makes an action of each keycode by the keymap
// and its modifiers and locks, and vt types the action.
// Actions the keymap's special entries hold (sys/kbio.h)
const (
fbsdNOP = 0x00 // nothing
fbsdLSH = 0x02 // left Shift
fbsdRSH = 0x03 // right Shift
fbsdCLK = 0x04 // Caps Lock
fbsdNLK = 0x05 // Num Lock
fbsdSLK = 0x06 // Scroll Lock
fbsdLALT = 0x07 // left Alt
fbsdBTAB = 0x08 // back tab
fbsdLCTR = 0x09 // left Ctrl
fbsdNEXT = 0x0a // the next console
fbsdFSCR = 0x0b // console 1, the others after it
fbsdFFN = 0x1b // function key 1, the others after it
fbsdLFN = 0x7a // the last function key
fbsdRCTR = 0x80 // right Ctrl
fbsdRALT = 0x81 // right Alt
fbsdALK = 0x82 // AltGr Lock
fbsdASH = 0x83 // AltGr
fbsdMETA = 0x84 // Meta
fbsdRBT = 0x85 // reboot
fbsdDBG = 0x86 // the debugger
fbsdSUSP = 0x87 // suspend
fbsdSPSC = 0x88 // the screen saver
fbsdFACC = 0x89 // the first dead key
fbsdLACC = 0x97 // the last dead key
fbsdSTBY = 0x98 // standby
fbsdPREV = 0x99 // the previous console
fbsdPNC = 0x9a // panic
fbsdLSHA = 0x9b // left Shift, AltGr Lock pressed alone
fbsdRSHA = 0x9c // right Shift, AltGr Lock pressed alone
fbsdLCTRA = 0x9d // left Ctrl, AltGr Lock pressed alone
fbsdRCTRA = 0x9e // right Ctrl, AltGr Lock pressed alone
fbsdLALTA = 0x9f // left Alt, AltGr Lock pressed alone
fbsdRALTA = 0xa0 // right Alt, AltGr Lock pressed alone
fbsdHALT = 0xa1 // halt
fbsdPDWN = 0xa2 // power down
)
// What the keyboard's driver flags an action with (sys/kbio.h)
const (
fbsdNoKey = 0x01000000 // NOKEY: nothing to type
fbsdFKey = 0x02000000 // FKEY: a function key
fbsdMKey = 0x04000000 // MKEY: typed with Meta
fbsdBKey = 0x08000000 // BKEY: back tab
fbsdErrKey = 0x20000000 // ERRKEY: a character a dead key can't accent
fbsdRelKey = 0x40000000 // RELKEY: a special key let go
fbsdSpclKey = 0x80000000 // SPCLKEY: a special action
fbsdFlags = 0xff000000 // KEYFLAGS
)
// The keyboard's state: the locks, as KDGKBSTATE reports them, and the
// modifiers (sys/dev/kbd/kbdreg.h)
const (
fbsdCLKED = 1 << 0 // Caps Lock on
fbsdNLKED = 1 << 1 // Num Lock on
fbsdSLKED = 1 << 2 // Scroll Lock on
fbsdALKED = 1 << 3 // AltGr Lock on
fbsdLockMask = fbsdCLKED | fbsdNLKED | fbsdSLKED | fbsdALKED
fbsdShifts1 = 1 << 16
fbsdShifts2 = 1 << 17
fbsdShifts = fbsdShifts1 | fbsdShifts2
fbsdCtls1 = 1 << 18
fbsdCtls2 = 1 << 19
fbsdCtls = fbsdCtls1 | fbsdCtls2
fbsdAlts1 = 1 << 20
fbsdAlts2 = 1 << 21
fbsdAlts = fbsdAlts1 | fbsdAlts2
fbsdAgrs1 = 1 << 22
fbsdAgrs2 = 1 << 23
fbsdAgrs = fbsdAgrs1 | fbsdAgrs2
fbsdMetas1 = 1 << 24
fbsdMetas2 = 1 << 25
fbsdMetas = fbsdMetas1 | fbsdMetas2
fbsdNLKDown = 1 << 26
fbsdSLKDown = 1 << 27
fbsdCLKDown = 1 << 28
fbsdALKDown = 1 << 29
fbsdShiftAOn = 1 << 30 // a Shift, Ctrl or Alt that locks AltGr pressed alone so far
)
const (
fbsdNumKeys = 256 // NUM_KEYS: entries in a keymap
fbsdAltGrKeys = 128 // ALTGR_OFFSET: where the keys' AltGr entries start
fbsdDeadKeys = 15 // NUM_DEADKEYS
fbsdAccents = 52 // NUM_ACCENTCHARS: the characters a dead key accents
fbsdLockC = 1 // FLAG_LOCK_C: Caps Lock shifts the key
fbsdLockN = 2 // FLAG_LOCK_N: Num Lock shifts the key
fbsdKeycodes = 128 // K_CODE's keycodes, 7 bits
fbsdWindows = 12 // VT_MAXWINDOWS: the consoles vt(4) has
)
// fbsdKeymap is a FreeBSD console keymap as GIO_KEYMAP and GIO_DEADKEYMAP
// read it, and whether vt(4) takes right Alt for AltGr
// (kern.vt.enable_altgr) rather than the Alt that types ESC before a
// character
type fbsdKeymap struct {
keys [fbsdNumKeys]fbsdKey
accents [fbsdDeadKeys]fbsdAccent
nAccents int
altGr bool
}
// fbsdKey is a key's entry (keyent_t), laid out as the kernel's: its action
// in each state of Shift, Ctrl and Alt, the states whose action is special,
// and the locks that shift it
type fbsdKey struct {
actions [8]uint32
spcl uint8
flgs uint8
}
// fbsdAccent is a dead key's accent (acc_t), laid out as the kernel's: its
// character, and the characters it makes of others, in pairs up to a zero
type fbsdAccent struct {
char uint32
pairs [fbsdAccents][2]uint32
}
// fbsdKeys translates keycodes as a vt(4) console does, keeping the
// keyboard's modifiers, locks and dead key, the special action each key down
// began, the code left Alt and the keypad compose, and Alt for vt's ESC. The
// machine's keys (reboot, halt, power down, suspend, standby, the debugger,
// panic), the screen saver and paste type nothing; Scroll Lock does nothing,
// leaving the console's history mode alone. The cursor keys type as they do
// outside application mode, which the package never sets.
type fbsdKeys struct {
km *fbsdKeymap
down [fbsdKeycodes / 64]uint64 // keys down
state uint32 // modifiers and locks
accent int // the dead key pending, from 1; zero for none
lastact [fbsdKeycodes]uint32 // the special action each key down began
composing bool // left Alt down, the keypad's digits composing a code
composed uint32 // the code composed so far
alt bool // Alt down, for vt's ESC before a character
}
// seed starts translation from the console's locks; modifiers already down
// are not known, the keyboard's driver forgetting them as it switches
func (k *fbsdKeys) seed(locks uint32) {
*k = fbsdKeys{km: k.km, state: locks & fbsdLockMask}
}
// isDown is whether a key is down
func (k *fbsdKeys) isDown(code int) bool {
return code >= 0 && code < fbsdKeycodes && k.down[code/64]&(1<<(code%64)) != 0
}
// key translates one keycode event: down for a press, or a repeat, which the
// keyboard sends as another press. It returns what the console types for it
// and what it asks of the console.
func (k *fbsdKeys) key(code int, down bool) (typed []byte, act consoleAct) {
if code <= 0 || code >= fbsdKeycodes {
return nil, act
}
if down {
k.down[code/64] |= 1 << (code % 64)
} else {
k.down[code/64] &^= 1 << (code % 64)
}
if k.compose(code, down) {
return nil, act
}
locks := k.state & fbsdLockMask
c := k.action(code, !down)
act.leds = k.state&fbsdLockMask != locks
typed, act.vt = k.vt(c)
if !k.composing && k.composed > 0 {
// Left Alt up types the code composed
ch, _ := k.vt(k.composed)
typed, k.composed = append(typed, ch...), 0
}
return typed, act
}
// The keycodes of left Alt and the keypad's digits, which the keyboard's
// driver composes a code with
const fbsdKeyLeftAlt = 0x38
var fbsdPadDigits = map[int]uint32{
0x47: 7, 0x48: 8, 0x49: 9, 0x4b: 4, 0x4c: 5, 0x4d: 6, 0x4f: 1, 0x50: 2, 0x51: 3, 0x52: 0,
}
// compose composes a code of the keypad's digits typed with left Alt down, as
// the keyboard's driver does before it makes an action of a keycode
// (kbdmux_read_char): true when it takes the key, which then types nothing.
// Another key ends a code begun, and is lost to it; a code past a byte types
// nothing.
func (k *fbsdKeys) compose(code int, down bool) bool {
if code == fbsdKeyLeftAlt {
if down && !k.composing {
k.composing, k.composed = true, 0
} else if !down && k.composing {
k.composing = false
if k.composed > 0xff {
k.composed = 0
}
}
return false
}
if !k.composing {
return false
}
if digit, ok := fbsdPadDigits[code]; ok {
if down {
k.composed = min(k.composed*10+digit, 0x100)
}
return true
}
if k.composed > 0 {
k.composing, k.composed = false, 0
return true
}
return false
}
// lockKey toggles a lock as its key goes down, not as it repeats
func lockKey(state *uint32, down, locked uint32) {
if *state&down == 0 {
*state |= down
*state ^= locked
}
}
// action makes an action of a keycode by the keymap, as the keyboard's
// driver does (genkbd_keyaction): a character, a special action, or a flag
// for none
func (k *fbsdKeys) action(code int, up bool) uint32 {
state := k.state
i := code
if f := state & (fbsdAgrs | fbsdALKED); f == fbsdAgrs1 || f == fbsdAgrs2 || f == fbsdALKED {
i += fbsdAltGrKeys
}
key := &k.km.keys[i]
col := 0
if state&fbsdShifts != 0 {
col |= 1
}
if state&fbsdCtls != 0 {
col |= 2
}
if state&fbsdAlts != 0 {
col |= 4
}
if key.flgs&fbsdLockC != 0 && state&fbsdCLKED != 0 || key.flgs&fbsdLockN != 0 && state&fbsdNLKED != 0 {
col ^= 1
}
if up {
action := k.lastact[code]
k.lastact[code] = fbsdNOP
if alone, ok := fbsdLockingMods[action]; ok {
// Pressed alone, the key toggles AltGr Lock
if state&fbsdShiftAOn != 0 {
lockKey(&state, fbsdALKDown, fbsdALKED)
state &^= fbsdALKDown
}
action = alone
}
switch action {
case fbsdNOP:
// Regular keys go up unreported
k.state &^= fbsdShiftAOn
return fbsdNoKey
case fbsdLSH:
state &^= fbsdShifts1
case fbsdRSH:
state &^= fbsdShifts2
case fbsdLCTR:
state &^= fbsdCtls1
case fbsdRCTR:
state &^= fbsdCtls2
case fbsdLALT:
state &^= fbsdAlts1
case fbsdRALT:
state &^= fbsdAlts2
case fbsdASH:
state &^= fbsdAgrs1
case fbsdMETA:
state &^= fbsdMetas1
case fbsdNLK:
state &^= fbsdNLKDown
case fbsdCLK:
state &^= fbsdCLKDown
case fbsdSLK:
state &^= fbsdSLKDown
case fbsdALK:
state &^= fbsdALKDown
}
k.state = state &^ fbsdShiftAOn
return fbsdSpclKey | fbsdRelKey | action
}
action := key.actions[col]
state &^= fbsdShiftAOn
if key.spcl&(0x80>>col) == 0 {
// A character, the dead key pending accenting it
k.lastact[code] = fbsdNOP
k.state = state
if k.accent > 0 {
if action = k.accented(action); action == fbsdErrKey {
return action
}
}
if state&fbsdMetas != 0 {
action |= fbsdMKey
}
return action
}
// A special key keeps the action it went down with while it repeats
if k.lastact[code] == fbsdNOP {
k.lastact[code] = action
}
if k.lastact[code] != action {
action = fbsdNOP
}
if alone, ok := fbsdLockingMods[action]; ok {
state |= fbsdShiftAOn
action = alone
}
switch action {
case fbsdNLK:
lockKey(&state, fbsdNLKDown, fbsdNLKED)
case fbsdCLK:
lockKey(&state, fbsdCLKDown, fbsdCLKED)
case fbsdSLK:
// The console's history mode follows Scroll Lock; raw, it stays
case fbsdALK:
lockKey(&state, fbsdALKDown, fbsdALKED)
case fbsdSPSC, fbsdRBT, fbsdSUSP, fbsdSTBY, fbsdDBG, fbsdNEXT, fbsdPREV, fbsdPNC, fbsdHALT, fbsdPDWN:
k.accent = 0
case fbsdBTAB:
k.accent = 0
action |= fbsdBKey
case fbsdLSH:
state |= fbsdShifts1
case fbsdRSH:
state |= fbsdShifts2
case fbsdLCTR:
state |= fbsdCtls1
case fbsdRCTR:
state |= fbsdCtls2
case fbsdLALT:
state |= fbsdAlts1
case fbsdRALT:
state |= fbsdAlts2
case fbsdASH:
state |= fbsdAgrs1
case fbsdMETA:
state |= fbsdMetas1
case fbsdNOP:
k.state = state
return fbsdNoKey
default:
k.state = state
if action >= fbsdFACC && action <= fbsdLACC {
action = k.deadKey(action)
if action == fbsdNoKey || action == fbsdErrKey {
return action
}
if state&fbsdMetas != 0 {
return action | fbsdMKey
}
return action
}
if k.accent > 0 {
k.accent = 0
return fbsdErrKey
}
if action >= fbsdFFN && action <= fbsdLFN {
action |= fbsdFKey
}
return fbsdSpclKey | action
}
k.state = state
return fbsdSpclKey | action
}
// fbsdLockingMods are the modifiers that lock AltGr pressed alone, and the
// modifier each is otherwise
var fbsdLockingMods = map[uint32]uint32{
fbsdLSHA: fbsdLSH, fbsdRSHA: fbsdRSH, fbsdLCTRA: fbsdLCTR,
fbsdRCTRA: fbsdRCTR, fbsdLALTA: fbsdLALT, fbsdRALTA: fbsdRALT,
}
// deadKey is a dead key: the next character takes its accent, and the dead
// key again types the accent itself
func (k *fbsdKeys) deadKey(action uint32) uint32 {
i := int(action-fbsdFACC) + 1
if i > k.km.nAccents || k.km.accents[i-1].char == 0 {
k.accent = 0
return fbsdErrKey
}
if i == k.accent {
k.accent = 0
return k.km.accents[i-1].char
}
k.accent = i
return fbsdNoKey
}
// accented is what the dead key pending makes of ch: its accent for a space,
// the accented character, or an error for a character it can't accent
func (k *fbsdKeys) accented(ch uint32) uint32 {
acc := &k.km.accents[k.accent-1]
k.accent = 0
if ch == ' ' {
return acc.char
}
for _, p := range acc.pairs {
if p[0] == 0 {
break
}
if p[0] == ch {
return p[1]
}
}
return fbsdErrKey
}
// vt types an action as vt(4) does (vt_processkey): a character as UTF-8,
// after an ESC while Alt is down; F1 to F12, the cursor and editing keys and
// back tab as xterm's sequences; and a console to switch to
func (k *fbsdKeys) vt(c uint32) (typed []byte, vt int) {
if c&fbsdRelKey != 0 {
switch c &^ fbsdRelKey {
case fbsdSpclKey | fbsdRALT:
if k.km.altGr {
break
}
fallthrough
case fbsdSpclKey | fbsdLALT:
k.alt = false
}
return nil, 0
}
switch c {
case fbsdSpclKey | fbsdRALT:
if !k.km.altGr {
k.alt = true
}
case fbsdSpclKey | fbsdLALT:
k.alt = true
}
if c&fbsdSpclKey != 0 {
c &^= fbsdSpclKey
switch {
case c >= fbsdFSCR && c < fbsdFSCR+fbsdWindows:
return nil, int(c-fbsdFSCR) + 1
case c == fbsdNEXT:
return nil, vtNext
case c == fbsdPREV:
return nil, vtPrev
case c == fbsdBKey|fbsdBTAB:
return []byte("\x1b[Z"), 0
case c&fbsdFKey != 0:
return fbsdFunctionKey(c &^ fbsdFKey), 0
}
return nil, 0
}
if c&fbsdFlags != 0 {
return nil, 0 // nothing, a character it can't accent, or with Meta, which vt drops
}
if k.alt {
typed = append(typed, 0x1b)
}
return utf8.AppendRune(typed, rune(c)), 0
}
// fbsdFunctionKeys are what vt(4) types for the function keys, by number:
// xterm's sequences for F1 to F12, and for the cursor and editing keys the
// keymap binds as function keys 49 to 61; nothing for the others
var fbsdFunctionKeys = map[uint32]string{
1: "\x1bOP", 2: "\x1bOQ", 3: "\x1bOR", 4: "\x1bOS",
5: "\x1b[15~", 6: "\x1b[17~", 7: "\x1b[18~", 8: "\x1b[19~",
9: "\x1b[20~", 10: "\x1b[21~", 11: "\x1b[23~", 12: "\x1b[24~",
49: "\x1b[H", 50: "\x1b[A", 51: "\x1b[5~", 53: "\x1b[D", 55: "\x1b[C",
57: "\x1b[F", 58: "\x1b[B", 59: "\x1b[6~", 60: "\x1b[2~", 61: "\x1b[3~",
}
// fbsdFunctionKey is what vt(4) types for a function key action
func fbsdFunctionKey(action uint32) []byte {
if s, ok := fbsdFunctionKeys[action-fbsdFFN+1]; ok {
return []byte(s)
}
return nil
}