From fdf377456f6fdc5c33e06ac15e858d4b849cce4f Mon Sep 17 00:00:00 2001 From: vasilito Date: Fri, 24 Jul 2026 12:04:17 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20input=20plan=20=E2=80=94=20implementati?= =?UTF-8?q?on=20status=20+=20console-discipline=20correction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../docs/INPUT-STACK-LINUX-ALIGNMENT-PLAN.md | 529 +++++++++++++++--- 1 file changed, 450 insertions(+), 79 deletions(-) diff --git a/local/docs/INPUT-STACK-LINUX-ALIGNMENT-PLAN.md b/local/docs/INPUT-STACK-LINUX-ALIGNMENT-PLAN.md index 982c0367df..853638894e 100644 --- a/local/docs/INPUT-STACK-LINUX-ALIGNMENT-PLAN.md +++ b/local/docs/INPUT-STACK-LINUX-ALIGNMENT-PLAN.md @@ -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` 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 scancode→bytes, (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 1–3. +- **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 }`. 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 1–3 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 P0–P2 clear and +this design is approved.**