Remapping & layers
Turn any key or button into another key, a chord, a modifier hold, or a whole layer — key remaps, combos, and conditional layers, the same job as AutoHotkey or Karabiner. The logic is identical on Windows, macOS, and Linux, works with any USB keyboard or mouse, and (with a Teensy 4.x attached) comes out as standard USB HID at up to 8,000 Hz. Every recipe below runs unchanged in software mode first.
The building blocks are small and the same throughout:
Bind.Remap(from, to)— a declarative one-line key-to-key remap.OnDown(key)/OnUp(key)— returntrueto let a key pass through,falseto swallow it. Send your replacement withHID.Down/HID.Up/HID.Press.Input.IsDown(key)— test whether a physical key is currently held, which is how you build chords, modifier holds, and layers.
Press Left Ctrl + Left Alt + K at any time to stop every running script. (The kill switch uses the left-hand modifiers specifically — right Ctrl / right Alt don’t trigger it.)
Single-key remap
Bind.Remap takes the key you press and the key you want sent in its place — no
hooks, no state. Caps Lock becomes Escape; your real Escape is untouched.
--[[
rebind: min_sdk=3.0.0
rebind: name=CapsLock to Escape
--]]
Bind.Remap("CapsLock", "Escape")
Bind.Remap returns a handle, so you can toggle a remap at runtime with
:disable() and :enable(). Here a hotkey flips it live, and the handle reports
its state through .enabled.
--[[
rebind: min_sdk=3.0.0
rebind: name=Toggleable Remap
--]]
local remap = Bind.Remap("CapsLock", "Escape")
-- Press F8 to switch the remap off and on without restarting the script.
Bind("F8", function()
if remap.enabled then
remap:disable()
UI.Notify("Remap off", "info")
else
remap:enable()
UI.Notify("Remap on", "info")
end
return false
end)
For conditions more nuanced than on/off — “only in this app,” “only while another
key is held” — drop down to OnDown / OnUp and decide per event. The remaining
recipes all use that form.
Chords and combos
A chord fires one action only when several keys are held together. Read the other
keys with Input.IsDown at the moment the trigger key goes down, and swallow the
trigger so it doesn’t type on its own.
--[[
rebind: min_sdk=3.0.0
rebind: name=Chord: J+K -> Escape
--]]
-- Press J and K together to send Escape. Either key alone types normally.
function OnDown(key)
if key == "J" and Input.IsDown("K") then
HID.Down("Escape")
HID.Up("Escape")
return false
end
if key == "K" and Input.IsDown("J") then
HID.Down("Escape")
HID.Up("Escape")
return false
end
return true
end
Checking both orders makes the chord order-independent. For a combo that includes a
real modifier, test it the same way — Input.IsDown("LCtrl") — and emit a keyboard
combo with the + syntax:
--[[
rebind: min_sdk=3.0.0
rebind: name=Combo: Ctrl+Backspace -> Delete Word
--]]
function OnDown(key)
if key == "Backspace" and Input.IsDown("LCtrl") then
-- send Ctrl+Shift+Left then Delete: select the previous word and remove it
Run(function()
HID.Press("LCtrl+LShift+Left")
HID.Press("Delete")
end)
return false
end
return true
end
Modifier hold
A modifier hold gives one key two jobs: tap it for its normal value, hold it to change what other keys do. Track when the key went down and whether another key was used while it was held. On release, if nothing else fired and the hold was short, send the tap.
--[[
rebind: min_sdk=3.0.0
rebind: name=Space as Hold-Modifier
--]]
-- Tap Space -> Space. Hold Space + H/J/K/L -> arrow keys.
local TAP_MS = 180
local spaceDown = nil
local usedAsMod = false
local arrows = { H = "Left", J = "Down", K = "Up", L = "Right" }
function OnDown(key)
if key == "Space" then
spaceDown = System.Time()
usedAsMod = false
return false -- hold the Space output until we know tap vs hold
end
if spaceDown and arrows[key] then
usedAsMod = true
HID.Down(arrows[key])
HID.Up(arrows[key])
return false
end
return true
end
function OnUp(key)
if key == "Space" then
local heldMs = System.Time() - (spaceDown or 0)
if not usedAsMod and heldMs < TAP_MS then
HID.Down("Space")
HID.Up("Space") -- it was a quick tap, emit the real key
end
spaceDown = nil
return false
end
return true
end
function OnBlur()
spaceDown = nil
end
usedAsMod prevents a stray Space when you genuinely used the hold, and TAP_MS
keeps a long, lone hold from emitting a late Space. OnBlur clears the state if
focus leaves while you’re mid-hold.
Hold-to-access layer
A layer is a modifier hold scaled up: hold one key to expose an alternate set of keys, release it to return to normal. Keep the mappings in a table so adding a key is a one-line edit.
--[[
rebind: min_sdk=3.0.0
rebind: name=Nav Layer
--]]
-- Hold CapsLock to turn the right hand into a navigation + editing cluster.
local layerKey = "CapsLock"
local layer = {
H = "Left",
J = "Down",
K = "Up",
L = "Right",
U = "PageUp",
D = "PageDown",
Y = "Home",
O = "End",
Backspace = "Delete",
}
function OnDown(key)
if key == layerKey then
return false -- swallow the layer key itself
end
if Input.IsDown(layerKey) and layer[key] then
HID.Down(layer[key])
HID.Up(layer[key])
return false
end
return true
end
Because the layer is gated on Input.IsDown(layerKey), every other key behaves
normally the instant you let go. To make it app-aware — active only in one program
— wrap the body in a System.Window() check, the same pattern the productivity
recipes use:
if Input.IsDown(layerKey) and layer[key] then
local win = System.Window()
if win.process:lower():find("code") then
HID.Down(layer[key])
HID.Up(layer[key])
return false
end
end
Opposing-key resolution (last pressed wins)
For movement keys mapped to opposite directions — left and right, or up and down — holding both at once is ambiguous: the two presses cancel and you stop. One way to resolve it deterministically: when two opposing directions are held at once, the most recently pressed one is active. Release the newer key and the older one, still held, takes over again.
Record the order in which each key of an opposing pair went down, then send only the winner and suppress its opposite.
--[[
rebind: min_sdk=3.0.0
rebind: name=Opposing Keys: Last Pressed Wins
--]]
-- Map a pair of opposing keys so the most recently pressed direction is the one
-- that registers while both are held. Here A/D drive Left/Right; swap the codes
-- for your own layout.
local pair = {
A = { send = "Left", opposite = "D" },
D = { send = "Right", opposite = "A" },
}
local pressedAt = {} -- key -> timestamp of its most recent press
local function activeOf(k1, k2)
-- both held: the one pressed most recently wins
local t1, t2 = pressedAt[k1], pressedAt[k2]
if t1 and t2 then
return (t1 >= t2) and k1 or k2
end
return t1 and k1 or (t2 and k2 or nil)
end
local function refresh(key)
local p = pair[key]
if not p then
return
end
local winner = activeOf(key, p.opposite)
-- ensure only the winning direction is currently sent
if winner == key then
HID.Up(pair[p.opposite].send)
HID.Down(p.send)
elseif winner == p.opposite then
HID.Up(p.send)
HID.Down(pair[winner].send)
else
HID.Up(p.send)
end
end
function OnDown(key)
if not pair[key] then
return true
end
pressedAt[key] = System.Time()
refresh(key)
return false -- we drive the output; swallow the raw key
end
function OnUp(key)
if not pair[key] then
return true
end
pressedAt[key] = nil
HID.Up(pair[key].send)
-- hand control back to the opposite key if it is still held
local other = pair[key].opposite
if Input.IsDown(other) then
refresh(other)
end
return false
end
function OnBlur()
pressedAt = {}
HID.Up("Left")
HID.Up("Right")
end
The behavior is timing-driven: pressedAt stores when each key last went down,
activeOf picks the larger timestamp, and refresh makes the live output match.
Releasing the newer key calls refresh on the survivor so the older direction
resumes with no extra tap. OnBlur releases both outputs if focus changes mid-hold,
so a direction can never get stuck on.
Where each recipe runs
| Capability | Namespaces | Platforms |
|---|---|---|
Bind.Remap, OnDown/OnUp, chords, layers | Bind, HID, Input | Windows, macOS, Linux |
App-aware layer (win.process check) | System.Window() | Windows, macOS; limited/absent on Linux |
See Platforms for the full capability matrix.