From 5c95dff2d6df1b0f8aac0620ce46e0bad59fde23 Mon Sep 17 00:00:00 2001 From: Matthew Turk Date: Sat, 21 Feb 2026 11:27:56 -0600 Subject: [PATCH 1/7] Implement keyboard locking for EJS fullscreen mode --- frontend/src/views/Player/EmulatorJS/Base.vue | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/src/views/Player/EmulatorJS/Base.vue b/frontend/src/views/Player/EmulatorJS/Base.vue index 9e7f4c86d..5ac7000ed 100644 --- a/frontend/src/views/Player/EmulatorJS/Base.vue +++ b/frontend/src/views/Player/EmulatorJS/Base.vue @@ -173,6 +173,22 @@ onMounted(async () => { emitter?.on("saveSelected", selectSave); emitter?.on("stateSelected", selectState); + document.addEventListener("fullscreenchange", () => { + if (fullScreenOnPlay.value && "keyboard" in navigator) { + if (document.fullscreenElement && navigator.keyboard.lock) { + navigator.keyboard.lock([ + "Escape", + "Tab", + "AltLeft", + "ControlLeft", + "MetaLeft", + ]); + } else if (!document.fullscreenElement && navigator.keyboard.unlock) { + navigator.keyboard.unlock(); + } + } + }); + // Determine default tab and selection (mutually exclusive) const compatibleStates = rom.value.user_states.filter( (s) => !s.emulator || s.emulator === supportedCores.value[0], From 9b51df96a8fb3395ada1c8bebf44b92b42a03221 Mon Sep 17 00:00:00 2001 From: Matthew Turk Date: Sat, 21 Feb 2026 14:25:07 -0600 Subject: [PATCH 2/7] Add config option for keyboard lock in EJS --- backend/config/config_manager.py | 11 +++++++++++ backend/endpoints/configs.py | 1 + backend/endpoints/responses/config.py | 1 + examples/config.example.yml | 1 + frontend/src/__generated__/models/ConfigResponse.ts | 1 + frontend/src/stores/config.ts | 1 + frontend/src/views/Player/EmulatorJS/Base.vue | 10 +++++----- 7 files changed, 21 insertions(+), 5 deletions(-) diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 79ec338fd..6d0cddf8b 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -114,6 +114,7 @@ class Config: HIGH_PRIO_STRUCTURE_PATH: str EJS_DEBUG: bool EJS_CACHE_LIMIT: int | None + EJS_KEYBOARD_LOCK: bool EJS_DISABLE_AUTO_UNLOAD: bool EJS_DISABLE_BATCH_BOOTUP: bool EJS_NETPLAY_ENABLED: bool @@ -275,6 +276,9 @@ class ConfigManager: EJS_CACHE_LIMIT=pydash.get( self._raw_config, "emulatorjs.cache_limit", None ), + EJS_KEYBOARD_LOCK=pydash.get( + self._raw_config, "emulatorjs.keyboard_lock", False + ), EJS_DISABLE_AUTO_UNLOAD=pydash.get( self._raw_config, "emulatorjs.disable_auto_unload", False ), @@ -475,6 +479,12 @@ class ConfigManager: ) sys.exit(3) + if not isinstance(self.config.EJS_KEYBOARD_LOCK, bool): + log.critical( + "Invalid config.yml: emulatorjs.keyboard_lock must be a boolean" + ) + sys.exit(3) + if not isinstance(self.config.EJS_DISABLE_AUTO_UNLOAD, bool): log.critical( "Invalid config.yml: emulatorjs.disable_auto_unload must be a boolean" @@ -601,6 +611,7 @@ class ConfigManager: "emulatorjs": { "debug": self.config.EJS_DEBUG, "cache_limit": self.config.EJS_CACHE_LIMIT, + "keyboard_lock": self.config.EJS_KEYBOARD_LOCK, "disable_auto_unload": self.config.EJS_DISABLE_AUTO_UNLOAD, "disable_batch_bootup": self.config.EJS_DISABLE_BATCH_BOOTUP, "netplay": { diff --git a/backend/endpoints/configs.py b/backend/endpoints/configs.py index 712025260..1e9b2fa20 100644 --- a/backend/endpoints/configs.py +++ b/backend/endpoints/configs.py @@ -37,6 +37,7 @@ def get_config() -> ConfigResponse: SKIP_HASH_CALCULATION=cfg.SKIP_HASH_CALCULATION, EJS_DEBUG=cfg.EJS_DEBUG, EJS_CACHE_LIMIT=cfg.EJS_CACHE_LIMIT, + EJS_KEYBOARD_LOCK=cfg.EJS_KEYBOARD_LOCK, EJS_DISABLE_AUTO_UNLOAD=cfg.EJS_DISABLE_AUTO_UNLOAD, EJS_DISABLE_BATCH_BOOTUP=cfg.EJS_DISABLE_BATCH_BOOTUP, EJS_NETPLAY_ENABLED=cfg.EJS_NETPLAY_ENABLED, diff --git a/backend/endpoints/responses/config.py b/backend/endpoints/responses/config.py index d9ebe126e..66e28b090 100644 --- a/backend/endpoints/responses/config.py +++ b/backend/endpoints/responses/config.py @@ -17,6 +17,7 @@ class ConfigResponse(TypedDict): SKIP_HASH_CALCULATION: bool EJS_DEBUG: bool EJS_CACHE_LIMIT: int | None + EJS_KEYBOARD_LOCK: bool EJS_DISABLE_AUTO_UNLOAD: bool EJS_DISABLE_BATCH_BOOTUP: bool EJS_NETPLAY_ENABLED: bool diff --git a/examples/config.example.yml b/examples/config.example.yml index cd65fd61d..04e382537 100644 --- a/examples/config.example.yml +++ b/examples/config.example.yml @@ -137,6 +137,7 @@ # emulatorjs: # debug: true # Available options will be logged to the browser console # cache_limit: null # Cache limit per ROM (in bytes) +# keyboard_lock: false # Should keyboard be locked in fullscreen? # disable_batch_bootup: false # disable_auto_unload: false # settings: diff --git a/frontend/src/__generated__/models/ConfigResponse.ts b/frontend/src/__generated__/models/ConfigResponse.ts index 5b9cf4681..114cee971 100644 --- a/frontend/src/__generated__/models/ConfigResponse.ts +++ b/frontend/src/__generated__/models/ConfigResponse.ts @@ -18,6 +18,7 @@ export type ConfigResponse = { SKIP_HASH_CALCULATION: boolean; EJS_DEBUG: boolean; EJS_CACHE_LIMIT: (number | null); + EJS_KEYBOARD_LOCK: boolean; EJS_DISABLE_AUTO_UNLOAD: boolean; EJS_DISABLE_BATCH_BOOTUP: boolean; EJS_NETPLAY_ENABLED: boolean; diff --git a/frontend/src/stores/config.ts b/frontend/src/stores/config.ts index 926083a92..da4e8efe5 100644 --- a/frontend/src/stores/config.ts +++ b/frontend/src/stores/config.ts @@ -26,6 +26,7 @@ const defaultConfig = { EJS_DEBUG: false, EJS_NETPLAY_ENABLED: false, EJS_CACHE_LIMIT: null, + EJS_KEYBOARD_LOCK: false, EJS_DISABLE_AUTO_UNLOAD: false, EJS_DISABLE_BATCH_BOOTUP: false, EJS_NETPLAY_ICE_SERVERS: [], diff --git a/frontend/src/views/Player/EmulatorJS/Base.vue b/frontend/src/views/Player/EmulatorJS/Base.vue index 5ac7000ed..2be647501 100644 --- a/frontend/src/views/Player/EmulatorJS/Base.vue +++ b/frontend/src/views/Player/EmulatorJS/Base.vue @@ -62,7 +62,7 @@ async function onPlay() { fullScreen.value = fullScreenOnPlay.value; playing.value = true; - const { EJS_NETPLAY_ENABLED } = configStore.config; + const { EJS_NETPLAY_ENABLED, EJS_KEYBOARD_LOCK } = configStore.config; const EMULATORJS_VERSION = EJS_NETPLAY_ENABLED ? "nightly" : "4.2.3"; const LOCAL_PATH = "/assets/emulatorjs/data"; const CDN_PATH = `https://cdn.emulatorjs.org/${EMULATORJS_VERSION}/data`; @@ -173,8 +173,8 @@ onMounted(async () => { emitter?.on("saveSelected", selectSave); emitter?.on("stateSelected", selectState); - document.addEventListener("fullscreenchange", () => { - if (fullScreenOnPlay.value && "keyboard" in navigator) { + if (configStore.config.EJS_KEYBOARD_LOCK && "keyboard" in navigator) { + document.addEventListener("fullscreenchange", () => { if (document.fullscreenElement && navigator.keyboard.lock) { navigator.keyboard.lock([ "Escape", @@ -186,8 +186,8 @@ onMounted(async () => { } else if (!document.fullscreenElement && navigator.keyboard.unlock) { navigator.keyboard.unlock(); } - } - }); + }); + } // Determine default tab and selection (mutually exclusive) const compatibleStates = rom.value.user_states.filter( From 5d21c6fefe1e2c8ca7d59e42e3c0aae5523d9bb5 Mon Sep 17 00:00:00 2001 From: Matthew Turk Date: Sat, 21 Feb 2026 14:34:49 -0600 Subject: [PATCH 3/7] Add tests --- backend/tests/config/fixtures/config/config.yml | 1 + backend/tests/config/test_config_loader.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/backend/tests/config/fixtures/config/config.yml b/backend/tests/config/fixtures/config/config.yml index 8975f4c94..8b4997b17 100644 --- a/backend/tests/config/fixtures/config/config.yml +++ b/backend/tests/config/fixtures/config/config.yml @@ -55,6 +55,7 @@ scan: emulatorjs: debug: true cache_limit: 1000 + keyboard_lock: true disable_auto_unload: true disable_batch_bootup: true netplay: diff --git a/backend/tests/config/test_config_loader.py b/backend/tests/config/test_config_loader.py index c1792858a..984c9fb75 100644 --- a/backend/tests/config/test_config_loader.py +++ b/backend/tests/config/test_config_loader.py @@ -29,6 +29,7 @@ def test_config_loader(): assert loader.config.EJS_DISABLE_AUTO_UNLOAD assert loader.config.EJS_DISABLE_BATCH_BOOTUP assert loader.config.EJS_CACHE_LIMIT == 1000 + assert loader.config.EJS_KEYBOARD_LOCK assert loader.config.EJS_NETPLAY_ENABLED assert loader.config.EJS_NETPLAY_ICE_SERVERS == [ {"urls": "stun:stun.relay.metered.ca:80"}, @@ -80,6 +81,7 @@ def test_empty_config_loader(): assert not loader.config.SKIP_HASH_CALCULATION assert not loader.config.EJS_DEBUG assert loader.config.EJS_CACHE_LIMIT is None + assert not loader.config.EJS_KEYBOARD_LOCK assert not loader.config.EJS_DISABLE_AUTO_UNLOAD assert not loader.config.EJS_DISABLE_BATCH_BOOTUP assert not loader.config.EJS_NETPLAY_ENABLED From 6bec7de368b8254ccfa9f6447e4cd2397918dd7c Mon Sep 17 00:00:00 2001 From: Matthew Turk Date: Sat, 21 Feb 2026 16:43:41 -0600 Subject: [PATCH 4/7] Switch to useEventListener --- frontend/src/views/Player/EmulatorJS/Base.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/Player/EmulatorJS/Base.vue b/frontend/src/views/Player/EmulatorJS/Base.vue index 2be647501..c4b1e9df3 100644 --- a/frontend/src/views/Player/EmulatorJS/Base.vue +++ b/frontend/src/views/Player/EmulatorJS/Base.vue @@ -1,5 +1,5 @@