Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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
HookWhen 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 normally
  • return false – input is swallowed (the PC never sees it)
  • no return / return nil – same as return true

This is how remapping works: block the original key, send a different one via HID.

Note: Bind uses the opposite default – it blocks unless you explicitly return 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.Press and HID.Type must be called inside a Run() coroutine or an Async() handler. HID.Down and HID.Up are 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:

PolicyBehavior
replace (default)Stop the old instance, start the new one
singleReject the new instance if one is already running
multipleAllow 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

ActionFieldsDescription
shorthandx, y, delayMouse movement (most common)
movedx, dy, delayMouse movement (explicit)
downcode, delayHold a key
upcode, delayRelease a key
presscode, holdMs, delayTap a key
typetext, charDelay, delayType a string
scrollamount, delayScroll wheel
sleepdelayPause

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.Post with high-frequency OnTick – HTTP calls block the main thread; move them inside Run(). 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