Files

146 lines
4.8 KiB
Lua
Raw Permalink Normal View History

-- hotkeys.lua — the mod's global key layer.
--
-- Sits in front of states.keypressed. Keys we own are handled and CONSUMED (the
-- game never sees them); everything else falls straight through.
--
-- Consuming matters more than it looks. keybindings.getCommand() dispatches on
-- scancode alone and ignores modifiers, checking held modifiers separately via
-- isCommandDown. So Ctrl+Up is seen by the game as plain "up" — a movement — with
-- moveCamera held. If we bound anything to Ctrl+Up and passed it through, the
-- player would move. Hence: our keys are chosen from scancodes the game does not
-- bind at all, and we still consume them.
--
-- Keys the game claims by default (do not use):
-- . 0-9 ` a b backspace c d delete down e escape f g i j kp1-kp9 kpenter
-- lalt lctrl left lshift mouse3 q r ralt rctrl return right rshift s space
-- t tab up x y z
--
-- Note Ctrl = moveCamera and Shift = repeatAction/adjustByFive are HELD modifiers
-- in game. Combining them with an unbound scancode (e.g. Ctrl+PageUp) is safe,
-- because the game maps the scancode to no command and does nothing.
local speech = require("accessibility.speech")
local history = require("accessibility.history")
local hotkeys = {}
--------------------------------------------------------------------------------
-- Where our keys must stay out of the way
--------------------------------------------------------------------------------
-- States that accept free text. Swallowing "[" while the player is naming a
-- character or typing in the developer console would be a real bug.
local textEntryStates = {
ingame_say = true,
new_game = true,
keybindings_menu = true, -- captures raw keys for rebinding
}
local function isTextEntryState()
local ok, states = pcall(require, "states")
if not ok then
return false
end
return textEntryStates[states.getCurrent()] == true
end
--------------------------------------------------------------------------------
-- Modifier helpers
--------------------------------------------------------------------------------
2026-08-01 21:26:35 +02:00
-- Indirected through overridable fields so the testbed can drive modified
-- hotkeys. love.keyboard.isScancodeDown reads real hardware state, which a
-- synthetic love.event.push cannot influence -- without this seam, every
-- modifier-gated key would be untestable.
hotkeys.isCtrlDown = function()
return love.keyboard.isScancodeDown("lctrl", "rctrl")
end
2026-08-01 21:26:35 +02:00
hotkeys.isShiftDown = function()
return love.keyboard.isScancodeDown("lshift", "rshift")
end
2026-08-01 21:26:35 +02:00
local function ctrlDown()
return hotkeys.isCtrlDown()
end
local function shiftDown()
return hotkeys.isShiftDown()
end
--------------------------------------------------------------------------------
-- Bindings
--
-- Each entry: scancode -> { {mods, action}, ... }, checked in order.
-- mods: "ctrl", "shift", or nil for "no modifier required".
--------------------------------------------------------------------------------
local bindings = {}
local function bind(scancode, mods, action)
bindings[scancode] = bindings[scancode] or {}
table.insert(bindings[scancode], {mods = mods, action = action})
end
-- Console review
bind("[", nil, history.reviewPrevious)
bind("]", nil, history.reviewNext)
bind("[", "shift", history.reviewFirst)
bind("]", "shift", history.reviewLast)
bind("\\", nil, history.repeatCurrent)
bind("\\", "shift", history.copyCurrent)
2026-08-03 01:31:58 +02:00
-- Other keys are registered by their own modules via hotkeys.bind, from
-- init.lua: surroundings, tracker, status, viewwatch, details.
function hotkeys.bind(scancode, mods, action)
bind(scancode, mods, action)
end
local function matches(entry)
if entry.mods == "ctrl" then
return ctrlDown()
elseif entry.mods == "shift" then
return shiftDown()
end
-- No modifier required: reject if one is held, so Shift+[ doesn't also fire [.
return not ctrlDown() and not shiftDown()
end
--------------------------------------------------------------------------------
-- Dispatch
--------------------------------------------------------------------------------
-- Returns true if the key was ours and should NOT reach the game.
function hotkeys.keypressed(_key, scancode)
if isTextEntryState() then
return false
end
local candidates = bindings[scancode]
if not candidates then
return false
end
for _, entry in ipairs(candidates) do
if matches(entry) then
local ok, err = pcall(entry.action)
if not ok then
print("[accessibility] hotkey '" .. tostring(scancode) .. "' error: " .. tostring(err))
end
return true
end
end
return false
end
-- Any key that reaches the game is an explicit player action, so it cancels the
-- pending speech backlog. Whatever that action produces is then spoken fresh.
-- This is the "speak everything queued, unless explicitly stopped" policy.
function hotkeys.onGameKey()
speech.silence()
end
return hotkeys