Scripting Guide
Rebind scripts are written in Luau, running on a Rust core that dispatches input hooks up to 8,000 times a second. Your code sits between your input devices and your computer: every keypress, mouse move, and scroll can be read, transformed, blocked, or replaced. Output is standard USB HID from a Teensy 4.x device, or OS-level input in software mode.
This guide teaches the model and the common patterns. For the exact parameters of every function, see the SDK reference; to run your first script, see the quickstart.
Lifecycle
Every script follows a predictable lifecycle:
Load -> OnStart -> (OnFocus/OnBlur cycle) -> OnStop -> Unload
You implement behavior by defining hooks — global functions the runtime calls when something happens:
OnStart()/OnStop()— set up and tear down (start timers, cancel tasks, release keys).OnFocus(window)/OnBlur(window)— fire when a targeted app gains or loses focus (see Targeting).OnTick(dtMs)— runs on a fixed cadence while the script is active; default 1,000/sec, up to 8,000/sec with thetick_rate=8000modeline.dtMsis the real time elapsed since the previousOnTick(milliseconds, fractional;0on the first call) — use it for frame-rate-independent motion.OnDown/OnUp/OnMove/OnScroll— input events, covered next.
A script with no targeting is always active, so OnFocus/OnBlur never fire. See Hooks for the exact signatures.
Input and blocking
Input hooks fire when physical input arrives from your captured devices. Their return value decides whether your PC sees the input:
return true— input passes through normally.return false— input is swallowed; the PC never sees it.- no return /
return nil— same asreturn true.
This is how remapping works: block the original key, then send a different one with HID.
function OnDown(key)
if key == "CapsLock" then
HID.Down("Escape")
HID.Up("Escape")
return false -- swallow CapsLock; the PC sees Escape instead
end
return true
end
function OnUp(key, duration)
-- duration = how many ms the key was held
return true
end
Bindis the declarative shortcut for simple remaps and uses the opposite default — it blocks unless youreturn true. See Bind.
Mouse moves are handled differently, and they need hardware. Any script that
defines OnMove requires a Rebind device — the relay refuses to load an
OnMove script in software mode, because the OS draws the cursor upstream of
anything a host process could intercept in time. (Key, button, and scroll blocking
work in both modes; only mouse movement is hardware-gated.)
On a device, OnMove fires asynchronously by default — the move is forwarded to
your PC immediately and the return value is ignored, so script time never sits in
the mouse path. To modify or swallow moves (acceleration curves, cursor smoothing),
add mouse_block=true to the modeline; then the return value matters:
--[[
rebind: min_sdk=3.0.0
rebind: mouse_block=true
--]]
function OnMove(dx, dy)
return true -- now return false would swallow the move
end
Keys are identified by case-insensitive string names ("A", "LCtrl", "Mouse1", …). The complete list — every alias and which keys are input- or output-only — is in the Key reference.
Sending output
The HID namespace sends keyboard and mouse output through the active transport; it appears as real input to your PC.
HID.Down/HID.Uphold and release a key across time.HID.Presstaps a key (optional hold in ms);HID.Typetypes a string.- Use
+for modifier combos:HID.Press("LCtrl+V")presses in order and releases in reverse.
HID.Down("LShift")
HID.Press("A") -- types "A" while Shift is held
HID.Up("LShift")
HID.Press("LCtrl+V") -- paste, timing handled for you
HID.PressandHID.Typeuse an internalSleep, so they must run inside aRun()coroutine or anAsync()handler.HID.Down/HID.Upare non-blocking and safe anywhere — including an input hook.
For multi-line or long text, paste is faster and more reliable than typing per character:
Clipboard.Set("Line one\nLine two")
HID.Press("LCtrl+V")
The full output surface (mouse movement, scroll, absolute mode) is in the HID reference.
Doing things over time
For sequences, delays, and loops, use Run() and Sleep():
function OnDown(key)
if key == "F9" then
Run(function()
HID.Down("W")
Sleep(500) -- pause this coroutine only
HID.Up("W")
end)
return false
end
return true
end
Run() launches a coroutine that executes concurrently; Sleep() pauses that coroutine without blocking anything else, and is only valid inside a Run() block. Reach for:
After(ms, fn)— a one-off delay without theRun/Sleepboilerplate.Async(fn)— wrap a callback so it runs in a coroutine; use it forBindcallbacks that needSleep.Timer.After/Timer.Every— simple delayed or repeated callbacks, when you don’t want to manage a coroutine loop yourself.
Coroutines are cooperative and tick-driven.
Sleepresumes on the next engine tick past its deadline, so its timing is tick-granular rather than exact milliseconds, and aRun()body that never callsSleepruns straight through on a single tick — an infinite loop with noSleepstalls the script (input freezes) until the runtime’s per-tick budget (~200 ms) cuts it off. Always put aSleepinside long-running loops.
Run() returns a handle (task:Cancel(), task:IsRunning()). Cancel long-running tasks in OnStop/OnBlur so they don’t leak:
local task = nil
function OnDown(key)
if key == "F9" then
task = Run(function()
while Input.IsDown("F9") do
HID.Press("Mouse1", 20)
Sleep(100)
end
end)
end
return false
end
function OnStop()
if task and task:IsRunning() then
task:Cancel()
end
end
Exact signatures are under Globals and Timer.
Reading state
When you need to know what’s happening right now rather than waiting for an event, poll it:
Inputreports currently held keys, modifiers, and how long a key has been down — useful insideOnTickor aRun()loop.Systemgives the current time, cursor position, screen size, and focused window, refreshed each tick.
See Input and System for the methods; the Double-tap pattern below shows System.Time in use.
Config panels
A script can define a settings panel that appears in the Rebind UI, so users tune it without editing code. UI.Schema declares the controls; you read and write values through the returned handle, and they persist automatically (keyed to the script’s file path):
local cfg = UI.Schema({
enabled = UI.Toggle(true, { label = "Enable" }),
speed = UI.Slider(50, { min = 0, max = 100, suffix = "%" }),
})
if cfg.enabled then
local s = cfg.speed
end
The full widget catalog (toggles, sliders, keybinds, selects, text, plus layout and notifications) is in the UI reference.
Splitting across files (require)
As a script grows, pull shared logic into its own file and load it with
require(). A module is just a .luau / .lua file that returns a value —
usually a table:
-- helpers.luau, next to your script
local M = {}
function M.clamp(n, lo, hi)
return math.max(lo, math.min(hi, n))
end
return M
-- main script
local helpers = require("helpers")
local x = helpers.clamp(value, 0, 100)
require resolves relative to the script’s own directory:
- An extensionless name is tried as
<name>.luau, then<name>.lua, then<name>/init.luau, then<name>/init.lua(so a folder with aninit.luauloads as a package). - Dots are path separators:
require("lib.math")loadslib/math.luau. - An explicit
.lua/.luausuffix pins that exact file; a leading./is ignored. - Modules are cached: the file is evaluated once on the first
require, and every laterrequireof the same path returns that same value.
It is sandboxed to the script’s folder. A name containing .., or any path that
would resolve outside the directory, is rejected (path traversal not allowed),
and an unresolved name raises module '<name>' not found. There are no external
packages, registries, package.path, or aliases — local files only.
Only the script you run needs a
min_sdkmodeline. Arequire’d module is loaded through that script rather than run directly, so it needs no header.
Macros
Macro.Play plays back a recorded input sequence — a table of move/press/scroll/sleep actions. Reach for a macro when you have a fixed sequence to replay; use raw HID calls when the output depends on logic. Patterns can be transformed before playback (Math.Scale, Math.Spline, Math.Resample). See the Macro reference for the action format and playback modes.
Targeting and the always-on model
Restrict a script to specific applications with the window= and process= modeline keys. window= matches a case-insensitive substring of the focused window’s title (so window=code also matches “Visual Studio Code”); process= matches the process name the same way. A script activates if any window= or any process= pattern matches — the two lists combine as OR — and it deactivates when the matching window loses focus:
-- rebind: process=Photoshop.exe
function OnFocus(window)
Log.Info("focused: " .. window.title)
end
function OnBlur()
Log.Info("left")
end -- clean up here
A targeted script that isn’t focused is skipped entirely — its hooks, timers, and OnTick never fire. So you can keep hundreds of scripts loaded at once and only the script(s) matching the focused window run, with no interference or wasted CPU:
Always running (no targeting):
CapsLock to Escape
Mouse Media Keys
Activated on demand:
Photoshop Shortcuts (process=Photoshop.exe)
Browser Tab Nav (process=chrome.exe, firefox.exe, msedge.exe)
Terminal Helpers (window=Terminal)
Scripts without any targeting are always active — use them for system-wide behaviors like remaps and media controls. When several scripts run at once, z_index controls which sees input first (a false return blocks lower-priority scripts), and instance controls what happens when a script loads while a copy is already running. Both are in the Modeline reference.
Patterns
Common shapes that compose the pieces above.
Toggle
local active = false
function OnDown(key)
if key == "F9" then
active = not active
UI.Notify(active and "ON" or "OFF", "info")
return false
end
return true
end
Hold loop
function OnDown(key)
if key == "Mouse1" then
Run(function()
while Input.IsDown("Mouse1") do
-- do something each iteration
Sleep(100)
end
end)
return false
end
return true
end
Double-tap
local lastTap = 0
local THRESHOLD = 250
function OnDown(key)
if key == "W" then
local now = System.Time()
if now - lastTap < THRESHOLD then
Log.Info("Double tap!")
end
lastTap = now
end
return true
end
Validation
The runtime lints your script on load and warns (in the Logs tab, without blocking the load) about common issues:
- No hooks defined — the script loads but never runs any logic.
Net.Get/Net.Postin a high-frequencyOnTick— synchronous HTTP blocks the main thread; move it insideRun(). WebSocket handlers (Net.WSListen/Net.WSConnect) are exempt — their I/O runs on dedicated threads.
Type checking
The SDK ships a type definition file (types/rebind.d.luau) for the Luau Language Server, giving you autocomplete, hover docs, and type checking in VS Code. Add a .luaurc next to your script:
{
"languageMode": "nonstrict",
"paths": ["path/to/lua-sdk/types"]
}