SOURCE / PINNED RELEASE
Made of little things.
Pünktlich!
- Release
- 9dc6c97948c0…
- Author-recorded commit
- aebcea49a666…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-bf1ae466031
Archive hash verified: 7705a83d7bdb3f9b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { KEYS_DOMAIN, keys } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
// Keyboard controls as NAP-KEYS actions. The shell owns the bindings (and may
// let the player rebind them); the runtime's keys shim turns a bound keydown
// into the action. Without the keys domain, or for an action the shell left
// unbound, the listed fallback codes are handled locally instead.
export type KeyAction = {
/** Stable `feature.action` id so the shell can remember rebinds. */
id: string;
label: string;
defaultKey: string;
/** KeyboardEvent.code values handled locally when the shell has no binding. */
fallbackCodes: string[];
/** Fallback key names for hints, e.g. "→ / D". */
fallbackLabel: string;
/** `repeat` is true for auto-repeat while a key is held. */
onPress: (repeat: boolean) => void;
/** Hold actions get a release when the key that started them goes up. */
onRelease?: () => void;
};
// Actions pushed by the shell carry no key-up; a held shell key repeats the
// action, so a hold lasts this long past the latest press.
const SHELL_HOLD_MS = 280;
const shellBindings = new Map<string, string>();
let currentKeydown: KeyboardEvent | null = null;
export function bindKeyboard(actions: KeyAction[], onBindingsChange: () => void): void {
const heldBy = new Map<string, string>();
const shellHoldTimers = new Map<string, number>();
const press = (action: KeyAction) => {
const event = currentKeydown;
if (event) {
action.onPress(event.repeat);
if (action.onRelease) heldBy.set(action.id, event.code);
return;
}
action.onPress(false);
if (action.onRelease) {
window.clearTimeout(shellHoldTimers.get(action.id));
shellHoldTimers.set(action.id, window.setTimeout(action.onRelease, SHELL_HOLD_MS));
}
};
// Window capture runs before the shim's document-capture listener, so a
// locally triggered action can see the keydown that caused it.
window.addEventListener('keydown', (event) => { currentKeydown = event; }, true);
window.addEventListener('keydown', (event) => {
currentKeydown = null;
if (event.defaultPrevented) return; // Already consumed by a shell binding.
const action = actions.find((a) => !shellBindings.has(a.id) && a.fallbackCodes.includes(event.code));
if (!action) return;
event.preventDefault();
action.onPress(event.repeat);
if (action.onRelease) heldBy.set(action.id, event.code);
});
window.addEventListener('keyup', (event) => {
for (const [id, code] of heldBy) {
if (code !== event.code) continue;
heldBy.delete(id);
actions.find((a) => a.id === id)?.onRelease?.();
}
});
window.addEventListener('blur', () => {
for (const id of heldBy.keys()) actions.find((a) => a.id === id)?.onRelease?.();
heldBy.clear();
});
if (!runtimeHasDomain(KEYS_DOMAIN)) return;
for (const action of actions) {
keys
.register({ id: action.id, label: action.label, defaultKey: action.defaultKey }, () => press(action))
.then((result) => {
if (!result.binding) return;
shellBindings.set(action.id, result.binding);
onBindingsChange();
window.addEventListener('pagehide', () => result.close(), { once: true });
})
.catch(() => {
// Keys is optional; this action keeps its local fallback keys.
});
}
}
/** Hint text for an action: the shell's binding, or the local fallback keys. */
export function keyLabel(action: KeyAction): string {
const binding = shellBindings.get(action.id);
if (!binding) return action.fallbackLabel;
return binding === ' ' ? 'Leertaste' : binding;
}
