vtty - virtual TTY manager

src/kernel/include/kernel/vtty.h + src/kernel/arch/i386/proc/vtty.c.

Purpose

Up to VTTY_MAX = 4 shell tasks run concurrently. Alt+F1–F4 switches the active (focused) TTY. Only the active TTY paints to the physical framebuffer; background TTYs keep writing into their own vt_buf_t and the accumulated state is repainted atomically when the user switches back. This is Linux’s classic VT model.

Authoritative state

task_t.tty (in kernel/task.h) is authoritative for which TTY a task is bound to. vtty itself owns:

Field Purpose
vtty_nslots How many slots have been registered
vtty_current Index of the focused slot
vtty_bufs[VTTY_MAX] Per-slot vt_buf_t backing grids
vtty_pending Deferred-paint target (set in IRQ, drained in task context)

The “owning task” of slot N is resolved by walking the task pool for the live task with the lowest pid whose task->tty == N (the registered shell — exec children inherit tty but always have higher pids).

API

Function Purpose
vtty_init() Allocate the four backing grids sized to the display geometry (last row reserved for the status bar).
vtty_register() Called by shell_run at start. Assigns the calling task to the next free slot and sets task->tty.
vtty_switch(n) Called from the keyboard IRQ when Alt+F<n+1> fires. Updates focus, sends KEY_FOCUS_GAIN to the new owner, records a pending repaint target. Returns immediately - the paint is deferred.
vtty_drain_pending() Drains a deferred switch repaint: if one is pending and the calling task is on the destination TTY, repaint that TTY’s backing grid to the framebuffer and refresh the status bar (CAS’d so it fires exactly once per switch). Called from keyboard_getchar’s wait loop and from the SYS_YIELD handler — the latter so a fullscreen ring-3 app that never reads the keyboard in its draw loop (basic/lines) still flushes the repaint, letting the status bar follow a switch back to its VT. The grid is always repainted, restoring the destination VT’s prior text + colour scheme and covering any raw pixels a fullscreen app left behind (display isolation); a continuously-drawing app repaints its own graphics next frame.
vtty_buf(n) / vtty_buf_current() / vtty_buf_focused() Accessors for renderer code.
vtty_active() / vtty_count() / vtty_is_focused() State queries.

Why deferred paint

Painting the framebuffer from vtty_switch directly meant doing thousands of pixel writes inside the keyboard IRQ handler. That held the i8042’s 1-byte OBF full long enough that the edge-triggered PIC missed subsequent key edges - the same failure shape as PR #127’s regression. Recording a pending target and letting keyboard_getchar drain it in task context keeps the IRQ short.

Status bar

vtty_init shrinks each backing grid by VTTY_STATUS_ROWS = 1 so the bottom framebuffer row is reserved for the status bar (vesa_tty_paint_status in vesa_tty.c). The multiplexer is named makmux (the name it’ll carry once lifted into a userspace daemon). The bar shows a fixed-width left label, the centred 1-based indicators VT1 VT2 VT3 VT4 (matching the Alt+Fn keys) with the active slot highlighted, and an Alt+F1-F4 hint on the right; it is repainted on vtty_register, vtty_drain_pending, and vesa_tty_clear so it survives focus changes and full-screen clears. Alt+F5 toggles the left label between Makar and a live RTC clock HH:MM:SS DD/MM/YY (see vesa_tty).

Display isolation for ring-3 apps

The framebuffer-painting syscalls are gated on vtty_is_focused() so a backgrounded fullscreen app can’t bleed onto whatever VT is currently shown:

VTTY_MAX is still 4, but the task pool (MAX_TASKS) is now 32, leaving room for a fullscreen app on every VT plus headroom.