Files
RedBear-OS/local/docs/INPUT-STACK-LINUX-ALIGNMENT-PLAN.md
T
vasilito fdf377456f 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.
2026-07-24 12:04:17 +09:00

33 KiB
Raw Blame History

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)

  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
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

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 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).

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).

1c. Console keyboard — keymap applied exactly oncedrivers/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) (:376vt_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_RELDISPvt_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 kbdmuxvt 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.
  • evdevdNOT 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.servicepackaged, started, but not integrated.
  • Compositor: redbear-compositor (local/recipes/wayland) renders via /scheme/drm/card0, advertises wl_seat, but its input is stubbedmain.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-490not 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)

                              ┌─▶ 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 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.

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 libseatseatd, 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 packagelocal/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.

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 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.