AxlInput — Input Substrate

Toolkit-agnostic input substrate: mouse, keyboard, and touch as raw event sources on an AxlLoop. Unified AxlInputEvent callback type for consumers that want a single dispatch path; per-device wrappers that bridge UEFI input protocols (EFI_SIMPLE_POINTER_PROTOCOL, EFI_SIMPLE_TEXT_INPUT_PROTOCOL, EFI_ABSOLUTE_POINTER_PROTOCOL) into the loop’s existing source pattern.

Header: <axl/axl-input.h>

Role

This module is the input substrate for higher-level toolkits. Per docs/AGT-Design.md §”Substrate discipline rules”, axl-input is pure C and paradigm-agnostic — it produces raw events, not widget messages. Toolkits translate AxlInputEvent into their own dispatch model (AGT message maps, GTK-style signals, immediate-mode polling, etc.) at the layer above.

Sibling of axl-gfx in axl-sdk core. Neither depends on the other, and neither depends on a toolkit.

Loop integration

axl-input does not introduce its own queue, poll, or wait API. Each input source registers with AxlLoop using the same primitives the loop already exposes for any other UEFI event source:

  • axl_input_attach_mouse / axl_input_attach_touch wrap axl_loop_add_event around the protocol’s WaitForInput event. The dispatch trampoline calls GetState, translates the result into one or more AxlInputEvent values, and invokes the callback.

  • axl_input_attach_key wraps axl_loop_add_key_press (which internally registers EFI_SIMPLE_TEXT_INPUT_PROTOCOL’s WaitForKey) and translates each AxlInputKey into an AxlInputEvent with type = AXL_INPUT_KEY_DOWN.

The benefit is uniformity: any code path that already understands axl_loop_run, axl_loop_remove_source, timeouts, and idle callbacks “just works” with input — no parallel event-pump for the consumer to remember to drive.

Unified event

typedef struct {
    AxlInputType  type;            // discriminator (mouse / key / touch / ...)
    uint64_t      timestamp_us;    // wall-clock microseconds since boot
    int32_t       x, y;            // cursor or touch position
    uint32_t      buttons;         // AXL_INPUT_BUTTON_* bitmask
    int32_t       wheel_dx;        // horizontal wheel delta (ticks)
    int32_t       wheel_dy;        // vertical wheel delta
    uint32_t      keycode;         // raw scan code (key events)
    uint32_t      unicode;         // translated codepoint (0 if none)
    uint32_t      modifiers;       // AXL_INPUT_MOD_* bitmask
} AxlInputEvent;

Fields are populated based on .type; unused fields are zero. The pointer passed to the callback is valid only for the duration of the call — copy out anything you want to keep.

Event kinds shipped in v0.1:

AxlInputType

Emitted by

When

AXL_INPUT_MOUSE_MOVE

mouse

non-zero relative motion

AXL_INPUT_MOUSE_BUTTON_DOWN/UP

mouse

button-state transition

AXL_INPUT_MOUSE_WHEEL

mouse

non-zero Z motion

AXL_INPUT_KEY_DOWN

keyboard

each key press

AXL_INPUT_KEY_UP

(deferred)

requires EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL

AXL_INPUT_TOUCH_DOWN

touch

first contact (active-buttons 0 → non-zero)

AXL_INPUT_TOUCH_UP

touch

contact end (non-zero → 0)

AXL_INPUT_TOUCH_MOVE

touch

active contact, position changed

KEY_DOWN events carry modifier + lock state in modifiers: held SHIFT / CTRL / ALT / META (left/right-distinct bits plus side-agnostic masks — AXL_INPUT_MOD_SHIFT == LSHIFT | RSHIFT) and CAPS_LOCK / NUM_LOCK / SCROLL_LOCK. These come from EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL; when the firmware doesn’t publish it (e.g. a serial console), modifiers == 0 — treat absent modifiers as “none”. Only KEY_DOWN fires: UEFI delivers no key-up or standalone-modifier events. (The live keyboard read is exercised on real hardware, not in the QEMU serial test harness.)

Usage

A single callback can demultiplex across all three sources:

#include <axl.h>

static bool
on_input(const AxlInputEvent *ev, void *data)
{
    AxlLoop *loop = (AxlLoop *)data;

    switch (ev->type) {
    case AXL_INPUT_MOUSE_MOVE:
        ui_cursor_to(ev->x, ev->y);
        break;
    case AXL_INPUT_MOUSE_BUTTON_DOWN:
        ui_click(ev->buttons);
        break;
    case AXL_INPUT_KEY_DOWN:
        if (ev->unicode == 'q') {
            axl_loop_quit(loop);
            return AXL_SOURCE_REMOVE;
        }
        break;
    case AXL_INPUT_TOUCH_DOWN:
        ui_touch_begin(ev->x, ev->y);
        break;
    default:
        break;
    }
    return AXL_SOURCE_CONTINUE;
}

int main(void) {
    AxlLoop *loop = axl_loop_new();
    axl_input_attach_mouse(loop, on_input, loop);
    axl_input_attach_key  (loop, on_input, loop);
    axl_input_attach_touch(loop, on_input, loop);
    axl_loop_run(loop);
    axl_loop_unref(loop);
    return 0;
}

Each attach_* returns the loop source ID (use with axl_loop_remove_source to detach), or 0 on failure: the protocol isn’t available, a source of that kind is already attached, or arguments were NULL.

Per-device notes

Mouse (axl_input_attach_mouse). Locates EFI_SIMPLE_POINTER_PROTOCOL via LocateProtocol. Cursor positions are accumulated relative deltas starting at (0, 0) — most firmware reports motion as deltas, not absolute screen coordinates. Callers wanting screen-bounded positions should clamp in the callback. Each dispatch may produce multiple discrete events (motion + button + wheel) from a single GetState call.

Keyboard (axl_input_attach_key). Thin translator over axl_loop_add_key_press — the existing loop primitive does the protocol handling. The wrapper exists so a consumer that wants one callback for mouse + keyboard + touch doesn’t have to maintain a separate AxlKeyCallback. Callers who already have a key-only flow can keep using axl_loop_add_key_press directly.

Touch (axl_input_attach_touch). Locates EFI_ABSOLUTE_POINTER_PROTOCOL. Positions are reported in the device’s native (CurrentX, CurrentY) range — see EFI_ABSOLUTE_POINTER_MODE’s AbsoluteMin/Max for the coordinate system. Callers wanting screen pixels should rescale in the callback.

v0.1 constraints

  • Single source per device kind per process. Multi-device or hotplug support can be added when a consumer requires it.

  • Mouse / touch positions are not screen-clamped — the substrate doesn’t know what the consumer’s coordinate system is.

Visual demo

sdk/examples/input-demo.c attaches all three sources, runs the loop for 5 seconds, and renders a live status panel via axl-gfx. Run with scripts/run-qemu.sh out/x64/input-demo.efi.

API Reference

Toolkit-agnostic input event types — codepoints, button + modifier bitfields, the unified AxlInputEvent discriminated union.

Source registration follows axl-loop’s existing pattern:

  • Keyboard already has axl_loop_add_key_press in <axl/axl-loop.h> (uses AxlInputKey — keep for legacy consumers; new code uses the unified AxlInputEvent via the wrappers below).

  • Mouse + touch are added in subsequent phases via axl_input_attach_mouse / axl_input_attach_touch, which register EFI_SIMPLE_POINTER_PROTOCOL / EFI_ABSOLUTE_POINTER_PROTOCOL as axl-loop sources through axl_loop_add_event.

Per substrate discipline rule 3 (docs/AGT-Design.md): this module produces raw events only — no widget dispatch, no toolkit-specific dialect. Toolkits translate AxlInputEvent into their own model (AGT message maps, signal/slot, etc.) at the layer above.

static bool on_input(const AxlInputEvent *ev, void *data) {
    (void)data;
    switch (ev->type) {
    case AXL_INPUT_MOUSE_MOVE:        ui_move(ev->x, ev->y); break;
    case AXL_INPUT_MOUSE_BUTTON_DOWN: ui_click(ev->buttons); break;
    case AXL_INPUT_KEY_DOWN:          ui_key(ev->unicode); break;
    default: break;
    }
    return AXL_SOURCE_CONTINUE;
}
axl_input_attach_mouse(loop, on_input, NULL);
axl_input_attach_key(loop, on_input, NULL);

Defines

AXL_INPUT_BUTTON_LEFT
AXL_INPUT_BUTTON_RIGHT
AXL_INPUT_BUTTON_MIDDLE
AXL_INPUT_MOD_LSHIFT
AXL_INPUT_MOD_RSHIFT
AXL_INPUT_MOD_LCTRL
AXL_INPUT_MOD_RCTRL
AXL_INPUT_MOD_LALT
AXL_INPUT_MOD_RALT
AXL_INPUT_MOD_LMETA
AXL_INPUT_MOD_RMETA
AXL_INPUT_MOD_CAPS_LOCK
AXL_INPUT_MOD_NUM_LOCK
AXL_INPUT_MOD_SCROLL_LOCK
AXL_INPUT_MOD_SHIFT
AXL_INPUT_MOD_CTRL
AXL_INPUT_MOD_ALT
AXL_INPUT_MOD_META

Typedefs

typedef bool (*AxlInputCallback)(const AxlInputEvent *event, void *data)

Callback signature for unified input events. Return AXL_SOURCE_CONTINUE to keep the source active or AXL_SOURCE_REMOVE to detach (same convention as AxlLoopCallback / AxlKeyCallback).

typedef struct AxlLoop AxlLoop

Enums

enum AxlInputType

Type tag for AxlInputEvent.

Values:

enumerator AXL_INPUT_NONE

Sentinel — not a real event.

enumerator AXL_INPUT_MOUSE_MOVE

Cursor moved to (.x, .y)

enumerator AXL_INPUT_MOUSE_BUTTON_DOWN

Mouse button pressed (see .buttons)

enumerator AXL_INPUT_MOUSE_BUTTON_UP

Mouse button released.

enumerator AXL_INPUT_MOUSE_WHEEL

Scroll wheel rotated (see .wheel_dx/dy)

enumerator AXL_INPUT_KEY_DOWN

Key pressed (see .keycode, .unicode, .modifiers; Ctrl+letter encoding below)

enumerator AXL_INPUT_KEY_UP

Key released (where the platform reports it)

enumerator AXL_INPUT_TOUCH_DOWN

Touch contact began at (.x, .y)

enumerator AXL_INPUT_TOUCH_UP

Touch contact ended.

enumerator AXL_INPUT_TOUCH_MOVE

Touch contact moved.

Functions

char axl_input_ctrl_letter(uint32_t unicode, uint32_t modifiers)

Decode a Ctrl+<letter> chord from a KEY_DOWN event, collapsing the two device-dependent encodings documented above into a single letter so a consumer can match Ctrl-chord shortcuts portably. Pass the event’s unicode and modifiers.

Returns:

the lowercase letter ‘a’..’z’ the chord names (Ctrl+A → ‘a’), or 0 when the inputs are not a Ctrl+<letter> chord. The four chords whose C0 codes double as dedicated editing keys — Ctrl+H / I / J / M (Backspace / Tab / LF / CR) — return 0, so a consumer never shadows those keys with a chord.

uint32_t axl_input_attach_mouse(AxlLoop *loop, AxlInputCallback cb, void *data)

Register mouse input as an event source on the loop.

Locates EFI_SIMPLE_POINTER_PROTOCOL, registers its WaitForInput event with the loop, and on each dispatch reads cursor state via GetState. Emits AXL_INPUT_MOUSE_MOVE for non-zero relative motion, AXL_INPUT_MOUSE_BUTTON_DOWN/UP on button-state transitions (debounced internally), AXL_INPUT_MOUSE_WHEEL for non-zero Z motion. Multiple discrete events may fire per dispatch.

Cursor position is accumulated relative deltas starting at (0, 0); callers wanting screen-bounded positions should clamp in their callback. Only one mouse source per process for v0.1.

Returns:

source ID for axl_loop_remove_source, or 0 on failure (NULL args, EFI_SIMPLE_POINTER_PROTOCOL not available, or a mouse source is already attached).

uint32_t axl_input_attach_key(AxlLoop *loop, AxlInputCallback cb, void *data)

Register keyboard input as an event source on the loop.

Thin wrapper over axl_loop_add_key_press that translates each AxlInputKey (scan_code + unicode_char) into a unified AxlInputEvent (type = AXL_INPUT_KEY_DOWN, keycode = scan_code, unicode = unicode_char, modifiers = AXL_INPUT_MOD_*). This lets callers register a single AxlInputCallback for mouse + keyboard + (future) touch instead of separate per-device callbacks.

event.modifiers carries held shift/ctrl/alt/meta (left/right distinct, plus side-agnostic masks) and caps/num/scroll lock state when the firmware publishes EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL; it is 0 when modifiers can’t be read (no ex protocol, e.g. serial). Only AXL_INPUT_KEY_DOWN fires — UEFI delivers no key-up or standalone-modifier events.

For how a held Ctrl is encoded — it differs between the physical keyboard (letter + AXL_INPUT_MOD_CTRL) and the serial console (folded C0 control code, no modifiers) — see the Ctrl+letter note on AxlInputEvent.unicode above; a portable consumer must handle both.

Only one keyboard source per process for v0.1.

Returns:

source ID for axl_loop_remove_source, or 0 on failure (NULL args or a keyboard source already attached).

uint32_t axl_input_attach_touch(AxlLoop *loop, AxlInputCallback cb, void *data)

Register touch input as an event source on the loop.

Locates EFI_ABSOLUTE_POINTER_PROTOCOL, registers its WaitForInput event with the loop, and on each dispatch reads absolute position via GetState. Emits AXL_INPUT_TOUCH_DOWN on first contact (ActiveButtons transition 0 → non-zero), AXL_INPUT_TOUCH_UP on contact end (non-zero → 0), AXL_INPUT_TOUCH_MOVE while contact is active and position changes.

Position is reported in the protocol’s (CurrentX, CurrentY) range — see EFI_ABSOLUTE_POINTER_MODE’s AbsoluteMin/Max for the device’s native coordinate system. Callers wanting screen pixels should rescale in their callback.

Only one touch source per process for v0.1.

Returns:

source ID for axl_loop_remove_source, or 0 on failure (NULL args, EFI_ABSOLUTE_POINTER_PROTOCOL not available, or a touch source already attached).

struct AxlInputEvent
#include <axl-input.h>

Raw input event — unified across keyboard / mouse / touch. Fields are populated based on .type; unused fields are zero. Passed by const AxlInputEvent * into the registered callback; the pointer is valid only for the duration of the call.

Public Members

AxlInputType type

Event kind (discriminator)

uint64_t timestamp_us

Wall-clock microseconds since boot.

int32_t x

Cursor / touch x in pixels.

int32_t y

Cursor / touch y in pixels.

uint32_t buttons

Current button state (AXL_INPUT_BUTTON_*)

int32_t wheel_dx

Horizontal wheel delta (notch ticks)

int32_t wheel_dy

Vertical wheel delta.

uint32_t keycode

Raw scan code (key events)

uint32_t unicode

Translated codepoint (0 if none) — see Ctrl+letter note below.

uint32_t modifiers

Modifier state (AXL_INPUT_MOD_*)