What is Rebind
Rebind is a free engine that makes your keyboard and mouse scriptable. The engine captures your input, runs every keypress and mouse movement through your Luau scripts at up to 8,000 Hz, and outputs the result. Add the Rebind Link — a tiny USB dongle — and output leaves through a real USB device your computer sees as a standard keyboard and mouse. Because the engine owns the input pipeline, timing is consistent, vendor-agnostic, and the same scripts run on Windows, macOS, and Linux.
You can run it two ways. Software mode emits output through the OS input APIs — no hardware, free to use. Hardware mode routes the output through a small dedicated device (a Teensy 4.x) as real USB HID, for deterministic timing and isolation from the host. The script is identical either way; only the transport changes. See How it works.
What you can do
- Program input with a real language. The runtime is Luau (a fast, typed Lua) on a Rust core — not a fixed feature list. Remap keys, transform mouse movement, run macros, sample the screen, drive windows, and call out over HTTP.
- Drive it from anywhere. A built-in HTTP and WebSocket server lets any program — a web app, a Python process, a home-automation hub, a local model — send input over the wire, with official TypeScript, Python, and Rust clients.
- Use any device, on any OS. Any USB keyboard and mouse you already own; the same scripts move with you and survive reinstalls.
- Start without code. Describe what you want to RebindGPT and get a working script, or install one from the marketplace — then drop into the SDK when you want more.
Who it’s for
- Developers and makers — drive physical input from code: automation, AI-vision sidecars, robotics, hardware-in-the-loop testing.
- Accessibility — dwell-click, sticky keys, tremor smoothing, one-handed layouts, at the hardware level, in every app.
- Power users — text expansion, per-app shortcuts, remaps, and macros — cross-OS and cross-vendor.
How Rebind compares
Factual feature comparison against the tools people reach for. (Karabiner-Elements is the standard macOS keyboard customizer; vendor software is the app that ships with Logitech / Razer / Corsair gear.)
| Rebind | AutoHotkey | Karabiner | Vendor software | Stream Deck | |
|---|---|---|---|---|---|
| Platforms | Windows, macOS, Linux | Windows | macOS | Windows (some macOS) | Windows, macOS |
| Scripting | Luau runtime | AHK language | JSON rules | Limited (G Hub: Lua) | Action presets |
| Works with any keyboard & mouse | Any USB device | Any device | Keyboard (mouse limited) | That brand only | Adds a separate keypad |
| Hardware-isolated output | Yes (Teensy) | No (software) | No (software) | That brand only | — |
| HTTP + WebSocket (client & server) | Yes | No | No | No | No |
| Coroutines | Yes | Limited | No | No | No |
| Pixel sampling | Yes (Win/macOS) | Yes | No | No | No |
| Window control | Yes | Yes | App conditions | No | No |
| Macro record / play | Yes | Yes | No | Basic | Action presets |
| Shared-memory IPC | Yes (Windows) | No | No | No | No |
| Remote SDKs (TS / Python / Rust) | Yes | No | No | No | No |
| Declarative config UI | Yes | No | GUI + JSON | App | Yes |
| Per-app activation | Yes (modeline) | Yes | Yes | Profiles | Profiles |
AutoHotkey is the closest in scripting power, but it is Windows-only software with no hardware output — see Coming from AutoHotkey. Karabiner-Elements is excellent on macOS, but macOS-only and keyboard-focused. Vendor software gives you hardware output, but only for that brand’s gear, and a macro recorder rather than a language. A Stream Deck is a great tactile control surface — it adds dedicated keys rather than transforming the keyboard and mouse you already use. Rebind is the one with a full scripting runtime, hardware-isolated output, and cross-platform support on any USB device.
Continue to the Quickstart.
Coming from AutoHotkey
If you’ve written AutoHotkey, Rebind will feel familiar: you bind keys, send input, react to windows, and loop over time. The differences are that scripts are Luau (a fast, typed Lua) instead of AHK syntax, they run on Windows, macOS, and Linux, and their output can come from real USB hardware instead of a software Send.
How concepts map
| AutoHotkey | Rebind |
|---|---|
Hotkey — F1:: | Bind("F1", fn) or the OnDown(key) hook |
Remap — CapsLock::Escape | Bind.Remap("CapsLock", "Escape") |
Send / SendInput | HID.Type, HID.Press, HID.Down / HID.Up |
Hotstring — ::btw::by the way | a Bind that calls HID.Type (or a key-sequence watcher) |
#HotIf WinActive(...) | the window= / process= modeline |
Sleep, Loop | Run(function() … Sleep(ms) … end) coroutines |
PixelGetColor | Screen.GetPixelColor |
WinActivate, WinGetTitle | Window.Activate, Window.GetTitle |
A_Clipboard | Clipboard.Get / Clipboard.Set |
| Persistent script | scripts stay loaded until you stop them |
Side by side
A remap and a hotkey that types text:
; AutoHotkey v2
CapsLock::Escape
F1::Send("Hello{Enter}")
--[[
rebind: min_sdk=3.0.0
rebind: name=Basics
--]]
Bind.Remap("CapsLock", "Escape")
-- HID.Type / HID.Press sleep between keystrokes, so they must run inside a
-- coroutine. A plain Bind callback runs synchronously, so wrap it with Async().
-- (Type the text, then press Enter explicitly — a literal "\n" is not a key.)
Bind(
"F1",
Async(function()
HID.Type("Hello")
HID.Press("Enter")
end)
)
Scope a script to one application:
; AutoHotkey v2
#HotIf WinActive("ahk_exe chrome.exe")
F2::Send("^t") ; new tab
#HotIf
--[[
rebind: min_sdk=3.0.0
rebind: name=Browser Helper
rebind: process=chrome.exe
--]]
Bind(
"F2",
Async(function()
HID.Press("LCtrl+T") -- new tab
end)
)
What’s different
- A required header. Unlike AHK, every Rebind script needs a modeline declaring the Rebind version (
min_sdk) before it will run — the relay refuses a script without it. The rest of the header (name,process=targeting) is optional. See Modeline. - Sending input yields.
HID.TypeandHID.Presssleep between keystrokes, andNet.*requests block, so they must run inside aRun()/Async()coroutine — not directly in a plainBind/OnDowncallback. - Cross-platform. The same script runs on Windows, macOS, and Linux. AutoHotkey is Windows-only.
- Hardware output. In hardware mode your
HID.*calls leave a Teensy as standard USB HID, with deterministic timing — not a softwareSendInput. The same script runs free in software mode first; see How it works. - A typed language. Luau brings types, coroutines, and a real standard library. Sequences and delays use
Run/Sleepinstead ofSetTimer/Loop. - More reach. Beyond remaps and macros, Rebind adds an HTTP and WebSocket server, shared-memory IPC, and remote clients for TypeScript, Python, and Rust — so other programs can drive input directly. See Remote control.
AutoHotkey has a deep Windows GUI and COM surface that Rebind doesn’t replicate — Rebind is about transforming input, not building desktop apps. For everything input-related, the Scripting guide and SDK reference are your next stops.
Quickstart
Rebind is a programmable input platform. Your keyboard and mouse pass through a small runtime, every event runs through your code, and the result is sent on as standard input. The runtime is Luau (a fast, typed Lua) on a Rust core, dispatching hooks at up to 8,000 Hz.
You can start free, with no hardware. Install, run a script in software mode, and have a working remap in a few minutes. This page gets you there.
Install or update
Re-run this any time to update in place to the latest build.
Windows — open PowerShell as Administrator and paste:
irm https://rebind.gg/install.ps1 | iex
macOS / Linux — open Terminal and paste:
curl -fsSL https://rebind.gg/install.sh | sh
Attach your devices
For Rebind to transform your input, route your keyboard and mouse through it — the same step in software mode and with hardware.
- Open the Rebind UI and go to the Devices tab.
- Click Auto-Detect to find your keyboard and mouse.
- Click Attach All.
Your input now passes through Rebind before reaching the system. Detach any time to restore normal operation.
Your first script
Here is a complete script that remaps CapsLock to Escape. Open the Rebind UI, go to the Scripts tab, click Create, and paste it:
--[[
rebind: min_sdk=3.0.0
rebind: name=CapsLock to Escape
--]]
Bind.Remap("CapsLock", "Escape")
Click Save, then Run. Press CapsLock — your system receives Escape.
Where scripts live. The Scripts tab edits real
.lua/.luaufiles on disk, in your Rebind scripts folder:
- Windows —
%APPDATA%\Rebind\scripts- macOS —
~/Library/Application Support/Rebind/scripts- Linux —
~/.local/share/Rebind/scriptsSubfolders are fine for organization, edits made in an external editor are picked up live, and
require()resolves modules relative to a script’s own folder.
How it reads:
- The
--[[ rebind: … --]]block at the top is the modeline — the script’s configuration, onerebind:line per setting. (A terse single-line-- rebind: key=valueform works too, but the block reads cleaner as scripts grow.) min_sdkis required. It’s the minimum Rebind version the script needs, and the relay refuses to run any script whose modeline doesn’t declare it. Use a version no newer than your installed build — these examples use3.0.0; a script asking for a newer version than your build is refused until you update.name=is the script’s display name. Every other modeline key is optional — see the SDK reference for the full list.Bind.Remap(from, to)takes two key names: the key you press and the key that’s sent instead. It installs the remap for as long as the script runs.
If you prefer to act on the keypress yourself rather than declare a remap, handle the OnDown hook directly. It fires on every key press with the key name, and returning false blocks the original key from passing through:
--[[
rebind: min_sdk=3.0.0
rebind: name=CapsLock to Escape
--]]
function OnDown(key)
if key == "CapsLock" then
HID.Down("Escape")
HID.Up("Escape")
return false
end
return true
end
Bind.Remap is the shorter path for a one-to-one remap; OnDown gives you room to add conditions and logic.
Kill switch
Press Left Ctrl + Left Alt + K at any time to immediately stop all scripts and release every held key, restoring normal keyboard and mouse behavior. (The kill switch is the left-hand modifiers specifically — right Ctrl / right Alt don’t trigger it.) Keep it in mind whenever you run a script that blocks input.
Hardware
Hardware output routes your input through a dedicated device — a Teensy 4.x microcontroller — so your scripts run on the device and leave as standard USB HID at up to 8,000 Hz, for deterministic timing and isolation from the host OS. Software mode needs none of this; How it works covers the software-vs-hardware split.
This page flashes a board and activates it.
What you need
- A Teensy 4.0 or 4.1 from PJRC, Amazon, or SparkFun. No soldering or wiring — just a USB cable.
- A Rebind license. You own the device; it keeps working without ongoing payment.
- The Rebind app, on the latest version — re-run the install command in the quickstart to update.
A pre-built device — pre-flashed, with a case and cable — is coming soon.
Flash your device
Plug the board in, open the Rebind app, and click the terminal icon in the lower-right corner. List connected devices — each is shown with its index:
$ devices list
[0] bootloader Teensy 4.1
vid:pid 16c0:0478
firmware none
Flash the device at that index:
$ devices flash 0
The board reboots into the Rebind firmware once flashing completes. Re-run
devices flash <#> any time to update a board. (The command is devices;
device works too as an alias.)
Activate
Your Rebind license key is emailed when you purchase. Activation ties the key to the device and is done once per board:
$ activate REBIND-XXXX-XXXX-XXXX
license activated
status active
(On failure the command prints activation failed: <message> instead.) The
license then lives on the hardware and works offline.
Verify
$ devices info
active device:
id a1b2c3d4e5f6
board Teensy 4.1
firmware 1.4.0
bus 1-2
vid:pid 16c0:0486
interfaces:
0 keyboard usage_page=0x01 usage=0x06
1 mouse usage_page=0x01 usage=0x02
2 rawhid usage_page=0xFF00 usage=0x01
If devices info shows your board with its firmware version, hardware output is
live. Run a script — its output now comes from the device.
How it works
Rebind puts a programmable layer between your input devices and your computer. Your keyboard and mouse pass through Rebind, your code transforms every event up to 8,000 times a second, and the result reaches your PC as standard USB input. Any device you already own, on Windows, macOS, and Linux. Core input is fully cross-platform; a few namespaces vary by OS — see platforms and limits.
This page explains the pipeline: how input is captured, where your scripts run, and how the result reaches your computer.
The three-stage pipeline
Input flows through Rebind in three stages: capture → script → output.
your devices Rebind your PC
┌────────────┐ ┌──────────────────────────┐ ┌──────────────────┐
│ keyboard │──▶│ capture · script · encode │──▶│ standard USB HID │
│ mouse │ │ (sealed) (Luau/Rust) │ │ keyboard/mouse │
└────────────┘ └──────────────────────────┘ └──────────────────┘
Your keyboard and mouse plug into Rebind instead of directly into your PC, so their raw input is captured inside a sealed environment isolated from your operating system. Each event fires your hooks — key handlers, mouse-move handlers, your tick loop, your Binds — in the Luau-on-Rust engine at up to 8,000 Hz, where you remap keys, transform movement, and automate. The result is encoded out as standard USB HID, and your operating system sees an ordinary peripheral.
-- Remap Caps Lock to Escape
Bind.Remap("CapsLock", "Escape")
Software vs hardware
The same script runs two ways; only the transport changes. Software mode emits output through your operating system’s input APIs — SendInput on Windows, CGEvent on macOS, uinput on Linux — free and with no device required, so you can write and test a script end to end before deciding where its output should come from.
Hardware output routes the result through a Teensy 4.x as real USB HID for deterministic, host-isolated timing at up to 8,000 Hz: the Rebind engine runs your scripts — up to 8,000 Hz — and output leaves through the device as real USB HID., so each event is handled in under a millisecond and your PC just sees a USB peripheral. A script declares its tick rate with a modeline, and on Windows the relay raises the system timer resolution to 1 ms so the loop runs at its declared rate.
Hardware also unlocks one capability software mode cannot offer on any OS: suppressing and transforming mouse movement (mouse_block scripts — inverters, acceleration curves, tremor filters). The operating system draws the cursor below anything a host process can intercept, so only a device that owns the input stream can rewrite motion. Keys, buttons, and scroll can be blocked in both modes.
-- Modeline: declare the tick rate at the top of a script
-- rebind: tick_rate=8000
See Quickstart to start in software mode.
Scripting Guide
Rebind scripts are written in Luau, running on a Rust core that dispatches input hooks up to 8,000 times a second. Your code sits between your input devices and your computer: every keypress, mouse move, and scroll can be read, transformed, blocked, or replaced. Output is standard USB HID from a Teensy 4.x device, or OS-level input in software mode.
This guide teaches the model and the common patterns. For the exact parameters of every function, see the SDK reference; to run your first script, see the quickstart.
Lifecycle
Every script follows a predictable lifecycle:
Load -> OnStart -> (OnFocus/OnBlur cycle) -> OnStop -> Unload
You implement behavior by defining hooks — global functions the runtime calls when something happens:
OnStart()/OnStop()— set up and tear down (start timers, cancel tasks, release keys).OnFocus(window)/OnBlur(window)— fire when a targeted app gains or loses focus (see Targeting).OnTick(dtMs)— runs on a fixed cadence while the script is active; default 1,000/sec, up to 8,000/sec with thetick_rate=8000modeline.dtMsis the real time elapsed since the previousOnTick(milliseconds, fractional;0on the first call) — use it for frame-rate-independent motion.OnDown/OnUp/OnMove/OnScroll— input events, covered next.
A script with no targeting is always active, so OnFocus/OnBlur never fire. See Hooks for the exact signatures.
Input and blocking
Input hooks fire when physical input arrives from your captured devices. Their return value decides whether your PC sees the input:
return true— input passes through normally.return false— input is swallowed; the PC never sees it.- no return /
return nil— same asreturn true.
This is how remapping works: block the original key, then send a different one with HID.
function OnDown(key)
if key == "CapsLock" then
HID.Down("Escape")
HID.Up("Escape")
return false -- swallow CapsLock; the PC sees Escape instead
end
return true
end
function OnUp(key, duration)
-- duration = how many ms the key was held
return true
end
Bindis the declarative shortcut for simple remaps and uses the opposite default — it blocks unless youreturn true. See Bind.
Mouse moves are handled differently, and they need hardware. Any script that
defines OnMove requires a Rebind device — the relay refuses to load an
OnMove script in software mode, because the OS draws the cursor upstream of
anything a host process could intercept in time. (Key, button, and scroll blocking
work in both modes; only mouse movement is hardware-gated.)
On a device, OnMove fires asynchronously by default — the move is forwarded to
your PC immediately and the return value is ignored, so script time never sits in
the mouse path. To modify or swallow moves (acceleration curves, cursor smoothing),
add mouse_block=true to the modeline; then the return value matters:
--[[
rebind: min_sdk=3.0.0
rebind: mouse_block=true
--]]
function OnMove(dx, dy)
return true -- now return false would swallow the move
end
Keys are identified by case-insensitive string names ("A", "LCtrl", "Mouse1", …). The complete list — every alias and which keys are input- or output-only — is in the Key reference.
Sending output
The HID namespace sends keyboard and mouse output through the active transport; it appears as real input to your PC.
HID.Down/HID.Uphold and release a key across time.HID.Presstaps a key (optional hold in ms);HID.Typetypes a string.- Use
+for modifier combos:HID.Press("LCtrl+V")presses in order and releases in reverse.
HID.Down("LShift")
HID.Press("A") -- types "A" while Shift is held
HID.Up("LShift")
HID.Press("LCtrl+V") -- paste, timing handled for you
HID.PressandHID.Typeuse an internalSleep, so they must run inside aRun()coroutine or anAsync()handler.HID.Down/HID.Upare non-blocking and safe anywhere — including an input hook.
For multi-line or long text, paste is faster and more reliable than typing per character:
Clipboard.Set("Line one\nLine two")
HID.Press("LCtrl+V")
The full output surface (mouse movement, scroll, absolute mode) is in the HID reference.
Doing things over time
For sequences, delays, and loops, use Run() and Sleep():
function OnDown(key)
if key == "F9" then
Run(function()
HID.Down("W")
Sleep(500) -- pause this coroutine only
HID.Up("W")
end)
return false
end
return true
end
Run() launches a coroutine that executes concurrently; Sleep() pauses that coroutine without blocking anything else, and is only valid inside a Run() block. Reach for:
After(ms, fn)— a one-off delay without theRun/Sleepboilerplate.Async(fn)— wrap a callback so it runs in a coroutine; use it forBindcallbacks that needSleep.Timer.After/Timer.Every— simple delayed or repeated callbacks, when you don’t want to manage a coroutine loop yourself.
Coroutines are cooperative and tick-driven.
Sleepresumes on the next engine tick past its deadline, so its timing is tick-granular rather than exact milliseconds, and aRun()body that never callsSleepruns straight through on a single tick — an infinite loop with noSleepstalls the script (input freezes) until the runtime’s per-tick budget (~200 ms) cuts it off. Always put aSleepinside long-running loops.
Run() returns a handle (task:Cancel(), task:IsRunning()). Cancel long-running tasks in OnStop/OnBlur so they don’t leak:
local task = nil
function OnDown(key)
if key == "F9" then
task = Run(function()
while Input.IsDown("F9") do
HID.Press("Mouse1", 20)
Sleep(100)
end
end)
end
return false
end
function OnStop()
if task and task:IsRunning() then
task:Cancel()
end
end
Exact signatures are under Globals and Timer.
Reading state
When you need to know what’s happening right now rather than waiting for an event, poll it:
Inputreports currently held keys, modifiers, and how long a key has been down — useful insideOnTickor aRun()loop.Systemgives the current time, cursor position, screen size, and focused window, refreshed each tick.
See Input and System for the methods; the Double-tap pattern below shows System.Time in use.
Config panels
A script can define a settings panel that appears in the Rebind UI, so users tune it without editing code. UI.Schema declares the controls; you read and write values through the returned handle, and they persist automatically (keyed to the script’s file path):
local cfg = UI.Schema({
enabled = UI.Toggle(true, { label = "Enable" }),
speed = UI.Slider(50, { min = 0, max = 100, suffix = "%" }),
})
if cfg.enabled then
local s = cfg.speed
end
The full widget catalog (toggles, sliders, keybinds, selects, text, plus layout and notifications) is in the UI reference.
Splitting across files (require)
As a script grows, pull shared logic into its own file and load it with
require(). A module is just a .luau / .lua file that returns a value —
usually a table:
-- helpers.luau, next to your script
local M = {}
function M.clamp(n, lo, hi)
return math.max(lo, math.min(hi, n))
end
return M
-- main script
local helpers = require("helpers")
local x = helpers.clamp(value, 0, 100)
require resolves relative to the script’s own directory:
- An extensionless name is tried as
<name>.luau, then<name>.lua, then<name>/init.luau, then<name>/init.lua(so a folder with aninit.luauloads as a package). - Dots are path separators:
require("lib.math")loadslib/math.luau. - An explicit
.lua/.luausuffix pins that exact file; a leading./is ignored. - Modules are cached: the file is evaluated once on the first
require, and every laterrequireof the same path returns that same value.
It is sandboxed to the script’s folder. A name containing .., or any path that
would resolve outside the directory, is rejected (path traversal not allowed),
and an unresolved name raises module '<name>' not found. There are no external
packages, registries, package.path, or aliases — local files only.
Only the script you run needs a
min_sdkmodeline. Arequire’d module is loaded through that script rather than run directly, so it needs no header.
Macros
Macro.Play plays back a recorded input sequence — a table of move/press/scroll/sleep actions. Reach for a macro when you have a fixed sequence to replay; use raw HID calls when the output depends on logic. Patterns can be transformed before playback (Math.Scale, Math.Spline, Math.Resample). See the Macro reference for the action format and playback modes.
Targeting and the always-on model
Restrict a script to specific applications with the window= and process= modeline keys. window= matches a case-insensitive substring of the focused window’s title (so window=code also matches “Visual Studio Code”); process= matches the process name the same way. A script activates if any window= or any process= pattern matches — the two lists combine as OR — and it deactivates when the matching window loses focus:
-- rebind: process=Photoshop.exe
function OnFocus(window)
Log.Info("focused: " .. window.title)
end
function OnBlur()
Log.Info("left")
end -- clean up here
A targeted script that isn’t focused is skipped entirely — its hooks, timers, and OnTick never fire. So you can keep hundreds of scripts loaded at once and only the script(s) matching the focused window run, with no interference or wasted CPU:
Always running (no targeting):
CapsLock to Escape
Mouse Media Keys
Activated on demand:
Photoshop Shortcuts (process=Photoshop.exe)
Browser Tab Nav (process=chrome.exe, firefox.exe, msedge.exe)
Terminal Helpers (window=Terminal)
Scripts without any targeting are always active — use them for system-wide behaviors like remaps and media controls. When several scripts run at once, z_index controls which sees input first (a false return blocks lower-priority scripts), and instance controls what happens when a script loads while a copy is already running. Both are in the Modeline reference.
Patterns
Common shapes that compose the pieces above.
Toggle
local active = false
function OnDown(key)
if key == "F9" then
active = not active
UI.Notify(active and "ON" or "OFF", "info")
return false
end
return true
end
Hold loop
function OnDown(key)
if key == "Mouse1" then
Run(function()
while Input.IsDown("Mouse1") do
-- do something each iteration
Sleep(100)
end
end)
return false
end
return true
end
Double-tap
local lastTap = 0
local THRESHOLD = 250
function OnDown(key)
if key == "W" then
local now = System.Time()
if now - lastTap < THRESHOLD then
Log.Info("Double tap!")
end
lastTap = now
end
return true
end
Validation
The runtime lints your script on load and warns (in the Logs tab, without blocking the load) about common issues:
- No hooks defined — the script loads but never runs any logic.
Net.Get/Net.Postin a high-frequencyOnTick— synchronous HTTP blocks the main thread; move it insideRun(). WebSocket handlers (Net.WSListen/Net.WSConnect) are exempt — their I/O runs on dedicated threads.
Type checking
The SDK ships a type definition file (types/rebind.d.luau) for the Luau Language Server, giving you autocomplete, hover docs, and type checking in VS Code. Add a .luaurc next to your script:
{
"languageMode": "nonstrict",
"paths": ["path/to/lua-sdk/types"]
}
Platforms
Rebind runs on Windows, macOS, and Linux, and the same scripts move between them.
The core runtime and the namespaces most scripts use — HID, remapping (Bind) and
the input hooks, Macro/Timer/Math/Net/File/JSON/Regex/Config/Process/System.Exec,
UI/Log/Input, and the Run/Sleep/After/Async globals — are identical on
all three OSes. A few namespaces depend on OS-specific APIs and are limited or absent
on some platforms. This page maps what runs where, and why.
Platform support
The table below lists every namespace with platform-specific behavior. Everything not in the table is fully cross-platform.
| Namespace | Windows | macOS | Linux |
|---|---|---|---|
| Screen | Full (GDI) | Full (CoreGraphics) | Stub (Screen.List via display-info) |
| Window | Full (Win32) | Enumeration + Kill/IsActive/WaitActive/WaitClose; rest best-effort | Stub |
| Clipboard | Full (Win32) | Full (arboard) | Stub |
| Pipe | Full (shared memory) | Not available | Not available |
| Registry | Full (Win32) | Not available | Not available |
| Dialog | Full (native) | Full (native) | Requires Zenity/KDialog/YAD |
| System.Exec | cmd.exe /C | sh -c | sh -c |
The rest — Input, Macro, Timer, Math, JSON, Hash, Codec, Env,
Log, File, Net, UI, Bind, Script, Regex, Config — is fully
cross-platform. Software and hardware mode run the same scripts; neither changes this
table — namespace availability is a property of the OS, not the transport. The one
capability split between modes is mouse_block: suppressing or transforming mouse
movement requires a Rebind device on every OS, because the cursor is drawn below
anything a host process can intercept. Key, button, and scroll blocking work in both
modes. For hardware setup, see Hardware.
- Screen samples pixels on Windows (GDI BitBlt) and macOS (CoreGraphics /
ScreenCaptureKit). On Linux pixel sampling is stubbed, not absent:
Screen.GetPixelColoris still callable but returns a fixed"000000"(black) instead of a real pixel, andScreen.SearchForColorreturnsnil— neither raises an error. OnlyScreen.List(display enumeration) is real on Linux. - Window is full on Windows. On macOS, enumeration (
Find,List) plusKill,IsActive,WaitActive, andWaitCloseare real. The manipulation calls split two ways:Move,Activate,Minimize,Maximize,Restore, andCloseare silent no-ops — they return success and do nothing, so apcallaround them will not catch an error — while the Accessibility-gated callsSetTitle,SetAlwaysOnTop,SetTransparency,GetClass,Hide, andShowraise a “not supported on macOS without accessibility” error you can guard withpcall.Window.GetTitlereturns an empty string on macOS; titles are only available via thetitlefield onWindow.List/Window.Findentries. On Linux, Window is a stub. - Pipe (shared-memory IPC) and Registry are Windows-only. They raise an error on macOS and Linux. POSIX shared-memory support for Pipe is planned.
- Clipboard works on Windows and macOS; it is a stub on Linux.
- Dialog is native on Windows and macOS; on Linux it needs Zenity, KDialog, or YAD installed.
If you target multiple platforms, guard the OS-specific calls. pcall catches the
“not available” error a Windows-only namespace raises on macOS or Linux:
local ok = pcall(function()
Registry.Write([[HKCU\Software\MyScript]], "REG_SZ", "Profile", "default")
end)
if not ok then
-- portable fallback: persist to the script directory
Config.WriteTOML("profile.toml", { profile = "default" })
end
Cookbook
Recipes you can copy, paste, and run. Every script is written against the real
SDK — the same namespaces you call from your own scripts. Most run unchanged in
free software mode (OS-level output) or on a Teensy 4.x in hardware mode; the
exceptions are recipes that implement the OnMove hook (tremor smoothing, mouse
rate limiting, mouse acceleration). Those intercept mouse movement, which requires
a Rebind hardware device — software mode can’t synchronously act on mouse input —
so the relay refuses to run them without a device. Each OnMove recipe notes this.
Each recipe begins with a modeline header (--[[ rebind: … --]]). The min_sdk
line in it is required — the relay won’t run a script whose modeline doesn’t
declare a Rebind version. See Modeline.
The recipes make your input devices do more work for you: productivity, accessibility, automation, and remapping — text expansion, per-app shortcut layers, dwell-click and tremor smoothing, HTTP-driven control, and turning any key into another key, a chord, or a layer.
Each recipe names the namespaces it uses and the hooks it implements (OnDown,
OnUp, OnMove, OnScroll, OnTick, OnStart, OnStop, OnBlur) so you can
see how it works before you adapt it.
Productivity recipes
Your AutoHotkey or Karabiner logic, on any USB keyboard and mouse. The Rebind engine captures your input and runs your scripts; add the Rebind Link dongle for hardware output. Core input remapping runs on Windows, macOS, and Linux; some recipes below also use Clipboard and per-app window targeting, which are Windows/macOS only (noted per recipe).
Each recipe is a complete script. Copy it into a new script in the Rebind app and adjust the config.
Text expansion
Type a short abbreviation, then press a trigger key to expand it into full text. HID.Type sends each character through the hardware; for long or multi-line blocks, paste through the clipboard instead so it lands instantly and intact.
--[[
rebind: min_sdk=3.0.0
rebind: name=Text Expander
--]]
local cfg = UI.Schema({
trigger = UI.Keybind("F8", { label = "Expand Key" }),
email = UI.Text("user@example.com", { label = "Email", maxLength = 60 }),
address = UI.Text(
"123 Example St\nPortland, OR 97201",
{ label = "Address", maxLength = 200 }
),
})
local snippets = {}
function OnStart()
snippets = {
["@@"] = cfg.email,
[";addr"] = cfg.address,
["/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
Run(function()
for i = 1, #abbr do
HID.Press("Backspace")
Sleep(20)
end
-- multi-line text pastes intact; short text can use HID.Type
if expansion:find("\n") then
Clipboard.Set(expansion)
HID.Press("LCtrl+V")
else
HID.Type(expansion, 15)
end
end)
buffer = ""
return false
end
end
return false
end
-- track typed characters so we can match abbreviations
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
Tuning. Add more entries to the snippets table. Raise the HID.Type char delay (the 15) if an app drops characters; lower it for faster output. Multi-line blocks route through Clipboard.Set automatically.
Per-app shortcuts
A modeline scopes a script to one application by window title (window=) or executable (process=). The same physical key means one thing in your editor and nothing elsewhere — no profile switching, zero overhead when the app isn’t focused.
--[[
rebind: min_sdk=3.0.0
rebind: name=Editor Shortcuts
rebind: process=Code.exe
--]]
function OnDown(key)
-- F1 opens the command palette, only inside the editor
if key == "F1" then
Run(function()
HID.Press("LCtrl+LShift+P")
end)
return false
end
-- 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
Tuning. Add multiple modelines to cover several apps; anything outside the match passes through untouched. (Windows/macOS only — see Platforms.) Full modeline semantics are in the SDK reference.
Clipboard transform
Read the clipboard, transform it in Luau, and paste the result. This example trims and lowercases the selection, but any string operation works — strip formatting, wrap in quotes, convert case, run a regex.
--[[
rebind: min_sdk=3.0.0
rebind: name=Clipboard Transform
--]]
local cfg = UI.Schema({
hotkey = UI.Keybind("F9", { label = "Transform + Paste" }),
})
Bind(
cfg.hotkey,
Async(function()
-- copy the current selection first
HID.Press("LCtrl+C")
Sleep(50)
local text = Clipboard.Get()
if not text or text == "" then
UI.Notify("Clipboard empty", "warning")
return
end
-- transform: trim whitespace and lowercase
local cleaned = text:gsub("^%s+", ""):gsub("%s+$", ""):lower()
Clipboard.Set(cleaned)
HID.Press("LCtrl+V")
end)
)
Tuning. Replace the cleaned line with your own transform. The Sleep(50) gives the foreground app time to populate the clipboard after the copy; raise it if the read comes back stale. Clipboard access is available on Windows and macOS.
Multi-step macro
Replay a fixed sequence of keystrokes on demand. Run plus Sleep controls each step and its timing; task:Cancel() aborts a long sequence mid-flight.
--[[
rebind: min_sdk=3.0.0
rebind: name=Macro Sequence
--]]
local cfg = UI.Schema({
play_key = UI.Keybind("F9", { label = "Play" }),
stop_key = UI.Keybind("F10", { label = "Stop" }),
})
local task = nil
function OnDown(key)
if key == cfg.play_key then
task = Run(function()
HID.Press("LCtrl+A") -- select all
Sleep(100)
HID.Press("LCtrl+C") -- copy
Sleep(100)
HID.Press("End") -- jump to end
HID.Press("Enter")
HID.Press("LCtrl+V") -- paste below
UI.Notify("Done", "success")
end)
return false
end
if key == cfg.stop_key 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
Tuning. Edit the body of the Run block to define your sequence. Each Sleep is in milliseconds — tighten the delays for speed, widen them if a step fires before the previous one lands. To capture a sequence live instead of hand-writing it, see the macro recorder below.
Record and replay a macro
Capture a live sequence of keystrokes and mouse moves, then play it back with exact timing. Macro.Record starts capturing, Macro.Finish returns the recorded macro, and Macro.Play replays it — at any speed.
--[[
rebind: min_sdk=3.0.0
rebind: name=Macro Recorder
--]]
local cfg = UI.Schema({
record_key = UI.Keybind("F9", { label = "Record / Stop" }),
play_key = UI.Keybind("F10", { label = "Play" }),
speed = UI.Slider(100, { min = 25, max = 400, suffix = "%" }),
})
local recording = false
local macro = nil
function OnDown(key)
if key == cfg.record_key then
if recording then
macro = Macro.Finish()
recording = false
UI.Notify("Recorded " .. #macro .. " actions", "success")
else
Macro.Record({ ignore_mouse = false })
recording = true
UI.Notify("Recording — press again to stop", "info")
end
return false
end
if key == cfg.play_key and macro then
Macro.Play(macro, cfg.speed / 100)
return false
end
return true
end
Tuning. speed scales playback — 100 is the recorded speed, 200 plays it twice as fast. Pass { ignore_mouse = true } to Macro.Record to record keystrokes only. For frame-accurate, device-side replay use Macro.Stream(macro) instead of Macro.Play. The recorded action format and the macro transforms (Math.Scale, Math.Resample) are in the SDK reference.
Scroll to zoom
Hold a modifier and scroll to zoom in browsers, editors, and image viewers — the standard Ctrl + scroll gesture, bound to any key you prefer.
--[[
rebind: min_sdk=3.0.0
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
Tuning. Change modifier to any key. To slow the zoom, scale delta before passing it to HID.Scroll (for example, HID.Scroll(delta // 2)). Returning false swallows the original scroll so only the zoom fires.
Set your mouse output rate
A live demonstration of the 8,000 Hz loop: capture every raw mouse movement and re-emit it at exactly the rate you choose — effectively setting your mouse’s output polling rate. mouse_block=true hands OnMove control of every event (this requires a Rebind device), and an OnTick running at 8 kHz decides when to flush the accumulated motion.
--[[
rebind: min_sdk=3.0.0
rebind: name=Mouse Rate Limiter
rebind: mouse_block=true
rebind: tick_rate=8000
--]]
local cfg = UI.Schema({
rate = UI.Slider(
125,
{ min = 60, max = 1000, suffix = "Hz", label = "Output Rate" }
),
})
local accX, accY = 0, 0
local startTime = nil
local lastEmitTime = nil
function OnMove(dx, dy)
accX = accX + dx
accY = accY + dy
return false
end
function OnTick()
local now = System.Time()
if not startTime then
startTime = now
lastEmitTime = now
return
end
local interval = 1000 / cfg.rate
local elapsed = now - startTime
local expectedEmits = math.floor(elapsed / interval)
local actualEmits = math.floor((lastEmitTime - startTime) / interval)
if expectedEmits > actualEmits then
lastEmitTime = now
if accX ~= 0 or accY ~= 0 then
HID.Move(accX, accY)
accX, accY = 0, 0
end
end
end
Tuning. rate is the output rate in Hz — lower it to throttle a high-polling-rate mouse, raise it toward the device’s own rate. No motion is lost: OnMove accumulates the raw deltas and OnTick flushes them on schedule, so the cursor’s path is preserved and only its timing changes. This needs mouse_block=true (so OnMove can hold each event, hardware mode only) and a high tick_rate for an accurate output clock — see the scripting guide.
Mouse acceleration
Add a speed-dependent acceleration curve to any mouse: slow movements stay close to 1:1 for precision, fast flicks get multiplied so you cross the screen with less travel. mouse_block=true hands OnMove every raw event (requires a Rebind device), so the script rescales each delta and re-emits it with HID.Move — curve, threshold, and ceiling are all live in the config panel.
--[[
rebind: min_sdk=3.0.0
rebind: name=Mouse Acceleration
rebind: mouse_block=true
--]]
local cfg = UI.Schema({
enabled = UI.Toggle(true, { label = "Enable Acceleration" }),
toggle_key = UI.Keybind("F7", { label = "Toggle Key" }),
curve = UI.Select(
"Quadratic",
{ "Linear", "Quadratic", "Cubic", "Exponential" },
{ label = "Curve Type" }
),
multiplier = UI.Slider(
2.0,
{ min = 1.0, max = 5.0, step = 0.1, suffix = "x", label = "Max Multiplier" }
),
threshold = UI.Slider(
5,
{ min = 1, max = 30, suffix = "px", label = "Accel Threshold" }
),
sensitivity = UI.Slider(
100,
{ min = 25, max = 200, suffix = "%", label = "Base Sensitivity" }
),
})
local function getSpeed(dx, dy)
return math.sqrt(dx * dx + dy * dy)
end
local function applyAccel(speed)
local base = cfg.sensitivity / 100
local threshold = cfg.threshold
if speed <= threshold then
return base
end
local excess = speed - threshold
local maxMult = cfg.multiplier
local factor
if cfg.curve == "Linear" then
factor = 1 + (excess / 20) * (maxMult - 1)
elseif cfg.curve == "Quadratic" then
factor = 1 + (excess / 20) ^ 2 * (maxMult - 1)
elseif cfg.curve == "Cubic" then
factor = 1 + (excess / 20) ^ 3 * (maxMult - 1)
elseif cfg.curve == "Exponential" then
factor = 1 + (math.exp(excess / 20) - 1) * (maxMult - 1) / (math.exp(1) - 1)
else
factor = 1
end
factor = math.min(factor, maxMult)
return base * factor
end
function OnDown(key)
if key == cfg.toggle_key then
cfg.enabled = not cfg.enabled
UI.Notify(cfg.enabled and "Acceleration ON" or "Acceleration OFF", "info")
return false
end
return true
end
function OnMove(dx, dy)
if not cfg.enabled then
return true
end
local speed = getSpeed(dx, dy)
if speed == 0 then
return false
end
local factor = applyAccel(speed)
HID.Move(dx * factor, dy * factor)
return false
end
Tuning. curve controls how aggressively the multiplier ramps once a movement passes threshold — Linear is gentle, Exponential is sharp. multiplier caps the fastest flicks; sensitivity scales everything, slow moves included. Anything under threshold px stays at base sensitivity for pixel-precise aiming, and the bound key toggles the whole effect on the fly. Like the rate limiter, this needs mouse_block=true so OnMove can swallow the raw event and emit the accelerated one — hardware mode only (see the scripting guide).
Automation recipes
These recipes turn a Rebind script into a control surface that any program can drive — a web app, a Python process, a home-automation hub, or a local model. The script runs Luau on the Rust core, hooks dispatched up to 8,000 Hz, and emits standard USB HID from a Teensy 4.x (or OS-level input in software mode).
The single-endpoint Net.Listen pattern — HTTP request in, output out — appears in
the Quickstart section of the SDK reference; these
recipes build on it. They lean on the localhost transport numbers documented in
Remote control.
A WebSocket remote
Net.WSListen is the same idea over a persistent connection. Use it when you need
push events (server to client) or a low-latency channel for high-frequency
commands. The server handles multiple clients, broadcasts to all of them, and
reacts to each message within a single tick.
--[[
rebind: min_sdk=3.0.0
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.Parse(payload)
if req.t == "type" then
Run(function()
HID.Type(req.text or "")
end)
client:Send(JSON.Stringify({ 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
server:Broadcast(payload)
end
end,
OnClose = function(client)
Log.Info(string.format("client %d disconnected", client.id))
end,
})
Log.Info("WebSocket remote 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) compose with the same
handler. For the official typed, auto-reconnecting clients, see
Remote control.
A screen-condition trigger
Screen.GetPixelColor samples a pixel; Screen.SearchForColor scans a region.
Polling them on a tick gives you a desktop-automation trigger: act when a known
visual state appears. This recipe watches a fixed point on screen and fires a
keystroke when a build indicator turns red — the kind of thing you’d wire into a
CI dashboard, a long-running render, or an accessibility cue.
Screen and Window are limited or absent on Linux; see
Platforms & limits.
--[[
rebind: min_sdk=3.0.0
rebind: name=Build Status Watcher
--]]
local cfg = UI.Schema({
enabled = UI.Toggle(true, { label = "Enable" }),
x = UI.Slider(40, { min = 0, max = 4000, label = "Sample X" }),
y = UI.Slider(40, { min = 0, max = 4000, label = "Sample Y" }),
target = UI.Text("E53935", { maxLength = 6, label = "Alert Color (hex)" }),
tolerance = UI.Slider(24, { min = 0, max = 100, label = "Tolerance" }),
})
local fired = false
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 distance(a, b)
local r1, g1, b1 = hexToRgb(a)
local r2, g2, b2 = hexToRgb(b)
return math.sqrt((r2 - r1) ^ 2 + (g2 - g1) ^ 2 + (b2 - b1) ^ 2)
end
function OnTick()
if not cfg.enabled then
return
end
local pixel = Screen.GetPixelColor(cfg.x, cfg.y)
local matched = distance(pixel, cfg.target) <= cfg.tolerance
-- edge-triggered: act once when the state appears, reset when it clears
if matched and not fired then
fired = true
UI.Notify("Build status: alert color is showing", "warning")
Run(function()
HID.Press("F5")
end) -- refresh the dashboard
elseif not matched then
fired = false
end
end
Use Screen.SearchForColor instead of a fixed point when the indicator can move:
function OnTick()
-- scan the top-left quadrant for the alert color
-- region is {x1, y1, x2, y2}; tolerance is 0.0-1.0 (per-channel)
local hit = Screen.SearchForColor({ 0, 0, 600, 400 }, cfg.target, 0.05)
if hit then
Log.Info(string.format("found at %d,%d", hit.x, hit.y))
end
end
A timer-driven task
Timer.Every runs a callback on a fixed interval without blocking the input loop.
This recipe nudges the cursor a few pixels every few minutes to keep a workstation
from idling during a long unattended job, with a hotkey to toggle it.
--[[
rebind: min_sdk=3.0.0
rebind: name=Idle Nudge
--]]
local cfg = UI.Schema({
interval = UI.Slider(
180,
{ min = 30, max = 900, suffix = "s", label = "Interval" }
),
toggle = UI.Keybind("F7", { label = "Toggle" }),
})
local active = false
local timer = nil
function OnDown(key)
if key ~= cfg.toggle then
return true
end
active = not active
if active then
timer = Timer.Every(cfg.interval * 1000, function()
Run(function()
HID.Move(1, 0)
Sleep(50)
HID.Move(-1, 0)
end)
end)
UI.Notify("Idle nudge ON", "info")
else
if timer then
timer:Cancel()
timer = nil
end
UI.Notify("Idle nudge OFF", "info")
end
return false
end
function OnStop()
if timer then
timer:Cancel()
end
end
A sidecar over shared-memory IPC
Keep the perception and decision loop in your own process, with your model and your
stack. When the sidecar decides what to do, it hands the decision to Rebind over
Pipe (shared-memory IPC, Windows-only) and the script emits the movement as
standard USB HID. The sidecar never touches the hardware; the script does,
deterministically, at up to 8,000 Hz.
Pipe is the low-latency path for a co-located process. For anything on the
network or off-Windows, use the HTTP or WebSocket recipes above.
--[[
rebind: min_sdk=3.0.0
rebind: name=Sidecar Bridge
--]]
local pipe = nil
function OnStart()
pipe = Pipe.Open("sidecar", { size = 65536 })
Log.Info("Pipe opened: " .. pipe.name)
Timer.Every(5, function() -- poll the pipe every 5ms
local msg = pipe:Read()
if not msg then
return
end
local cmd = JSON.Parse(msg)
if cmd.action == "move" then
HID.Move(cmd.dx or 0, cmd.dy or 0)
elseif cmd.action == "press" and cmd.key then
Run(function()
HID.Press(cmd.key)
end)
elseif cmd.action == "type" and cmd.text then
Run(function()
HID.Type(cmd.text, 30)
end)
end
end)
end
function OnStop()
if pipe then
pipe:Close()
end
end
The external process maps the same shared-memory region. The region is split into
two channels: the script writes to channel A (offset 0) and reads from channel B
(offset size / 2), so the peer does the inverse — it writes into channel B and
reads from channel A. Each channel is framed as [u64 seq][u32 len][payload]
(little-endian); the writer lays down the payload and length first and bumps the
sequence number last, so the reader only acts on a complete frame. In Python:
import mmap
import json
import struct
SIZE = 65536
HALF = SIZE // 2 # channel B (the script's inbox) begins here
shm = mmap.mmap(-1, SIZE, tagname="Local\\Rebind_sidecar")
seq = 0
def send(payload: dict) -> None:
# Frame the message the way pipe:Read() expects and write it into channel B.
# Payload + length first, sequence number last (the read barrier).
global seq
data = json.dumps(payload).encode()
seq += 1
shm[HALF + 12 : HALF + 12 + len(data)] = data
shm[HALF + 8 : HALF + 12] = struct.pack("<I", len(data))
shm[HALF : HALF + 8] = struct.pack("<Q", seq)
# Each channel is a single slot — a new write overwrites an unread one — so pace
# your writes to the script's poll interval rather than bursting.
send({"action": "move", "dx": 12, "dy": -4})
send({"action": "press", "key": "Mouse1"})
Accessibility recipes
Make any keyboard and mouse work the way you need — at the hardware level, in every application. The scripts on this page run on the Rebind device and come out as standard USB input, so they keep working in apps that ignore or block software assistive tools, and they move with you across Windows, macOS, and Linux.
Each recipe explains what it helps with, gives you a complete script to paste into the Rebind UI, and lists the settings worth tuning. Every script exposes those settings through the UI panel, so you can dial in timings and thresholds with sliders and toggles without editing code.
Dwell-click
For anyone who can move a pointer but can’t reliably press a button. Rest the cursor in one place and, after a short pause, Rebind clicks for you. Moving away before the timer finishes cancels the click.
--[[
rebind: min_sdk=3.0.0
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
Tuning notes
- dwell_time — how long the cursor must rest before a click. Start at one second. Lower it as you build confidence; raise it if clicks fire too eagerly.
- tolerance — how much wobble is allowed while you hold position, in pixels. Larger forgives unsteady movement; smaller makes the dwell stricter.
- click_type — switch between left, right, and double click without changing any code.
- sound — a short beep confirms each click so you don’t have to watch closely.
Click assist
Three modes for people who can reach a button but struggle to hold it or time a double click. Hold Assist turns one press into a timed hold that releases itself. Toggle Click latches a single click down until you click again — useful for dragging without holding. Double Click fires two clicks from one press.
--[[
rebind: min_sdk=3.0.0
rebind: name=Click Assist
--]]
local cfg = UI.Schema({
enabled = UI.Toggle(true),
mode = UI.Select(
"Hold Assist",
{ "Hold Assist", "Toggle Click", "Double Click" }
),
tune_hold = UI.Toggle(false, { label = "Tune hold duration" }),
hold_time = UI.Slider(500, {
min = 100,
max = 3000,
suffix = "ms",
showIf = "tune_hold",
}),
tune_double = UI.Toggle(false, { label = "Tune double-click gap" }),
double_delay = UI.Slider(50, {
min = 20,
max = 200,
suffix = "ms",
showIf = "tune_double",
}),
})
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
Tuning notes
- mode — Hold Assist for tasks that need a sustained press, Toggle Click for drag-and-drop without holding, Double Click for interfaces that expect rapid double clicks.
- hold_time — how long Hold Assist keeps the button down before releasing it. Match it to the longest hold you typically need.
- double_delay — the gap between the two clicks in Double Click. If the application misses the second click, increase this a little.
- The script releases the button cleanly on focus loss and on stop, so a latched toggle never gets stuck down.
Tremor smoothing
For users with hand tremor or repetitive-strain motion. Movements below a threshold
are dropped, and larger movements are smoothed with an exponential filter, so the
cursor follows your intent instead of the jitter. Filtering movement uses
mouse_block=true, which requires a Rebind device.
--[[
rebind: min_sdk=3.0.0
rebind: name=Steady Hand
rebind: mouse_block=true
--]]
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
Tuning notes
- threshold — the smallest movement Rebind will pass through, in pixels. Raise it to ignore more involuntary jitter; lower it if intentional small movements feel suppressed.
- smoothing — how much each movement is averaged with the previous one. Higher gives a steadier cursor at the cost of a little latency; find the point where the pointer feels calm but still responsive.
- The filtering happens on the device, before the movement reaches the OS.
Sticky keys
For one-handed use or limited grip strength, holding two keys at once is hard. This turns a modifier into a tap-to-lock toggle: tap once and it stays held, tap again to release. A notification tells you whether it’s active, and it releases automatically on focus change so you never get stuck in a held state.
--[[
rebind: min_sdk=3.0.0
rebind: name=Sticky Keys
--]]
local cfg = UI.Schema({
target = UI.Keybind("LShift", { label = "Modifier to Lock" }),
})
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
Tuning notes
- target — choose any modifier to make sticky:
LShift,LCtrl,LAlt,LWin, or their right-hand variants. Run a separate copy of the script for each modifier you want to latch. - To lock several modifiers at once (for a combination like Ctrl+Shift), enable one sticky script per key; each latches independently and they combine naturally.
- The script swallows the modifier’s own release event, so a single tap fully toggles the lock without the OS seeing an immediate key-up.
One-handed layout
Remaps keys so a single hand can reach everything it needs. The example mirrors the right side of the keyboard onto the left, brings Enter and Backspace under the home position, and frees a thumb key for Space — all configurable. Use it as a starting point and remap to whatever suits your hand.
--[[
rebind: min_sdk=3.0.0
rebind: name=One-Handed Layout
--]]
local cfg = UI.Schema({
enabled = UI.Toggle(true, { label = "Enable Layout" }),
layer_key = UI.Keybind("CapsLock", { label = "Layer Hold Key" }),
})
-- base remaps: always active when enabled
local base = {
-- bring common edit keys under the left hand
["F1"] = "Backspace",
["F2"] = "Enter",
["F3"] = "Space",
}
-- layered remaps: active only while the layer key is held,
-- mirroring the right hand onto the left
local layer = {
["Q"] = "P",
["W"] = "O",
["E"] = "I",
["R"] = "U",
["A"] = "Semicolon",
["S"] = "L",
["D"] = "K",
["F"] = "J",
}
local function resolve(key)
if Input.IsDown(cfg.layer_key) and layer[key] then
return layer[key]
end
return base[key]
end
function OnDown(key)
if not cfg.enabled then
return true
end
if key == cfg.layer_key then
return false -- the layer key itself never types
end
local mapped = resolve(key)
if mapped then
HID.Down(mapped)
return false
end
return true
end
function OnUp(key)
if not cfg.enabled then
return true
end
if key == cfg.layer_key then
return false
end
local mapped = resolve(key)
if mapped then
HID.Up(mapped)
return false
end
return true
end
Tuning notes
- base — keys remapped all the time. Add an entry as
["FromKey"] = "ToKey"for each key you want to relocate under your hand. - layer — a second set of remaps that only apply while layer_key is held, doubling how many keys one hand can reach. Hold the layer key with a thumb or spare finger to flip into the mirrored set.
- layer_key — the hold key that activates the layer.
CapsLockis a common choice because it sits at the home row and is rarely needed otherwise. - Use the SDK reference to discover the exact key names Rebind reports, then fill in the tables to match your hand.
Key-repeat control
For users whose presses linger or who trigger unintended repeats, default keyboard auto-repeat can flood an application with extra characters. Two independent controls fix this. Debounce ignores a second press of the same key within a set window, filtering accidental double-taps. Repeat suppression blocks a held key from auto-repeating after the first character, so a long press produces one keystroke.
--[[
rebind: min_sdk=3.0.0
rebind: name=Key Timing Control
--]]
local cfg = UI.Schema({
debounce = UI.Toggle(
true,
{ label = "Ignore double-presses", group = "Debounce" }
),
debounce_ms = UI.Slider(200, {
min = 50,
max = 1000,
suffix = "ms",
group = "Debounce",
}),
no_repeat = UI.Toggle(
true,
{ label = "Suppress auto-repeat", group = "Repeat" }
),
})
local lastDown = {} -- key -> timestamp of last accepted press
local downNow = {} -- key -> currently held
function OnDown(key)
-- suppress OS auto-repeat: a key already marked down repeats no further
if cfg.no_repeat and downNow[key] then
return false
end
-- debounce: reject a fresh press too soon after the previous one
if cfg.debounce then
local now = System.Time()
local prev = lastDown[key]
if prev and now - prev < cfg.debounce_ms then
return false
end
lastDown[key] = now
end
downNow[key] = true
return true
end
function OnUp(key)
downNow[key] = nil
return true
end
Tuning notes
- debounce / debounce_ms — turn on to ignore a repeated press of the same key that lands within the window. Widen debounce_ms if accidental double-presses still slip through; narrow it if deliberate fast presses get dropped.
- no_repeat — when on, holding a key sends one character instead of a stream. Leave it off for keys where you do want auto-repeat, such as arrow keys.
- The script passes accepted presses through unchanged (
return true), so every keystroke it lets through is ordinary USB input — only unwanted repeats and bounces are filtered.
Remapping & layers
Turn any key or button into another key, a chord, a modifier hold, or a whole layer — key remaps, combos, and conditional layers, the same job as AutoHotkey or Karabiner. The logic is identical on Windows, macOS, and Linux, works with any USB keyboard or mouse, and (with a Teensy 4.x attached) comes out as standard USB HID at up to 8,000 Hz. Every recipe below runs unchanged in software mode first.
The building blocks are small and the same throughout:
Bind.Remap(from, to)— a declarative one-line key-to-key remap.OnDown(key)/OnUp(key)— returntrueto let a key pass through,falseto swallow it. Send your replacement withHID.Down/HID.Up/HID.Press.Input.IsDown(key)— test whether a physical key is currently held, which is how you build chords, modifier holds, and layers.
Press Left Ctrl + Left Alt + K at any time to stop every running script. (The kill switch uses the left-hand modifiers specifically — right Ctrl / right Alt don’t trigger it.)
Single-key remap
Bind.Remap takes the key you press and the key you want sent in its place — no
hooks, no state. Caps Lock becomes Escape; your real Escape is untouched.
--[[
rebind: min_sdk=3.0.0
rebind: name=CapsLock to Escape
--]]
Bind.Remap("CapsLock", "Escape")
Bind.Remap returns a handle, so you can toggle a remap at runtime with
:disable() and :enable(). Here a hotkey flips it live, and the handle reports
its state through .enabled.
--[[
rebind: min_sdk=3.0.0
rebind: name=Toggleable Remap
--]]
local remap = Bind.Remap("CapsLock", "Escape")
-- Press F8 to switch the remap off and on without restarting the script.
Bind("F8", function()
if remap.enabled then
remap:disable()
UI.Notify("Remap off", "info")
else
remap:enable()
UI.Notify("Remap on", "info")
end
return false
end)
For conditions more nuanced than on/off — “only in this app,” “only while another
key is held” — drop down to OnDown / OnUp and decide per event. The remaining
recipes all use that form.
Chords and combos
A chord fires one action only when several keys are held together. Read the other
keys with Input.IsDown at the moment the trigger key goes down, and swallow the
trigger so it doesn’t type on its own.
--[[
rebind: min_sdk=3.0.0
rebind: name=Chord: J+K -> Escape
--]]
-- Press J and K together to send Escape. Either key alone types normally.
function OnDown(key)
if key == "J" and Input.IsDown("K") then
HID.Down("Escape")
HID.Up("Escape")
return false
end
if key == "K" and Input.IsDown("J") then
HID.Down("Escape")
HID.Up("Escape")
return false
end
return true
end
Checking both orders makes the chord order-independent. For a combo that includes a
real modifier, test it the same way — Input.IsDown("LCtrl") — and emit a keyboard
combo with the + syntax:
--[[
rebind: min_sdk=3.0.0
rebind: name=Combo: Ctrl+Backspace -> Delete Word
--]]
function OnDown(key)
if key == "Backspace" and Input.IsDown("LCtrl") then
-- send Ctrl+Shift+Left then Delete: select the previous word and remove it
Run(function()
HID.Press("LCtrl+LShift+Left")
HID.Press("Delete")
end)
return false
end
return true
end
Modifier hold
A modifier hold gives one key two jobs: tap it for its normal value, hold it to change what other keys do. Track when the key went down and whether another key was used while it was held. On release, if nothing else fired and the hold was short, send the tap.
--[[
rebind: min_sdk=3.0.0
rebind: name=Space as Hold-Modifier
--]]
-- Tap Space -> Space. Hold Space + H/J/K/L -> arrow keys.
local TAP_MS = 180
local spaceDown = nil
local usedAsMod = false
local arrows = { H = "Left", J = "Down", K = "Up", L = "Right" }
function OnDown(key)
if key == "Space" then
spaceDown = System.Time()
usedAsMod = false
return false -- hold the Space output until we know tap vs hold
end
if spaceDown and arrows[key] then
usedAsMod = true
HID.Down(arrows[key])
HID.Up(arrows[key])
return false
end
return true
end
function OnUp(key)
if key == "Space" then
local heldMs = System.Time() - (spaceDown or 0)
if not usedAsMod and heldMs < TAP_MS then
HID.Down("Space")
HID.Up("Space") -- it was a quick tap, emit the real key
end
spaceDown = nil
return false
end
return true
end
function OnBlur()
spaceDown = nil
end
usedAsMod prevents a stray Space when you genuinely used the hold, and TAP_MS
keeps a long, lone hold from emitting a late Space. OnBlur clears the state if
focus leaves while you’re mid-hold.
Hold-to-access layer
A layer is a modifier hold scaled up: hold one key to expose an alternate set of keys, release it to return to normal. Keep the mappings in a table so adding a key is a one-line edit.
--[[
rebind: min_sdk=3.0.0
rebind: name=Nav Layer
--]]
-- Hold CapsLock to turn the right hand into a navigation + editing cluster.
local layerKey = "CapsLock"
local layer = {
H = "Left",
J = "Down",
K = "Up",
L = "Right",
U = "PageUp",
D = "PageDown",
Y = "Home",
O = "End",
Backspace = "Delete",
}
function OnDown(key)
if key == layerKey then
return false -- swallow the layer key itself
end
if Input.IsDown(layerKey) and layer[key] then
HID.Down(layer[key])
HID.Up(layer[key])
return false
end
return true
end
Because the layer is gated on Input.IsDown(layerKey), every other key behaves
normally the instant you let go. To make it app-aware — active only in one program
— wrap the body in a System.Window() check, the same pattern the productivity
recipes use:
if Input.IsDown(layerKey) and layer[key] then
local win = System.Window()
if win.process:lower():find("code") then
HID.Down(layer[key])
HID.Up(layer[key])
return false
end
end
Opposing-key resolution (last pressed wins)
For movement keys mapped to opposite directions — left and right, or up and down — holding both at once is ambiguous: the two presses cancel and you stop. One way to resolve it deterministically: when two opposing directions are held at once, the most recently pressed one is active. Release the newer key and the older one, still held, takes over again.
Record the order in which each key of an opposing pair went down, then send only the winner and suppress its opposite.
--[[
rebind: min_sdk=3.0.0
rebind: name=Opposing Keys: Last Pressed Wins
--]]
-- Map a pair of opposing keys so the most recently pressed direction is the one
-- that registers while both are held. Here A/D drive Left/Right; swap the codes
-- for your own layout.
local pair = {
A = { send = "Left", opposite = "D" },
D = { send = "Right", opposite = "A" },
}
local pressedAt = {} -- key -> timestamp of its most recent press
local function activeOf(k1, k2)
-- both held: the one pressed most recently wins
local t1, t2 = pressedAt[k1], pressedAt[k2]
if t1 and t2 then
return (t1 >= t2) and k1 or k2
end
return t1 and k1 or (t2 and k2 or nil)
end
local function refresh(key)
local p = pair[key]
if not p then
return
end
local winner = activeOf(key, p.opposite)
-- ensure only the winning direction is currently sent
if winner == key then
HID.Up(pair[p.opposite].send)
HID.Down(p.send)
elseif winner == p.opposite then
HID.Up(p.send)
HID.Down(pair[winner].send)
else
HID.Up(p.send)
end
end
function OnDown(key)
if not pair[key] then
return true
end
pressedAt[key] = System.Time()
refresh(key)
return false -- we drive the output; swallow the raw key
end
function OnUp(key)
if not pair[key] then
return true
end
pressedAt[key] = nil
HID.Up(pair[key].send)
-- hand control back to the opposite key if it is still held
local other = pair[key].opposite
if Input.IsDown(other) then
refresh(other)
end
return false
end
function OnBlur()
pressedAt = {}
HID.Up("Left")
HID.Up("Right")
end
The behavior is timing-driven: pressedAt stores when each key last went down,
activeOf picks the larger timestamp, and refresh makes the live output match.
Releasing the newer key calls refresh on the survivor so the older direction
resumes with no extra tap. OnBlur releases both outputs if focus changes mid-hold,
so a direction can never get stuck on.
Where each recipe runs
| Capability | Namespaces | Platforms |
|---|---|---|
Bind.Remap, OnDown/OnUp, chords, layers | Bind, HID, Input | Windows, macOS, Linux |
App-aware layer (win.process check) | System.Window() | Windows, macOS; limited/absent on Linux |
See Platforms for the full capability matrix.
SDK Reference
Rebind scripts are Luau running on a Rust core. The runtime dispatches your hooks up to 8,000 times a second; output leaves as standard USB HID from a Teensy 4.x, or as OS-level input in software mode. This page is the complete reference for every namespace, global, hook, and modeline option.
The whole SDK runs free in software mode — no hardware required. To install or update, run the one-line installer in the quickstart.
A few namespaces depend on OS-specific APIs; see Platforms & limits for the authoritative per-namespace support matrix.
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
If the coroutine errors, the runtime logs it with a traceback (visible in the Logs tab), stops that coroutine (handle:IsRunning() then returns false), and marks the script’s error state — other coroutines and hooks keep running. Wrap fallible work in pcall to let a coroutine survive a recoverable error.
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)
)
require
require(module: string) -> any
Loads a sibling module file relative to the script’s own directory and returns
its value (a module is a file that returns something, usually a table). Modules
are cached — the file is evaluated once on first require, and later requires of
the same path return that same value.
Resolution for an extensionless name, in order: <name>.luau, <name>.lua,
<name>/init.luau, <name>/init.lua. Dots are path separators
(require("lib.math") → lib/math.luau); a leading ./ is stripped; an explicit
.lua / .luau suffix pins that exact file.
Sandboxed to the script directory: a name containing .., or any candidate that
resolves outside the directory, raises path traversal not allowed; an unresolved
name raises module '<name>' not found. There are no external packages,
registries, package.path, or aliases — local files only.
local helpers = require("helpers") -- helpers.luau next to the script
local m = require("lib.math") -- lib/math.luau
Only a directly-run script needs a
min_sdkmodeline; arequire’d module is loaded through that script, not run directly, so it needs no header. See the scripting guide.
_REBIND
_REBIND: {
name: string,
version: string,
min_sdk: string,
tick_rate: number,
runtime_uuid: string,
instance: string,
}
Read-only table injected by the host with metadata about the current script and runtime. Available immediately at load time.
Log.Info("Running " .. _REBIND.name .. " v" .. _REBIND.version)
Log.Info("SDK: " .. _REBIND.min_sdk .. ", tick rate: " .. _REBIND.tick_rate)
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(dtMs) -- dtMs = ms elapsed since the previous OnTick (0 on the first)
OnTick fires on a fixed cadence set by the tick_rate modeline (default 1,000/s,
max 8,000/s) while the script is active. Its argument dtMs is the real time
elapsed since this script’s previous OnTick, in milliseconds — fractional
(at 8,000/s it’s ~0.125), and 0 on the very first call. Use it for frame-rate-
independent work, e.g. pos += velocity * dtMs. Run() coroutines, Timer
callbacks, and network I/O are serviced on every engine tick regardless of tick_rate.
OnStart runs once per load, before any other hook. For a targeted script that
is already focused at load time, OnFocus fires immediately on the same load; an
unfocused targeted script gets OnStart only, and its OnFocus/OnTick/timers
wait until the target gains focus.
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, capturing output |
System.ExecDetached(cmd, args?, options?) | number | Launch a detached process, return its PID |
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 macOS/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.
Permission: System.Exec requires the exec permission. It is granted by default, but once the modeline declares any permission= line you must include permission=exec, or the call raises System.Exec() requires 'exec' permission at call time. (System.ExecDetached is not gated — see Permissions.)
System.ExecDetached
local pid = System.ExecDetached(cmd: string, args?: string[], options?: { cwd?: string })
Launches cmd as a detached, fire-and-forget process and returns immediately with its PID. Unlike System.Exec, it does not wait for the process or capture its output — stdio is discarded (null).
Returns: the spawned process PID as a number.
Options:
cwd– working directory for the process.
The console window is suppressed on Windows. The process keeps running after the script exits.
-- open a file in the default editor and keep going
local pid = System.ExecDetached("notepad.exe", { "notes.txt" })
Log.Info("launched editor, PID " .. pid)
System.Exec vs System.ExecDetached:
System.Exec– synchronous, blocks until the command finishes, capturesstdout/stderr/exit. Use when you need the output.System.ExecDetached– detached, returns instantly with a PID, no output captured. Use to launch apps or background processes.
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 fast cursor movement.
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 |
Input.IsDown / Input.GetDuration accept the same names as the rest of the SDK, including mouse buttons (Mouse1–Mouse5); an unknown or never-pressed code returns false / 0 rather than erroring.
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 |
UI.Color(default, opts?) | string | Color picker; value is a hex string (e.g. "#ff0000") |
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 |
A Slider with no min/max defaults to a 0–100 range. tooltip, group, tab, and showIf apply to every widget, including UI.Color.
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.GetSchema() | Full schema descriptor array (for advanced introspection) |
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: { ignore_mouse = false, ignore_keyboard = false, precision = "normal" } (keys are snake_case — camelCase is silently ignored)
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, mode?) | Resample the mouse path at a fixed interval — mode is "catmull" (default, smooth curves) or "linear". Movement-only: clicks, key presses, scroll, and sleeps are dropped from the result. |
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 |
Hash
Common cryptographic and checksum hashing. All inputs are treated as raw bytes.
| Function | Returns | Description |
|---|---|---|
Hash.MD5(data) | string | MD5 digest as a lowercase hex string |
Hash.SHA1(data) | string | SHA-1 digest as a lowercase hex string |
Hash.SHA256(data) | string | SHA-256 digest as a lowercase hex string |
Hash.SHA512(data) | string | SHA-512 digest as a lowercase hex string |
Hash.CRC32(data) | number | CRC32 checksum as a number (not hex) |
Hash.HMAC(algo, key, data) | string | Keyed HMAC digest as a lowercase hex string |
Hash.HMAC accepts algo of "sha256", "sha512", or "sha1". Any other value raises an error.
Log.Info(Hash.SHA256("abc")) --> "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
-- sign an API request
local sig = Hash.HMAC("sha256", apiSecret, payload)
Codec
Base64 and hex encoding/decoding. These are encodings, not encryption — they provide no confidentiality.
| Function | Returns | Description |
|---|---|---|
Codec.Base64(data) | string | Encode bytes to a Base64 string |
Codec.Base64Decode(str) | string | Decode a Base64 string to bytes |
Codec.Hex(data) | string | Encode bytes to a lowercase hex string |
Codec.HexDecode(str) | string | Decode a hex string to bytes |
local encoded = Codec.Base64("hello") --> "aGVsbG8="
local decoded = Codec.Base64Decode(encoded) --> "hello"
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.RmDir(path) | – | Remove a directory and all its contents (recursive) |
File.IsDir(path) | boolean | Whether the path is a directory. false on error. |
File.IsFile(path) | boolean | Whether the path is a file. false on error. |
File.GetSize(path) | number | File size in bytes |
File.GetTime(path) | number | Last-modified time (Unix seconds) |
File.Copy(src, dst) | – | Copy a file from src to dst |
File.Move(src, dst) | – | Move or rename a file from src to dst |
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 |
File.IsDir and File.IsFile never throw — they return false for missing or out-of-sandbox paths. File.Move uses a rename and may fail across filesystems.
Net
Client
Client calls yield — wrap them in
Run()/Async(). Every HTTP client method below blocks on a background thread and yields the coroutine, so calling one directly inOnDown, top-level code, or any synchronous hook raisesNet.X() must be called inside Run(). The snippets below assume they run inside aRun()block or anAsync()handler. (The WebSocket callsNet.WSListen/Net.WSConnectare exempt — their I/O runs on dedicated threads.)
| 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? }.
Request fields: req.method (e.g. "GET", "POST"), req.path (e.g. "/click"), req.body (string), req.headers (table).
Server handle: server:Stop()
Net.Listen binds 127.0.0.1 (localhost only) — it is not reachable from
other machines. For a LAN-facing endpoint use Net.WSListen, which binds 0.0.0.0.
Under burst load only the newest queued request per server is dispatched to your
handler each tick; older queued requests receive an automatic 200 OK, so keep
handlers fast and idempotent.
For a worked “HTTP request in, hardware out” example, see Automation in the cookbook.
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().
Binary frames arrive as Lua strings (read bytes with string.byte / string.unpack); the is_binary flag distinguishes them from text. The maximum message/frame size is 64 MiB per connection — a larger frame drops the connection. WebSocket handlers run on dedicated threads, so they are exempt from the Run() requirement the HTTP client methods have.
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.
For measured throughput and latency, see Remote control.
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,
})
Remote clients are available for TypeScript, Python, and Rust — drive a running script’s HTTP/WebSocket server from any program.
Screen
Pixel color sampling from the screen. Works on Windows (GDI BitBlt) and macOS (CoreGraphics / ScreenCaptureKit). Not yet available on Linux.
| Function | Returns | Description |
|---|---|---|
Screen.GetPixelColor(x?, y?) | string | Hex color "rrggbb" at pixel coordinates. If no args, reads at current mouse position. |
Screen.SearchForColor(region, color, tolerance?) | {x, y}? | Search a screen region for a pixel matching color. Returns {x, y} or nil. |
Screen.List() | {MonitorEntry} | List all connected monitors. Each entry has index, x, y, width, height, primary. |
Screen.Capture(opts?) | {image, width, height, display, cursor} | Capture a display (or a region of it) as a base64 PNG at native resolution, plus geometry and cursor for mapping image coordinates back to the screen. |
GetPixelColor and SearchForColor read from a non-blocking buffer when one is available; the first 1-2 calls may return nil while the first frame is captured. Screen.Capture always captures live.
On macOS, capture handles Retina displays automatically. GetPixelColor/SearchForColor coordinates are in logical (non-retina) pixels; Screen.Capture returns native (physical) pixels and reports the scale via the returned dimensions vs the display’s logical size.
Screen.SearchForColor
region—{x1, y1, x2, y2}table of screen coordinatescolor— hex string"rrggbb", case-insensitive (matchesGetPixelColoroutput)tolerance— optional, 0.0 to 1.0 (default 0.0 = exact match). Per-channel percentage of 255.- Returns
{x, y}of first match (top-left to bottom-right scan), ornilif not found
-- find a dark-red pixel in a region of the screen
local match = Screen.SearchForColor({ 900, 500, 1100, 530 }, "8b0000", 0.05)
if match then
Log.Info("found at " .. match.x .. ", " .. match.y)
end
-- sample a color, then search for it elsewhere
local color = Screen.GetPixelColor(960, 540)
local match = Screen.SearchForColor({ 0, 0, 1920, 1080 }, color, 0.1)
Screen.List
Returns one entry per connected monitor. Use #Screen.List() for the monitor count.
Each entry:
| Field | Type | Description |
|---|---|---|
index | number | Monitor index |
x | number | Top-left X in virtual desktop coordinates |
y | number | Top-left Y in virtual desktop coordinates |
width | number | Monitor width in pixels |
height | number | Monitor height in pixels |
primary | boolean | Whether this is the primary monitor |
for _, m in ipairs(Screen.List()) do
Log.Info(
"Monitor "
.. m.index
.. ": "
.. m.width
.. "x"
.. m.height
.. (m.primary and " (primary)" or "")
)
end
Screen.Capture
Captures a display live and returns a lossless PNG plus the geometry needed to
map image pixels back to screen coordinates. This is the primitive behind
screenshot-driven automation (see the remote-control protocol’s screen.capture).
opts (all optional):
display— 1-based index fromScreen.List(). Defaults to the display under the cursor.region—{x, y, w, h}in that display’s local coordinates. Defaults to the full display; clamped to the display’s bounds.
Returns:
| Field | Type | Description |
|---|---|---|
image | string | Base64 lossless PNG of the captured pixels at native resolution. |
width / height | number | Native pixel dimensions of the returned frame. |
display | table | {index, x, y, width, height, primary} — signed virtual-desktop origin, so monitors left of / above the primary are addressable. |
cursor | table | {x, y} in screen coordinates, read together with the frame. |
local cap = Screen.Capture() -- display under the cursor
local cap = Screen.Capture({ display = 2 }) -- a specific monitor
local cap = Screen.Capture({ region = { x = 0, y = 0, w = 320, h = 240 } })
Log.Info(
"captured "
.. cap.width
.. "x"
.. cap.height
.. " from display "
.. cap.display.index
)
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.
Platform support: Full on Windows (Win32 API). On macOS, window enumeration (Find, List, GetTitle of active window) works via CoreGraphics, and Kill, IsActive, WaitActive, and WaitClose are fully supported; the remaining manipulation functions (Move, Activate, Minimize, SetTitle, SetAlwaysOnTop, etc.) are best-effort — those that require Accessibility entitlements either work via the Accessibility API or raise a “not supported on macOS” error. Not yet available on Linux.
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.GetClass(handle?) | string | Get the window class name. |
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. |
Window.IsActive(handle?) | boolean | Check if the window is the active (foreground) window. |
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.Hide(handle?) | Hide the window. |
Window.Show(handle?) | Show a hidden window. |
Window.SetTitle(handle, title) | Set the window title. |
Window.SetAlwaysOnTop(handle, enabled) | Pin (true) or unpin (false) the window above others. |
Window.SetTransparency(handle, alpha) | Set window opacity. alpha is 0 (transparent) to 255 (opaque). |
Window.Close(handle?) | Graceful close (sends WM_CLOSE). |
Window.Kill(handle?) | Force-terminate the owning process. |
Window.Close asks the window to close gracefully (it may prompt to save). Window.Kill force-terminates the process that owns the window — there is no prompt and unsaved work is lost. Window.Kill can take down sibling windows in the same process.
Window.SetAlwaysOnTop and Window.SetTransparency are best-effort: some fullscreen/DirectX apps repaint over them.
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. |
Window.WaitActive(title, timeout?) | boolean | Wait for a matching window to become active. Must be called inside Run(). Timeout in ms (default 5000). Returns true if it activated, false on timeout. |
Window.WaitClose(title, timeout?) | boolean | Wait for a matching window to disappear. Must be called inside Run(). Timeout in ms (default 5000). Returns true if it closed, false on timeout. |
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
-- list all windows
for _, w in ipairs(Window.List()) do
Log.Info(w.title .. " [" .. w.process .. "]")
end
-- wait for an application to launch
Run(function()
local hw = Window.Wait("Untitled - Notepad", 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. Works on Windows and macOS. Not yet available on Linux.
| 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 a chat app
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.
Process
Query and manage system processes.
| Function | Returns | Description |
|---|---|---|
Process.Exists(name) | number? | Find first process matching name (case-insensitive substring). Returns PID or nil. |
Process.List(name?) | {ProcessEntry} | List processes. Optional name filter. Each entry has pid and name. |
Process.Kill(pid) | boolean | Force-kill a process by PID. Returns true if killed, or false (never errors) when no such PID exists or the OS denies the kill (e.g. an elevated or another-user process). |
-- check if an application is running
local pid = Process.Exists("notepad")
if pid then
Log.Info("Notepad is running (PID " .. pid .. ")")
end
-- list all chrome processes
for _, p in ipairs(Process.List("chrome")) do
Log.Info(p.name .. " [" .. p.pid .. "]")
end
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" (alias "warn"), or "error". Matching is case-insensitive, and any unrecognized value falls back to "info".
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
-- profile = "default"
local cfg = Config.ReadTOML("settings.toml")
Log.Info(cfg.general.sensitivity) --> 0.8
Log.Info(cfg.general.profile) --> "default"
-- 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.
Env
Environment variables and known user folders.
Variables
| Function | Returns | Description |
|---|---|---|
Env.Get(name) | string? | Read an environment variable. nil if unset. |
Env.Set(name, value) | – | Set an environment variable for this process and its children. |
Env.Set mutates the Rebind process-wide environment: the change is visible to Env.Get in every other running script and to any later System.Exec / System.ExecDetached, and persists until Rebind exits — it is not scoped to your script. Prefer File / Config for per-script state.
Known Folders
Each returns an absolute path, or nil if the location cannot be determined. Env.Temp is the exception — it always returns a string.
| Function | Returns | Description |
|---|---|---|
Env.Home() | string? | User home directory |
Env.Config() | string? | User config directory |
Env.Data() | string? | User data directory |
Env.Desktop() | string? | Desktop directory |
Env.Documents() | string? | Documents directory |
Env.Downloads() | string? | Downloads directory |
Env.AppData() | string? | Application data directory |
Env.Temp() | string | System temporary directory (always present) |
These paths point outside the File sandbox — use them for informational purposes (such as a Dialog.OpenFile start location), not for sandboxed File.* operations.
local downloads = Env.Downloads()
local path = Dialog.OpenFile({ location = downloads })
Pipe
Shared memory IPC for communicating with external processes (Python, Node.js, etc). Windows only. Returns an error on macOS and Linux (POSIX shared memory support planned).
Opening
local pipe = Pipe.Open(name, options?)
Creates or opens a shared memory region backed by the OS object
Local\Rebind_<name> — the tag an external process opens (so Pipe.Open("vision")
maps Local\Rebind_vision). The name must be 1–64 characters of ASCII letters,
digits, -, or _. The size option (default 65536) is rounded up to the
next power of two, then clamped to 1024–16 MiB, so pipe.size (and
pipe.capacity = size/2 − 12) may exceed what you requested.
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 |
Wire protocol
The region is split into two fixed channels so both sides can read and write
without locking: the script writes to channel A (offset 0) and reads from
channel B (offset size / 2). An external peer does the inverse — it writes to
channel B and reads from channel A.
Each channel is a 12-byte header followed by the payload:
| Bytes | Field |
|---|---|
0..8 | u64 sequence number, little-endian (bumped on every write) |
8..12 | u32 payload length, little-endian |
12.. | payload bytes (up to pipe.capacity = size/2 − 12) |
A writer lays down the payload, then the length, then the sequence number last
(a release barrier); a reader compares the sequence to the one it last saw and only
reads when it changed. Each channel is a single slot, last-writer-wins — not a
queue, so an unread message is overwritten by the next write. Pace writes to the
reader’s poll interval. Working Python, Rust, and Node peers are in
packages/lua-sdk/examples/sidecars/.
Registry
Read and write the Windows registry. Windows only. Every function raises an error on macOS and Linux.
| Function | Returns | Description |
|---|---|---|
Registry.Read(keyPath, valueName) | string or number | Read a value. Returns a string or number depending on the value type. |
Registry.Write(keyPath, type, valueName, value) | – | Write a typed value. |
Registry.DeleteValue(keyPath, valueName) | – | Delete a single value. |
Registry.DeleteKey(keyPath) | – | Delete a key and all its subkeys (recursive). |
Registry.CreateKey(keyPath) | – | Create a key. |
keyPath begins with a hive: HKLM / HKEY_LOCAL_MACHINE, HKCU, HKCR, HKU, or HKCC.
Registry.Write accepts a type of REG_SZ, REG_DWORD, REG_QWORD, REG_EXPAND_SZ, REG_MULTI_SZ, or REG_BINARY.
Registry.CreateKey([[HKCU\Software\MyScript]])
Registry.Write([[HKCU\Software\MyScript]], "REG_SZ", "Profile", "default")
local profile = Registry.Read([[HKCU\Software\MyScript]], "Profile")
Log.Info(profile) --> "default"
Registry.DeleteKey([[HKCU\Software\MyScript]])
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
A modeline at the top of a script sets its configuration. A directly-run script
must include a modeline that declares min_sdk — the relay refuses to run a
plaintext script whose modeline omits the Rebind version (Script can't run without a Rebind version in its modeline). Every other key is optional and falls
back to its default. (require’d modules and DRM/packaged scripts load by other
paths and are not gated, so this requirement applies only to scripts you run directly.)
Three comment forms are accepted. The multi-line block is the recommended
header — one rebind: line per setting reads cleanly as a script grows, and it is
the exact form the relay suggests on failure:
-- multi-line block (recommended) — one rebind: per line
--[[
rebind: min_sdk=3.0.0
rebind: name=My Script
rebind: process=chrome.exe
rebind: process=firefox.exe
--]]
-- single-line, best for one or two keys
-- rebind: min_sdk=3.0.0 name="My Script" tick_rate=8000
-- inline block
--[[ rebind: min_sdk=3.0.0 name="My Script" tick_rate=8000 --]]
Values may be unquoted (ending at the next space) or wrapped in " or ' to include spaces and =: name="My Script". A string-valued key alone on its own line (as in the block form) takes the rest of the line, so rebind: name=My Script needs no quotes. Repeatable keys (window, process, permission) take one value per entry. Unknown keys are ignored, so a newer SDK key never breaks an older script. The legacy -- ghostpeek: prefix is also accepted.
| 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 | required | Minimum Rebind version the script needs. Must be numeric (partials allowed: 3 means 3.0.0; a leading v and any pre-release/build suffix are ignored). The relay refuses to run a plaintext script that omits it or asks for a version newer than the installed build. |
window | string | — | Window-title match (repeatable, case-insensitive substring) |
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 sees input first) |
instance | string | "replace" | "replace", "single", or "multiple" |
mouse_mode | string | "relative" | "relative" or "absolute" |
mouse_block | boolean | false | When true, OnMove can block by returning false; when false, moves forward immediately and OnMove fires asynchronously. Requires a Rebind device — software mode cannot suppress mouse movement. (Note: defining OnMove at all requires a device, even without this key.) |
hardware_only | boolean | false | When true, the script refuses to load unless an authenticated Rebind device is present (This script requires a Rebind device). Accepts true/1/yes. |
permission | string | — | Restrict the script’s access (repeatable): exec, net. See Permissions below. |
The former id key is deprecated and ignored — UI state now persists automatically by file path.
Permissions
By default a script can access every namespace. The moment any permission=
line is present, the script switches to allow-list mode: only the listed
permissions are granted and everything else is denied — so permission=net alone
also denies exec, and vice-versa.
Only exec and net actually gate anything; any other permission= value grants
nothing real but still flips the script into allow-list mode. A denied call fails
as a runtime error when it runs (e.g. Net.Get() requires 'net' permission),
not as a load-time refusal — so a gated call inside a branch only fails when reached.
--[[
rebind: min_sdk=3.0.0
rebind: permission=exec
rebind: permission=net
--]]
| Permission | Gates |
|---|---|
exec | System.Exec only — System.ExecDetached is not gated and runs even under a restricted allow-list |
net | the Net namespace (HTTP client, Net.Listen, and WebSocket) |
Scripts published to the marketplace should declare the minimum permissions they need.
Platform Support
Most SDK namespaces work identically across all platforms. For the per-namespace support matrix, see Platforms & limits.
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 |
Remote Control
Drive physical input from any program. Rebind runs a small WebSocket server that speaks plain JSON: external code sends HID output, reads screen and input state, and subscribes to live event streams — in the language you already use, no Luau on the client side.
A web app, a Python process, a home-automation hub, or a local AI model connects to the running Rebind instance and controls a real keyboard and mouse — standard USB HID from a Teensy 4.x, or OS-level output in software mode.
Three ways to drive it
All three speak the same JSON protocol against the same server; pick whichever fits how you work.
- From the browser — open a web page that connects to the server and sends commands as you click. Good for dashboards, control panels, and a phone or tablet on the same network. See what this looks like in the browser demo.
- From your language — the official TypeScript, Python, and Rust clients wrap the protocol in typed, auto-reconnecting APIs. The recommended path for real programs. See Client libraries.
- From any language — the protocol is just JSON over WebSocket, so anything that can open a socket can drive Rebind directly. The Protocol is a single page.
Run the server
The server is one script — remote_access.lua — a reference
implementation you can read and extend locally, no SDK update needed.
- Download
remote_access.luaand copy it to your Rebind scripts directory (Windows:%APPDATA%\Rebind\scripts\). - Open Rebind → Scripts → start Remote Access.
- It binds a WebSocket server on
ws://0.0.0.0:19561(the port is a slider in the script’s settings panel). - Connect with the browser demo, a client library, or your own code.
Client libraries
Typed, auto-reconnecting wrappers over the remote-control protocol in the language you already use.
Available clients
The official clients are thin, typed wrappers over the protocol with reconnection handled for you.
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 the browser. 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)
# stream live mouse position
for pos in r.mouse_events():
print(pos.x, pos.y)
if pos.x > 500:
break
Blocking and asyncio APIs. Pure Python 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?;
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
Speaking the protocol directly? See Protocol.
Protocol
Message format
Every message is one JSON object. A request names a command in t; include an
id when you want a correlated reply (reads), omit it for fire-and-forget (HID
writes).
// request → fire-and-forget (no id)
{ "t": "hid.move", "dx": 30, "dy": -5 }
// request → with id, expects a reply
{ "t": "screen.pixel", "id": 7, "x": 100, "y": 50 }
// response →
{ "id": 7, "r": 139, "g": 0, "b": 0 }
// subscribe → the server then pushes frames as state changes
{ "t": "subscribe", "events": ["mouse", "input"] }
{ "t": "mouse", "x": 812, "y": 344 }
On connect, the server sends a hello frame with the protocol version and whether
auth is required, so a client can verify compatibility with no round-trip.
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, screen.capture, screen.displays |
| 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 (streams: mouse, window, input) |
| Meta | hello, ping, auth, lua.exec |
system.window reads the active (foreground) window; the window.* commands
list, find, activate, and move windows.
screen.capture (protocol 1.1.0+) returns a display as a base64 PNG at native
resolution plus its geometry and the cursor position, and screen.displays
enumerates monitors with signed virtual-desktop origins — together the basis for
screenshot-driven, computer-use-style automation. The hello frame advertises the
server’s protocol version so a client can check for these before using them.
Screen and Window commands depend on platform support — both are limited or
absent on Linux; Clipboard is available on Windows and macOS. See
Platforms & limits.
Authentication
Auth is off by default (fine on localhost). To require a token, set AUTH_TOKEN
at the top of remote_access.lua before installing, then have the client send
{ "t": "auth", "token": "..." } first. The lua.exec escape hatch (arbitrary
Lua) is disabled unless you set ALLOW_LUA_EXEC = true — leave it off unless you
trust every 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 |
| Server-side message drain | ~12,000 msg/s |
| Fire-and-forget wire throughput | ~100,000 msg/s |
Round-trips are sub-millisecond over localhost and a wired LAN — the transport won’t be your bottleneck.
Browser Demo
Drive Rebind from a web page. This panel talks straight to the WebSocket server
that remote_access.lua opens — no client library, no build step, no install.
It’s the browser equivalent of the TypeScript, Python, and Rust
remote clients: send JSON over a socket, get hardware input out.
The wire protocol is identical in either mode.
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 to the Rebind WebSocket server via the browser’s native
WebSocket API — no libraries, no build step. The trackpad sends hid.move
deltas as you drag; the live Position readout updates from the mouse
subscription; every other button sends its command and logs the result.
For the message shapes — fire-and-forget writes, id-correlated reads, and event subscriptions — see the protocol. For localhost throughput, see the transport numbers.
View the page source for the full implementation.
Support
Questions, bug reports, or help getting set up — reach the team and community on Discord.