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.
33 KiB
Input Stack — Linux-Aligned Consolidation Plan
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.
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.
All file:line citations below were verified against those trees.
0. Executive summary (read this first)
- 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.
- 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. - 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.
- 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 ofEVIOCGRAB+KDSKBMODE(K_OFF)+VT_SETMODE(VT_PROCESS). - 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 (itskeymap.lookupreturnsESC [/ESC Ofor arrows/F-keys and falls back to inputd's printablecharacter) — 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 |
|---|---|---|
| 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) |
| DEFERRED / not required — console is not a 2nd canonical discipline and login works |
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).
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:
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]
1a. Raw core + fan-out + exclusive grab — drivers/input/input.c
input_event()(:391) is the driver entry; batched values reachinput_pass_values()(:111).- Fan-out with grab short-circuit (
:119-133): under RCU it first checksdev->grab; if a handle holds the grab, events go only to it (break), else every open handle ondev->h_listgetshandle_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, setsrcu_assign_pointer(dev->grab, handle):528,-EBUSYif already held; release__input_release_device():535-551+synchronize_rcu). Injection respects it too (input_inject_event():412-424).
1b. evdev handler (the GUI feed) — drivers/input/evdev.c
- Per-client ring buffer
struct evdev_client(:40);__pass_event()(:214) writes oneinput_event, and on overrun drops all unconsumed + synthesizesSYN_DROPPED(:220-235) — the client-visible contract for lost events. evdev_events()(:291) is the registeredhandle_events; mirrors grab at the client layer.- Read to
/dev/input/eventN:evdev_read()(:556). Two-level grab:EVIOCGRABioctl (:1084) →evdev_grab()(:332) takes the core grab then the client grab;EVIOCREVOKE(:1090).
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 lookupkeysym = key_map[keycode](:1527), dispatch(*k_handler[type])(:1565).- Escape sequences for special keys:
applkey()(:338) emitsESC O x/ESC [ x; cursor keysk_cursor()usecur_chars="BDCA"(:796) with modifier formESC [ 1 ; N X; keypadk_pad()(:800). UTF-8 viato_utf8()(:353); the sinkput_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-1563returns beforeput_queuewhenVC_RAW/VC_OFF. A compositor sets this viaKDSKBMODE(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.cfalls 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 processingn_tty_receive_char_special()(:1330): ISIG (:1338-1349, INTR/QUIT/SUSP→signals via__isig:1044), ICRNL/IGNCR/INLCR (:1356-1362). Canonical editingn_tty_receive_char_canon()(:1245): ERASE/KILL→eraser()(:925), EOF (:1296). Outputprocess_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 usesinit_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) untilVT_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
inputdis the root anomaly. - Two independent keymaps are normal and correct (console layout vs GUI layout can even differ;
CachyOS keeps
vconsole.confseparate 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 schemeinputwith pathsproducer/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(schemefbcon,drivers/graphics/fbcond),vesad,fbbootlogd. ptyd(local/sources/base/ptyd) — schemepty, a full POSIX line discipline already (§3d). This is the piece already closest to Linux.evdevd— NOT in the base fork; a recipe atlocal/recipes/system/evdevd. Registers schemeevdev, synthesizes kbd/mouse/touchpad, consumes/scheme/input/consumer(main.rs:66), started byinit.d/10_evdevd.service.- Seat/session:
seatd(recipelocal/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, advertiseswl_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). Libslibinput/libevdev/libxkbcommonare staged inlocal/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 inoutput()(: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 inputVecDeque<u8>drained byread()(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)
┌─▶ 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)
Component contracts (each maps to a Linux role):
| 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_events 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. |
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.)
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):
- New
inputdcontrol ops (extend the existingcontrolscheme,main.rs:358-375):GrabDevice(vt)/UngrabDevice(vt)— RedBearEVIOCGRAB. 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 (RedBearKDSKBMODE(K_OFF)).SetVtMode(vt, {Text|Graphics})— RedBearKDSETMODE.Graphicsmeans "a compositor owns this VT": stop delivering cooked console bytes; deliver raw events to the grabber.SetVtSwitch(vt, {Auto|Process})+AckVtSwitch(vt)— RedBearVT_SETMODE(VT_PROCESS)+VT_RELDISP: a graphics VT can defer/ack switches so the compositor releases DRM/input cleanly before the switch completes (Linuxchange_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 onkbmode==Xlate && vcmode==Text; raw grab delivery ongrab==Some. This is exactly the Linux gate atkeyboard.c:1562-1563. - Why in inputd: inputd is the raw core (the
input.cseat), so the grab/mode state belongs there — the same place Linux keepsdev->graband per-VTkbd->kbdmode.
4b. Seat acquisition — libseat/seatd (no systemd/logind)
- RedBear packages
seatdbut the compositor doesn't use it (§3a). The seat manager islibseat/seatd— this is the RedBear seat layer. The compositor callslibseat→seatd, which opens the/scheme/evdev+/scheme/drmhandles and issues the inputdGrabDevice/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):
seatdbecomes a first-class local Red Bear source package —local/recipes/system/seatdwith[source] path = "source", identical in kind to its siblingsevdevdandredbear-compositor. Not alocal/sources/*submodule and not alocal/patches/seatd/*.patch. Rationale: we own the implementation and integrate upstream commits into our tree on our terms (upstreamjackpot51/seatdis 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 fetchesgit = jackpot51/seatdwhile a fullsource/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: openseatd→ receive/scheme/evdevfds → feed libinput → xkbcommon. No keymap touches this before xkbcommon.
5. Does this solve input long-term, in ALL situations, incl. the FULL desktop?
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:258connect_op_regions) must compile — nothing here is verifiable until then. - P1. Upstream diff. Diff
inputd/fbcond/ptydagainst 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_charstamping frominputd(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'sapplkey/cur_chars/to_utf8behavior). 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 inputVecDequecooking (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))=OKon fb console →RB=42=OKexecutes; 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-gatedconsumer) that is always-on and pre-keymap. Repointevdevdat it (main.rs:66). evdevd applies no keymap; overrun →SYN_DROPPED. - Boot check:
evdevdsees 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
seatdto a local source package (per §4b decision): fliprecipe.toml [source]fromgit = jackpot51/seatdtopath = "source"; remove the leftover nestedsource/.git(so it's a plain vendored tree like evdevd, not an embedded clone) and the straytarget/in the recipe dir; make it actually compile (currently#TODO not compiled). - Wire
redbear-compositor(main.rs:2542) throughlibseat/seatdto open/scheme/evdev, feed libinput, apply xkbcommon; on VT-enter itGrabDevice+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:
- One keymap per path. No keymap in
inputd. Exactly one console keymap (console-kbd handler); exactly one GUI keymap (xkbcommon). Grep proof: noget_charininputd; nokeymap.lookupin fbcond. - One line discipline. All canonical/echo/CR-LF/ISIG in ptyd; none in fbcond/getty. Grep proof:
no
\r→\nand no cooked buffer outsideptyd/src/pty.rs. - 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. - Grab is exclusive and revocable. A grabbing consumer gets events exclusively; console resumes
on ungrab/close. Modeled on
input_grab_device/__input_release_device. - VT switch handshakes for graphics VTs (defer + ack), like
VT_PROCESS/VT_RELDISP. - 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.
- Upstream to Redox (best). Redox's own direction is Wayland/COSMIC + it already vendors
seatdandlibinput/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. - Compatibility-preserving interim. During transition keep
inputdable to serve both shapes: a legacy keymappedconsumer(for any current consumer that expectscharacter) 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. - 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.
seatdmust 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.