docs: input plan — implementation status + console-discipline correction

Record what landed (S1 raw evdev tap, S4-core VT graphics-mode op, S5 seatd
local package; all compile-checked, boot-validation P0-gated) and correct the
Defect-3 framing: fbcond is a byte-queue + CR/LF + special-key escapes, not a
second canonical line discipline (ptyd is the only one), and the console login
works — so the old "collapse two disciplines" console surgery is deferred as
unnecessary and risky. Grab arbitration + compositor wiring remain gated on the
Mesa desktop bring-up.
This commit is contained in:
2026-07-24 12:04:17 +09:00
parent 9e67a7417f
commit fdf377456f
+450 -79
View File
@@ -1,105 +1,476 @@
# Input Stack — Linux-Aligned Consolidation Plan
**Status:** Design (implementation staged below). Goal: make the console-input path match the
Linux/CachyOS model — one keymap owner, one line discipline shared by console and pty — instead of
the current duplicated, two-discipline chain that loses typed characters (echoed but not executed).
**Status:** Design (no code yet). **Goal:** replace RedBear's ad-hoc, duplicated console-input chain
with the architecture every mainstream OS converged on — a **single raw input core**, a **keymap
applied exactly once per consumer path**, a **single line discipline** shared by console + pty +
serial, and an **exclusive-grab / VT-mode mechanism** that lets a GUI compositor take a seat from the
text console cleanly. This fixes the console "echoed-but-not-executed" bug class *and* unblocks a
correct `redbear-full` Wayland/KDE desktop, which the current design cannot support.
CachyOS's input/tty is stock upstream Linux (the `local/reference/cachyos/` tree is packaging +
kernel *performance* patches; the input subsystem is unmodified). So the reference model below is
`local/reference/linux-7.1/drivers/{input,tty}/`.
This revision is grounded in three source audits performed for this doc:
- **Linux 7.1** reference (`local/reference/linux-7.1/drivers/{input,tty}/`) — the citable model.
- **CachyOS** ISO + package manifest (`local/reference/cachyos/`) — the userspace/session layer on
top of stock Linux.
- **RedBear base fork + recipes** (`local/sources/base`, `local/recipes/`) — what exists today.
## The Linux model (single owner per stage)
All file:line citations below were verified against those trees.
| Stage | Linux file | Property |
---
## 0. Executive summary (read this first)
1. **Every serious OS uses the same shape**: raw device → one raw core → *N* peer handlers, each
applying its **own** keymap, with an **exclusive grab** so a GUI can take a device from the
console. Linux, FreeBSD, macOS, and Windows all do this (§2). RedBear is the outlier.
2. **RedBear's current stack violates this three ways** (§3): the keymap is applied **globally inside
the core** (`inputd`), the "evdev" path is a **downstream shim that also competes for a VT** (so it
can never be a raw peer tap), and there is **no grab / graphics-mode** anywhere — a GUI cannot take
input from the console.
3. **The console bug is a symptom** of the same disease: **two line disciplines** (fbcond's ad-hoc
input cooking + ptyd's real one) and **double keymap / double CR→LF**. One typed line is processed
twice and characters are lost between the two stages.
4. **The target** (§4) maps 1:1 onto Linux: `inputd` = raw core + VT muxer (no keymap); a **console
keyboard handler** owns the *one* console keymap and feeds **ptyd** (the *one* line discipline);
**evdevd** becomes a **raw peer** off the core (not downstream of the keymap) feeding
libinput→compositor (xkbcommon keymap); a **grab/VT-mode** control op is the RedBear equivalent of
`EVIOCGRAB` + `KDSKBMODE(K_OFF)` + `VT_SETMODE(VT_PROCESS)`.
5. **This is bulletproof because it is not novel** — it is the converged design, plus an explicit
conformance checklist (§7) and a per-stage, independently-bootable rollout (§6).
---
## 0b. Implementation status (2026-07-24)
Grounded in a full read of the actual code (not just the audit), two things changed
the plan in flight and are reflected below:
- **The console is NOT a second *canonical* line discipline.** fbcond's `input()`
(`text.rs:131-166`) is a byte-queue with CR→LF plus special-key **escape
sequences** (its `keymap.lookup` returns `ESC [`/`ESC O` for arrows/F-keys and
falls back to inputd's printable `character`) — it does **no** cooking, echo, or
canonical buffering. The one canonical discipline is ptyd. And per memory
([[brush-login-pty-slave-input-blocker]]) the "echoed-not-executed" bug was
root-caused to brush's own backend and **fixed** — the console login works. So
the old "collapse two disciplines" console surgery is **not required** and is
**deferred** (it would rip apart a working boot-critical path with no boot-test).
- **The real, high-value defect was the evdev/GUI path** — now fixed additively
without touching the console.
| Stage | What | Status |
|---|---|---|
| Raw input pipeline | `drivers/input/input.c` (`input_pass_values`) | one driver→core→handler pipeline |
| **Keymap → bytes** | `drivers/tty/vt/keyboard.c` (`put_queue`, `to_utf8`, `applkey`) | **applied ONCE**; emits the full byte stream incl. escape sequences for special keys |
| **Line discipline** | `drivers/tty/n_tty.c` (`read_buf` cooked, `echo_buf`, ICRNL, ISIG) | **ONE module**: canonical, echo, cooked buffer, CR/LF, signal keys |
| Console vs pty | `drivers/tty/pty.c` (`tty_port`, shared ldisc) | pty is a thin bridge that **reuses `n_tty`** no second discipline |
| **1** | inputd raw evdev peer tap (`consumer_raw`, pre-keymap, all-VT); evdevd reads it | **DONE** — base `bd5e3db3`, main `8597609c`; compile-checked x86_64-redox |
| **4 (core)** | inputd VT graphics-mode op (`set_vt_mode`, console suppression = RedBear `KDSKBMODE(K_OFF)`) | **DONE** — base `a916a661`, bumped `60946a3c`; compile-checked |
| **5 (recipe)** | seatd → local source package (`path=source`) | **DONE** — main `9e67a741` |
| **4 (rest)** | exclusive device-grab arbitration (EVIOCGRAB) + VT_PROCESS switch-ack | **TODO** — raw broadcast suffices for one compositor; no consumer to validate yet |
| **5 (rest)** | seatd Redox port (compile) + wire `redbear-compositor` input | **BLOCKED** — desktop bring-up, gated on Mesa EGL/DRI ([[full-wayland-build-cascade-2026-07]]) |
| ~~2~~ | ~~collapse fbcond input cooking~~ | **DEFERRED / not required** — console is not a 2nd canonical discipline and login works |
Key invariants Linux guarantees and we currently violate:
1. The keymap is applied **exactly once**.
2. CR/LF (`ICRNL`/`ONLCR`) is one `termios` policy in **one** place.
3. Canonical/echo/cooked lives in **one** buffer pair, and the **console tty and ptys share the
same line discipline**. "Echoed but not delivered" is structurally impossible because the reader
drains the very buffer the discipline cooked into.
**Validation state:** all landed changes are **compile-checked** against
`x86_64-unknown-redox` (via the prefix `rust-install` toolchain) but **not
boot-validated** — that is gated on **P0 build-green** (`acpi-rs`, operator fixing
elsewhere). The changes are structured to be safe until then: the raw tap and
VT-mode op are **purely additive** and leave the working console fan-out
byte-for-byte unchanged (graphics_vts empty by default).
## Current RedBear path (what to fix)
---
## 1. The Linux model, with citations (the reference we align to)
Reference tree: `local/reference/linux-7.1` (VERSION 7.1.0). One hardware event flows:
```
ps2d ─▶ inputd ─▶ fbcond ─▶ getty(bridge) ─▶ ptyd ─▶ pty-slave ─▶ shell
keymap keymap+ byte pump FULL line discipline
(get_char) CR→LF (canonical/echo/cooked/CR→LF/signals)
+ own input
queue
hardware ─▶ input_event() ─▶ input_pass_values() ─┬─(grab? exclusive)─▶ one handler
(raw) input.c:391 input.c:111 │
├─▶ evdev handler ─▶ /dev/input/eventN ─▶ libinput ─▶ compositor (xkbcommon keymap)
│ evdev.c:291 [GUI path]
└─▶ console keyboard ─▶ put_queue ─▶ n_tty ─▶ tty/pty ─▶ getty/shell
keyboard.c:1423 (keymap ONCE) n_tty.c [TEXT path]
```
Confirmed wiring (`local/sources/userutils/src/bin/getty.rs`):
- getty opens `/scheme/fbcon/{vt}` (fbcond) **and** `/scheme/pty/ptmx` (ptyd), then byte-pumps
console↔pty; login/shell stdin = pty **slave**.
### 1a. Raw core + fan-out + exclusive grab — `drivers/input/input.c`
- `input_event()` (`:391`) is the driver entry; batched values reach `input_pass_values()` (`:111`).
- **Fan-out with grab short-circuit** (`:119-133`): under RCU it first checks `dev->grab`; if a
handle holds the grab, events go **only** to it (`break`), else every **open** handle on
`dev->h_list` gets `handle_events()` in list order. So evdev and the console keyboard are **peer
handles off the same raw device** and *both* see every event — unless grabbed.
- **The grab** (`input_grab_device()` `:520`, sets `rcu_assign_pointer(dev->grab, handle)` `:528`,
`-EBUSY` if already held; release `__input_release_device()` `:535-551` + `synchronize_rcu`).
Injection respects it too (`input_inject_event()` `:412-424`).
Duplication:
- **Keymap twice**: inputd `active_keymap.get_char` (`drivers/inputd/src/main.rs:452`) **and** fbcond
`self.keymap.lookup` (`drivers/graphics/fbcond/src/text.rs:140`).
- **CR→LF twice**: fbcond (`text.rs:164`) **and** ptyd (`ICRNL`, `drivers/ptyd/src/pty.rs:76`).
- **Two disciplines**: fbcond has its own input queue/cooking; ptyd has the real one. The bridge
between them is where typed characters are lost while bare `\n` survives (the reported bug:
`tlc` echoes but never executes; manual Enters produce empty prompts).
### 1b. evdev handler (the GUI feed) — `drivers/input/evdev.c`
- Per-client ring buffer `struct evdev_client` (`:40`); `__pass_event()` (`:214`) writes one
`input_event`, and on **overrun drops all unconsumed + synthesizes `SYN_DROPPED`** (`:220-235`)
the client-visible contract for lost events.
- `evdev_events()` (`:291`) is the registered `handle_events`; mirrors grab at the client layer.
- Read to `/dev/input/eventN`: `evdev_read()` (`:556`). Two-level grab: `EVIOCGRAB` ioctl (`:1084`) →
`evdev_grab()` (`:332`) takes the **core** grab then the **client** grab; `EVIOCREVOKE` (`:1090`).
## Target (Linux-aligned)
### 1c. Console keyboard — keymap applied **exactly once** — `drivers/tty/vt/keyboard.c`
- `kbd_keycode()` (`:1423`) is the single translation site: `key_map = key_maps[shift_final]`
(`:1499`), the one lookup `keysym = key_map[keycode]` (`:1527`), dispatch `(*k_handler[type])`
(`:1565`).
- **Escape sequences for special keys**: `applkey()` (`:338`) emits `ESC O x` / `ESC [ x`; cursor keys
`k_cursor()` use `cur_chars="BDCA"` (`:796`) with modifier form `ESC [ 1 ; N X`; keypad `k_pad()`
(`:800`). UTF-8 via `to_utf8()` (`:353`); the sink `put_queue()` (`:326`) pushes bytes **into the
tty flip buffer** — i.e. into the line discipline.
- **Suppression when a GUI owns the VT**: `raw_mode = (kbd->kbdmode == VC_RAW)` (`:1450`); the gate at
`:1562-1563` returns before `put_queue` when `VC_RAW`/`VC_OFF`. A compositor sets this via
`KDSKBMODE(K_OFF/K_RAW)``vt_do_kdskbmode()` (`:1869`). Result: **console keymap + n_tty are
silenced for that VT.**
### 1d. The one line discipline — `drivers/tty/n_tty.c`
- Registered `n_tty_ops` (`:2500`, `.num = N_TTY`, `.name = "n_tty"`) — the kernel **default for every
tty**; `tty_ldisc.c` falls back to it on any failure. `n_tty_inherit_ops()` (`:2525`) lets
subclasses reuse it rather than write a second discipline.
- Cooked buffer `n_tty_data.read_buf` (`:84`/`:106`). Special processing
`n_tty_receive_char_special()` (`:1330`): **ISIG** (`:1338-1349`, INTR/QUIT/SUSP→signals via
`__isig` `:1044`), **ICRNL/IGNCR/INLCR** (`:1356-1362`). Canonical editing
`n_tty_receive_char_canon()` (`:1245`): ERASE/KILL→`eraser()` (`:925`), EOF (`:1296`). Output
`process_output()` (`:476`): **ONLCR/OCRNL** (`:414`/`:426`).
### 1e. pty reuses n_tty — `drivers/tty/pty.c`
- `pty_write()` (`:109`) just shoves bytes into the peer's flip buffer; slave uses
`init_termios = tty_std_termios` (`:570`) and the default **N_TTY**. A shell on a pty gets the
**same** ISIG/ICRNL/echo/canonical as the console — **there is no second discipline.**
### 1f. VT ownership handshake — `drivers/tty/vt/vt_ioctl.c`
- `KDSETMODE(KD_GRAPHICS)` (`:376``vt_kdsetmode` `:252`) releases the framebuffer; `KDSKBMODE`
(`:394`) silences the keyboard (§1c). `VT_SETMODE(VT_PROCESS)` (`:754`) makes VT switches
**handshake through the owning process** via signals; `change_console()` (`:1198`) defers the switch
(`:1221-1231`) until `VT_RELDISP``vt_reldisp()` (`:553`)→`complete_change_console()` (`:1124`). A
graphics VT in AUTO mode blocks switches outright (`:1259`).
- `uinput` (`drivers/input/misc/uinput.c`) is the **reverse** path: userspace injects synthetic
devices into the core — the inverse of evdev.
**Linux invariants we must inherit:** (1) keymap applied **once per path**; (2) CR/LF is one termios
policy in **one** place; (3) **one** line discipline for console + pty + serial; (4) evdev and console
keyboard are **peer taps of the raw core**, not chained; (5) a GUI takes a device via **exclusive
grab** + puts the VT in **graphics/keyboard-off** mode.
---
## 2. Cross-OS convergence — why this is best-in-class, not just "Linux-like"
Every production OS independently arrived at *raw core → per-path keymap → exclusive grab*. RedBear is
the only one that applies the keymap in the shared core and lacks a grab.
| OS | Raw core | Console/text keymap | GUI keymap | Grab / mode-switch |
|---|---|---|---|---|
| **Linux** | `input.c` core, evdev + kbd are peer handles | `vt/keyboard.c` (kernel keymap) → `n_tty` | libinput→**xkbcommon** in compositor | `EVIOCGRAB` + `KDSKBMODE(K_OFF)` + `VT_PROCESS` |
| **FreeBSD** | `evdev` core + `vt(4)`/newcons; `kbdmux`/`sysmouse` | `kbdmux``vt` keymap → tty ldisc | libinput (evdev) → **xkbcommon** | `EVIOCGRAB` (evdev clone) + `VT_SETMODE`/`KDSKBMODE` (vt_ioctl compat) |
| **macOS** | `IOHIDFamily` (kernel) → `IOHIDSystem` | Text terminal via `Terminal.app` over pty; no VT keymap in kernel for GUI | **WindowServer**/HIToolbox+TIS keymap (`.keylayout`/`uchr`) | HID **seizing** (`IOHIDDeviceOpen(kIOHIDOptionsTypeSeizeDevice)`); WindowServer owns the event stream |
| **Windows** | Raw Input Thread (RIT) in `win32k` | Console host (`conhost`) + `n_tty`-like cooked mode in CONDRV | `win32k` layout (`HKL`) per-thread; **Raw Input API** `WM_INPUT` for raw | `RegisterRawInputDevices(RIDEV_EXINPUTSINK/NOLEGACY)`; foreground/session arbitration |
| **Redox today** | `inputd` merges producers into **one** stream | keymap applied **in `inputd` (core!)** → fbcond → ptyd | evdevd shim **downstream** of the core keymap | **none** |
| **RedBear target** | `inputd` = raw core + VT mux (no keymap) | **console-kbd handler** (one keymap) → **ptyd** (one ldisc) | **evdevd** raw peer → libinput → **xkbcommon** | inputd **grab + VT graphics-mode** control op |
Takeaways that shape the design:
- **The keymap is never in the shared core** in any of these systems — it is always in the *consumer*
(kernel VT for text, compositor for GUI). Redox putting it in `inputd` is the root anomaly.
- **Two independent keymaps are normal and correct** (console layout vs GUI layout can even differ;
CachyOS keeps `vconsole.conf` separate from the xkb layout — §5). One keymap "shared" across both
paths is the wrong goal.
- **Exclusive grab is universal.** macOS "seize", Windows raw-input sink, Linux/BSD `EVIOCGRAB` — all
are the same primitive. RedBear must grow one.
- **A pty always reuses the console's line discipline.** No OS ships two disciplines. This is exactly
the RedBear console bug.
---
## 3. RedBear today — exact inventory and the three defects
(From the base-fork + recipes audit. Citations verified.)
### 3a. What exists
- **`inputd`** (`local/sources/base/drivers/inputd`) — the only real multiplexer. Registers scheme
`input` with paths `producer`/`consumer`/`consumer_bootlog`/`handle`/`control` (`main.rs:682`).
- **Producers** (all write to `input`): `ps2d`, `usbhidd`, `i2c-hidd`, `intel-thc-hidd`
(`drivers/input/*`).
- **Console consumers**: `fbcond` (scheme `fbcon`, `drivers/graphics/fbcond`), `vesad`, `fbbootlogd`.
- **`ptyd`** (`local/sources/base/ptyd`) — scheme `pty`, a **full POSIX line discipline** already
(§3d). This is the piece already closest to Linux.
- **`evdevd`** — **NOT in the base fork**; a recipe at `local/recipes/system/evdevd`. Registers scheme
`evdev`, synthesizes kbd/mouse/touchpad, **consumes `/scheme/input/consumer`** (`main.rs:66`),
started by `init.d/10_evdevd.service`.
- **Seat/session**: `seatd` (recipe `local/recipes/system/seatd`, marked *"TODO not compiled"*),
`redbear-sessiond`, `init.d/13_seatd.service`**packaged, started, but not integrated**.
- **Compositor**: `redbear-compositor` (`local/recipes/wayland`) renders via `/scheme/drm/card0`,
advertises `wl_seat`, but its **input is stubbed**`main.rs:2542`: *"Keyboard events dispatched to
focused client surface when input daemon is wired."* It opens only `/scheme/drm/card0` + `/dev/null`
(keymap fd). Libs `libinput`/`libevdev`/`libxkbcommon` are staged in `local/recipes/libs`.
### 3b. Defect 1 — keymap applied **globally in the core**
`inputd`'s producer `write()` calls `self.active_keymap.get_char(key, shift)` (`main.rs:452`) and
stamps `key_event.character` in place (`main.rs:454`), with one global `active_keymap` (`main.rs:71`,
default US `:89`, switched via control cmd 2 `:136-148`). **Every downstream consumer receives
pre-keymapped events.** This is the anomaly §2 identifies: no other OS applies the keymap in the
shared core.
### 3c. Defect 2 — the "evdev" path is downstream **and** VT-competing (cannot be a raw peer)
`evdevd` opens a plain `/scheme/input/consumer` (`main.rs:66`), so inputd (a) hands it
**already-keymapped** events (Defect 1), and (b) allocates it **its own VT** and only feeds it while
**that VT is active** (`inputd` fan-out is VT-gated, `main.rs:485-490`*not* a broadcast). So
RedBear's evdev can **never** be the passive, always-on raw tap that libinput/xkbcommon require. In
Linux terms, evdev has been wired **after** `keyboard.c` and **behind** a VT gate, instead of as a
**peer handle off `input.c`**. A correct Wayland stack is impossible on this wiring.
### 3d. Defect 3 — duplicated console input processing (NOT a second canonical discipline)
> **Correction (post-code-read):** the original draft called this "two line
> disciplines." That overstated it. fbcond does **not** cook, echo, or maintain a
> canonical buffer — it is a byte-queue + CR→LF + special-key escape sequences.
> The one canonical discipline is ptyd. The "echoed-not-executed" bug was brush's
> own backend (fixed). So this is real *duplication* (double keymap/CR→LF) worth
> removing eventually, but it is **not** the console-login bug and does not need
> the risky surgery now — see §0b.
- **ptyd** already implements the real discipline: `Pty::input()` (`ptyd/src/pty.rs:41-249`) — cooked
buffer (`:12`,`:244`), flush on newline/EOF (`:88-93`), ECHO/ECHOE (`:57-59`,`:83-87`), ICRNL/INLCR/
IGNCR (`:53-55`,`:69-79`), ISIG VINTR/VQUIT/VSUSP (`:173-206`), VERASE/VKILL/VWERASE/VREPRINT/VEOF
(`:95-170`), OPOST/ONLCR in `output()` (`:251-270`), VMIN/VTIME (`:272-335`).
- **fbcond** *also* cooks input: keymap lookup `self.keymap.lookup(sc)` (`text.rs:140`), **CR→LF** for
both keyboard (`text.rs:159-165`) and serial-injected input (`text.rs:189-195`), and its own input
`VecDeque<u8>` drained by `read()` (`text.rs:174-183`).
- **getty** (`local/sources/userutils/src/bin/getty.rs`) byte-pumps `/scheme/fbcon/{vt}`
`/scheme/pty/ptmx`; the shell's stdin is the pty **slave**.
So a keystroke is keymapped in `inputd`, keymapped/CR→LF'd/queued again in `fbcond`, pumped by getty,
then run through ptyd's *real* discipline. **Two disciplines + double keymap + double CR→LF.** Bytes
are lost between fbcond's ad-hoc cooking and ptyd's canonical cooking — the reported
`tlc`-echoes-but-never-executes bug, and the empty-prompt-on-Enter behavior.
### 3e. Defect 4 — no grab / graphics-mode anywhere
Grep for `KD_GRAPHICS|KDSETMODE|VT_SETMODE|EVIOCGRAB|grab` across base + recipes: **nothing**. The
display "handoff" (`inputd main.rs:301-305`, fbcond `text.rs:94-129`) transfers the *framebuffer*, not
input ownership. A GUI cannot take the keyboard from the console; it can only open another VT and rely
on VT-active gating. This is the single largest divergence and the reason the compositor's input is
still stubbed (§3a).
---
## 4. Target architecture (Linux-aligned, both paths)
```
ps2d ─▶ inputd ─▶ fbcond(display only) ─▶ getty(dumb pipe) ─▶ ptyd ─▶ slave ─▶ shell
keymap→ passthrough of the THE single line
BYTES already-decoded byte discipline (n_tty)
(incl. stream; renders output;
escapes) NO keymap, NO CR/LF,
NO input cooking
─▶ console-kbd handler ─▶ ptyd (n_tty) ─▶ getty/shell [TEXT VT]
ps2d/usbhidd/i2c-hidd ─▶ inputd ┤ ONE console keymap ONE line discipline
(raw producers) (RAW │ (raw scancodebytes, (canonical/echo/cooked/
CORE │ esc seqs; NO CR/LF here) CR/LF/ISIG — the only one)
+ VT │
mux + └─▶ evdevd (RAW peer) ─▶ /scheme/evdev ─▶ libinput ─▶ compositor [GUI VT]
GRAB) raw events, NO keymap (xkbcommon keymap)
(tap of the raw stream, not the console consumer)
```
Rules:
1. **inputd owns the keymap** and emits the **full byte stream** for each key, including escape
sequences for special keys (arrows, F-keys, Home/End) — the `keyboard.c`/`applkey` role. Today it
only sets a single `character`; extend it to emit bytes.
2. **fbcond is display-only + a raw byte passthrough.** Remove `self.keymap.lookup` and the
`text.rs:164` CR→LF. It forwards inputd's decoded bytes verbatim into the console read stream and
renders output. It performs **no** cooking, echo, or CR/LF.
3. **ptyd is the sole line discipline** (the `n_tty`). Canonical/echo/cooked/CR→LF/signals stay here
and **only** here. This is already implemented (`pty.rs:41-249`); it just becomes the single owner.
4. **getty stays a dumb byte pipe** (no processing) — matches a terminal emulator feeding a pty.
**Component contracts (each maps to a Linux role):**
Result: keymap once (inputd), CR/LF once (ptyd `termios`), one cooked buffer (ptyd) that the shell
drains directly. Structurally identical to Linux → the "echoed but not delivered" class is gone.
| Component | Linux analog | Contract |
|---|---|---|
| `inputd` | `input.c` core | Merge producers → raw stream. **No keymap.** Owns VT set + active-VT switch + the **grab/VT-mode** state machine (§4a). Fans raw events to the active VT's **console handler** *and*, in parallel, to any **grabbing** GUI consumer. |
| console-kbd handler | `vt/keyboard.c` | The **one** console keymap: raw scancode+mods → byte stream incl. escape sequences (arrows/F-keys/Home/End, `ESC [`/`ESC O`). Feeds ptyd. Emits **no** CR/LF (that's termios). In RedBear this lives on the console side (in/near fbcond), **not** in the core. |
| `ptyd` | `n_tty` + `pty.c` | The **only** line discipline. Console tty, ptys, serial all use it. Already implemented — becomes the sole owner. |
| `getty` | agetty | Dumb byte pipe (no processing), like a terminal emulator feeding a pty. |
| `evdevd` | `evdev.c` | **Raw peer** off inputd's raw stream (a new `producer`-side tap, *not* the console `consumer`). Emits Linux `input_event`s on `/scheme/evdev`. **No keymap.** Overrun → `SYN_DROPPED` (match Linux contract). |
| compositor | KWin | Acquires devices via seat (§4b), reads raw `/scheme/evdev`, applies **its own** xkbcommon keymap, drives VT grab. |
## Staged implementation (each stage builds + boots independently; validate on an idle host)
**Keymap ownership (final):** console keymap in the console-kbd handler; GUI keymap (xkbcommon) in the
compositor. **Two keymaps, one per path — never in the core.** (This corrects the earlier draft's
"inputd owns the keymap" rule, which reproduced Defect 1.)
1. **inputd: emit full byte sequences.** Add a keymap `get_bytes(scancode, mods) -> &[u8]` that
returns the char OR the escape sequence for special keys (model on `keyboard.c`). Deliver bytes on
the consumer stream (or keep events but carry the full byte payload). Keep behavior identical for
plain chars first; add escapes next.
2. **fbcond: drop keymap + CR/LF, become passthrough.** Remove `self.keymap.lookup` and the CR→LF at
`text.rs:164`; forward inputd's bytes verbatim. fbcond keeps only rendering + scrollback + the
serial mirror (which should also stop translating CR/LF — leave that to ptyd).
3. **Verify one discipline.** Confirm ptyd is the only place doing canonical/echo/cooked/CR→LF; delete
fbcond's residual input-cooking paths (`push_input_bytes` CR→LF at `pty.rs`-style sites in fbcond).
4. **Regression pass.** Console login on framebuffer AND serial: type a command, confirm execution;
test backspace, Ctrl-C (ISIG), arrows (escape sequences), and the serial mirror.
### 4a. The missing piece — a grab / VT-mode state machine in `inputd`
This is the heart of "bulletproof for the full target." Model it directly on Linux (§1a/§1f):
Immediate stopgap (independent of this refactor, already partly done): the brush minimal backend
reads raw and accumulates a persistent line buffer; extend it to treat **both** `\r` and `\n` as
submit so the shell is robust even before the stack is consolidated. This is a shell-side band-aid,
not the fix — the fix is stages 13.
- **New `inputd` control ops** (extend the existing `control` scheme, `main.rs:358-375`):
- `GrabDevice(vt)` / `UngrabDevice(vt)` — RedBear `EVIOCGRAB`. While a VT holds a grab, inputd
routes that device's raw events **only** to the grabbing consumer and **suspends** the
console-kbd→ptyd feed for that VT (RedBear `KDSKBMODE(K_OFF)`).
- `SetVtMode(vt, {Text|Graphics})` — RedBear `KDSETMODE`. `Graphics` means "a compositor owns this
VT": stop delivering cooked console bytes; deliver raw events to the grabber.
- `SetVtSwitch(vt, {Auto|Process})` + `AckVtSwitch(vt)` — RedBear `VT_SETMODE(VT_PROCESS)` +
`VT_RELDISP`: a graphics VT can **defer/ack** switches so the compositor releases DRM/input
cleanly before the switch completes (Linux `change_console`/`complete_change_console`).
- **State per VT**: `{ kbmode: Xlate|Raw|Off, vcmode: Text|Graphics, switchmode: Auto|Process,
grab: Option<ConsumerId> }`. Console-kbd delivery is gated on `kbmode==Xlate && vcmode==Text`; raw
grab delivery on `grab==Some`. This is exactly the Linux gate at `keyboard.c:1562-1563`.
- **Why in inputd**: inputd is the raw core (the `input.c` seat), so the grab/mode state belongs there
— the same place Linux keeps `dev->grab` and per-VT `kbd->kbdmode`.
## Validation
### 4b. Seat acquisition — `libseat`/`seatd` (no systemd/logind)
- **RedBear packages `seatd`** but the compositor doesn't use it (§3a). The seat manager is
**`libseat`/`seatd`** — this is the RedBear seat layer.
The compositor calls `libseat` → `seatd`, which opens the `/scheme/evdev` + `/scheme/drm` handles
and issues the inputd `GrabDevice`/`SetVtMode(Graphics)` ops on the compositor's behalf. This is the
wlroots/COSMIC-on-seatd pattern (used on Linux/BSD *without* systemd-logind), which is the model
RedBear follows. **systemd-logind is explicitly out of scope — RedBear has no systemd, and seatd is
the whole seat layer.**
- **Ownership decision (operator, locked):** `seatd` becomes a **first-class local Red Bear source
package** — `local/recipes/system/seatd` with `[source] path = "source"`, identical in kind to its
siblings `evdevd` and `redbear-compositor`. **Not** a `local/sources/*` submodule and **not** a
`local/patches/seatd/*.patch`. Rationale: we own the implementation and integrate upstream commits
*into* our tree on our terms (upstream `jackpot51/seatd` is near-dormant), which is Local Fork
Supremacy (`local/AGENTS.md:43-58`, `:114`) — our in-tree source is the complete authoritative
implementation. This also resolves the current inconsistency where the recipe fetches
`git = jackpot51/seatd` while a full `source/` tree (52 files, incl. `libseat`/`seatd-launch`) is
already tracked in-repo but shadowed by the fetch. Trade-off accepted: a flat vendored tree has no
shared git ancestry with upstream, so rare future upstream commits are ported by hand rather than a
3-way merge — which is exactly the intended direction (we pull upstream in, never the reverse).
- The compositor's stubbed input (`redbear-compositor main.rs:2542`) gets wired to: open `seatd` →
receive `/scheme/evdev` fds → feed libinput → xkbcommon. No keymap touches this before xkbcommon.
- Framebuffer screendump (QMP) is ground truth. **One boot proves nothing** (input races); compare
across boots on an idle host — external load (opencode) contaminates timing.
- Success: type `echo RB=$((21*2))=OK` on the framebuffer console → `RB=42=OK` executes; backspace,
Ctrl-C, and arrows behave; serial-console login works the same way.
---
## Risk / rollback
## 5. Does this solve input long-term, in ALL situations, incl. the FULL desktop?
- Boot-critical path; each stage is independently bootable and revertable (one component per stage).
- Do not land any stage validated only under external load.
- Build is currently blocked on `acpi-rs` (`aml/mod.rs:258` `connect_op_regions` undefined) — none of
this can be verified until that compiles.
Yes — because it inherits the converged coverage, not a bespoke design. Concretely:
| Situation | Covered by | Evidence it's the right model |
|---|---|---|
| Framebuffer console login | console-kbd → ptyd n_tty | the reported bug's home; one discipline ⇒ "echoed-not-executed" is structurally impossible |
| Serial console login | serial → ptyd n_tty | same discipline; drop fbcond's serial CR→LF (`text.rs:189-195`) |
| Terminal emulator (konsole) | pty via ptyd n_tty | konsole opens a pty — identical to Linux `pty.c` reusing n_tty |
| SSH / remote shell | pty via ptyd n_tty | pty reuse |
| Wayland/KDE GUI keyboard | evdevd raw → libinput → xkbcommon | needs a **raw peer tap** — works only after Defects 13 fixed |
| Raw-input games/apps | evdev grab (§4a) | universal grab primitive (§2) |
| Layout switch (US/RU/…) | console: console-kbd keymap; GUI: xkbcommon | two independent keymaps is the correct model (CachyOS keeps `vconsole.conf` separate from xkb, §agent-2) |
| Multi-VT + switch to GUI and back | inputd VT mux + grab/VT-mode + ack handshake (§4a) | Linux `VT_PROCESS`/`change_console` proven |
| Compositor crash while grabbed | inputd drops grab → console-kbd resumes | Linux revokes grab on close (`__input_release_device`) |
**Long-term viability** rests on: (a) it is the model Linux/BSD/macOS/Windows all use (§2), so every
integration guide, and Redox's own Wayland/COSMIC direction, assume exactly this; (b) our own history
shows the ad-hoc path doesn't survive — memory records ~17 input regressions, overwhelmingly
duplication/ordering faults this structure removes by construction.
---
## 6. Bulletproof rollout — staged, each stage boots independently
**Prerequisites (hard gates):**
- **P0. Build green.** `acpi-rs` (`aml/mod.rs:258` `connect_op_regions`) must compile — nothing here is
verifiable until then.
- **P1. Upstream diff.** Diff `inputd`/`fbcond`/`ptyd` against the Redox base to separate
*RedBear-added duplication we simply delete* (e.g. fbcond CR→LF was RedBear-added, per memory) from
*upstream behavior we must adapt/upstream-propose*. This picks the convergence tier (§8).
- **P2. Idle host.** Input is race-sensitive; validate on framebuffer ground truth without external
load (opencode contaminates timing). One boot proves nothing — compare N≥5.
**Stage 1 — Move the keymap out of the core (fix Defect 1).**
- Remove `active_keymap.get_char` stamping from `inputd` (`main.rs:452-454`); inputd emits **raw**
scancode+modifier events on the producer→consumer path.
- Give the console-kbd handler (initially inside fbcond) the keymap: raw → bytes incl. escape
sequences (port `keyboard.c`'s `applkey`/`cur_chars`/`to_utf8` behavior). Plain chars first, escape
sequences next.
- Boot check: framebuffer console still logs in and types.
**Stage 2 — Collapse to one line discipline (fix Defect 3).**
- Delete fbcond's input cooking: keymap→bytes now happens in the console-kbd handler, and **all**
canonical/echo/CR-LF/ISIG happens in **ptyd only**. Remove fbcond CR→LF (`text.rs:159-165`,
`:189-195`) and its input `VecDeque` cooking (`text.rs:174-183`); fbcond keeps render + scrollback +
serial *output* mirror only.
- getty becomes a pure byte pipe (verify no translation).
- Boot check: type `echo RB=$((21*2))=OK` on fb console → `RB=42=OK` executes; backspace, Ctrl-C,
arrows behave; serial login identical.
**Stage 3 — Make evdev a raw peer (fix Defect 2).**
- Add a raw-tap consumer type to inputd (a `producer`-side mirror, not a VT-gated `consumer`) that is
**always-on** and **pre-keymap**. Repoint `evdevd` at it (`main.rs:66`). evdevd applies **no**
keymap; overrun → `SYN_DROPPED`.
- Boot check: `evdevd` sees events regardless of active VT; console path unaffected.
**Stage 4 — Grab / VT-mode state machine (fix Defect 4).**
- Implement §4a control ops in inputd; gate console-kbd delivery on `kbmode/vcmode`; route grabbed raw
events to the grabbing consumer; implement the switch defer/ack handshake.
- Boot check: a test client can `GrabDevice`, receive raw events, `Ungrab`, and the console resumes.
**Stage 5 — Wire the compositor via seat (unblocks `redbear-full`).**
- **Promote `seatd` to a local source package** (per §4b decision): flip `recipe.toml [source]` from
`git = jackpot51/seatd` to `path = "source"`; remove the leftover nested `source/.git` (so it's a
plain vendored tree like evdevd, not an embedded clone) and the stray `target/` in the recipe dir;
make it actually compile (currently `#TODO not compiled`).
- Wire `redbear-compositor` (`main.rs:2542`) through `libseat`/`seatd` to open `/scheme/evdev`, feed
libinput, apply xkbcommon; on VT-enter it `GrabDevice`+`SetVtMode(Graphics)`, on VT-leave it
releases.
- Boot check: switch to the GUI VT, type into a Wayland client; switch back to the text VT, console
still works.
Each stage is independently bootable and revertable (one component per stage). **Do not land any stage
validated only under external load.**
---
## 7. Conformance checklist (acceptance — the "bulletproof" bar)
A change conforms **iff** all hold:
1. **One keymap per path.** No keymap in `inputd`. Exactly one console keymap (console-kbd handler);
exactly one GUI keymap (xkbcommon). Grep proof: no `get_char` in `inputd`; no `keymap.lookup` in
fbcond.
2. **One line discipline.** All canonical/echo/CR-LF/ISIG in ptyd; none in fbcond/getty. Grep proof:
no `\r`→`\n` and no cooked buffer outside `ptyd/src/pty.rs`.
3. **evdev is a raw, always-on peer** (pre-keymap, not VT-gated). evdevd receives events on any active
VT; its stream carries raw scancodes, not `character`.
4. **Grab is exclusive and revocable.** A grabbing consumer gets events exclusively; console resumes
on ungrab/close. Modeled on `input_grab_device`/`__input_release_device`.
5. **VT switch handshakes** for graphics VTs (defer + ack), like `VT_PROCESS`/`VT_RELDISP`.
6. **Test matrix passes** (§below) on an idle host, N≥5 boots.
**Test matrix:** fb console login; serial console login; `echo RB=42` executes; backspace; Ctrl-C
(ISIG); arrows/F-keys (escape sequences); RU/US layout switch (console) independent of GUI layout;
konsole-in-GUI pty; VT switch text↔graphics↔text; compositor crash resumes console.
---
## 8. Upstream strategy (the divergence question)
Ordered by preference; all structured to **converge**, never to grow a permanent lower-quality fork.
1. **Upstream to Redox (best).** Redox's own direction is Wayland/COSMIC + it already vendors `seatd`
and `libinput`/`libxkbcommon` — it has the **same** need for a raw evdev peer and a grab/VT-mode
op, and the **same** latent console duplication. The raw-core + single-ldisc + grab design is a fix
upstream benefits from. If accepted, divergence → 0. Propose it as: (a) move keymap out of inputd,
(b) raw-tap consumer, (c) grab/VT-mode control ops.
2. **Compatibility-preserving interim.** During transition keep `inputd` able to serve **both** shapes:
a legacy keymapped `consumer` (for any current consumer that expects `character`) **and** the new
raw tap. Old upstream/orbital-style consumers keep working via the legacy path; RedBear's console +
GUI use the raw split. Minimizes merge-conflict surface; lets us rebase upstream with local
adapters, not a hard fork.
3. **Documented fork divergence (last resort).** Isolate each component's change so upstream rebases
apply around it; record rationale here and in commit messages; re-propose upstream each cycle. This
is the base-fork model already in use.
**Policy fit (`local/AGENTS.md`).** "Red Bear adapts to upstream, prefer the upstream solution when it
already solves the same problem." Upstream does **not** currently solve this (its Redox console path
duplicates too, and its evdev is a downstream shim), so a forward fix is legitimate — but the
prerequisite **P1 upstream diff** must run first to keep the change convergent, and some of what we
remove (fbcond CR→LF) is RedBear-added, so the fix *reduces* net divergence.
---
## 9. Positives / negatives (honest ledger)
**Positives**
- Fixes the console bug **class** permanently (one discipline ⇒ the failure is structurally
impossible).
- **Unblocks `redbear-full`** — the compositor's input can finally be wired (raw evdev peer + seat +
grab).
- Single source of truth per path ⇒ far fewer input regressions (history: ~17, mostly this class).
- Portable, documented model; easier onboarding and upstream alignment.
- Net **reduces** divergence (removes RedBear-added duplication).
**Negatives / risks**
- Boot-critical, multi-component refactor (inputd, fbcond, ptyd, evdevd, getty, compositor) — high
blast radius. Mitigated by the per-stage independently-bootable rollout (§6).
- **New surface**: the grab/VT-mode state machine (§4a) is genuinely new code; get the gate wrong and
input goes to the wrong consumer or a VT switch wedges. Mitigated by modeling exactly on Linux and
the conformance checklist (§7).
- Escape-sequence/keymap completeness must not regress special keys (arrows, F-keys, compose, dead
keys, RU). Covered by the test matrix.
- `seatd` must actually be compiled (currently *"TODO not compiled"*) for Stage 5.
- Can't validate until **P0** (build green) and needs **P2** (idle host).
---
## 10. Bottom line
The current stack is not merely "primitive" — it is the one arrangement that no shipping OS uses:
keymap in the shared core, evdev chained downstream behind a VT gate, two line disciplines, and no
grab. The target is the arrangement **all** of them use. Adopting it fixes the console bug by
construction and is the **precondition** for a working Wayland/KDE desktop on RedBear. The rollout is
staged, each stage boots, and the acceptance bar (§7) is explicit. **No code until P0P2 clear and
this design is approved.**