Scripting Guide
This guide covers the core concepts for writing Rebind scripts. If you haven’t read Getting Started yet, start there.
Lifecycle
Every script goes through a predictable lifecycle:
Load -> OnStart -> (OnFocus/OnBlur cycle) -> OnStop -> Unload
| Hook | When it fires |
|---|---|
OnStart() | Script is loaded into memory |
OnStop() | Script is about to be unloaded |
OnFocus(window) | Target window/process gained focus. window is { title, process, x, y, width, height } |
OnBlur(window) | Target window/process lost focus. window is the new foreground window. |
OnTick(delta) | Every frame while active. Default 1,000/sec, up to 8,000/sec with tick_rate=8000 modeline. |
If your script has no window= or process= targeting, it’s always active – OnFocus/OnBlur won’t fire.
Input
These are the core of Rebind. Input hooks fire when physical input arrives from your captured devices.
function OnDown(key)
-- key or mouse button was pressed
-- return false to block it, true to pass through
return true
end
function OnUp(key, duration)
-- key or mouse button was released
-- duration = how many milliseconds it was held
return true
end
function OnMove(dx, dy)
-- mouse moved by (dx, dy) pixels
-- by default, the return value is ignored and the move is forwarded
-- immediately. set mouse_block=true in the modeline to enable blocking.
return true
end
function OnScroll(delta)
-- scroll wheel moved
-- delta > 0 = scroll up, delta < 0 = scroll down
return true
end
Blocking
The return value of OnDown, OnUp, and OnScroll controls whether the physical input reaches your PC:
return true– input passes through normallyreturn 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, send a different one via HID.
Note:
Binduses the opposite default – it blocks unless you explicitlyreturn true. See the SDK Reference for details.
Mouse moves are handled differently for performance. By default, OnMove fires asynchronously – the move is forwarded to your PC immediately and the return value is ignored. This guarantees zero mouse latency regardless of script execution time.
To enable blocking mouse moves (e.g., for mouse acceleration or aim snapping), add mouse_block=true to your modeline:
-- rebind: mouse_block=true
function OnMove(dx, dy)
-- now the return value matters: return false to swallow the move
return true
end
Only use mouse_block when you genuinely need to modify or swallow mouse input. It forces the engine to acquire a lock on every mouse event, which can affect input latency when scripts are doing heavy work.
Keys
Keys are identified by string names. Names are case-insensitive: "A", "a", "LCtrl", and "lctrl" all work.
Common examples: "A"-"Z", "0"-"9", "F1"-"F12", "Mouse1"-"Mouse5", "LCtrl", "LShift", "LAlt", "Space", "Enter", "Escape", "Up", "Down", "Left", "Right".
For the complete list of every key name, alias, and which keys are input-only vs output-only, see the Key Reference in the SDK Reference.
HID Output
The HID namespace sends keyboard and mouse output through the active transport. These appear as real input to your PC.
-- keyboard
HID.Down("W") -- hold W
HID.Up("W") -- release W
HID.Press("Space") -- tap Space (press and release)
HID.Press("Space", 100) -- tap Space, hold for 100ms
HID.Type("Hello!") -- type a string
-- modifier combos
HID.Press("LCtrl+C") -- Ctrl+C (copy)
HID.Press("LCtrl+V") -- Ctrl+V (paste)
HID.Press("LCtrl+LShift+T") -- Ctrl+Shift+T (reopen tab)
HID.Press("LShift+Enter") -- Shift+Enter (newline in chat apps)
-- mouse
HID.Move(10, -5) -- move cursor by (10, -5) pixels
HID.Scroll(3) -- scroll up 3 notches
HID.Scroll(-3) -- scroll down 3 notches
Note:
HID.PressandHID.Typemust be called inside aRun()coroutine or anAsync()handler.HID.DownandHID.Upare non-blocking and can be called anywhere.
HID.Down and HID.Up are for holding keys across time. HID.Press is a convenience that does both with an optional hold duration.
Combos
Use + in any key string to press multiple keys as a combo. HID.Press("LCtrl+V") presses Ctrl, presses V, holds, then releases V and Ctrl in reverse order. This replaces the manual Down/Up pattern for modifier combos:
-- before: manual modifier management
HID.Down("LCtrl")
HID.Press("V")
HID.Up("LCtrl")
-- after: single call, correct timing handled automatically
HID.Press("LCtrl+V")
Down and Up also support combos for held modifiers:
HID.Down("LCtrl+LShift") -- hold both
-- ... do things ...
HID.Up("LCtrl+LShift") -- release both (reverse order)
Clipboard Paste
For multi-line text or long strings, clipboard paste is faster and more reliable than typing:
Clipboard.Set("Line one\nLine two\nLine three")
HID.Press("LCtrl+V")
This works in any application and has zero per-character timing concerns.
Coroutines
For anything that needs to happen over time (sequences, delays, loops), use Run() and Sleep():
function OnDown(key)
if key == "F9" then
Run(function()
HID.Down("W")
Sleep(500) -- wait 500ms
HID.Up("W")
Sleep(200)
HID.Press("Space")
end)
return false
end
return true
end
Run() launches a coroutine that executes concurrently. Sleep() pauses that coroutine without blocking anything else. You can launch multiple Run() blocks and they’ll all execute in parallel.
Sleep() can only be called inside a Run() block. Calling it outside one raises a clear error with guidance.
Sugar
After
Run a function after a delay without the Run/Sleep boilerplate:
After(2000, function()
Log.Info("2 seconds later")
end)
Async
Wrap a function so it automatically runs in a coroutine when called. Designed for Bind callbacks that need Sleep():
-- Bind callbacks are synchronous, so Sleep() would error.
-- Async() wraps the callback in Run() automatically.
Bind(
"F10",
Async(function()
HID.Down("W")
Sleep(500)
HID.Up("W")
end)
)
Async is smart about nesting: if called inside an existing coroutine (e.g. inside Run()), it calls the function directly instead of spawning a second coroutine.
Tasks
Run() returns a handle you can use to control the coroutine:
local task = Run(function()
while true do
HID.Press("Mouse1", 20)
Sleep(100)
end
end)
-- later...
task:Cancel() -- stop the coroutine
task:IsRunning() -- check if it's still going
Always cancel long-running tasks in OnStop() or OnBlur() to prevent them from leaking:
local task = nil
function OnDown(key)
if key == "F9" then
task = Run(function()
-- long sequence...
end)
end
end
function OnStop()
if task and task:IsRunning() then
task:Cancel()
end
end
Input State
The Input namespace lets you check what’s currently pressed without waiting for an event:
if Input.IsDown("Mouse1") then
-- left mouse button is currently held
end
local duration = Input.GetDuration("W")
-- how long W has been held (0 if not pressed)
local keys = Input.GetActiveKeys()
-- list of all currently held keys
local mods = Input.GetModifiers()
-- { ctrl = true/false, shift = true/false, alt = true/false, win = true/false }
This is useful for polling-based logic in OnTick or inside Run() loops.
System
System provides read-only methods updated automatically every tick.
System.Time() -- current timestamp in milliseconds
local mx, my = System.Mouse() -- cursor position (pixels)
local sw, sh = System.Screen() -- primary display dimensions
local win = System.Window() -- { title, process, x, y, width, height }
Useful for time-based logic, cursor tracking, and window awareness:
local lastPress = 0
function OnDown(key)
if key == "W" then
local now = System.Time()
if now - lastPress < 250 then
Log.Info("Double-tap detected!")
end
lastPress = now
end
return true
end
Timers
For simple delayed or repeated callbacks, use Timer instead of a Run/Sleep loop:
-- fire once after 2 seconds
local t = Timer.After(2000, function()
Log.Info("2 seconds passed")
end)
-- fire every 500ms
local repeater = Timer.Every(500, function()
Log.Info("tick")
end)
-- control timers
repeater:Pause()
repeater:Resume()
repeater:Cancel()
-- cancel all timers
Timer.CancelAll()
UI Config
Scripts can define a settings panel that appears in the Rebind UI. Users can tweak values without editing code.
local cfg = UI.Schema({
enabled = UI.Toggle(true, { label = "Enable" }),
speed = UI.Slider(50, { min = 0, max = 100, suffix = "%" }),
hotkey = UI.Keybind("F9", { label = "Toggle Key" }),
mode = UI.Select("Normal", { "Normal", "Fast", "Precise" }),
tag = UI.Text("", { placeholder = "Enter name", maxLength = 20 }),
})
Read values directly from the returned handle:
if cfg.enabled then
local s = cfg.strength
end
-- write values programmatically
cfg.enabled = false
Values are saved automatically and restored when the script reloads. Persistence is keyed to the script’s file path — no configuration required.
Layout
UI.Slider(50, {
group = "Advanced", -- group controls under a header
tab = "Settings", -- organize into tabs
showIf = "enabled", -- only show when the "enabled" toggle is on
tooltip = "Hover text", -- tooltip on hover
})
Notifications
UI.Notify("Script enabled", "info") -- "info", "success", or "error"
Targeting
Scripts can restrict themselves to specific applications:
-- rebind: window=Photoshop
-- rebind: process=Photoshop.exe
window=matches against the window title (case-insensitive substring)process=matches against the executable name (case-insensitive)- Multiple entries act as OR – any match activates the script
- When the matching window loses focus, the script deactivates
Use OnFocus and OnBlur to set up and tear down resources:
-- rebind: process=Photoshop.exe
function OnFocus(window)
Log.Info("Photoshop is focused: " .. window.title)
end
function OnBlur()
Log.Info("Left Photoshop")
-- clean up: cancel tasks, release held keys, etc.
end
Always-on
A targeted script that isn’t focused has zero overhead. Its hooks are never called, its timers don’t fire, and its OnTick is never invoked. The runtime skips it entirely.
This means you can have dozens or even hundreds of scripts loaded simultaneously – one for Photoshop, one for your browser, one for your terminal, one for each game you play – and they all coexist without interfering with each other or consuming resources. Only the script(s) matching the currently focused window will actually run.
Scripts without any window= or process= targeting are always active (global scripts). Use these for system-wide behaviors like key remaps or media controls.
A typical setup looks like:
Always running:
CapsLock to Escape (global, no targeting)
Mouse Media Keys (global, no targeting)
Activated on demand:
Photoshop Shortcuts (process=Photoshop.exe)
Browser Tab Nav (process=chrome.exe, firefox.exe, msedge.exe)
Terminal Helpers (window=Terminal)
Blender Macros (process=blender.exe)
All of these can stay loaded at all times. There is no reason to manually start and stop scripts as you switch between apps.
Priority
When multiple scripts are running, z_index controls which one processes input first:
-- rebind: z_index=100
Higher z-index scripts see input first. If a higher-priority script returns false from a hook, lower-priority scripts never see that event.
Default z-index is 1.
Instances
Control what happens when a script is loaded while another copy is already running:
| Policy | Behavior |
|---|---|
replace (default) | Stop the old instance, start the new one |
single | Reject the new instance if one is already running |
multiple | Allow multiple copies to run simultaneously |
-- rebind: instance=single
Macros
Record and play back input sequences:
-- define a macro as a table of actions
local pattern = {
{ x = 0, y = 2, delay = 15 }, -- shorthand: mouse move
{ x = -1, y = 3, delay = 15 },
{ x = 1, y = 2, delay = 15 },
}
function OnDown(key)
if key == "Mouse1" then
local handle = Macro.Play(pattern, 1.0, "replace")
-- speed = 1.0 (normal), mode = "replace" (stop previous macros)
end
-- sequence two macros using Wait() inside a coroutine
if key == "F9" then
Run(function()
local h1 = Macro.Play(pattern)
h1:Wait()
local h2 = Macro.Play(pattern)
h2:Wait()
Log.Info("both macros finished")
end)
end
end
function OnUp(key)
if key == "Mouse1" then
Macro.StopAll()
end
end
Actions
| Action | Fields | Description |
|---|---|---|
| shorthand | x, y, delay | Mouse movement (most common) |
move | dx, dy, delay | Mouse movement (explicit) |
down | code, delay | Hold a key |
up | code, delay | Release a key |
press | code, holdMs, delay | Tap a key |
type | text, charDelay, delay | Type a string |
scroll | amount, delay | Scroll wheel |
sleep | delay | Pause |
Modes
"parallel"– run alongside other macros (default)"replace"– stop others, then play
Transforms
Transform macro patterns before playing:
local scaled = Math.Scale(pattern, 1.5, 1.0) -- scale X by 1.5
local smooth = Math.Spline(pattern, 0.5) -- smooth curves
local resampled = Math.Resample(pattern, 10) -- normalize timing
local interpolated = Math.Interpolate(pattern, 2) -- add smooth steps
local fitted = Math.TimeComp(pattern, 1000) -- fit to 1 second
Patterns
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
end
Hold loop
function OnDown(key)
if key == "Mouse1" then
Run(function()
while Input.IsDown("Mouse1") do
-- do something
Sleep(100)
end
end)
return false
end
end
Cleanup
local task = nil
function OnBlur()
if task and task:IsRunning() then
task:Cancel()
end
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 automatically lints your script on load and warns about common issues:
- No hooks defined – script will load but never run any logic
Net.Get/Net.Postwith high-frequencyOnTick– HTTP calls block the main thread; move them insideRun(). WebSocket handlers (Net.WSListen,Net.WSConnect) do not have this issue – their I/O runs on dedicated threads and only the handler callback runs on the tick.
These warnings appear in the Rebind logs tab. They don’t prevent loading – they’re guidance to help you catch mistakes early.
Type Checking
The SDK ships a type definition file (types/rebind.d.luau) for the Luau Language Server. This gives you autocomplete, hover docs, and type checking in VS Code.
To enable it, add a .luaurc file next to your script:
{
"languageMode": "nonstrict",
"paths": ["path/to/lua-sdk/types"]
}
Next
- SDK Reference – complete function reference for every namespace
- Examples – full scripts you can use or learn from