SOURCE / PINNED RELEASE
Made of little things.
Pickle Rustlers
- Release
- 039e51f55e34…
- Author-recorded commit
- 40608586a8a1…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35
Archive hash verified: f38e99afa0992052…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { keys } from '@napplet/sdk';
import { runtimeHasDomain } from '../domain-availability.js';
export type KeyboardAction = 'shoot' | 'lasso' | 'pause';
const actions = [
{ name: 'shoot', label: 'Shoot / confirm', defaultKey: ' ' },
{ name: 'lasso', label: 'Throw lasso', defaultKey: 'e' },
{ name: 'pause', label: 'Pause / resume', defaultKey: 'p' },
] as const;
const aliases: Record<KeyboardAction, string[]> = {
shoot: [' ', 'j', 'enter'], lasso: ['e', 'shift', 'k', 'l'], pause: ['p', 'escape'],
};
function editable(target: EventTarget | null): boolean {
return target instanceof HTMLElement &&
(target.isContentEditable || Boolean(target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])')));
}
/** Local held movement plus optional shell-owned discrete actions. */
export class KeyboardInput {
private held = new Map<string, string>();
private pressed = new Set<KeyboardAction>();
private registered = new Set<KeyboardAction>();
private handles: Array<{ close(): void }> = [];
private listeners = new AbortController();
private currentEvent: KeyboardEvent | null = null;
private disposed = false;
constructor(private activity: () => void, private allowAction: (action: KeyboardAction) => boolean) {
const signal = this.listeners.signal;
// The injected shim runs later at document capture. Remember the event for
// its parameterless callback, without intercepting forwarding or bindings.
window.addEventListener('keydown', (event) => {
this.currentEvent = event;
// Trusted browser events may run microtasks between listeners. Keep this
// through document capture, then release it after the dispatch task.
setTimeout(() => { if (this.currentEvent === event) this.currentEvent = null; }, 0);
}, { capture: true, signal });
window.addEventListener('keydown', (event) => {
try { this.keydown(event); } finally { this.currentEvent = null; }
}, { signal });
window.addEventListener('keyup', (event) => this.held.delete(event.code || event.key.toLowerCase()), { signal });
window.addEventListener('blur', () => this.clear(), { signal });
document.addEventListener('visibilitychange', () => this.clear(), { signal });
if (runtimeHasDomain('keys')) void this.register();
}
private eligible(event: KeyboardEvent): boolean {
return !event.isComposing && !editable(event.target);
}
private trigger(action: KeyboardAction): void {
const event = this.currentEvent;
if (this.disposed || document.hidden || !document.hasFocus() || editable(document.activeElement) ||
(event && (event.repeat || !this.eligible(event))) || !this.allowAction(action)) return;
this.pressed.add(action);
this.activity();
}
private async register(): Promise<void> {
await Promise.all(actions.map(async ({ name, label, defaultKey }) => {
try {
const id = `pickle-rustlers.${name}`;
await keys.registerAction({ id, label, defaultKey });
let handle: { close(): void };
try {
const subscription = keys.onAction(id, () => this.trigger(name));
handle = { close: () => { try { subscription.close(); } finally { keys.unregisterAction(id); } } };
} catch (error) {
keys.unregisterAction(id);
throw error;
}
if (this.disposed) { handle.close(); return; }
this.handles.push(handle);
this.registered.add(name);
this.pressed.delete(name);
} catch (error) {
// A missing or denied optional operation must leave local controls usable.
console.warn(`Keyboard action ${name} unavailable; using local controls`, error);
}
}));
}
private keydown(event: KeyboardEvent): void {
if (event.defaultPrevented || !this.eligible(event) || event.ctrlKey || event.altKey || event.metaKey) return;
const key = event.key.toLowerCase();
if (['w', 'a', 's', 'd', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright'].includes(key)) {
this.held.set(event.code || key, key);
if (key.startsWith('arrow')) event.preventDefault();
this.activity();
}
for (const { name } of actions) {
if (!this.registered.has(name) && aliases[name].includes(key)) {
if (key === ' ') event.preventDefault();
this.trigger(name);
}
}
}
down(...names: string[]): boolean { return [...this.held.values()].some((key) => names.includes(key)); }
read(): Set<KeyboardAction> {
const result = new Set(this.pressed);
this.pressed.clear();
return result;
}
clear(): void { this.held.clear(); this.pressed.clear(); }
dispose(): void {
if (this.disposed) return;
this.disposed = true;
this.listeners.abort();
this.clear();
for (const handle of this.handles.splice(0)) {
try { handle.close(); } catch (error) { console.warn('Could not release keyboard action', error); }
}
this.registered.clear();
}
}
