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
| HID | keyboard and mouse output, modifier combo syntax |
| Input | real-time physical key state |
| Macro | record, play, stream, transform input sequences |
| Screen | pixel color sampling |
| Window | find, move, resize, pin, control windows |
| Net | HTTP client/server, WebSocket server/client |
| Audio | WAV/MP3/OGG/FLAC playback |
| Clipboard | system clipboard read/write |
| File | sandboxed file I/O, JSON, TOML |
| Regex | PCRE pattern matching |
| Pipe | shared memory IPC with external processes |
| Dialog | native OS file/message dialogs |
| Timer | delayed and repeating callbacks |
| Math | random, gaussian, spline, interpolation |
| UI | declarative config schema |
| System | cursor, screen, window info, shell commands |
| Config | TOML config file I/O |
| Bind | declarative key bindings, remaps, toggles |
| Script | self-control (exit, reload) |
| Log | structured logging to the UI |
How Rebind compares
| Rebind | AutoHotkey | G Hub / Synapse / iCUE | Kmbox | Titan Two | XIM | Stream Deck | |
|---|---|---|---|---|---|---|---|
| Cross-platform | Windows, macOS, Linux | Windows only | Windows only | Windows only | console + PC | console only | Windows, macOS |
| Scripting language | Luau | AHK | limited Lua (G Hub only) | none | GPC (compiled) | none | none |
| Hardware-isolated output | yes | no | own brand only | yes | yes | yes | n/a |
| Any mouse | yes | yes | no | yes | yes | yes | n/a |
| Any keyboard | yes | yes | no | no | yes | no | n/a |
| HTTP client/server | yes | no | no | no | no | no | no |
| WebSocket server/client | yes | no | no | no | no | no | no |
| Coroutines | yes | limited | no | no | no | no | no |
| Pixel sampling | yes | yes | no | no | no | no | no |
| Window control | yes | yes | no | no | no | no | no |
| Macro record/play | yes | yes | basic | no | basic | no | no |
| Shared memory IPC | yes | no | no | no | no | no | no |
| Declarative UI config | yes | no | no | no | no | no | yes |
| Single PC | yes | yes | yes | two required | yes | yes | yes |
| VM-level input capture | yes | no | no | no | no | no | no |
| Per-app activation | yes | yes | no | no | no | no | yes |
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).
- Open Rebind with your device plugged in
- Click the terminal icon in the lower right corner
- Type
device listand locate your device - Type
device flash <BUS ID>(the bus ID is a value like1-2or2-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.
- With Rebind open and your device flashed (see above)
- Click the terminal icon in the lower right corner
- Type
activate <LICENSE KEY>
Your license is tied to your hardware device. Once activated, it works offline.
Confirm it’s working
- Click the terminal icon in the lower right corner
- Type
reauth - Type
device infoto confirm everything is working
If device info shows your device as authenticated, you’re ready to go.
Attach your devices
- Open the Rebind UI
- Go to the Devices tab
- Click Auto-Detect to find your mouse and keyboard
- 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:
- Open the Rebind UI
- Go to the Scripts tab
- 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
- 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=trueprevents 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 deltaHID.Move(-dx, -dy)sends the inverted movement through the Rebind devicereturn falseblocks 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 – learn the UI
- Scripting Guide – an introduction to common patterns for Rebind Luau scripts
- SDK Reference – full API documentation
Interface Tour
The Rebind UI runs in your browser at http://localhost:19480. A narrow icon sidebar on the left switches between views.
Scripts

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

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
INFOentries fromlog(),print(), andLog.*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

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

The wizard explains what it will do. Click Start Detection to begin.
Step 2 — Detecting

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

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

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

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 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
| Hook | When it fires |
|---|---|
OnStart() | Script is loaded into memory |
OnStop() | Script is about to be unloaded |
OnFocus(window) | Target window/process gained focus. window is { title, process, x, y, width, height } |
OnBlur(window) | Target window/process lost focus. window is the new foreground window. |
OnTick(delta) | Every frame while active. Default 1,000/sec, up to 8,000/sec with tick_rate=8000 modeline. |
If your script has no window= or process= targeting, it’s always active – OnFocus/OnBlur won’t fire.
Input
These are the core of Rebind. Input hooks fire when physical input arrives from your captured devices.
function OnDown(key)
-- key or mouse button was pressed
-- return false to block it, true to pass through
return true
end
function OnUp(key, duration)
-- key or mouse button was released
-- duration = how many milliseconds it was held
return true
end
function OnMove(dx, dy)
-- mouse moved by (dx, dy) pixels
-- by default, the return value is ignored and the move is forwarded
-- immediately. set mouse_block=true in the modeline to enable blocking.
return true
end
function OnScroll(delta)
-- scroll wheel moved
-- delta > 0 = scroll up, delta < 0 = scroll down
return true
end
Blocking
The return value of OnDown, OnUp, and OnScroll controls whether the physical input reaches your PC:
return true– input passes through normallyreturn false– input is swallowed (the PC never sees it)- no return /
return nil– same asreturn true
This is how remapping works: block the original key, send a different one via HID.
Note:
Binduses the opposite default – it blocks unless you explicitlyreturn true. See the SDK Reference for details.
Mouse moves are handled differently for performance. By default, OnMove fires asynchronously – the move is forwarded to your PC immediately and the return value is ignored. This guarantees zero mouse latency regardless of script execution time.
To enable blocking mouse moves (e.g., for mouse acceleration or aim snapping), add mouse_block=true to your modeline:
-- rebind: mouse_block=true
function OnMove(dx, dy)
-- now the return value matters: return false to swallow the move
return true
end
Only use mouse_block when you genuinely need to modify or swallow mouse input. It forces the engine to acquire a lock on every mouse event, which can affect input latency when scripts are doing heavy work.
Keys
Keys are identified by string names. Names are case-insensitive: "A", "a", "LCtrl", and "lctrl" all work.
Common examples: "A"-"Z", "0"-"9", "F1"-"F12", "Mouse1"-"Mouse5", "LCtrl", "LShift", "LAlt", "Space", "Enter", "Escape", "Up", "Down", "Left", "Right".
For the complete list of every key name, alias, and which keys are input-only vs output-only, see the Key Reference in the SDK Reference.
HID Output
The HID namespace sends keyboard and mouse output through the active transport. These appear as real input to your PC.
-- keyboard
HID.Down("W") -- hold W
HID.Up("W") -- release W
HID.Press("Space") -- tap Space (press and release)
HID.Press("Space", 100) -- tap Space, hold for 100ms
HID.Type("Hello!") -- type a string
-- modifier combos
HID.Press("LCtrl+C") -- Ctrl+C (copy)
HID.Press("LCtrl+V") -- Ctrl+V (paste)
HID.Press("LCtrl+LShift+T") -- Ctrl+Shift+T (reopen tab)
HID.Press("LShift+Enter") -- Shift+Enter (newline in chat apps)
-- mouse
HID.Move(10, -5) -- move cursor by (10, -5) pixels
HID.Scroll(3) -- scroll up 3 notches
HID.Scroll(-3) -- scroll down 3 notches
Note:
HID.PressandHID.Typemust be called inside aRun()coroutine or anAsync()handler.HID.DownandHID.Upare non-blocking and can be called anywhere.
HID.Down and HID.Up are for holding keys across time. HID.Press is a convenience that does both with an optional hold duration.
Combos
Use + in any key string to press multiple keys as a combo. HID.Press("LCtrl+V") presses Ctrl, presses V, holds, then releases V and Ctrl in reverse order. This replaces the manual Down/Up pattern for modifier combos:
-- before: manual modifier management
HID.Down("LCtrl")
HID.Press("V")
HID.Up("LCtrl")
-- after: single call, correct timing handled automatically
HID.Press("LCtrl+V")
Down and Up also support combos for held modifiers:
HID.Down("LCtrl+LShift") -- hold both
-- ... do things ...
HID.Up("LCtrl+LShift") -- release both (reverse order)
Clipboard Paste
For multi-line text or long strings, clipboard paste is faster and more reliable than typing:
Clipboard.Set("Line one\nLine two\nLine three")
HID.Press("LCtrl+V")
This works in any application and has zero per-character timing concerns.
Coroutines
For anything that needs to happen over time (sequences, delays, loops), use Run() and Sleep():
function OnDown(key)
if key == "F9" then
Run(function()
HID.Down("W")
Sleep(500) -- wait 500ms
HID.Up("W")
Sleep(200)
HID.Press("Space")
end)
return false
end
return true
end
Run() launches a coroutine that executes concurrently. Sleep() pauses that coroutine without blocking anything else. You can launch multiple Run() blocks and they’ll all execute in parallel.
Sleep() can only be called inside a Run() block. Calling it outside one raises a clear error with guidance.
Sugar
After
Run a function after a delay without the Run/Sleep boilerplate:
After(2000, function()
Log.Info("2 seconds later")
end)
Async
Wrap a function so it automatically runs in a coroutine when called. Designed for Bind callbacks that need Sleep():
-- Bind callbacks are synchronous, so Sleep() would error.
-- Async() wraps the callback in Run() automatically.
Bind(
"F10",
Async(function()
HID.Down("W")
Sleep(500)
HID.Up("W")
end)
)
Async is smart about nesting: if called inside an existing coroutine (e.g. inside Run()), it calls the function directly instead of spawning a second coroutine.
Tasks
Run() returns a handle you can use to control the coroutine:
local task = Run(function()
while true do
HID.Press("Mouse1", 20)
Sleep(100)
end
end)
-- later...
task:Cancel() -- stop the coroutine
task:IsRunning() -- check if it's still going
Always cancel long-running tasks in OnStop() or OnBlur() to prevent them from leaking:
local task = nil
function OnDown(key)
if key == "F9" then
task = Run(function()
-- long sequence...
end)
end
end
function OnStop()
if task and task:IsRunning() then
task:Cancel()
end
end
Input State
The Input namespace lets you check what’s currently pressed without waiting for an event:
if Input.IsDown("Mouse1") then
-- left mouse button is currently held
end
local duration = Input.GetDuration("W")
-- how long W has been held (0 if not pressed)
local keys = Input.GetActiveKeys()
-- list of all currently held keys
local mods = Input.GetModifiers()
-- { ctrl = true/false, shift = true/false, alt = true/false, win = true/false }
This is useful for polling-based logic in OnTick or inside Run() loops.
System
System provides read-only methods updated automatically every tick.
System.Time() -- current timestamp in milliseconds
local mx, my = System.Mouse() -- cursor position (pixels)
local sw, sh = System.Screen() -- primary display dimensions
local win = System.Window() -- { title, process, x, y, width, height }
Useful for time-based logic, cursor tracking, and window awareness:
local lastPress = 0
function OnDown(key)
if key == "W" then
local now = System.Time()
if now - lastPress < 250 then
Log.Info("Double-tap detected!")
end
lastPress = now
end
return true
end
Timers
For simple delayed or repeated callbacks, use Timer instead of a Run/Sleep loop:
-- fire once after 2 seconds
local t = Timer.After(2000, function()
Log.Info("2 seconds passed")
end)
-- fire every 500ms
local repeater = Timer.Every(500, function()
Log.Info("tick")
end)
-- control timers
repeater:Pause()
repeater:Resume()
repeater:Cancel()
-- cancel all timers
Timer.CancelAll()
UI Config
Scripts can define a settings panel that appears in the Rebind UI. Users can tweak values without editing code.
local cfg = UI.Schema({
enabled = UI.Toggle(true, { label = "Enable" }),
speed = UI.Slider(50, { min = 0, max = 100, suffix = "%" }),
hotkey = UI.Keybind("F9", { label = "Toggle Key" }),
mode = UI.Select("Normal", { "Normal", "Fast", "Precise" }),
tag = UI.Text("", { placeholder = "Enter name", maxLength = 20 }),
})
Read values directly from the returned handle:
if cfg.enabled then
local s = cfg.strength
end
-- write values programmatically
cfg.enabled = false
Values are saved automatically and restored when the script reloads. Persistence is keyed to the script’s file path — no configuration required.
Layout
UI.Slider(50, {
group = "Advanced", -- group controls under a header
tab = "Settings", -- organize into tabs
showIf = "enabled", -- only show when the "enabled" toggle is on
tooltip = "Hover text", -- tooltip on hover
})
Notifications
UI.Notify("Script enabled", "info") -- "info", "success", or "error"
Targeting
Scripts can restrict themselves to specific applications:
-- rebind: window=Photoshop
-- rebind: process=Photoshop.exe
window=matches against the window title (case-insensitive substring)process=matches against the executable name (case-insensitive)- Multiple entries act as OR – any match activates the script
- When the matching window loses focus, the script deactivates
Use OnFocus and OnBlur to set up and tear down resources:
-- rebind: process=Photoshop.exe
function OnFocus(window)
Log.Info("Photoshop is focused: " .. window.title)
end
function OnBlur()
Log.Info("Left Photoshop")
-- clean up: cancel tasks, release held keys, etc.
end
Always-on
A targeted script that isn’t focused has zero overhead. Its hooks are never called, its timers don’t fire, and its OnTick is never invoked. The runtime skips it entirely.
This means you can have dozens or even hundreds of scripts loaded simultaneously – one for Photoshop, one for your browser, one for your terminal, one for each game you play – and they all coexist without interfering with each other or consuming resources. Only the script(s) matching the currently focused window will actually run.
Scripts without any window= or process= targeting are always active (global scripts). Use these for system-wide behaviors like key remaps or media controls.
A typical setup looks like:
Always running:
CapsLock to Escape (global, no targeting)
Mouse Media Keys (global, no targeting)
Activated on demand:
Photoshop Shortcuts (process=Photoshop.exe)
Browser Tab Nav (process=chrome.exe, firefox.exe, msedge.exe)
Terminal Helpers (window=Terminal)
Blender Macros (process=blender.exe)
All of these can stay loaded at all times. There is no reason to manually start and stop scripts as you switch between apps.
Priority
When multiple scripts are running, z_index controls which one processes input first:
-- rebind: z_index=100
Higher z-index scripts see input first. If a higher-priority script returns false from a hook, lower-priority scripts never see that event.
Default z-index is 1.
Instances
Control what happens when a script is loaded while another copy is already running:
| Policy | Behavior |
|---|---|
replace (default) | Stop the old instance, start the new one |
single | Reject the new instance if one is already running |
multiple | Allow multiple copies to run simultaneously |
-- rebind: instance=single
Macros
Record and play back input sequences:
-- define a macro as a table of actions
local pattern = {
{ x = 0, y = 2, delay = 15 }, -- shorthand: mouse move
{ x = -1, y = 3, delay = 15 },
{ x = 1, y = 2, delay = 15 },
}
function OnDown(key)
if key == "Mouse1" then
local handle = Macro.Play(pattern, 1.0, "replace")
-- speed = 1.0 (normal), mode = "replace" (stop previous macros)
end
-- sequence two macros using Wait() inside a coroutine
if key == "F9" then
Run(function()
local h1 = Macro.Play(pattern)
h1:Wait()
local h2 = Macro.Play(pattern)
h2:Wait()
Log.Info("both macros finished")
end)
end
end
function OnUp(key)
if key == "Mouse1" then
Macro.StopAll()
end
end
Actions
| Action | Fields | Description |
|---|---|---|
| shorthand | x, y, delay | Mouse movement (most common) |
move | dx, dy, delay | Mouse movement (explicit) |
down | code, delay | Hold a key |
up | code, delay | Release a key |
press | code, holdMs, delay | Tap a key |
type | text, charDelay, delay | Type a string |
scroll | amount, delay | Scroll wheel |
sleep | delay | Pause |
Modes
"parallel"– run alongside other macros (default)"replace"– stop others, then play
Transforms
Transform macro patterns before playing:
local scaled = Math.Scale(pattern, 1.5, 1.0) -- scale X by 1.5
local smooth = Math.Spline(pattern, 0.5) -- smooth curves
local resampled = Math.Resample(pattern, 10) -- normalize timing
local interpolated = Math.Interpolate(pattern, 2) -- add smooth steps
local fitted = Math.TimeComp(pattern, 1000) -- fit to 1 second
Patterns
Toggle
local active = false
function OnDown(key)
if key == "F9" then
active = not active
UI.Notify(active and "ON" or "OFF", "info")
return false
end
end
Hold loop
function OnDown(key)
if key == "Mouse1" then
Run(function()
while Input.IsDown("Mouse1") do
-- do something
Sleep(100)
end
end)
return false
end
end
Cleanup
local task = nil
function OnBlur()
if task and task:IsRunning() then
task:Cancel()
end
end
Double-tap
local lastTap = 0
local THRESHOLD = 250
function OnDown(key)
if key == "W" then
local now = System.Time()
if now - lastTap < THRESHOLD then
Log.Info("Double tap!")
end
lastTap = now
end
return true
end
Validation
The runtime automatically lints your script on load and warns about common issues:
- No hooks defined – script will load but never run any logic
Net.Get/Net.Postwith high-frequencyOnTick– HTTP calls block the main thread; move them insideRun(). WebSocket handlers (Net.WSListen,Net.WSConnect) do not have this issue – their I/O runs on dedicated threads and only the handler callback runs on the tick.
These warnings appear in the Rebind logs tab. They don’t prevent loading – they’re guidance to help you catch mistakes early.
Type Checking
The SDK ships a type definition file (types/rebind.d.luau) for the Luau Language Server. This gives you autocomplete, hover docs, and type checking in VS Code.
To enable it, add a .luaurc file next to your script:
{
"languageMode": "nonstrict",
"paths": ["path/to/lua-sdk/types"]
}
Next
- SDK Reference – complete function reference for every namespace
- Examples – full scripts you can use or learn from
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 coroutinehandle:IsRunning()– returnsboolean
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.
| Function | Returns | Description |
|---|---|---|
System.Time() | number | Current timestamp in milliseconds |
System.Mouse() | x, y | Cursor position in pixels (multiple return values) |
System.Screen() | width, height | Primary display dimensions in pixels (multiple return values) |
System.Window() | table | Active window info: { title, process, x, y, width, height } |
System.Exec(cmd, options?) | table | Run 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
| Function | Description |
|---|---|
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.PressandHID.TypeuseSleep()internally and must be called inside aRun()coroutine or anAsync()handler. Calling them outside a coroutine (e.g. directly inOnDown) will raise an error. UseHID.Down/HID.Upfor non-blocking key control in hooks, or wrap your logic withAsync().
-- 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
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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.
| Function | Returns | Description |
|---|---|---|
Input.IsDown(key) | boolean | Whether key/button is currently held |
Input.GetDuration(key) | number | Milliseconds 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
| Constructor | Default Type | Description |
|---|---|---|
UI.Toggle(default, opts?) | boolean | On/off switch |
UI.Slider(default, opts?) | number | Numeric slider |
UI.Keybind(default, opts?) | string | Key binding selector |
UI.Select(default, choices, opts?) | string | Dropdown selection |
UI.Text(default, opts?) | string | Text input field |
Widget Options
| Option | Type | Applies to | Description |
|---|---|---|---|
label | string | all | Display label (overrides key name) |
tooltip | string | all | Hover description |
group | string | all | Visual section header |
tab | string | all | Tab panel name |
showIf | string | all | Show only when referenced toggle is on |
min | number | Slider | Minimum value |
max | number | Slider | Maximum value |
step | number | Slider | Increment size |
suffix | string | Slider | Unit label (e.g. "%") |
placeholder | string | Text | Hint when empty |
maxLength | number | Text | Character limit |
Other
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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()– returnsbooleanhandle:GetProgress()– returns0.0to1.0handle:Wait()– blocks the current coroutine until playback ends. must be called insideRun()
Hardware Streaming
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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:
| Action | Fields | Description |
|---|---|---|
| (shorthand) | x, y, delay | Mouse movement |
"move" | dx, dy, delay | Mouse movement (explicit) |
"down" | code, delay | Hold key |
"up" | code, delay | Release key |
"press" | code, holdMs, delay | Tap key |
"type" | text, charDelay, delay | Type string |
"scroll" | amount, delay | Scroll wheel |
"sleep" | delay | Pause |
Timer
| Function | Returns | Description |
|---|---|---|
Timer.After(ms, callback) | handle | Execute once after delay |
Timer.Every(ms, callback) | handle | Execute repeatedly at interval |
Timer.CancelAll() | – | Cancel all active timers |
Handle methods: handle:Cancel(), handle:Pause(), handle:Resume()
Math
Random
| Function | Returns | Description |
|---|---|---|
Math.Random(min, max) | number | Uniform random number |
Math.Gaussian(mean, stdDev) | number | Normal distribution random |
Transforms
All transform functions return a new table (original is unchanged).
| Function | Description |
|---|---|
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:
Resamplechanges all delays uniformly (normalizing recorded macros)Interpolateadds intermediate steps between existing points (smoothing patterns)
JSON
| Function | Returns | Description |
|---|---|---|
JSON.Parse(str) | table | Parse JSON string to Luau table |
JSON.Stringify(tbl) | string | Serialize table to JSON string |
Log
| Function | Description |
|---|---|
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.
| Function | Returns | Description |
|---|---|---|
File.Read(path) | string | Read file contents |
File.Write(path, content) | – | Overwrite file |
File.Append(path, content) | – | Append to file |
File.Exists(path) | boolean | Check if file exists |
File.Delete(path) | boolean | Delete file |
File.List(path) | string[] | List directory contents |
File.MkDir(path) | boolean | Create directory |
File.ReadJSON(path) | table | Read and parse JSON file |
File.WriteJSON(path, table) | – | Write table as JSON |
File.GetScriptDir() | string | Absolute path to script directory |
Net
Client
| Function | Returns | Description |
|---|---|---|
Net.Get(url, headers?, options?) | response | HTTP GET |
Net.Post(url, body, headers?, options?) | response | HTTP POST |
Net.Put(url, body, headers?, options?) | response | HTTP PUT |
Net.Patch(url, body, headers?, options?) | response | HTTP PATCH |
Net.Delete(url, headers?, options?) | response | HTTP DELETE |
Net.Head(url, headers?, options?) | response | HTTP HEAD |
Net.Request(options) | response | Generic 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.
| Handler | Signature | Fires when |
|---|---|---|
OnConnect | function(client) | a new client has completed the WS handshake |
OnMessage | function(client, payload, is_binary) | the server received a frame |
OnClose | function(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):
| Metric | Value |
|---|---|
| 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.
| Handler | Signature | Fires when |
|---|---|---|
OnOpen | function() | the WS handshake completed |
OnMessage | function(payload, is_binary) | a frame arrived from the server |
OnClose | function() | 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
| Function | Returns | Description |
|---|---|---|
Screen.GetPixelColor(x?, y?) | string | Hex 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
| Function | Returns | Description |
|---|---|---|
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?) | string | Get window title. |
Window.GetPos(handle?) | {x, y, width, height} | Get window position and size. |
Window.GetPID(handle?) | number | Get the process ID that owns the window. |
Window.IsVisible(handle) | boolean | Check if a window is visible. |
Activation
| Function | Description |
|---|---|
Window.Activate(handle) | Bring window to foreground. Auto-restores if minimized. |
Movement / Sizing
| Function | Description |
|---|---|
Window.Move(handle, x?, y?, w?, h?) | Move and/or resize. Omit any param to leave unchanged. |
State Control
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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
| Function | Returns | Description |
|---|---|---|
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
| Function | Returns | Description |
|---|---|---|
Audio.Beep() | – | Play system beep |
Audio.Play(path, options?) | SoundHandle | Play 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() | number | Get current master volume |
Audio.Play
local sound = Audio.Play("alert.wav")
local music = Audio.Play("bgm.mp3", { volume = 0.5, loop = true })
Options:
| Option | Type | Default | Description |
|---|---|---|---|
volume | number | 1.0 | Playback volume (0.0 to 1.0) |
loop | boolean | false | Repeat when playback finishes |
File paths are relative to the script directory (same sandboxing rules as File).
SoundHandle
The object returned by Audio.Play:
| Method | Returns | Description |
|---|---|---|
sound:Stop() | – | Stop playback and release resources |
sound:Pause() | – | Pause playback |
sound:Resume() | – | Resume paused playback |
sound:IsPlaying() | boolean | True if playing (not paused, not finished) |
sound:SetVolume(vol) | – | Set per-sound volume (0.0 to 1.0) |
sound:GetVolume() | number | Get current per-sound volume |
Always call Audio.StopAll() in OnStop and OnBlur to clean up playing sounds.
Clipboard
Read and write the system clipboard.
| Function | Returns | Description |
|---|---|---|
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
| Function | Returns | Description |
|---|---|---|
Dialog.Message(text, options?) | – | Show an alert box. Yields until dismissed. |
Dialog.Confirm(text, options?) | boolean | Show 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
| Function | Returns | Description |
|---|---|---|
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 displaynameand a list ofextensions(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+".
| Function | Returns | Description |
|---|---|---|
Regex.IsMatch(text, pattern) | boolean | Test 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) | string | Replace first match |
Regex.ReplaceAll(text, pattern, rep) | string | Replace 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.
| Function | Returns | Description |
|---|---|---|
Config.ParseTOML(text) | table | Parse a TOML string into a Lua table |
Config.ToTOML(table) | string | Serialize a Lua table to a TOML string |
Config.ReadTOML(path) | table | Read 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
| Method | Returns | Description |
|---|---|---|
pipe:Read() | string or nil | Read 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
| Property | Type | Description |
|---|---|---|
pipe.name | string | Pipe name |
pipe.size | number | Total shared memory size |
pipe.capacity | number | Max payload size per message |
Script
| Function | Description |
|---|---|
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 value | OnDown/OnUp | Bind action |
|---|---|---|
return true | pass through | pass through |
return false | block | block |
no return / nil | pass through | block |
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 ...]
| Property | Type | Default | Description |
|---|---|---|---|
name | string | filename | Display name |
version | string | "0.0.0" | Script version |
author | string | – | Creator attribution |
description | string | – | Brief explanation |
min_sdk | string | current | Minimum SDK version required |
window | string | – | Window title match (repeatable, case-insensitive) |
process | string | – | Process name match (repeatable, case-insensitive) |
tick_rate | number | 1000 | OnTick frequency in Hz (max 8000) |
z_index | integer | 1 | Input priority (higher = first) |
instance | string | "replace" | "replace", "single", or "multiple" |
mouse_mode | string | "relative" | "relative" or "absolute" |
mouse_block | boolean | false | When true, OnMove can block mouse input by returning false. When false, moves are forwarded immediately and OnMove fires asynchronously. |
permission | string | – | Grant a permission to this script (repeatable). Valid values: exec, net. See Permissions below. |
id | – | Deprecated. 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, oneprocess=per process, onepermission=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
| Permission | Restricts |
|---|---|
exec | System.Exec() |
net | Net.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
| Button | Name |
|---|---|
| Left click | Mouse1 |
| Right click | Mouse2 |
| Middle click | Mouse3 |
| Back | Mouse4 |
| Forward | Mouse5 |
Modifiers
| Key | Name | Aliases |
|---|---|---|
| Left Ctrl | LCtrl | Ctrl, Control |
| Right Ctrl | RCtrl | |
| Left Shift | LShift | Shift |
| Right Shift | RShift | |
| Left Alt | LAlt | Alt |
| Right Alt | RAlt | |
| Left Win / Cmd | LWin | Win, GUI, Windows, Command, Meta |
| Right Win / Cmd | RWin | RGui, RMeta |
Editing and Control
| Key | Name | Aliases |
|---|---|---|
| Enter | Enter | Return |
| Escape | Escape | Esc |
| Backspace | Backspace | |
| Tab | Tab | |
| Space | Space | |
| Caps Lock | CapsLock |
Navigation
| Key | Name | Aliases |
|---|---|---|
| Insert | Insert | |
| Delete | Delete | Del |
| Home | Home | |
| End | End | |
| Page Up | PageUp | PgUp |
| Page Down | PageDown | PgDn |
| Arrow Up | Up | |
| Arrow Down | Down | |
| Arrow Left | Left | |
| Arrow Right | Right |
System
| Key | Name | Aliases |
|---|---|---|
| Print Screen | PrintScreen | PrintScr, PrtSc |
| Scroll Lock | ScrollLock | |
| Pause / Break | Pause | Break |
| Application / Menu | Menu | App, ContextMenu |
Punctuation
These names refer to the physical key, regardless of shift state.
| Key | Name | Aliases |
|---|---|---|
- / _ | Minus | |
= / + | Equal | Equals |
[ / { | LeftBracket | LeftBrace, LBracket |
] / } | RightBracket | RightBrace, RBracket |
\ / | | Backslash | |
; / : | Semicolon | |
' / " | Apostrophe | Quote |
` / ~ | Grave | Backtick, Tilde |
, / < | Comma | |
. / > | Period | Dot |
/ / ? | Slash |
Numpad
| Key | Name | Aliases |
|---|---|---|
| Num Lock | NumLock | |
| Numpad / | KpDivide | NpDivide |
| Numpad * | KpMultiply | NpMultiply, KpAsterisk |
| Numpad - | KpMinus | NpSubtract |
| Numpad + | KpPlus | NpAdd |
| Numpad Enter | KpEnter | NpEnter |
| Numpad . | KpDot | NpDecimal, NpDot |
| Numpad 0-9 | Kp0 through Kp9 | NP0-NP9, Numpad0-Numpad9 |
Media Keys (HID Output Only)
These can be sent via HID.Press but will not appear in input hooks.
| Key | Name | Aliases |
|---|---|---|
| Next Track | MediaNext | MediaNextTrack |
| Previous Track | MediaPrev | MediaPrevTrack |
| Stop | MediaStop | |
| Play / Pause | MediaPlay | MediaPlayPause |
| Volume Up | VolumeUp | VolUp |
| Volume Down | VolumeDown | VolDown |
| Mute | Mute | VolumeMute |
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
- Download remote_access.lua and copy it to your Rebind
scripts directory:
- Windows:
%APPDATA%\Rebind\save_data\scripts\
- Windows:
- Open Rebind → Scripts → start Remote Access.
- The script binds a WebSocket server on
ws://0.0.0.0:19561(port is configurable in the script’s settings panel). - Install a client library for your language and connect.
Command surface
| Category | Commands |
|---|---|
| HID writes | hid.down, hid.up, hid.press, hid.type, hid.move, hid.move_to, hid.scroll |
| Screen | screen.pixel, screen.resolution |
| System | system.mouse, system.window, system.time |
| Input | input.keys, input.is_down, input.modifiers |
| Clipboard | clipboard.get, clipboard.set |
| Window | window.list, window.find, window.activate, window.move |
| Events | subscribe, unsubscribe (push: mouse, window, input) |
| Meta | ping, 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):
| Metric | Value |
|---|---|
| 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.
- npm:
@rebind.gg/client-ts - GitHub:
usinput/rebind-client-ts
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.
- GitHub:
usinput/rebind-client-py
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.
- crates.io:
rebind-client - GitHub:
usinput/rebind-client-rs
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
- Rebind is running with the Remote Access script started.
- The default WebSocket port is
19561.
Demo
Mouse
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.
Opening a support ticket
- Join the Discord server using the link above
- Navigate to the #support channel
- Create a ticket describing your issue
Include the following if applicable:
- Your operating system (Windows, macOS, or Linux)
- Output of
device infofrom the Rebind terminal - Any error messages you’re seeing
Common issues
| Problem | Solution |
|---|---|
| Device not detected | Unplug and replug the device, then run device list |
| Flash fails | Ensure the device is in bootloader mode (press the physical button on the Teensy) |
| Activation fails | Verify your license key is correct and hasn’t already been activated on another device |
| Rebind won’t start | Re-run the install command to update to the latest version |
For anything not covered here, open a support ticket on Discord.