Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.)

RebindAutoHotkeyKarabinerVendor softwareStream Deck
PlatformsWindows, macOS, LinuxWindowsmacOSWindows (some macOS)Windows, macOS
ScriptingLuau runtimeAHK languageJSON rulesLimited (G Hub: Lua)Action presets
Works with any keyboard & mouseAny USB deviceAny deviceKeyboard (mouse limited)That brand onlyAdds a separate keypad
Hardware-isolated outputYes (Teensy)No (software)No (software)That brand only
HTTP + WebSocket (client & server)YesNoNoNoNo
CoroutinesYesLimitedNoNoNo
Pixel samplingYes (Win/macOS)YesNoNoNo
Window controlYesYesApp conditionsNoNo
Macro record / playYesYesNoBasicAction presets
Shared-memory IPCYes (Windows)NoNoNoNo
Remote SDKs (TS / Python / Rust)YesNoNoNoNo
Declarative config UIYesNoGUI + JSONAppYes
Per-app activationYes (modeline)YesYesProfilesProfiles

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

AutoHotkeyRebind
Hotkey — F1::Bind("F1", fn) or the OnDown(key) hook
Remap — CapsLock::EscapeBind.Remap("CapsLock", "Escape")
Send / SendInputHID.Type, HID.Press, HID.Down / HID.Up
Hotstring — ::btw::by the waya Bind that calls HID.Type (or a key-sequence watcher)
#HotIf WinActive(...)the window= / process= modeline
Sleep, LoopRun(function() … Sleep(ms) … end) coroutines
PixelGetColorScreen.GetPixelColor
WinActivate, WinGetTitleWindow.Activate, Window.GetTitle
A_ClipboardClipboard.Get / Clipboard.Set
Persistent scriptscripts 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.Type and HID.Press sleep between keystrokes, and Net.* requests block, so they must run inside a Run() / Async() coroutine — not directly in a plain Bind/OnDown callback.
  • 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 software SendInput. 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/Sleep instead of SetTimer/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.

  1. Open the Rebind UI and go to the Devices tab.
  2. Click Auto-Detect to find your keyboard and mouse.
  3. 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 / .luau files on disk, in your Rebind scripts folder:

  • Windows%APPDATA%\Rebind\scripts
  • macOS~/Library/Application Support/Rebind/scripts
  • Linux~/.local/share/Rebind/scripts

Subfolders 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, one rebind: line per setting. (A terse single-line -- rebind: key=value form works too, but the block reads cleaner as scripts grow.)
  • min_sdk is 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 use 3.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 the tick_rate=8000 modeline. dtMs is the real time elapsed since the previous OnTick (milliseconds, fractional; 0 on 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 as return 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

Bind is the declarative shortcut for simple remaps and uses the opposite default — it blocks unless you return 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.Up hold and release a key across time.
  • HID.Press taps a key (optional hold in ms); HID.Type types 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.Press and HID.Type use an internal Sleep, so they must run inside a Run() coroutine or an Async() handler. HID.Down/HID.Up are 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 the Run/Sleep boilerplate.
  • Async(fn) — wrap a callback so it runs in a coroutine; use it for Bind callbacks that need Sleep.
  • 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. Sleep resumes on the next engine tick past its deadline, so its timing is tick-granular rather than exact milliseconds, and a Run() body that never calls Sleep runs straight through on a single tick — an infinite loop with no Sleep stalls the script (input freezes) until the runtime’s per-tick budget (~200 ms) cuts it off. Always put a Sleep inside 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:

  • Input reports currently held keys, modifiers, and how long a key has been down — useful inside OnTick or a Run() loop.
  • System gives 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 an init.luau loads as a package).
  • Dots are path separators: require("lib.math") loads lib/math.luau.
  • An explicit .lua / .luau suffix pins that exact file; a leading ./ is ignored.
  • Modules are cached: the file is evaluated once on the first require, and every later require of 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_sdk modeline. A require’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.Post in a high-frequency OnTick — synchronous HTTP blocks the main thread; move it inside Run(). 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.

NamespaceWindowsmacOSLinux
ScreenFull (GDI)Full (CoreGraphics)Stub (Screen.List via display-info)
WindowFull (Win32)Enumeration + Kill/IsActive/WaitActive/WaitClose; rest best-effortStub
ClipboardFull (Win32)Full (arboard)Stub
PipeFull (shared memory)Not availableNot available
RegistryFull (Win32)Not availableNot available
DialogFull (native)Full (native)Requires Zenity/KDialog/YAD
System.Execcmd.exe /Csh -csh -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.GetPixelColor is still callable but returns a fixed "000000" (black) instead of a real pixel, and Screen.SearchForColor returns nil — neither raises an error. Only Screen.List (display enumeration) is real on Linux.
  • Window is full on Windows. On macOS, enumeration (Find, List) plus Kill, IsActive, WaitActive, and WaitClose are real. The manipulation calls split two ways: Move, Activate, Minimize, Maximize, Restore, and Close are silent no-ops — they return success and do nothing, so a pcall around them will not catch an error — while the Accessibility-gated calls SetTitle, SetAlwaysOnTop, SetTransparency, GetClass, Hide, and Show raise a “not supported on macOS without accessibility” error you can guard with pcall. Window.GetTitle returns an empty string on macOS; titles are only available via the title field on Window.List / Window.Find entries. 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. CapsLock is 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) — return true to let a key pass through, false to swallow it. Send your replacement with HID.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

CapabilityNamespacesPlatforms
Bind.Remap, OnDown/OnUp, chords, layersBind, HID, InputWindows, 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 coroutine
  • handle:IsRunning() – returns boolean

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_sdk modeline; a require’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.

FunctionReturnsDescription
System.Time()numberCurrent timestamp in milliseconds
System.Mouse()x, yCursor position in pixels (multiple return values)
System.Screen()width, heightPrimary display dimensions in pixels (multiple return values)
System.Window()tableActive window info: { title, process, x, y, width, height }
System.Exec(cmd, options?)tableRun a shell command synchronously, capturing output
System.ExecDetached(cmd, args?, options?)numberLaunch 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, captures stdout/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

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

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

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

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

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

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

Combos

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

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

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

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

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

Mouse

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

Mouse Mode

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

Relative mode (default): single-pass movement, fast (~0.5ms), may drift. Best for 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.

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

Input.IsDown / Input.GetDuration accept the same names as the rest of the SDK, including mouse buttons (Mouse1Mouse5); 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

ConstructorDefault TypeDescription
UI.Toggle(default, opts?)booleanOn/off switch
UI.Slider(default, opts?)numberNumeric slider
UI.Keybind(default, opts?)stringKey binding selector
UI.Select(default, choices, opts?)stringDropdown selection
UI.Text(default, opts?)stringText input field
UI.Color(default, opts?)stringColor picker; value is a hex string (e.g. "#ff0000")

Widget Options

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

A Slider with no min/max defaults to a 0100 range. tooltip, group, tab, and showIf apply to every widget, including UI.Color.

Other

FunctionDescription
UI.Get(id)Read value by string key
UI.Set(id, value)Write value by string key
UI.GetAll()All current values as a table
UI.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

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

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

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

Handle methods:

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

Hardware Streaming

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

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

Recording

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

Record options: { ignore_mouse = false, ignore_keyboard = false, precision = "normal" } (keys are snake_case — camelCase is silently ignored)

Format

Actions in a macro table:

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

Timer

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

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


Math

Random

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

Transforms

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

FunctionDescription
Math.Scale(macro, xFactor, yFactor)Multiply movement values
Math.Spline(macro, tension)Catmull-Rom curve smoothing
Math.Resample(macro, intervalMs)Normalize timing to fixed intervals
Math.Interpolate(macro, intervalMs, 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:

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

JSON

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

Hash

Common cryptographic and checksum hashing. All inputs are treated as raw bytes.

FunctionReturnsDescription
Hash.MD5(data)stringMD5 digest as a lowercase hex string
Hash.SHA1(data)stringSHA-1 digest as a lowercase hex string
Hash.SHA256(data)stringSHA-256 digest as a lowercase hex string
Hash.SHA512(data)stringSHA-512 digest as a lowercase hex string
Hash.CRC32(data)numberCRC32 checksum as a number (not hex)
Hash.HMAC(algo, key, data)stringKeyed 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.

FunctionReturnsDescription
Codec.Base64(data)stringEncode bytes to a Base64 string
Codec.Base64Decode(str)stringDecode a Base64 string to bytes
Codec.Hex(data)stringEncode bytes to a lowercase hex string
Codec.HexDecode(str)stringDecode a hex string to bytes
local encoded = Codec.Base64("hello") --> "aGVsbG8="
local decoded = Codec.Base64Decode(encoded) --> "hello"

Log

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

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

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

Logs appear in the Rebind UI Logs tab.


File

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

FunctionReturnsDescription
File.Read(path)stringRead file contents
File.Write(path, content)Overwrite file
File.Append(path, content)Append to file
File.Exists(path)booleanCheck if file exists
File.Delete(path)booleanDelete file
File.List(path)string[]List directory contents
File.MkDir(path)booleanCreate directory
File.RmDir(path)Remove a directory and all its contents (recursive)
File.IsDir(path)booleanWhether the path is a directory. false on error.
File.IsFile(path)booleanWhether the path is a file. false on error.
File.GetSize(path)numberFile size in bytes
File.GetTime(path)numberLast-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)tableRead and parse JSON file
File.WriteJSON(path, table)Write table as JSON
File.GetScriptDir()stringAbsolute 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 in OnDown, top-level code, or any synchronous hook raises Net.X() must be called inside Run(). The snippets below assume they run inside a Run() block or an Async() handler. (The WebSocket calls Net.WSListen / Net.WSConnect are exempt — their I/O runs on dedicated threads.)

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

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

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

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

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

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

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

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

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

HTTP Server

local server = Net.Listen(port, handler)

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

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.

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

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

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

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.

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

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

Example:

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

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.

FunctionReturnsDescription
Screen.GetPixelColor(x?, y?)stringHex 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 coordinates
  • color — hex string "rrggbb", case-insensitive (matches GetPixelColor output)
  • 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), or nil if 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:

FieldTypeDescription
indexnumberMonitor index
xnumberTop-left X in virtual desktop coordinates
ynumberTop-left Y in virtual desktop coordinates
widthnumberMonitor width in pixels
heightnumberMonitor height in pixels
primarybooleanWhether 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 from Screen.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:

FieldTypeDescription
imagestringBase64 lossless PNG of the captured pixels at native resolution.
width / heightnumberNative pixel dimensions of the returned frame.
displaytable{index, x, y, width, height, primary} — signed virtual-desktop origin, so monitors left of / above the primary are addressable.
cursortable{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

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

Activation

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

Movement / Sizing

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

State Control

FunctionDescription
Window.Minimize(handle?)Minimize to taskbar.
Window.Maximize(handle?)Maximize to full screen.
Window.Restore(handle?)Restore from minimized or maximized.
Window.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

FunctionReturnsDescription
Window.Wait(title, timeout?)number?Wait for a window to appear. Must be called inside Run(). Timeout in ms (default 5000). Returns handle or nil.
Window.WaitActive(title, timeout?)booleanWait 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?)booleanWait 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

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

Audio.Play

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

Options:

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

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

SoundHandle

The object returned by Audio.Play:

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

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


Clipboard

Read and write the system clipboard. Works on Windows and macOS. Not yet available on Linux.

FunctionReturnsDescription
Clipboard.Get()string?Read current clipboard text. Returns nil if empty or non-text.
Clipboard.Set(text)Set clipboard text.
-- paste a multi-line message into 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.

FunctionReturnsDescription
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)booleanForce-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

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

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

level controls the icon: "info" (default), "warning" (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

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

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

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

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

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

Regex

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

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

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

Match result

Regex.Find and Regex.FindAll return tables with:

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

Replacement syntax

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

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

Examples

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

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

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

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


Config

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

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

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

-- settings.toml:
-- [general]
-- sensitivity = 0.8
-- enabled = true
-- 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

FunctionReturnsDescription
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.

FunctionReturnsDescription
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()stringSystem 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

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

Properties

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

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:

BytesField
0..8u64 sequence number, little-endian (bumped on every write)
8..12u32 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.

FunctionReturnsDescription
Registry.Read(keyPath, valueName)string or numberRead 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

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

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

die("shutting down for maintenance")

Bind

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

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

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

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

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

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

Blocking

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

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

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

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

Handle

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

Routing

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


Modeline

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.

PropertyTypeDefaultDescription
namestringfilenameDisplay name
versionstring"0.0.0"Script version
authorstringCreator attribution
descriptionstringBrief explanation
min_sdkstringrequiredMinimum 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.
windowstringWindow-title match (repeatable, case-insensitive substring)
processstringProcess-name match (repeatable, case-insensitive)
tick_ratenumber1000OnTick frequency in Hz (max 8000)
z_indexinteger1Input priority (higher sees input first)
instancestring"replace""replace", "single", or "multiple"
mouse_modestring"relative""relative" or "absolute"
mouse_blockbooleanfalseWhen 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_onlybooleanfalseWhen true, the script refuses to load unless an authenticated Rebind device is present (This script requires a Rebind device). Accepts true/1/yes.
permissionstringRestrict 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
--]]
PermissionGates
execSystem.Exec only — System.ExecDetached is not gated and runs even under a restricted allow-list
netthe 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

ButtonName
Left clickMouse1
Right clickMouse2
Middle clickMouse3
BackMouse4
ForwardMouse5

Modifiers

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

Editing and Control

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

System

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

Punctuation

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

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

Numpad

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

Media Keys (HID Output Only)

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

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

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.

  1. 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.
  2. 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.
  3. 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.

  1. Download remote_access.lua and copy it to your Rebind scripts directory (Windows: %APPDATA%\Rebind\scripts\).
  2. Open Rebind → Scripts → start Remote Access.
  3. It binds a WebSocket server on ws://0.0.0.0:19561 (the port is a slider in the script’s settings panel).
  4. 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.

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.

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.

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

CategoryCommands
HID writeshid.down, hid.up, hid.press, hid.type, hid.move, hid.move_to, hid.scroll
Screenscreen.pixel, screen.resolution, screen.capture, screen.displays
Systemsystem.mouse, system.window, system.time
Inputinput.keys, input.is_down, input.modifiers
Clipboardclipboard.get, clipboard.set
Windowwindow.list, window.find, window.activate, window.move
Eventssubscribe, unsubscribe (streams: mouse, window, input)
Metahello, 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):

MetricValue
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

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

Demo

disconnected

Mouse

drag to move
--

Keyboard

Clipboard

System

How it works

This page connects 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.

discord.gg/rebind