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

Rebind

Rebind is a scriptable input engine. It captures your physical keyboard and mouse through an isolated VM, runs every input event through Luau scripts at up to 8kHz, and outputs the result through a dedicated USB device (a Teensy 4.x running Rebind firmware). Your real peripherals disappear from the OS. The system sees only the Rebind device – a standard keyboard and mouse.

It works on Windows, macOS (ARM and Intel), and Linux with a unified protocol. Same scripts, same hardware, same behavior across all three platforms. Write once, run anywhere.

It works with any mouse and keyboard. One set of scripts across all your hardware. Switch peripherals or switch operating systems and nothing changes.

Scripting

Scripts are written in Luau, a fast typed variant of Lua. Real control flow, coroutines, timers, state management, and a complete standard library. Anything from a single key remap to a multi-step automation that reacts to window focus, reads pixel colors, communicates with external processes, or serves HTTP and WebSocket requests.

function OnDown(key)
    if key == "CapsLock" then
        HID.Press("Escape")
        return false -- block the original key
    end
    return true -- pass everything else through
end

Scripts define their own settings panels – toggles, sliders, keybinds, dropdowns – rendered automatically in the browser. Users configure without editing code.

SDK

HIDkeyboard and mouse output, modifier combo syntax
Inputreal-time physical key state
Macrorecord, play, stream, transform input sequences
Screenpixel color sampling
Windowfind, move, resize, pin, control windows
NetHTTP client/server, WebSocket server/client
AudioWAV/MP3/OGG/FLAC playback
Clipboardsystem clipboard read/write
Filesandboxed file I/O, JSON, TOML
RegexPCRE pattern matching
Pipeshared memory IPC with external processes
Dialognative OS file/message dialogs
Timerdelayed and repeating callbacks
Mathrandom, gaussian, spline, interpolation
UIdeclarative config schema
Systemcursor, screen, window info, shell commands
ConfigTOML config file I/O
Binddeclarative key bindings, remaps, toggles
Scriptself-control (exit, reload)
Logstructured logging to the UI

How Rebind compares

RebindAutoHotkeyG Hub / Synapse / iCUEKmboxTitan TwoXIMStream Deck
Cross-platformWindows, macOS, LinuxWindows onlyWindows onlyWindows onlyconsole + PCconsole onlyWindows, macOS
Scripting languageLuauAHKlimited Lua (G Hub only)noneGPC (compiled)nonenone
Hardware-isolated outputyesnoown brand onlyyesyesyesn/a
Any mouseyesyesnoyesyesyesn/a
Any keyboardyesyesnonoyesnon/a
HTTP client/serveryesnononononono
WebSocket server/clientyesnononononono
Coroutinesyeslimitednonononono
Pixel samplingyesyesnonononono
Window controlyesyesnonononono
Macro record/playyesyesbasicnobasicnono
Shared memory IPCyesnononononono
Declarative UI configyesnononononoyes
Single PCyesyesyestwo requiredyesyesyes
VM-level input captureyesnononononono
Per-app activationyesyesnonononoyes

AutoHotkey is the closest in scripting capability but has no hardware isolation. Peripheral software (G Hub, Synapse, iCUE) provides hardware output but locks you to one brand with a macro recorder instead of a language. Kmbox has hardware isolation but requires two PCs and zero scripting. Titan Two has a compiled language (GPC) but no network I/O, no coroutines, and no runtime UI. XIM and Stream Deck solve narrower problems with no general scripting surface.

Getting Started

Requirements

  • Windows 10/11 (64-bit), macOS (Apple Silicon or Intel), or Linux (x86_64)
  • A supported MCU (Teensy 4.0 or 4.1)
  • A USB mouse and/or keyboard to capture
  • An active Rebind license

Install or update

You can install or update Rebind with a single command. Re-running the command will update your client to the latest version.

Windows

Open PowerShell as Administrator and paste:

irm https://rebind.gg/install.ps1 | iex

macOS

Open the Terminal app and paste:

curl -fsSL https://rebind.gg/install.sh | sh

Linux

curl -fsSL https://rebind.gg/install.sh | sh

Flash your device

This installs the latest firmware on a supported MCU (Teensy 4.0 or 4.1).

  1. Open Rebind with your device plugged in
  2. Click the terminal icon in the lower right corner
  3. Type device list and locate your device
  4. Type device flash <BUS ID> (the bus ID is a value like 1-2 or 2-4)

The device will reboot with the latest firmware once flashing completes.

Activate your device

You only need to do this once per device. Requires a valid license key.

  1. With Rebind open and your device flashed (see above)
  2. Click the terminal icon in the lower right corner
  3. Type activate <LICENSE KEY>

Your license is tied to your hardware device. Once activated, it works offline.

Confirm it’s working

  1. Click the terminal icon in the lower right corner
  2. Type reauth
  3. Type device info to confirm everything is working

If device info shows your device as authenticated, you’re ready to go.

Attach your devices

  1. Open the Rebind UI
  2. Go to the Devices tab
  3. Click Auto-Detect to find your mouse and keyboard
  4. Click Attach All

Once attached, your devices are captured through the Rebind pipeline. Detach at any time to restore normal operation.

Kill switch: Press Ctrl+Alt+K at any time to immediately stop all scripts and release all held keys.

Your first script

Scripts are written in Luau, a fast typed variant of Lua. Here’s a simple example that inverts your mouse direction at 8kHz:

  1. Open the Rebind UI
  2. Go to the Scripts tab
  3. Click Create and paste the following:
-- rebind: name=Mouse Inverter
-- rebind: mouse_block=true
-- rebind: tick_rate=8000

function OnMove(dx, dy)
    HID.Move(-dx, -dy)
    return false
end
  1. Click Save and then Run

Move your mouse – it now moves in the opposite direction. Every input event passes through your script at up to 8,000 times per second before reaching the system.

How it works

  • -- rebind: lines configure the script (name, tick rate, input blocking)
  • mouse_block=true prevents raw mouse input from passing through (so only your script’s output reaches the OS)
  • OnMove(dx, dy) fires on every mouse movement with the raw delta
  • HID.Move(-dx, -dy) sends the inverted movement through the Rebind device
  • return false blocks the original input from passing through

Press Ctrl+Alt+K to stop the script and restore normal mouse behavior.

See the Scripting Guide for the full hook reference, coroutines, timers, and more.

Next steps

Interface Tour

The Rebind UI runs in your browser at http://localhost:19480. A narrow icon sidebar on the left switches between views.


Scripts

Scripts list showing loaded and stopped scripts with a detail panel on the right

The Scripts view lists every .lua file in your scripts directory. Four filter tabs narrow the list: All, Loaded, Stopped, and Error. A counter in the top right shows how many are loaded out of the total.

Clicking a script opens its detail panel on the right, showing:

  • Name and description from the script’s modeline
  • Enabled toggle — whether the script is currently running
  • Auto-start toggle — whether it loads automatically when Rebind starts
  • Edit Source, Share, and Remove actions

The top-right toolbar has Disable all (Ctrl+Alt+K), Open folder, Import, and Create.


Script Editor

Script editor showing snaptap-socd.lua with the CONFIG panel, log stream, and spy panel

Clicking Edit Source on any script opens the editor. The editor has three regions:

Code panel (left) — a full syntax-highlighted editor. Changes are saved automatically; the dot next to the filename indicates unsaved changes. The running commit hash is shown in the header. Save and Stop controls are in the top-right corner.

CONFIG panel (right) — live controls generated from the script’s UI.Schema. Sliders, toggles, keybind fields, and a Reset to defaults button appear here. Changes take effect immediately without restarting the script.

Bottom panel — split between two tabs:

  • LOG — a per-script log stream filtered to this script only. Shows timestamped INFO entries from log(), print(), and Log.* calls. A Clear button and entry count are in the header.
  • SPY — live inspector showing the current mouse position (screen coordinates), screen resolution, pixel colour under the cursor, the active window title, and the active process name. Useful for writing scripts that target specific windows or read screen state.

Devices

Devices view with a mouse and keyboard both showing as Attached

The Devices view shows every USB input device Rebind has claimed. Each card displays the device name, USB path, vendor ID, and type (Mouse or Keyboard). A green dot in the top-right corner of each card indicates the device is attached and active.

Attaching devices is done through the Auto Detect wizard. Click Auto Detect in the top-right corner.

Auto Detect wizard

Step 1 — Start

Auto Detect dialog prompting to start detection

The wizard explains what it will do. Click Start Detection to begin.

Step 2 — Detecting

Auto Detect dialog showing a spinner and “Detecting… 3s” countdown

Rebind monitors raw input for 5 seconds. Move your mouse and press a few keys. The countdown is shown live.

Step 3 — Results

Auto Detect results showing an HID-compliant mouse and HID Keyboard Device with an Attach All button

Detected devices are listed with their type, vendor ID, and input count. Click Attach All to capture all of them, or Try Again if something was missed.

Click Detach on any card to restore a device to normal operation immediately.


Macros

Macros view showing a figure-eight macro with a summary and timeline breakdown

The Macros view is a library of recorded input sequences. The left panel lists saved macros with their action count, duration, and age.

Selecting a macro shows two panels:

Summary — total actions broken down by type: mouse moves, clicks, keypresses, scrolls, and total duration.

Timeline — the full action sequence grouped into runs of movements and individual key/button events. Each group shows its action count and duration. Groups can be expanded to inspect individual events.

The Play button at the bottom plays the macro once. A progress scrubber shows playback position. Save commits any edits; Discard reverts them.

To record a new macro, use Macro.StartRecord() and Macro.StopRecord() from a script, then Macro.Save(name) to persist it.


Logs

Logs view showing timestamped INFO entries from relay and scripts with filter tabs

The Logs view is a live stream of all output from the relay, the Lua VM, and every running script. Log entries show a timestamp, level badge (INFO, WARN, ERROR, DEBUG), source (relay or script filename), and message.

Filter tabs at the top narrow by level. Source tabs below let you isolate output from specific scripts or the relay itself. The stream auto-scrolls; the entry count is shown in the top-right corner.

Log.Info, Log.Warn, Log.Error, log(), and print() all write here.


Settings

Settings view showing Software, System, Transport sections on the left and License, Account on the right

Settings is divided into two columns.

Left column:

  • Software — current version and available update. If an update is available, a Download setup button appears.
  • System — relay status (Online/Offline), VM status (Ready/Error), and uptime.
  • Transport — select between Hardware (Teensy) (real USB HID device, recommended) and Software (enigo) (OS-level driver, for testing without hardware).

Right column:

  • License — plan name, time remaining, and the active capability badges (e.g. device_activated, unlimited_devices, marketplace_access). Refresh re-validates the license; Manage opens the account portal.
  • Account — connected Discord account and a Sign out button.
  • About — current version number with links to the website and SDK documentation.

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

SDK Reference

Complete reference for every namespace, function, and hook available in Rebind scripts.

Globals

Run

local handle = Run(fn: function) -> TaskHandle

Launches a function as a coroutine. Returns immediately. The function executes concurrently with other script code.

Returns a TaskHandle with:

  • handle:Cancel() – stop the coroutine
  • handle:IsRunning() – returns boolean

Sleep

Sleep(ms: number)

Pauses the current coroutine for ms milliseconds. Only valid inside a Run() block. Calling Sleep() outside a coroutine will raise a clear error. Does not block anything else.

After

local handle = After(ms: number, fn: function) -> TaskHandle

Shorthand for running a function after a delay. Equivalent to Run(function() Sleep(ms) fn() end). Returns a TaskHandle.

After(2000, function()
    Log.Info("2 seconds later")
end)

Async

local wrappedFn = Async(fn: function) -> function

Returns a new function that automatically runs fn in a coroutine when called. This lets you use Sleep() and other coroutine features inside callbacks that are normally synchronous (like Bind handlers).

When the wrapped function is called from the main thread, it spawns a Run() and returns false (swallow). When called from inside an existing coroutine, it calls fn directly to avoid double-wrapping.

-- without Async: manual Run() wrapping
Bind("F10", function()
    Run(function()
        HID.Down("W")
        Sleep(500)
        HID.Up("W")
    end)
    return false
end)

-- with Async: Sleep() just works
Bind(
    "F10",
    Async(function()
        HID.Down("W")
        Sleep(500)
        HID.Up("W")
    end)
)

Hooks

Global functions your script defines. The runtime calls them when events occur.

Lifecycle

function OnStart()          -- script loaded
function OnStop()           -- script about to unload
function OnFocus(window)    -- target window gained focus ({ title, process, x, y, width, height })
function OnBlur(window)     -- target window lost focus ({ title, process, x, y, width, height })
function OnTick(delta)      -- per-frame update (delta = ms since last tick)

Top-level code runs immediately when the script file is loaded, before OnStart is called. Use it for constants, module initialization, and guards that must run before anything else.

-- top-level: runs at load time
local BASE_SENS = 0.8

function OnStart()
    -- runs after the script is fully registered
    Log.Info("ready, sensitivity = " .. BASE_SENS)
end

OnStart is the right place for anything that needs the script to be fully registered first — such as reading persisted UI values or starting timers.

Input

All input hooks can return false to block the event from reaching the PC, or return true to pass it through.

function OnDown(key)                -- key/button pressed
function OnUp(key, durationMs)      -- key/button released
function OnMove(dx, dy)             -- mouse moved
function OnScroll(delta)            -- vertical scroll

Other (reserved, not yet dispatched)

These hooks are recognized by the script validator but are not dispatched by the relay in the current release. Defining them will not cause errors; they simply won’t fire.

function OnScrollH(delta)           -- horizontal scroll (tilt wheel)
function OnReload()                 -- script reloaded from disk
function OnError(message)           -- runtime error in a hook or coroutine


System

Read-only methods updated automatically every tick.

FunctionReturnsDescription
System.Time()numberCurrent timestamp in milliseconds
System.Mouse()x, yCursor position in pixels (multiple return values)
System.Screen()width, heightPrimary display dimensions in pixels (multiple return values)
System.Window()tableActive window info: { title, process, x, y, width, height }
System.Exec(cmd, options?)tableRun a shell command synchronously

System.Exec

local result = System.Exec(cmd: string, options?: { timeout?: number, cwd?: string })

Runs a shell command (cmd.exe /C on Windows, sh -c on Linux) and returns when it completes.

Returns: { exit: number, stdout: string, stderr: string }

Options:

  • timeout – max execution time in ms (default: 5000). The process is killed if it exceeds this.
  • cwd – working directory for the command.

Output is capped at 64KB per stream. The console window is suppressed on Windows.

-- list files
local result = System.Exec("dir /b", { cwd = "C:\\Users" })
Log.Info(result.stdout)

-- run a Python script
local result = System.Exec("python analyze.py", { timeout = 10000 })
if result.exit ~= 0 then
    Log.Error("Failed: " .. result.stderr)
end

Warning: System.Exec blocks the script tick while the command runs. Place long-running commands inside Run() to avoid stalling input processing.


HID

Sends keyboard and mouse output through the active transport.

Keyboard

FunctionDescription
HID.Down(key)Hold a key or button. Non-blocking, safe anywhere.
HID.Up(key)Release a key or button. Non-blocking, safe anywhere.
HID.Press(key, holdMs?)Tap a key (Down + Sleep + Up). Coroutine only. Default hold: 50ms
HID.Type(text, delayMs?)Type a string character by character. Coroutine only. Default delay: 30ms

Important: HID.Press and HID.Type use Sleep() internally and must be called inside a Run() coroutine or an Async() handler. Calling them outside a coroutine (e.g. directly in OnDown) will raise an error. Use HID.Down/HID.Up for non-blocking key control in hooks, or wrap your logic with Async().

-- WRONG: Press in a hook blocks the hot path
function OnDown(key)
    if key == "F1" then
        HID.Press("A") -- ERROR: not in a coroutine
    end
end

-- CORRECT: use Async to wrap the handler
Bind("F1", Async(function()
    HID.Press("LCtrl+V") -- works: Async provides a coroutine context
end))

-- CORRECT: use Run inside a hook
function OnDown(key)
    if key == "F1" then
        Run(function()
            HID.Press("A") -- works: Run provides a coroutine context
        end)
        return false
    end
    return true
end

-- CORRECT: non-blocking alternative (no coroutine needed)
function OnDown(key)
    if key == "F1" then
        HID.Down("A") -- instant, non-blocking
        return false
    end
    return true
end

Combos

Down, Up, and Press all support + for modifier combos:

HID.Press("LCtrl+V") -- Ctrl+V paste (coroutine only)
HID.Press("LCtrl+LShift+T") -- Ctrl+Shift+T (coroutine only)

HID.Down("LCtrl+LShift") -- hold both modifiers (non-blocking, works anywhere)
HID.Up("LCtrl+LShift") -- release both in reverse order (non-blocking)

HID.Press with combos presses keys left-to-right, sleeps for the hold duration (default 50ms), then releases right-to-left. HID.Down presses left-to-right. HID.Up releases right-to-left.

The + key itself is spelled Equal (unshifted) or KpPlus (numpad), so there is no ambiguity.

Mouse

FunctionDescription
HID.Move(dx, dy)Relative mouse movement (pixels)
HID.MoveTo(x, y)Move to absolute position via relative movement
HID.Scroll(amount)Vertical scroll (positive = up)

Mouse Mode

FunctionDescription
HID.SetMouseMode(mode)"relative" or "absolute"
HID.GetMouseMode()Returns current mode string

Relative mode (default): single-pass movement, fast (~0.5ms), may drift. Best for games.

Absolute mode: iterative correction with position validation, slower (1-3ms), sub-5px accuracy. Best for desktop automation.


Input

Reads the current physical state of input devices.

FunctionReturnsDescription
Input.IsDown(key)booleanWhether key/button is currently held
Input.GetDuration(key)numberMilliseconds held, 0 if not pressed
Input.GetActiveKeys()string[]All currently held keys
Input.GetMousePos(){x, y}Current cursor position
Input.GetModifiers()table{ctrl, shift, alt, win} booleans

UI

Defines a settings panel rendered in the Rebind UI.

Schema

local cfg = UI.Schema({
    key = UI.Widget(default, options?),
    ...
})

Returns a proxy handle. Read values with cfg.key, write with cfg.key = value.

Widgets

ConstructorDefault TypeDescription
UI.Toggle(default, opts?)booleanOn/off switch
UI.Slider(default, opts?)numberNumeric slider
UI.Keybind(default, opts?)stringKey binding selector
UI.Select(default, choices, opts?)stringDropdown selection
UI.Text(default, opts?)stringText input field

Widget Options

OptionTypeApplies toDescription
labelstringallDisplay label (overrides key name)
tooltipstringallHover description
groupstringallVisual section header
tabstringallTab panel name
showIfstringallShow only when referenced toggle is on
minnumberSliderMinimum value
maxnumberSliderMaximum value
stepnumberSliderIncrement size
suffixstringSliderUnit label (e.g. "%")
placeholderstringTextHint when empty
maxLengthnumberTextCharacter limit

Other

FunctionDescription
UI.Get(id)Read value by string key
UI.Set(id, value)Write value by string key
UI.GetAll()All current values as a table
UI.Notify(message, variant?)Show notification. Variant: "info" (default), "success", "warning", "error"

Persistence

Values are saved automatically on every change and restored at startup. Persistence is keyed to the script’s file path – no configuration required.


Macro

Record and play back input sequences.

Playback

FunctionDescription
Macro.Play(macro, speed?, mode?)Play a macro. Returns a handle.
Macro.StopAll()Stop all running macros

Speed: 1.0 = normal, 0.5 = half speed, 2.0 = double speed. Default: 1.0.

Mode: "parallel" (default), "replace".

Handle methods:

  • handle:Stop()
  • handle:Pause()
  • handle:Resume()
  • handle:IsPlaying() – returns boolean
  • handle:GetProgress() – returns 0.0 to 1.0
  • handle:Wait() – blocks the current coroutine until playback ends. must be called inside Run()

Hardware Streaming

FunctionDescription
Macro.Stream(macro)Stream to device for hardware-precision playback
Macro.Abort()Stop device-side playback immediately

Macros containing "type" actions always play host-side, since firmware cannot render text. Macro.Play() automatically falls back to host-side execution when any Type action is present.

Recording

FunctionDescription
Macro.Record(options?)Start recording input events
Macro.Finish()Stop recording, return macro table

Record options: { ignoreMouse = false, ignoreKeyboard = false, precision = "normal" }

Format

Actions in a macro table:

ActionFieldsDescription
(shorthand)x, y, delayMouse movement
"move"dx, dy, delayMouse movement (explicit)
"down"code, delayHold key
"up"code, delayRelease key
"press"code, holdMs, delayTap key
"type"text, charDelay, delayType string
"scroll"amount, delayScroll wheel
"sleep"delayPause

Timer

FunctionReturnsDescription
Timer.After(ms, callback)handleExecute once after delay
Timer.Every(ms, callback)handleExecute repeatedly at interval
Timer.CancelAll()Cancel all active timers

Handle methods: handle:Cancel(), handle:Pause(), handle:Resume()


Math

Random

FunctionReturnsDescription
Math.Random(min, max)numberUniform random number
Math.Gaussian(mean, stdDev)numberNormal distribution random

Transforms

All transform functions return a new table (original is unchanged).

FunctionDescription
Math.Scale(macro, xFactor, yFactor)Multiply movement values
Math.Spline(macro, tension)Catmull-Rom curve smoothing
Math.Resample(macro, intervalMs)Normalize timing to fixed intervals
Math.Interpolate(macro, intervalMs)Add smooth intermediate movement steps
Math.TimeComp(macro, targetMs)Scale total duration to target

Interpolate vs Resample:

  • Resample changes all delays uniformly (normalizing recorded macros)
  • Interpolate adds intermediate steps between existing points (smoothing patterns)

JSON

FunctionReturnsDescription
JSON.Parse(str)tableParse JSON string to Luau table
JSON.Stringify(tbl)stringSerialize table to JSON string

Log

FunctionDescription
Log.Info(message)Info-level log
Log.Warn(message)Warning-level log
Log.Error(message)Error-level log
Log.Debug(message)Debug-level log (dev mode only)

Globals: print(...) and log(...) are both aliases for Log.Info. They accept multiple arguments, joined by tabs — identical to standard Lua print behaviour.

log("feature enabled, sensitivity =", sensitivity)
print("device ready")

Logs appear in the Rebind UI Logs tab.


File

All paths are relative to your script’s directory. Path traversal (..) is rejected.

FunctionReturnsDescription
File.Read(path)stringRead file contents
File.Write(path, content)Overwrite file
File.Append(path, content)Append to file
File.Exists(path)booleanCheck if file exists
File.Delete(path)booleanDelete file
File.List(path)string[]List directory contents
File.MkDir(path)booleanCreate directory
File.ReadJSON(path)tableRead and parse JSON file
File.WriteJSON(path, table)Write table as JSON
File.GetScriptDir()stringAbsolute path to script directory

Net

Client

FunctionReturnsDescription
Net.Get(url, headers?, options?)responseHTTP GET
Net.Post(url, body, headers?, options?)responseHTTP POST
Net.Put(url, body, headers?, options?)responseHTTP PUT
Net.Patch(url, body, headers?, options?)responseHTTP PATCH
Net.Delete(url, headers?, options?)responseHTTP DELETE
Net.Head(url, headers?, options?)responseHTTP HEAD
Net.Request(options)responseGeneric HTTP request

Response: { status: number, body: string, headers: table }

Options: { timeout = milliseconds } – optional trailing table for convenience methods.

Net.Request options: { method: string, url: string, body?: string, headers?: table, timeout?: number }

-- simple GET
local resp = Net.Get("https://api.example.com/data")

-- POST with headers
local resp = Net.Post(
    "https://api.example.com/data",
    JSON.Stringify({ key = "value" }),
    { ["Content-Type"] = "application/json", ["Authorization"] = "Bearer token" }
)

-- DELETE with timeout
local resp = Net.Delete("https://api.example.com/item/123", nil, { timeout = 5000 })

-- generic request
local resp = Net.Request({
    method = "PATCH",
    url = "https://api.example.com/item/123",
    body = JSON.Stringify({ name = "updated" }),
    headers = { ["Content-Type"] = "application/json" },
    timeout = 10000,
})

Custom headers (including User-Agent) can be set on any request via the headers table.

HTTP Server

local server = Net.Listen(port, handler)

Handler: receives request, returns { status, body, headers? }.

Server handle: server:Stop()

Example:

local server = Net.Listen(8080, function(req)
    if req.method == "POST" then
        local data = JSON.Parse(req.body)
        return { status = 200, body = "OK" }
    end
    return { status = 404, body = "Not Found" }
end)

WebSocket Server

local server = Net.WSListen(port, handlers)

Handlers table: all optional.

HandlerSignatureFires when
OnConnectfunction(client)a new client has completed the WS handshake
OnMessagefunction(client, payload, is_binary)the server received a frame
OnClosefunction(client)the connection was closed (by either side)

Client handle (passed to handlers): client.id, client:Send(text), client:SendBinary(bytes), client:Close().

Server handle: server:Broadcast(text), server:BroadcastBinary(bytes), server:ClientCount(), server:Stop().

The server binds 0.0.0.0:<port>. Events are dispatched to Lua handlers on the script’s tick loop. Up to 32 events are processed per tick per server; excess messages backlog into the next tick. There is no built-in rate limiting — scripts exposed to untrusted networks should implement their own.

Performance characteristics (localhost, Windows host):

MetricValue
Round-trip RPC (p50)~1 ms
Round-trip RPC (p99)~2 ms
Sustained RPC throughput (16 in-flight)~10,000 req/s
Server-side message drain~12,000 msg/s
Client wire throughput (fire-and-forget)~100,000 msg/s

Each connection uses a dedicated reader thread that blocks on the socket and a writer thread that blocks on the outbound queue — writes dispatch with zero polling latency. TCP_NODELAY is set on all accepted sockets so small request/response traffic does not pay Nagle’s algorithm cost. On Windows the relay raises the system timer resolution to 1 ms so the tick loop actually runs at its declared rate.

Example:

local server = Net.WSListen(19561, {
    OnMessage = function(client, payload, is_binary)
        if payload == "hello" then
            client:Send("world")
        else
            server:Broadcast("someone said: " .. payload)
        end
    end,
})

WebSocket Client

local conn = Net.WSConnect(url, handlers)

URL: ws://host:port/path or wss://host:port/path. TLS via rustls (webpki roots).

Handlers table: all optional.

HandlerSignatureFires when
OnOpenfunction()the WS handshake completed
OnMessagefunction(payload, is_binary)a frame arrived from the server
OnClosefunction()the connection closed

Connection handle: conn:Send(text), conn:SendBinary(bytes), conn:Close().

Example:

local conn = Net.WSConnect("wss://echo.websocket.events", {
    OnOpen = function()
        conn:Send("hello from rebind")
    end,
    OnMessage = function(payload, is_binary)
        Log.Info("got: " .. payload)
    end,
})

Screen

FunctionReturnsDescription
Screen.GetPixelColor(x?, y?)stringHex color "RRGGBB" at pixel coordinates. If no args, reads at current mouse position.

Note: ~500us latency per call. Use sparingly.


Window

Window manipulation functions. Handles are integer values obtained from Window.Find() or Window.List(). Passing nil for a handle targets the active (foreground) window.

Query

FunctionReturnsDescription
Window.Find(title)number?Find first visible window whose title contains title (case-insensitive). Returns handle or nil.
Window.List(title?){WindowEntry}List all visible windows. Optional title filter. Each entry has handle, title, process, x, y, width, height.
Window.GetTitle(handle?)stringGet window title.
Window.GetPos(handle?){x, y, width, height}Get window position and size.
Window.GetPID(handle?)numberGet the process ID that owns the window.
Window.IsVisible(handle)booleanCheck if a window is visible.

Activation

FunctionDescription
Window.Activate(handle)Bring window to foreground. Auto-restores if minimized.

Movement / Sizing

FunctionDescription
Window.Move(handle, x?, y?, w?, h?)Move and/or resize. Omit any param to leave unchanged.

State Control

FunctionDescription
Window.Minimize(handle?)Minimize to taskbar.
Window.Maximize(handle?)Maximize to full screen.
Window.Restore(handle?)Restore from minimized or maximized.
Window.Close(handle?)Graceful close (sends WM_CLOSE).

Appearance

FunctionDescription
Window.SetAlwaysOnTop(handle, on?)Pin window above all others. on defaults to true.
Window.SetTransparency(handle, level)Set opacity: 0 (invisible) to 255 (opaque).
Window.SetTitle(handle, title)Change the window’s title bar text.

Waiting

FunctionReturnsDescription
Window.Wait(title, timeout?)number?Wait for a window to appear. Must be called inside Run(). Timeout in ms (default 5000). Returns handle or nil.

Examples

-- find and reposition a window
local hw = Window.Find("Notepad")
if hw then
    Window.Move(hw, 0, 0, 800, 600)
    Window.Activate(hw)
end

-- pin active window on top
local hw = Window.Find(System.Window().title)
if hw then
    Window.SetAlwaysOnTop(hw, true)
end

-- list all windows
for _, w in ipairs(Window.List()) do
    Log.Info(w.title .. " [" .. w.process .. "]")
end

-- wait for a game to launch
Run(function()
    local hw = Window.Wait("Counter-Strike 2", 30000)
    if hw then
        Window.Maximize(hw)
    end
end)

Audio

FunctionReturnsDescription
Audio.Beep()Play system beep
Audio.Play(path, options?)SoundHandlePlay an audio file (WAV, MP3, OGG, FLAC). Non-blocking.
Audio.StopAll()Stop all playing sounds
Audio.SetMasterVolume(vol)Set master volume (0.0 to 1.0)
Audio.GetMasterVolume()numberGet current master volume

Audio.Play

local sound = Audio.Play("alert.wav")
local music = Audio.Play("bgm.mp3", { volume = 0.5, loop = true })

Options:

OptionTypeDefaultDescription
volumenumber1.0Playback volume (0.0 to 1.0)
loopbooleanfalseRepeat when playback finishes

File paths are relative to the script directory (same sandboxing rules as File).

SoundHandle

The object returned by Audio.Play:

MethodReturnsDescription
sound:Stop()Stop playback and release resources
sound:Pause()Pause playback
sound:Resume()Resume paused playback
sound:IsPlaying()booleanTrue if playing (not paused, not finished)
sound:SetVolume(vol)Set per-sound volume (0.0 to 1.0)
sound:GetVolume()numberGet current per-sound volume

Always call Audio.StopAll() in OnStop and OnBlur to clean up playing sounds.


Clipboard

Read and write the system clipboard.

FunctionReturnsDescription
Clipboard.Get()string?Read current clipboard text. Returns nil if empty or non-text.
Clipboard.Set(text)Set clipboard text.
-- paste a multi-line message into Discord
local msg = "Line one\nLine two\nLine three"
Clipboard.Set(msg)
HID.Press("LCtrl+V")

-- read clipboard contents
local text = Clipboard.Get()
if text then
    Log.Info("Clipboard: " .. text)
end

Clipboard paste is the most reliable way to input multi-line or long text. Applications handle pasted newlines correctly, and there are no per-character timing concerns.


Dialog

Native OS dialogs for messages, confirmations, and file selection. All functions must be called inside Run() — they yield the coroutine while the dialog is open. Input processing, timers, and other coroutines continue running uninterrupted.

Linux/BSD: requires Zenity, KDialog, or YAD to be installed.

Message and Confirm

FunctionReturnsDescription
Dialog.Message(text, options?)Show an alert box. Yields until dismissed.
Dialog.Confirm(text, options?)booleanShow a yes/no dialog. Returns true if the user clicked Yes.

Options: { title?: string, level?: string }

level controls the icon: "info" (default), "warning", "error".

Run(function()
    Dialog.Message("Script finished.", { title = "Done" })

    local ok = Dialog.Confirm("Overwrite existing file?", { title = "Confirm", level = "warning" })
    if not ok then
        return
    end
end)

File Dialogs

FunctionReturnsDescription
Dialog.OpenFile(options?)string?Pick a single file. Returns path or nil if cancelled.
Dialog.OpenDir(options?)string?Pick a directory. Returns path or nil if cancelled.
Dialog.SaveFile(options?)string?Choose a save location. Returns path or nil if cancelled.

Options: { title?: string, location?: string, filters?: { { name: string, extensions: { string } } } }

  • location – initial directory the dialog opens in.
  • filters – restrict the file types shown. Each entry has a display name and a list of extensions (without leading dot).
Run(function()
    -- open a single Lua file
    local path = Dialog.OpenFile({
        title = "Open Script",
        filters = {
            { name = "Lua Scripts", extensions = { "lua", "luau" } },
        },
    })
    if path then
        local code = File.Read(path)
    end

    -- save a file
    local dest = Dialog.SaveFile({
        title = "Save As",
        location = File.GetScriptDir(),
        filters = {
            { name = "JSON", extensions = { "json" } },
        },
    })
    if dest then
        File.WriteJSON(dest, data)
    end

    -- pick a directory
    local dir = Dialog.OpenDir({ title = "Select Output Folder" })
    if dir then
        Log.Info("Output: " .. dir)
    end
end)

Regex

Pattern matching using regular expressions (PCRE-style syntax via the Rust regex crate). Backtracking-free by design – no risk of catastrophic backtracking.

Use [[ ]] long strings for patterns to avoid Luau escape interpretation: [[\d+]] instead of "\\d+".

FunctionReturnsDescription
Regex.IsMatch(text, pattern)booleanTest if text matches the pattern
Regex.Find(text, pattern)table?First match with captures, or nil
Regex.FindAll(text, pattern){table}All matches with captures
Regex.Replace(text, pattern, rep)stringReplace first match
Regex.ReplaceAll(text, pattern, rep)stringReplace all matches
Regex.Split(text, pattern){string}Split text on pattern

Match result

Regex.Find and Regex.FindAll return tables with:

{
    match = "full matched text",
    captures = { "group1", "group2" },  -- positional capture groups
    start = 8,    -- 1-indexed byte offset of match start
    finish = 11,  -- 1-indexed byte offset of match end
}

Replacement syntax

Replacements use $1, $2, etc. for capture group references:

Regex.ReplaceAll("John Smith", [[(\w+) (\w+)]], "$2, $1") --> "Smith, John"

Examples

-- test a pattern
if Regex.IsMatch(win.process, "discord|slack|teams") then
    -- chat app behavior
end

-- extract data
local m = Regex.Find("Price: 500g", [[(\d+)g]])
if m then
    local amount = tonumber(m.captures[1]) --> 500
end

-- split CSV
local fields = Regex.Split("a,b,,d", ",") --> {"a", "b", "", "d"}

Note: Luau also has built-in Lua patterns (string.find, string.match, string.gmatch) which use different syntax (%d instead of \d). The Regex namespace uses standard regex syntax and supports features Lua patterns lack: alternation (|), non-greedy quantifiers, lookahead, and more.


Config

Read and write TOML configuration files. TOML is a superset of INI for common use cases – simple key = value files work as-is, with support for typed values (booleans, numbers, strings), arrays, and nested tables.

FunctionReturnsDescription
Config.ParseTOML(text)tableParse a TOML string into a Lua table
Config.ToTOML(table)stringSerialize a Lua table to a TOML string
Config.ReadTOML(path)tableRead and parse a TOML file
Config.WriteTOML(path, table)Serialize and write a TOML file

File paths are relative to the script directory (same sandboxing rules as File).

-- settings.toml:
-- [general]
-- sensitivity = 0.8
-- enabled = true
-- weapon = "ak47"

local cfg = Config.ReadTOML("settings.toml")
Log.Info(cfg.general.sensitivity) --> 0.8
Log.Info(cfg.general.weapon) --> "ak47"

-- modify and save
cfg.general.sensitivity = 1.0
Config.WriteTOML("settings.toml", cfg)

For JSON config files, use the File.ReadJSON / File.WriteJSON functions in the File namespace.


Pipe

Shared memory IPC for communicating with external processes (Python, Node.js, etc).

Opening

local pipe = Pipe.Open(name, options?)

Creates or opens a shared memory region. Options: { size = 65536 } (min 1024, max 16MB).

Methods

MethodReturnsDescription
pipe:Read()string or nilRead latest data from external process. nil if nothing new.
pipe:Write(data)Write data for external process to read
pipe:Close()Release shared memory

Properties

PropertyTypeDescription
pipe.namestringPipe name
pipe.sizenumberTotal shared memory size
pipe.capacitynumberMax payload size per message

Script

FunctionDescription
Script.Exit(reason?)Stop the current script. Optional reason is logged.
Script.Reload()Reload the current script from disk

Globals: exit(reason?) and die(reason?) are aliases for Script.Exit.

die("shutting down for maintenance")

Bind

Declarative key binding as an alternative to writing OnDown/OnUp handlers.

-- simple binding (action on press, key is blocked by default)
local handle = Bind("F10", function()
    HID.Type("Hello!")
end)

-- explicitly pass the key through to the PC
local handle = Bind("F10", function()
    Log.Info("F10 was pressed")
    return true -- pass through (must be explicit)
end)

-- binding with condition, press, and release handlers
local handle = Bind("Mouse1", {
    when = function()
        return Input.IsDown("LAlt") -- only activate when Alt is held
    end,
    action = function()
        Log.Info("pressed")
    end,
    release = function()
        Log.Info("released")
    end,
})

-- toggle a UI boolean on keypress
local handle = Bind.Toggle("F9", "enabled")

-- simple remap (no logic needed)
local handle = Bind.Remap("Mouse4", "4")

Blocking

Bind blocks by default. This is the opposite of OnDown/OnUp.

Return valueOnDown/OnUpBind action
return truepass throughpass through
return falseblockblock
no return / nilpass throughblock

Most binds remap or trigger actions where blocking the original key is what you want. To pass the key through, you must explicitly return true.

When using Async() with Bind, the wrapper always returns false (block) immediately when it spawns the coroutine. Any return value inside the coroutine body has no effect on propagation – the decision was already made.

Handle

Handle methods: handle:unbind(), handle:disable(), handle:enable(), handle.enabled (read-only).

Routing

Keys claimed by Bind do not reach OnDown/OnUp. If a bind’s when guard returns false, the key falls through to the next bind or to OnDown/OnUp.


Modeline

-- rebind: key=value [key=value ...]
PropertyTypeDefaultDescription
namestringfilenameDisplay name
versionstring"0.0.0"Script version
authorstringCreator attribution
descriptionstringBrief explanation
min_sdkstringcurrentMinimum SDK version required
windowstringWindow title match (repeatable, case-insensitive)
processstringProcess name match (repeatable, case-insensitive)
tick_ratenumber1000OnTick frequency in Hz (max 8000)
z_indexinteger1Input priority (higher = first)
instancestring"replace""replace", "single", or "multiple"
mouse_modestring"relative""relative" or "absolute"
mouse_blockbooleanfalseWhen true, OnMove can block mouse input by returning false. When false, moves are forwarded immediately and OnMove fires asynchronously.
permissionstringGrant a permission to this script (repeatable). Valid values: exec, net. See Permissions below.
idstringDeprecated. Silently ignored. UI persistence is now automatic via file path.

Rules:

  • String-valued keys (name, description, author, window, process) must appear alone on their line
  • Scalar keys (tick_rate, z_index, etc.) can share a line
  • List fields use repeated keys: one window= per pattern, one process= per process, one permission= per permission

Permissions

By default, scripts can access all namespaces. If any permission= line is present, only the listed permissions are granted and all others are denied.

-- rebind: permission=exec
-- rebind: permission=net

-- now only exec and net are allowed; other restricted namespaces are open
PermissionRestricts
execSystem.Exec()
netNet.Get(), Net.Post(), Net.Put(), Net.Patch(), Net.Delete(), Net.Head(), Net.Request(), Net.Listen(), Net.WSListen(), Net.WSConnect()

Scripts published to the marketplace should declare the minimum permissions they need. Scripts without any permission= line have full access (backward compatible default).


Key Reference

Key names are case-insensitive. "F1", "f1", and "F1" are all identical. Use these strings with HID.Down, HID.Up, HID.Press, Bind, and in OnDown/OnUp hooks.

Letters

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

Numbers

0 1 2 3 4 5 6 7 8 9

Function Keys

F1 through F24. F13-F24 are HID output only (they work with HID.Press but will not appear in OnDown/OnUp hooks).

Mouse Buttons

ButtonName
Left clickMouse1
Right clickMouse2
Middle clickMouse3
BackMouse4
ForwardMouse5

Modifiers

KeyNameAliases
Left CtrlLCtrlCtrl, Control
Right CtrlRCtrl
Left ShiftLShiftShift
Right ShiftRShift
Left AltLAltAlt
Right AltRAlt
Left Win / CmdLWinWin, GUI, Windows, Command, Meta
Right Win / CmdRWinRGui, RMeta

Editing and Control

KeyNameAliases
EnterEnterReturn
EscapeEscapeEsc
BackspaceBackspace
TabTab
SpaceSpace
Caps LockCapsLock
KeyNameAliases
InsertInsert
DeleteDeleteDel
HomeHome
EndEnd
Page UpPageUpPgUp
Page DownPageDownPgDn
Arrow UpUp
Arrow DownDown
Arrow LeftLeft
Arrow RightRight

System

KeyNameAliases
Print ScreenPrintScreenPrintScr, PrtSc
Scroll LockScrollLock
Pause / BreakPauseBreak
Application / MenuMenuApp, ContextMenu

Punctuation

These names refer to the physical key, regardless of shift state.

KeyNameAliases
- / _Minus
= / +EqualEquals
[ / {LeftBracketLeftBrace, LBracket
] / }RightBracketRightBrace, RBracket
\ / |Backslash
; / :Semicolon
' / "ApostropheQuote
` / ~GraveBacktick, Tilde
, / <Comma
. / >PeriodDot
/ / ?Slash

Numpad

KeyNameAliases
Num LockNumLock
Numpad /KpDivideNpDivide
Numpad *KpMultiplyNpMultiply, KpAsterisk
Numpad -KpMinusNpSubtract
Numpad +KpPlusNpAdd
Numpad EnterKpEnterNpEnter
Numpad .KpDotNpDecimal, NpDot
Numpad 0-9Kp0 through Kp9NP0-NP9, Numpad0-Numpad9

Media Keys (HID Output Only)

These can be sent via HID.Press but will not appear in input hooks.

KeyNameAliases
Next TrackMediaNextMediaNextTrack
Previous TrackMediaPrevMediaPrevTrack
StopMediaStop
Play / PauseMediaPlayMediaPlayPause
Volume UpVolumeUpVolUp
Volume DownVolumeDownVolDown
MuteMuteVolumeMute

Examples

Complete, ready-to-use scripts organized by complexity. Copy any of these into a new script in the Rebind UI to get started.

Beginner

Logger

Logs all input events without modifying them. Great for understanding what key names Rebind uses and debugging your own scripts.

-- rebind: name=Input Logger

function OnDown(key)
    Log.Info("DOWN: " .. key)
    return true
end

function OnUp(key, duration)
    Log.Info("UP: " .. key .. " (held " .. duration .. "ms)")
    return true
end

function OnScroll(delta)
    Log.Info("SCROLL: " .. delta)
    return true
end

Key Remap

Simple global key remap. Block the original key, send a different one. Useful for Vim users or anyone who never uses CapsLock.

-- rebind: name=CapsLock to Escape

function OnDown(key)
    if key == "CapsLock" then
        HID.Down("Escape")
        return false
    end
    return true
end

function OnUp(key)
    if key == "CapsLock" then
        HID.Up("Escape")
        return false
    end
    return true
end

Scroll Zoom

Hold a modifier key and scroll to zoom. Works in browsers, editors, image viewers, and most desktop apps.

-- rebind: name=Scroll Zoom

local cfg = UI.Schema({
    modifier = UI.Keybind("RAlt", { label = "Modifier Key" }),
})

function OnScroll(delta)
    if Input.IsDown(cfg.modifier) then
        HID.Down("LCtrl")
        HID.Scroll(delta)
        HID.Up("LCtrl")
        return false
    end
    return true
end

Clipboard Paste

Paste multi-line text with a single hotkey. Demonstrates Clipboard.Set with combo syntax for reliable bulk text input.

-- rebind: name=Quick Paste
local cfg = UI.Schema({
    hotkey = UI.Keybind("F8", { label = "Paste Key" }),
    text = UI.Text("Hello World", { label = "Text to Paste", maxLength = 500 }),
})

Bind(cfg.hotkey, Async(function()
    Clipboard.Set(cfg.text)
    HID.Press("LCtrl+V")
end))

Media Keys

Remap Mouse4/Mouse5 to media controls. Only active on the desktop – passes through normally inside other apps.

-- rebind: name=Mouse Media Keys

local cfg = UI.Schema({
    enabled = UI.Toggle(true, { label = "Enable" }),
})

function OnDown(key)
    if not cfg.enabled then
        return true
    end

    if key == "Mouse4" then
        HID.Down("MediaPrev")
        return false
    end
    if key == "Mouse5" then
        HID.Down("MediaNext")
        return false
    end
    return true
end

function OnUp(key)
    if key == "Mouse4" then
        HID.Up("MediaPrev")
        return false
    end
    if key == "Mouse5" then
        HID.Up("MediaNext")
        return false
    end
    return true
end

Intermediate

Expander

Type abbreviations and have them expanded into full text. Triggered by a hotkey after typing the shortcut.

-- rebind: name=Text Expander
local cfg = UI.Schema({
    trigger = UI.Keybind("F8", { label = "Expand Key" }),
    email = UI.Text("user@example.com", { label = "Email Address", maxLength = 60 }),
    sig = UI.Text("Best regards,", { label = "Signature", maxLength = 100 }),
})

local snippets = {}

function OnStart()
    snippets = {
        ["@@"] = cfg.email,
        ["##"] = cfg.sig,
        ["/date"] = os.date("%Y-%m-%d"),
        ["/time"] = os.date("%H:%M"),
    }
end

local buffer = ""

function OnDown(key)
    if key == cfg.trigger then
        for abbr, expansion in pairs(snippets) do
            if buffer:sub(-#abbr) == abbr then
                -- erase the abbreviation
                Run(function()
                    for i = 1, #abbr do
                        HID.Press("Backspace")
                        Sleep(20)
                    end
                    HID.Type(expansion, 15)
                end)
                buffer = ""
                return false
            end
        end
        return false
    end

    -- track typed characters
    if #key == 1 then
        buffer = buffer .. key
        if #buffer > 20 then
            buffer = buffer:sub(-20)
        end
    elseif key == "Backspace" and #buffer > 0 then
        buffer = buffer:sub(1, -2)
    elseif key == "Space" or key == "Enter" then
        buffer = ""
    end

    return true
end

Audio Feedback

Play sound effects on key events. Useful for toggle confirmations, cooldown timers, or just making your macros feel responsive.

-- rebind: name=Audio Feedback

local cfg = UI.Schema({
    toggle = UI.Keybind("F8", { label = "Toggle Key" }),
    volume = UI.Slider(0.5, { min = 0, max = 1, label = "Volume" }),
})

local active = false

Bind(cfg.toggle, function()
    active = not active
    if active then
        Audio.Play("on.wav", { volume = cfg.volume })
    else
        Audio.Play("off.wav", { volume = cfg.volume })
    end
end)

function OnBlur()
    Audio.StopAll()
end

function OnStop()
    Audio.StopAll()
end

Chat Filter

Use Regex to detect patterns in window titles or process names. This example applies different behavior depending on which chat app is active.

-- rebind: name=Chat Filter

local chat_apps = [[discord|slack|teams|telegram]]

local cfg = UI.Schema({
    paste_key = UI.Keybind("F9", { label = "Quick Paste" }),
    message = UI.Text("", { label = "Message", maxLength = 200 }),
})

Bind(cfg.paste_key, Async(function()
    local win = System.Window()
    if not Regex.IsMatch(win.process, chat_apps) then
        return true
    end
    Clipboard.Set(cfg.message)
    HID.Press("LCtrl+V")
end))

Config-Driven Script

Load settings from a TOML file instead of hardcoding them. Users can edit the config file with any text editor without touching the script.

-- rebind: name=Config Demo

-- default config written on first run
local defaults = {
    sensitivity = 0.8,
    enabled = true,
    weapon = "ak47",
    keybinds = { toggle = "F8", reload = "F5" },
}

-- load or create config
local cfg
if File.Exists("settings.toml") then
    cfg = Config.ReadTOML("settings.toml")
else
    Config.WriteTOML("settings.toml", defaults)
    cfg = defaults
end

Log.Info("Loaded config: weapon=" .. cfg.weapon .. " sensitivity=" .. tostring(cfg.sensitivity))

Bind(cfg.keybinds.toggle, function()
    cfg.enabled = not cfg.enabled
    Config.WriteTOML("settings.toml", cfg)
    Log.Info("Enabled: " .. tostring(cfg.enabled))
end)

Photoshop

Custom brush size shortcuts that only activate when Photoshop is in the foreground. Completely inert otherwise – zero overhead.

-- rebind: name=Photoshop Shortcuts
-- rebind: process=Photoshop.exe

local cfg = UI.Schema({
    brush_step = UI.Slider(5, { min = 1, max = 20, label = "Brush Size Step" }),
})

function OnFocus()
    Log.Info("Photoshop detected -- shortcuts active")
end

function OnScroll(delta)
    -- alt+scroll to change brush size
    if Input.IsDown("LAlt") then
        local steps = cfg.brush_step
        Run(function()
            for i = 1, steps do
                if delta > 0 then
                    HID.Press("RightBrace")
                else
                    HID.Press("LeftBrace")
                end
                Sleep(10)
            end
        end)
        return false
    end
    return true
end

function OnDown(key)
    -- mouse4 = undo, mouse5 = redo
    if key == "Mouse4" then
        Run(function() HID.Press("LCtrl+Z") end)
        return false
    end
    if key == "Mouse5" then
        Run(function() HID.Press("LCtrl+LShift+Z") end)
        return false
    end
    return true
end

Browser Tabs

Navigate browser tabs with mouse side buttons. Works with Chrome, Firefox, Edge, and most Chromium-based browsers.

-- rebind: name=Browser Tab Nav
-- rebind: process=chrome.exe
-- rebind: process=firefox.exe
-- rebind: process=msedge.exe
-- rebind: process=brave.exe

function OnDown(key)
    -- mouse4 = previous tab, mouse5 = next tab
    if key == "Mouse4" then
        Run(function() HID.Press("LCtrl+PageUp") end)
        return false
    end
    if key == "Mouse5" then
        Run(function() HID.Press("LCtrl+PageDown") end)
        return false
    end

    -- middle-click scroll wheel = close tab
    if key == "Mouse3" and Input.IsDown("LAlt") then
        Run(function() HID.Press("LCtrl+W") end)
        return false
    end

    return true
end

Sticky Mod

Turn any key into a toggle instead of a hold. Tap Shift once and it stays “held” until you tap it again. Useful for accessibility or one-handed workflows.

-- rebind: name=Sticky Modifier
local cfg = UI.Schema({
    target = UI.Keybind("LShift", { label = "Modifier to Toggle" }),
})

local held = false

function OnDown(key)
    if key == cfg.target then
        held = not held
        if held then
            HID.Down(cfg.target)
            UI.Notify("Modifier locked", "info")
        else
            HID.Up(cfg.target)
            UI.Notify("Modifier released", "info")
        end
        return false
    end
    return true
end

function OnUp(key)
    if key == cfg.target then
        return false
    end
    return true
end

function OnBlur()
    if held then
        held = false
        HID.Up(cfg.target)
    end
end

Precision

Hold a key to temporarily slow down mouse movement for fine positioning. Great for image editing, CAD, or any precision work.

-- rebind: name=Precision Mouse
local cfg = UI.Schema({
    hold_key = UI.Keybind("Mouse3", { label = "Precision Key" }),
    slowdown = UI.Slider(25, { min = 5, max = 75, suffix = "%", label = "Speed" }),
})

function OnMove(dx, dy)
    if Input.IsDown(cfg.hold_key) then
        local factor = cfg.slowdown / 100
        HID.Move(dx * factor, dy * factor)
        return false
    end
    return true
end

Mouse Inversion

Invert mouse axes. Swallows the original input and sends the negated version. Remove one negation to invert only horizontal or vertical.

-- rebind: name=Mouse Invert
-- rebind: mouse_block=true

function OnMove(dx, dy)
    HID.Move(-dx, -dy)
    return false
end

Sequence

Demonstrates sequential actions with Run() and Sleep(), plus cancellation. A general pattern for multi-step automation.

-- rebind: name=Sequence Demo

local cfg = UI.Schema({
    start_key = UI.Keybind("F9", { label = "Start Sequence" }),
})

local task = nil

function OnDown(key)
    if key == cfg.start_key then
        task = Run(function()
            Log.Info("Starting sequence...")

            HID.Press("LCtrl+A") -- select all
            Sleep(200)

            HID.Press("LCtrl+C") -- copy
            Sleep(200)

            HID.Press("End") -- move to end
            Sleep(100)
            HID.Press("Enter")
            HID.Press("Enter")
            Sleep(100)

            HID.Press("LCtrl+V") -- paste

            Log.Info("Sequence complete!")
        end)
        return false
    end

    if key == "F10" and task and task:IsRunning() then
        task:Cancel()
        UI.Notify("Cancelled", "info")
        return false
    end

    return true
end

function OnStop()
    if task and task:IsRunning() then
        task:Cancel()
    end
end

Advanced

UI Widgets

Demonstrates all available UI widgets and layout options.

-- rebind: name=UI Showcase

local cfg = UI.Schema({
    enabled = UI.Toggle(true, { label = "Enable Feature" }),

    strength = UI.Slider(100, {
        min = 0,
        max = 200,
        suffix = "%",
        step = 5,
        tooltip = "Adjust the strength",
        group = "Settings",
    }),

    hotkey = UI.Keybind("F6", { label = "Toggle Key" }),

    mode = UI.Select("Normal", { "Normal", "Fast", "Precise", "Custom" }, {
        label = "Operating Mode",
    }),

    tag = UI.Text("", {
        label = "Username",
        placeholder = "Enter your name",
        maxLength = 20,
    }),

    sensitivity = UI.Slider(50, {
        min = 1,
        max = 100,
        suffix = "%",
        group = "Advanced",
        tab = "Controls",
    }),
})

function OnStart()
    Log.Info("Enabled: " .. tostring(cfg.enabled))
    Log.Info("Strength: " .. cfg.strength .. "%")
    Log.Info("Mode: " .. cfg.mode)
end

Workflow Hub

A single script that provides different shortcuts for different applications. Demonstrates the power of System.Window() for runtime routing without needing separate scripts for each app.

-- rebind: name=Workflow Hub
local cfg = UI.Schema({
    enabled = UI.Toggle(true, { label = "Enable" }),
})

local function isProcess(name)
    local win = System.Window()
    return win.process:lower():find(name:lower()) ~= nil
end

function OnDown(key)
    if not cfg.enabled then
        return true
    end

    -- mouse4/mouse5 do different things per app
    if key == "Mouse4" then
        if isProcess("Photoshop") then
            Run(function() HID.Press("LCtrl+Z") end)
        elseif isProcess("chrome") or isProcess("firefox") or isProcess("msedge") then
            Run(function() HID.Press("LCtrl+PageUp") end)
        elseif isProcess("explorer") then
            Run(function() HID.Press("LAlt+Left") end)
        else
            return true
        end
        return false
    end

    if key == "Mouse5" then
        if isProcess("Photoshop") then
            Run(function() HID.Press("LCtrl+LShift+Z") end)
        elseif isProcess("chrome") or isProcess("firefox") or isProcess("msedge") then
            Run(function() HID.Press("LCtrl+PageDown") end)
        elseif isProcess("explorer") then
            Run(function() HID.Press("LAlt+Right") end)
        else
            return true
        end
        return false
    end

    return true
end

Auto-Clicker

A simple auto-clicker that uses Timer.Every instead of a Run/Sleep loop. Demonstrates the timer API and toggle pattern.

-- rebind: name=Auto Clicker
local cfg = UI.Schema({
    cps = UI.Slider(5, { min = 1, max = 20, label = "Clicks Per Second" }),
    toggle_key = UI.Keybind("F7", { label = "Toggle Key" }),
})

local active = false
local clickTimer = nil

function OnDown(key)
    if key == cfg.toggle_key then
        active = not active

        if active then
            local interval = 1000 / cfg.cps
            clickTimer = Timer.Every(interval, function()
                Run(function() HID.Press("Mouse1", 20) end)
            end)
            UI.Notify("Auto-clicker ON", "info")
        else
            if clickTimer then
                clickTimer:Cancel()
                clickTimer = nil
            end
            UI.Notify("Auto-clicker OFF", "info")
        end

        return false
    end
    return true
end

function OnStop()
    if clickTimer then
        clickTimer:Cancel()
    end
end

function OnBlur()
    if active then
        active = false
        if clickTimer then
            clickTimer:Cancel()
            clickTimer = nil
        end
    end
end

Macros

Play pre-defined input sequences with configurable speed. Useful for automating repetitive tasks, form filling, or testing.

-- rebind: name=Macro Demo

local cfg = UI.Schema({
    speed = UI.Slider(100, { min = 25, max = 400, suffix = "%", label = "Speed" }),
    play_key = UI.Keybind("F9", { label = "Play" }),
    stop_key = UI.Keybind("F10", { label = "Stop All" }),
})

-- a keyboard combo macro (explicit format)
local formFill = {
    { action = "press", code = "Tab", holdMs = 30, delay = 100 },
    { action = "type", text = "John Doe", charDelay = 30, delay = 100 },
    { action = "press", code = "Tab", holdMs = 30, delay = 100 },
    { action = "type", text = "john@example.com", charDelay = 30, delay = 100 },
    { action = "press", code = "Tab", holdMs = 30, delay = 100 },
    { action = "press", code = "Tab", holdMs = 30, delay = 100 },
    { action = "press", code = "Enter", holdMs = 50, delay = 0 },
}

function OnDown(key)
    if key == cfg.play_key then
        local speed = cfg.speed / 100
        Macro.Play(formFill, speed, "replace")
        UI.Notify("Playing", "info")
        return false
    end

    if key == cfg.stop_key then
        Macro.StopAll()
        UI.Notify("Stopped", "info")
        return false
    end

    return true
end

function OnStop()
    Macro.StopAll()
end

Sidecar Pipe

Communicate with an external Python/Node.js process using shared memory. This lets you bridge Rebind scripts with computer vision, AI models, or any other external tool.

-- rebind: name=Pipe Demo

local pipe = nil

function OnStart()
    pipe = Pipe.Open("myapp", { size = 65536 })
    Log.Info("Pipe opened: " .. pipe.name)

    Timer.Every(10, function()
        local msg = pipe:Read()
        if msg then
            local data = JSON.Parse(msg)
            Log.Info("Received command: " .. (data.action or "unknown"))

            if data.action == "type" and data.text then
                Run(function() HID.Type(data.text, 30) end)
            elseif data.action == "press" and data.key then
                Run(function() HID.Press(data.key) end)
            end
        end
    end)
end

function OnStop()
    if pipe then
        pipe:Close()
    end
end

The external process connects to the same shared memory region. For example, in Python:

import mmap
import struct
import json

shm = mmap.mmap(-1, 65536, tagname="Local\\Rebind_myapp")

# write to channel B (script's inbox) at offset size/2
offset = 65536 // 2
data = json.dumps({"action": "type", "text": "Hello from Python!"}).encode()

shm.seek(offset)
shm.write(struct.pack('<Q', 1))           # sequence number
shm.write(struct.pack('<I', len(data)))   # payload length
shm.write(data)

Macro Recorder

Full macro recording workflow: record input to a file, play it back with configurable speed and smoothing.

-- rebind: name=Macro Recorder
local cfg = UI.Schema({
    record = UI.Keybind("F9", { label = "Record / Save" }),
    play = UI.Keybind("F10", { label = "Play" }),
    stop = UI.Keybind("Escape", { label = "Stop" }),
    name = UI.Text("macro", { maxLength = 50 }),
    smooth = UI.Toggle(false, { label = "Smooth Playback" }),
    speed = UI.Slider(100, { min = 25, max = 400, suffix = "%" }),
})

local isRecording = false
local activeMacro = nil

function OnStart()
    File.MkDir("macros")
end

function OnStop()
    if isRecording then
        Macro.Finish()
    end
    if activeMacro then
        activeMacro:Stop()
    end
end

function OnDown(key)
    if key == cfg.record then
        if isRecording then
            local macro = Macro.Finish()
            if cfg.smooth then
                macro = Math.Spline(macro, 0.5)
            end
            File.WriteJSON("macros/" .. cfg.name .. ".json", macro)
            UI.Notify("Saved", "success")
            Audio.Beep()
            isRecording = false
        else
            Macro.Record({ precision = "high" })
            UI.Notify("Recording...", "info")
            Audio.Beep()
            isRecording = true
        end
        return false
    end

    if key == cfg.play then
        local path = "macros/" .. cfg.name .. ".json"
        if File.Exists(path) then
            activeMacro = Macro.Play(File.ReadJSON(path), cfg.speed / 100)
            UI.Notify("Playing", "info")
        else
            UI.Notify("Not found", "error")
        end
        return false
    end

    if key == cfg.stop and activeMacro then
        activeMacro:Stop()
        activeMacro = nil
        UI.Notify("Stopped", "info")
        return false
    end

    return true
end

function OnBlur()
    if activeMacro then
        activeMacro:Stop()
        activeMacro = nil
    end
end

Color Trigger

Click when a target color is detected at the center of the screen. Demonstrates pixel color sampling and polling.

-- rebind: name=Color Trigger
local cfg = UI.Schema({
    enabled = UI.Toggle(true),
    color = UI.Text("FF0000", { maxLength = 6, label = "Target Color (hex)" }),
    tolerance = UI.Slider(20, { min = 0, max = 100 }),
    trigger = UI.Keybind("Mouse2", { label = "Hold to Scan" }),
})

local function hexToRgb(hex)
    return tonumber(hex:sub(1, 2), 16), tonumber(hex:sub(3, 4), 16), tonumber(hex:sub(5, 6), 16)
end

local function colorDistance(hex1, hex2)
    local r1, g1, b1 = hexToRgb(hex1)
    local r2, g2, b2 = hexToRgb(hex2)
    return math.sqrt((r2 - r1) ^ 2 + (g2 - g1) ^ 2 + (b2 - b1) ^ 2)
end

function OnTick()
    if not cfg.enabled then
        return
    end
    if not Input.IsDown(cfg.trigger) then
        return
    end

    local w, h = System.Screen()
    local pixel = Screen.GetPixelColor(math.floor(w / 2), math.floor(h / 2))

    if colorDistance(pixel, cfg.color) <= cfg.tolerance then
        Run(function() HID.Press("Mouse1", 20) end)
    end
end

HTTP Remote

Control Rebind from external tools via HTTP. Any program that can send HTTP requests (curl, Python, browser) can trigger actions.

-- rebind: name=HTTP Remote
-- rebind: permission=net
local cfg = UI.Schema({
    enabled = UI.Toggle(true),
    port = UI.Slider(8080, { min = 1024, max = 65535 }),
})

local server = nil

function OnStart()
    server = Net.Listen(cfg.port, function(req)
        if req.path == "/click" then
            Run(function() HID.Press("Mouse1") end)
            return { status = 200, body = "clicked" }
        elseif req.path == "/type" and req.body then
            Run(function() HID.Type(req.body) end)
            return { status = 200, body = "typed" }
        elseif req.path == "/move" then
            local data = JSON.Parse(req.body)
            HID.Move(data.dx or 0, data.dy or 0)
            return { status = 200, body = "moved" }
        end
        return { status = 404, body = "not found" }
    end)
    Log.Info("Remote control on port " .. cfg.port)
end

function OnStop()
    if server then
        server:Stop()
    end
end

WebSocket Remote

Same idea as HTTP Remote but over a persistent WebSocket connection. The server handles multiple simultaneous clients, broadcasts to all of them, and reacts to messages within a single tick. Use this when you need push events (server → client) or a stable low-latency channel for high-frequency commands.

-- rebind: name=WebSocket Remote
-- rebind: permission=net

local cfg = UI.Schema({
    port = UI.Slider(9000, { min = 1024, max = 65535, label = "WS port" }),
})

local server = nil

function OnStart()
    server = Net.WSListen(cfg.port, {
        OnConnect = function(client)
            Log.Info(string.format("client %d connected (%d total)", client.id, server:ClientCount()))
            client:Send("welcome " .. client.id)
        end,

        OnMessage = function(client, payload, is_binary)
            local req = JSON.Decode(payload)
            if req.t == "click" then
                Run(function() HID.Press("Mouse1") end)
                client:Send(JSON.Encode({ id = req.id, ok = true }))
            elseif req.t == "type" then
                Run(function() HID.Type(req.text or "") end)
                client:Send(JSON.Encode({ id = req.id, ok = true }))
            elseif req.t == "move" then
                HID.Move(req.dx or 0, req.dy or 0)
            elseif req.t == "broadcast" then
                -- forward a message to every connected client
                server:Broadcast(payload)
            end
        end,

        OnClose = function(client)
            Log.Info(string.format("client %d disconnected", client.id))
        end,
    })
    Log.Info("WebSocket remote listening on ws://0.0.0.0:" .. cfg.port)
end

function OnStop()
    if server then
        server:Stop()
    end
end

Connect from Python with websocket-client:

import json
from websocket import create_connection

ws = create_connection("ws://127.0.0.1:9000")
print(ws.recv())  # "welcome 1"
ws.send(json.dumps({"t": "type", "text": "hello from python"}))
ws.send(json.dumps({"t": "move", "dx": 50, "dy": 0}))

Net.WSListen can expose arbitrary JSON-RPC surfaces — reads (screen pixels, window state, clipboard, input state), writes (HID commands), and push subscriptions (mouse position, window changes, input events) all compose naturally with the same handler pattern shown above. See the WebSocket performance characteristics in the SDK reference for throughput and latency bounds.

Twitch Clip Creator

Create Twitch clips on demand with a hotkey. Demonstrates Net.Post for external API calls, Bind with Async for non-blocking execution, and File.Append for logging results.

-- rebind: min_sdk=0.1.0
-- rebind: name=Twitch Clip Creator
-- rebind: permission=net

local cfg = UI.Schema({
    trigger = UI.Keybind("F9", { label = "Clip Hotkey" }),
    client_id = UI.Text("", { label = "Twitch Client ID", placeholder = "Your Twitch API Client ID" }),
    oauth_token = UI.Text("", { label = "OAuth Token", placeholder = "Bearer your_oauth_token" }),
    broadcaster_id = UI.Text("", { label = "Broadcaster ID", placeholder = "Your Twitch numeric user ID" }),
    clip_cooldown = UI.Slider(5, { min = 1, max = 30, label = "Clip Cooldown", suffix = "sec" }),
})

local lastClipTime = 0

Bind(
    cfg.trigger,
    Async(function()
        local now = System.Time()
        local cooldownMs = cfg.clip_cooldown * 1000

        if now - lastClipTime < cooldownMs then
            UI.Notify("Clip on cooldown", "warning")
            return false
        end

        if not cfg.client_id or not cfg.oauth_token or not cfg.broadcaster_id then
            UI.Notify("Missing Twitch credentials", "error")
            return false
        end

        local response =
            Net.Post("https://api.twitch.tv/helix/clips", JSON.Stringify({ broadcaster_id = cfg.broadcaster_id }), {
                ["Client-ID"] = cfg.client_id,
                ["Authorization"] = cfg.oauth_token,
                ["Content-Type"] = "application/json",
            })

        if response.status == 200 then
            local clipData = JSON.Parse(response.body)
            local clipUrl = clipData.data[1].edit_url:gsub("/edit", "")

            UI.Notify("Clip created!", "success")
            Log.Info("Clip URL: " .. clipUrl)

            File.Append("clips.txt", clipUrl .. "\n")

            lastClipTime = now
        else
            UI.Notify("Clip creation failed", "error")
            Log.Error("Clip API Error: " .. response.status)
        end

        return false
    end)
)

Twitch Streamer Toolkit

A comprehensive streaming control panel combining clips, ad breaks, slow mode, and stream markers into a single script. Demonstrates multiple Bind/Async handlers, Net.Patch, shared helper functions, UI groups, and session logging via File.Append.

-- rebind: min_sdk=0.1.0
-- rebind: name=Twitch Streamer Toolkit
-- rebind: permission=net

local cfg = UI.Schema({
    -- Credentials
    client_id = UI.Text("", {
        label = "Twitch Client ID",
        placeholder = "Your Twitch API Client ID",
        group = "Credentials",
    }),
    oauth_token = UI.Text("", {
        label = "OAuth Token",
        placeholder = "Bearer your_oauth_token",
        group = "Credentials",
    }),
    broadcaster_id = UI.Text("", {
        label = "Broadcaster ID",
        placeholder = "Your Twitch numeric user ID",
        group = "Credentials",
    }),
    moderator_id = UI.Text("", {
        label = "Moderator ID",
        placeholder = "Moderator's Twitch numeric user ID",
        group = "Credentials",
    }),

    -- Hotkeys
    clip_key = UI.Keybind("F9", { label = "Clip Hotkey", group = "Hotkeys" }),
    ad_key = UI.Keybind("F10", { label = "Ad Break Hotkey", group = "Hotkeys" }),
    slowmode_key = UI.Keybind("F11", { label = "Slow Mode Hotkey", group = "Hotkeys" }),
    marker_key = UI.Keybind("F12", { label = "Stream Marker Hotkey", group = "Hotkeys" }),

    -- Ad Configuration
    ad_length = UI.Select(30, {
        30,
        60,
        90,
        120,
        150,
        180,
        label = "Ad Length",
        suffix = "sec",
        group = "Ad Break",
    }),

    -- Slow Mode Configuration
    slowmode_delay = UI.Slider(5, {
        min = 1,
        max = 120,
        label = "Slow Mode Delay",
        suffix = "sec",
        group = "Slow Mode",
    }),
    slowmode_active = UI.Toggle(false, {
        label = "Slow Mode Enabled",
        group = "Slow Mode",
    }),

    -- Marker Configuration
    marker_desc = UI.Text("", {
        label = "Marker Description",
        placeholder = "Optional marker description",
        group = "Stream Marker",
    }),

    -- Global Settings
    global_cooldown = UI.Slider(5, {
        min = 1,
        max = 30,
        label = "Global Action Cooldown",
        suffix = "sec",
        group = "General",
    }),
})

local lastActionTime = 0

local function isOnCooldown()
    local now = System.Time()
    local cooldownMs = cfg.global_cooldown * 1000

    if now - lastActionTime < cooldownMs then
        UI.Notify("Action on cooldown", "warning")
        return true
    end

    lastActionTime = now
    return false
end

local function logAction(action, details)
    local timestamp = os.date("%Y-%m-%d %H:%M:%S")
    local logEntry = string.format("[%s] %s - %s\n", timestamp, action, details or "")
    File.Append("stream-session.txt", logEntry)
end

local function checkCredentials()
    if not cfg.client_id or not cfg.oauth_token or not cfg.broadcaster_id or not cfg.moderator_id then
        UI.Notify("Missing Twitch credentials", "error")
        return false
    end
    return true
end

Bind(
    cfg.clip_key,
    Async(function()
        if isOnCooldown() or not checkCredentials() then
            return false
        end

        local response =
            Net.Post("https://api.twitch.tv/helix/clips", JSON.Stringify({ broadcaster_id = cfg.broadcaster_id }), {
                ["Client-ID"] = cfg.client_id,
                ["Authorization"] = cfg.oauth_token,
                ["Content-Type"] = "application/json",
            })

        if response.status == 200 then
            local clipData = JSON.Parse(response.body)
            local clipUrl = clipData.data[1].edit_url:gsub("/edit", "")

            UI.Notify("Clip created successfully!", "success")
            logAction("CLIP_CREATED", clipUrl)
        else
            UI.Notify("Clip creation failed", "error")
            logAction("CLIP_FAILED", "Status: " .. response.status)
        end

        return false
    end)
)

Bind(
    cfg.ad_key,
    Async(function()
        if isOnCooldown() or not checkCredentials() then
            return false
        end

        local response = Net.Post(
            "https://api.twitch.tv/helix/channels/commercial",
            JSON.Stringify({
                broadcaster_id = cfg.broadcaster_id,
                length = cfg.ad_length,
            }),
            {
                ["Client-ID"] = cfg.client_id,
                ["Authorization"] = cfg.oauth_token,
                ["Content-Type"] = "application/json",
            }
        )

        if response.status == 200 then
            UI.Notify(cfg.ad_length .. "s Ad Break Started", "success")
            logAction("AD_BREAK", tostring(cfg.ad_length) .. " seconds")
        else
            UI.Notify("Ad Break Failed", "error")
            logAction("AD_BREAK_FAILED", "Status: " .. response.status)
        end

        return false
    end)
)

Bind(
    cfg.slowmode_key,
    Async(function()
        if isOnCooldown() or not checkCredentials() then
            return false
        end

        cfg.slowmode_active = not cfg.slowmode_active

        local response = Net.Patch(
            "https://api.twitch.tv/helix/chat/settings",
            JSON.Stringify({
                broadcaster_id = cfg.broadcaster_id,
                moderator_id = cfg.moderator_id,
                slow_mode = cfg.slowmode_active,
                slow_mode_wait_time = cfg.slowmode_active and cfg.slowmode_delay or 0,
            }),
            {
                ["Client-ID"] = cfg.client_id,
                ["Authorization"] = cfg.oauth_token,
                ["Content-Type"] = "application/json",
            }
        )

        if response.status == 200 then
            local status = cfg.slowmode_active and "ENABLED" or "DISABLED"
            UI.Notify("Slow Mode " .. status, cfg.slowmode_active and "warning" or "info")
            logAction("SLOWMODE_" .. status, tostring(cfg.slowmode_delay) .. " seconds")
        else
            UI.Notify("Slow Mode Toggle Failed", "error")
            logAction("SLOWMODE_FAILED", "Status: " .. response.status)
            cfg.slowmode_active = not cfg.slowmode_active
        end

        return false
    end)
)

Bind(
    cfg.marker_key,
    Async(function()
        if isOnCooldown() or not checkCredentials() then
            return false
        end

        local response = Net.Post(
            "https://api.twitch.tv/helix/streams/markers",
            JSON.Stringify({
                user_id = cfg.broadcaster_id,
                description = cfg.marker_desc or "Stream Marker",
            }),
            {
                ["Client-ID"] = cfg.client_id,
                ["Authorization"] = cfg.oauth_token,
                ["Content-Type"] = "application/json",
            }
        )

        if response.status == 200 then
            UI.Notify("Stream Marker Created", "success")
            logAction("STREAM_MARKER", cfg.marker_desc or "Unnamed Marker")
        else
            UI.Notify("Stream Marker Failed", "error")
            logAction("MARKER_FAILED", "Status: " .. response.status)
        end

        return false
    end)
)

Accessibility

Tremor Smoothing

Smooths mouse movement for users with hand tremors. Filters small jittery movements and applies exponential smoothing to larger ones.

-- rebind: name=Steady Hand
local cfg = UI.Schema({
    enabled = UI.Toggle(true),
    smoothing = UI.Slider(50, {
        min = 0,
        max = 90,
        suffix = "%",
        tooltip = "Higher = smoother but more latency",
    }),
    threshold = UI.Slider(5, {
        min = 1,
        max = 20,
        suffix = "px",
        tooltip = "Filter movements smaller than this",
    }),
})

local buffer = { x = 0, y = 0 }

function OnMove(dx, dy)
    if not cfg.enabled then
        return true
    end

    local magnitude = math.sqrt(dx * dx + dy * dy)
    if magnitude < cfg.threshold then
        return false
    end

    local s = cfg.smoothing / 100
    buffer.x = buffer.x * s + dx * (1 - s)
    buffer.y = buffer.y * s + dy * (1 - s)

    HID.Move(buffer.x, buffer.y)
    return false
end

function OnBlur()
    buffer.x = 0
    buffer.y = 0
end

Click Assist

Multiple click modes for users with difficulty clicking or holding. Hold Assist auto-releases after a timer, Toggle Click makes single clicks act as hold toggles, Double Click fires two clicks from one.

-- rebind: name=Click Assist
local cfg = UI.Schema({
    enabled = UI.Toggle(true),
    mode = UI.Select("Hold Assist", { "Hold Assist", "Toggle Click", "Double Click" }),
    hold_time = UI.Slider(500, {
        min = 100,
        max = 3000,
        suffix = "ms",
        showIf = "mode:Hold Assist",
    }),
    double_delay = UI.Slider(50, {
        min = 20,
        max = 200,
        suffix = "ms",
        showIf = "mode:Double Click",
    }),
})

local toggleState = false

function OnStop()
    if toggleState then
        HID.Up("Mouse1")
    end
end

function OnDown(key)
    if key ~= "Mouse1" or not cfg.enabled then
        return true
    end

    if cfg.mode == "Hold Assist" then
        Run(function()
            HID.Down("Mouse1")
            Audio.Beep()
            Sleep(cfg.hold_time)
            HID.Up("Mouse1")
        end)
        return false
    elseif cfg.mode == "Toggle Click" then
        toggleState = not toggleState
        if toggleState then
            HID.Down("Mouse1")
            Audio.Beep()
        else
            HID.Up("Mouse1")
        end
        return false
    elseif cfg.mode == "Double Click" then
        Run(function()
            HID.Press("Mouse1", 30)
            Sleep(cfg.double_delay)
            HID.Press("Mouse1", 30)
        end)
        return false
    end

    return true
end

function OnBlur()
    if toggleState then
        HID.Up("Mouse1")
        toggleState = false
    end
end

Dwell Click

Click by hovering the cursor in one spot. For users who cannot physically click a mouse button.

-- rebind: name=Dwell Click
local cfg = UI.Schema({
    enabled = UI.Toggle(true),
    dwell_time = UI.Slider(1000, { min = 300, max = 3000, suffix = "ms" }),
    tolerance = UI.Slider(10, { min = 5, max = 50, suffix = "px" }),
    click_type = UI.Select("Left Click", { "Left Click", "Right Click", "Double Click" }),
    sound = UI.Toggle(true, { label = "Audio Feedback" }),
})

local dwellStart = nil
local dwellPos = { x = 0, y = 0 }

function OnTick()
    if not cfg.enabled then
        dwellStart = nil
        return
    end

    local pos = Input.GetMousePos()
    local dx = pos.x - dwellPos.x
    local dy = pos.y - dwellPos.y
    local distance = math.sqrt(dx * dx + dy * dy)

    if distance > cfg.tolerance then
        dwellPos = pos
        dwellStart = System.Time()
        return
    end

    if not dwellStart then
        dwellStart = System.Time()
        return
    end

    if System.Time() - dwellStart >= cfg.dwell_time then
        if cfg.click_type == "Left Click" then
            Run(function() HID.Press("Mouse1") end)
        elseif cfg.click_type == "Right Click" then
            Run(function() HID.Press("Mouse2") end)
        elseif cfg.click_type == "Double Click" then
            Run(function()
                HID.Press("Mouse1", 30)
                Sleep(50)
                HID.Press("Mouse1", 30)
            end)
        end

        if cfg.sound then
            Audio.Beep()
        end
        dwellStart = nil
    end
end

function OnBlur()
    dwellStart = nil
end

Remote Control

Rebind exposes a JSON-RPC WebSocket protocol that lets external programs send HID commands, read screen and input state, and subscribe to live event streams — without writing a single line of Lua.

The protocol is served by the Remote Access script that ships with Rebind. Install it once and any conforming client can connect.

Setup

  1. Download remote_access.lua and copy it to your Rebind scripts directory:
    • Windows: %APPDATA%\Rebind\save_data\scripts\
  2. Open Rebind → Scripts → start Remote Access.
  3. The script binds a WebSocket server on ws://0.0.0.0:19561 (port is configurable in the script’s settings panel).
  4. Install a client library for your language and connect.

Command surface

CategoryCommands
HID writeshid.down, hid.up, hid.press, hid.type, hid.move, hid.move_to, hid.scroll
Screenscreen.pixel, screen.resolution
Systemsystem.mouse, system.window, system.time
Inputinput.keys, input.is_down, input.modifiers
Clipboardclipboard.get, clipboard.set
Windowwindow.list, window.find, window.activate, window.move
Eventssubscribe, unsubscribe (push: mouse, window, input)
Metaping, lua.exec

Authentication

Disabled by default. To require a token, edit AUTH_TOKEN at the top of remote_access.lua before installing, then pass the same value to your client.

Performance

Measured on localhost (Windows host, release build):

MetricValue
RPC round-trip p50~1 ms
RPC round-trip p99~2 ms
Sustained RPC throughput (16 in-flight)~10,000 req/s
Fire-and-forget wire throughput~100,000 msg/s

JavaScript / TypeScript

npm install @rebind.gg/client-ts
import { RebindRemote } from "@rebind.gg/client-ts";

const r = new RebindRemote("ws://127.0.0.1:19561");
await r.connect();

r.hidMove(30, -5);
r.hidPress("Mouse1", 20);
r.hidType("hello\n");

const { x, y } = await r.systemMouse();
const pixel = await r.screenPixel(x, y);

for await (const pos of r.mouseEvents()) {
  console.log(pos.x, pos.y);
  if (pos.x > 500) break;
}

r.close();

Works in Node 22+, Bun, Deno, and browsers. Full TypeScript types. Zero runtime dependencies.


Python

pip install git+https://github.com/usinput/rebind-client-py.git
from rebind import RebindRemote

with RebindRemote("ws://127.0.0.1:19561") as r:
    r.hid_move(30, -5)
    r.hid_press("Mouse1", hold_ms=20)
    r.hid_type("hello\n")

    x, y = r.system_mouse()
    pixel = r.screen_pixel(x, y)

    # sample a color and react
    if pixel.r > 200 and pixel.g < 50:
        r.hid_press("Mouse1")

    # stream mouse position
    for pos in r.mouse_events():
        print(pos.x, pos.y)
        if pos.x > 500:
            break

Blocking and asyncio APIs. Pure Python, works on Windows, macOS, and Linux.


Rust

cargo add rebind-client
use rebind_client::RebindClient;

#[tokio::main]
async fn main() -> rebind_client::Result<()> {
    let client = RebindClient::connect("ws://127.0.0.1:19561").await?;

    client.hid_move(30, -5);
    client.hid_press("Mouse1", 20);
    client.hid_type("hello\n");

    let (x, y) = client.system_mouse().await?;
    let pixel = client.screen_pixel(x, y).await?;

    // sample a color and react
    if pixel.r > 200 && pixel.g < 50 {
        client.hid_press("Mouse1", 20);
    }

    // stream mouse position
    let mut events = client.mouse_events().await?;
    while let Some(pos) = events.recv().await {
        println!("{} {}", pos.x, pos.y);
    }

    client.close().await;
    Ok(())
}

Async, typed, built on tokio.

Browser Demo

A minimal browser-based remote control panel for Rebind. Connects directly to the WebSocket server served by remote_access.lua – no install required.

Prerequisites

  1. Rebind is running with the Remote Access script started.
  2. The default WebSocket port is 19561.

Demo

disconnected

Mouse

drag to move
--

Keyboard

Clipboard

System

How it works

This page connects directly to the Rebind WebSocket server using the browser’s native WebSocket API. No libraries, no build step. The protocol is plain JSON:

// fire-and-forget (no id)
{"t": "hid.move", "dx": 10, "dy": -5}

// request/response (has id)
{"t": "screen.resolution", "id": 1}
// -> {"id": 1, "width": 1920, "height": 1080}

// subscribe to push events
{"t": "subscribe", "events": ["mouse"]}
// -> {"t": "mouse", "x": 502, "y": 301} (continuous)

The trackpad uses hid.move with pointer deltas. Mouse position is streamed via the mouse subscription. All other buttons send the corresponding command and log the result.

View the page source to see the full implementation – it’s under 200 lines of vanilla JavaScript with no dependencies.

Support

Discord

The fastest way to get help is through our Discord server. Join and open a support ticket for one-on-one assistance.

discord.gg/rebind

Opening a support ticket

  1. Join the Discord server using the link above
  2. Navigate to the #support channel
  3. Create a ticket describing your issue

Include the following if applicable:

  • Your operating system (Windows, macOS, or Linux)
  • Output of device info from the Rebind terminal
  • Any error messages you’re seeing

Common issues

ProblemSolution
Device not detectedUnplug and replug the device, then run device list
Flash failsEnsure the device is in bootloader mode (press the physical button on the Teensy)
Activation failsVerify your license key is correct and hasn’t already been activated on another device
Rebind won’t startRe-run the install command to update to the latest version

For anything not covered here, open a support ticket on Discord.