fix: Added keyboard focus shortcut

This commit is contained in:
Simeon Radivoev 2026-03-30 20:51:48 +03:00
parent ccc5a05ed7
commit b4e9112989
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
7 changed files with 141 additions and 15 deletions

View file

@ -5,9 +5,23 @@ import { GamepadManager } from './manager';
export default async function Initialize ()
{
let startSelectPressed = false;
let endPressed = false;
const manager = new GamepadManager();
function handleFocus ()
{
const launchGameTask = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob);
if (launchGameTask)
{
launchGameTask.abort('exit');
taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300));
} else
{
events.emit('focus');
}
}
setInterval(() =>
{
for (const pad of manager.getGamepads())
@ -20,21 +34,26 @@ export default async function Initialize ()
if (!startSelectPressed)
{
startSelectPressed = true;
console.log("Focus");
const launchGameTask = taskQueue.findJob(LaunchGameJob.id, LaunchGameJob);
if (launchGameTask)
{
launchGameTask.abort('exit');
taskQueue.waitForJob(LaunchGameJob.id).then(() => setTimeout(() => events.emit('focus'), 300));
} else
{
events.emit('focus');
}
handleFocus();
}
} else
{
startSelectPressed = false;
}
}
const keyboard = manager.getKeyboard();
const keyState = keyboard.update();
if (keyState?.keys.End && keyState?.keys.LeftControl)
{
if (!endPressed)
{
endPressed = true;
handleFocus();
}
} else
{
endPressed = false;
}
}, 100);
}

View file

@ -0,0 +1,22 @@
import type { IKeyboardBackend, KeyboardState } from "./types";
export class Keybaord
{
private backend: IKeyboardBackend | undefined;
async init ()
{
if (process.platform === "win32")
{
const { KeyboardWindows } = await import("./windows");
this.backend = new KeyboardWindows();
} else
{
}
}
update (): KeyboardState | null
{
return this.backend?.update() ?? null;
}
}

View file

@ -1,19 +1,23 @@
import { Gamepad } from "./gamepad";
import { platform } from "os";
import { Keybaord } from "./keyboard";
export class GamepadManager
{
private gamepads: Gamepad[] = [];
private keyboard: Keybaord;
private scanInterval: any;
constructor()
{
this.scanGamepads();
this.keyboard = new Keybaord();
this.keyboard.init();
// scan every second for new/disconnected devices
this.scanInterval = setInterval(() => this.scanGamepads(), 1000);
this.scanInterval = setInterval(async () => this.scanGamepads(), 1000);
}
private scanGamepads ()
private async scanGamepads ()
{
const max = platform() === "win32" ? 4 : 8; // max controllers
for (let i = 0; i < max; i++)
@ -23,6 +27,7 @@ export class GamepadManager
try
{
const pad = new Gamepad(i);
await pad.init();
if (pad.update())
{
this.gamepads[i] = pad;
@ -42,6 +47,11 @@ export class GamepadManager
}
}
getKeyboard ()
{
return this.keyboard;
}
getGamepads ()
{
return this.gamepads.filter(Boolean);

View file

@ -5,6 +5,21 @@ export type ButtonName =
| "START" | "SELECT"
| "L3" | "R3";
export type KeyCode =
| "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight"
| "KeyW" | "KeyA" | "KeyS" | "KeyD"
| "Enter" | "Escape" | "Space" | "End" | "LeftShift" | "RightShift" | "LeftControl" | "RightControl" | "LeftAlt" | "RightAlt";
export interface KeyboardState
{
keys: Record<KeyCode, boolean>;
}
export interface IKeyboardBackend
{
update (): KeyboardState;
}
export interface Stick
{
x: number; // -1 → 1

View file

@ -1,11 +1,72 @@
import { IGamepadBackend, GamepadState, ButtonName } from "./types";
import { IGamepadBackend, GamepadState, ButtonName, IKeyboardBackend, KeyboardState, KeyCode } from "./types";
import { dlopen, FFIType } from "bun:ffi";
const xinput = dlopen("xinput1_4.dll", {
XInputGetState: { args: [FFIType.u32, FFIType.ptr], returns: FFIType.u32 },
});
const user32 = dlopen("user32.dll", {
GetAsyncKeyState: {
args: [FFIType.i32],
returns: FFIType.i16,
},
});
// Virtual key codes
const VK: Record<KeyCode, number> = {
ArrowUp: 0x26,
ArrowDown: 0x28,
ArrowLeft: 0x25,
ArrowRight: 0x27,
KeyW: 0x57,
KeyA: 0x41,
KeyS: 0x53,
KeyD: 0x44,
Enter: 0x0D,
Escape: 0x1B,
Space: 0x20,
End: 0x23,
LeftShift: 0xA0,
RightShift: 0xA1,
LeftControl: 0xA2,
RightControl: 0xA3,
LeftAlt: 0xA4,
RightAlt: 0xA5,
};
const ERROR_SUCCESS = 0;
export class KeyboardWindows implements IKeyboardBackend
{
private keys: Record<KeyCode, boolean> = {} as any;
update (): KeyboardState
{
const next: Record<KeyCode, boolean> = {} as any;
// default all keys to false
// poll keys globally
for (const vkStr in VK)
{
const vk = Number(VK[vkStr as KeyCode]);
const key = vkStr;
const state = user32.symbols.GetAsyncKeyState(vk);
if ((state & 0x8000) !== 0)
{
next[key as KeyCode] = true;
}
}
this.keys = next;
return { keys: this.keys };
}
}
export class GamepadWindows implements IGamepadBackend
{
private index: number;

View file

@ -70,7 +70,6 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
// We have full control over launching integrated emulators better to use bun spawn
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs], {
cwd: this.validCommand.startDir,
windowsVerbatimArguments: true,
signal: context.abortSignal
});

View file

@ -464,7 +464,7 @@ const assets = new Set<string>([
]);
// Store basePath resolved from Vite config
const BASE_PATH = "./";
const BASE_PATH = "/";
/**