From b4e911298935483bec7e315d2eebee47562bd448 Mon Sep 17 00:00:00 2001 From: Simeon Radivoev Date: Mon, 30 Mar 2026 20:51:48 +0300 Subject: [PATCH] fix: Added keyboard focus shortcut --- src/bun/api/controls/controls.ts | 39 ++++++++++---- src/bun/api/controls/keyboard.ts | 22 ++++++++ src/bun/api/controls/manager.ts | 14 ++++- src/bun/api/controls/types.ts | 15 ++++++ src/bun/api/controls/windows.ts | 63 +++++++++++++++++++++- src/bun/api/jobs/launch-game-job.ts | 1 - src/mainview/gen/static-icon-assets.gen.ts | 2 +- 7 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 src/bun/api/controls/keyboard.ts diff --git a/src/bun/api/controls/controls.ts b/src/bun/api/controls/controls.ts index 63acda0..320c12b 100644 --- a/src/bun/api/controls/controls.ts +++ b/src/bun/api/controls/controls.ts @@ -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); } \ No newline at end of file diff --git a/src/bun/api/controls/keyboard.ts b/src/bun/api/controls/keyboard.ts new file mode 100644 index 0000000..7ad9df1 --- /dev/null +++ b/src/bun/api/controls/keyboard.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/bun/api/controls/manager.ts b/src/bun/api/controls/manager.ts index 4d2077a..6143c20 100644 --- a/src/bun/api/controls/manager.ts +++ b/src/bun/api/controls/manager.ts @@ -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); diff --git a/src/bun/api/controls/types.ts b/src/bun/api/controls/types.ts index 03298fd..5c45b37 100644 --- a/src/bun/api/controls/types.ts +++ b/src/bun/api/controls/types.ts @@ -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; +} + +export interface IKeyboardBackend +{ + update (): KeyboardState; +} + export interface Stick { x: number; // -1 → 1 diff --git a/src/bun/api/controls/windows.ts b/src/bun/api/controls/windows.ts index 4583b17..40fc7d9 100644 --- a/src/bun/api/controls/windows.ts +++ b/src/bun/api/controls/windows.ts @@ -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 = { + 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 = {} as any; + + update (): KeyboardState + { + const next: Record = {} 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; diff --git a/src/bun/api/jobs/launch-game-job.ts b/src/bun/api/jobs/launch-game-job.ts index 4572fde..06ddbb3 100644 --- a/src/bun/api/jobs/launch-game-job.ts +++ b/src/bun/api/jobs/launch-game-job.ts @@ -70,7 +70,6 @@ export class LaunchGameJob implements IJob([ ]); // Store basePath resolved from Vite config -const BASE_PATH = "./"; +const BASE_PATH = "/"; /**