reading input

goo has no input blob. you read the keyboard yourself, once per frame, and feed the result into your tree the same way you feed any other per-frame state: poll in Tick, mutate a field, and return true on the frames something changed.

the helper for that is Goo.Input.KeyTracker. it wraps the engine's per-key query and hands you a list of every key that was just pressed this frame, plus a snapshot of the modifier keys.

the problem it solves

the engine exposes Sandbox.Input.Keyboard.Down( name ), which answers "is this one key down right now" for a single named key. there is no built-in "which keys were just pressed across the whole keyboard this frame". KeyTracker polls a catalog of keys once per frame, remembers what was down last frame, and surfaces the rising edges.

polling each frame

hold a KeyTracker in a field, call Poll() once per frame in Tick, then read JustPressed and Modifiers. reset it in OnEnabled so a re-enable starts clean.

readonly KeyTracker _tracker = new() { ReemitHeldOnModifierRise = true };

protected override void OnEnabled()
{
    base.OnEnabled();
    _tracker.Reset();
}

protected override bool Tick( float dt )
{
    _tracker.Poll();

    bool changed = false;
    foreach ( var key in _tracker.JustPressed )
    {
        _lastKey = key.DisplayName;   // any field your tree reads
        changed  = true;
    }
    return changed;
}

returning true only on the frames a key actually fired keeps rebuilds gated to real changes, the same Tick contract build method covers for continuous motion. (Rebuild() still works for mutations outside Tick, but a per-frame poll belongs on the Tick clock, not on a manual-Rebuild() loop.) building a keystroke hud works through a full capstone on this same poll-each-frame loop: a queued, animated stack of chips.

what you get back

Poll() updates two members.

JustPressed is an IReadOnlyList<KeyDescriptor>: every key whose state rose from up to down on this frame. it is rebuilt each Poll(), so read it right after polling.

each KeyDescriptor describes one key:

field type what it is
EngineName string the name Sandbox.Input.Keyboard.Down accepts (lowercase for modifiers and f1..f12, uppercase for letters)
Class KeyClass Modifier, Printable, or Special
BaseGlyph char unshifted character, '\0' for non-printable keys
ShiftGlyph char shifted character, '\0' for non-printable keys
DisplayName string human label, e.g. "Ctrl", "Esc", "F1"

Modifiers is a ModifierState snapshot of the four modifier keys:

member type what it is
Ctrl bool control held
Shift bool shift held
Alt bool alt held
Meta bool the windows / meta key held
AnyNonShift bool Ctrl, Alt, or Meta held
Any bool any of the four held

when you only care about one key, skip the list walk: Pressed( engineName ) returns true if that named key had a rising edge this frame (just-pressed, not held). call Poll() first, same as the other reads.

the catalog: KnownKeys

a default KeyTracker() polls KnownKeys.All, a curated catalog of about eighty keys: the four modifiers, A to Z, the US-layout digits and symbols, common specials (space, enter, arrows, and so on), and F1 to F12. it is not exhaustive. non-US layouts and the numpad are out of scope.

to poll a narrower set, filter KnownKeys.All and pass the result to the constructor:

var tracker = new KeyTracker( KnownKeys.All.Where( k =>
    k.EngineName is "uparrow" or "downarrow" or "leftarrow" or "rightarrow" ).ToList() );

this is the arrows-only filter Code/Demo/DocsProbes/DocsEventsInputProbeUI.cs (blessed demo) uses to prove a restricted catalog registers arrows and ignores letters. note the engine names: arrows are the Source bind names uparrow/downarrow/leftarrow/rightarrow, never up/down/left/right (those resolve to no key and silently never fire). filter by DisplayName (Up, Down, Left, Right) if you want the friendly names.

KnownKeys.Shift( baseGlyph, shiftDown ) maps a base glyph to its shifted form on a US layout ('a' to 'A', '1' to '!'), returning the input unchanged when shift is not down.

chords: ReemitHeldOnModifierRise

by default ReemitHeldOnModifierRise is false: each key reports its own rising edge and nothing more. that misses a common case. if you hold a letter and then press a modifier, the letter's edge already fired on an earlier frame, so the modifier-down frame reports only the modifier. you wanted a chord.

set ReemitHeldOnModifierRise = true and, on any frame where a modifier rises while a non-modifier key is already held, that held key re-emits as just-pressed. now "hold w, then press ctrl" produces a ctrl + w chord rather than a missed combo. the keystroke visualizer turns this on.

see also