Style
Rebind scripts are Luau. Follow these conventions — every snippet below does.- Indent with 2 spaces. Never tabs, never 4.
- Naming:
- Runtime hooks and SDK calls are
PascalCase— dictated, not a choice (OnDown,HID.Press,Input.IsDown). - Your locals and local functions are
snake_case(dwell_start,local function set_virt). - Module-level constants are
UPPER_SNAKE(local OPPOSITE = {...}). - The UI config table is conventionally named
cfg.
- Runtime hooks and SDK calls are
localeverything. The only globals are the runtime hooks (OnStart,OnDown, …); declare everything elselocal, including functions.- Double-quoted strings. Use
[[ ]]long strings for regex patterns and multi-line text. - Stay flat — early return. Guard and
returnrather than nesting; usea and b or cfor a ternary. - Types are optional but welcome. Luau is typed; annotate where it clarifies
(
local count: number = 0), don’t annotate the obvious. - Comments: explain why, not what, and stay sparse. Write them in
lowercase. Use
--for a single line; for anything longer, use one--[[ ]]block instead of stacking several--lines. - Keep lines to roughly 80 characters. Wrap long calls and tables across lines rather than running well past it.
- Space after commas and around operators; one statement per line.
Handling input: Bind vs hooks
There are two ways to react to input, and they use opposite blocking defaults. Pick by the verb:Bind— you’re claiming a key. Blocks by default (the original key is swallowed). Intercepts only its declared trigger. Use for remaps and per-key actions.On*hooks — you’re watching the input stream. Pass through by default;return falseto swallow. DefiningOnDown/OnUpmakes Rebind intercept every key. Use when you need to read or transform arbitrary input.
Bind; reading/transforming the whole stream →
hooks. A hook that never blocks anything can add key_block=false to its
modeline to observe input without intercepting it.
Bind patterns
return true/return false inside an Async(...) body passed to
Bind — the return value has no effect on propagation there.
Hook patterns
Mouse movement (requires a Rebind device)
Any script that definesOnMove or sets mouse_block=true requires a Rebind
device (Rebind Link) — the relay refuses to load it in software mode, because
the OS draws the cursor upstream of anything a host process could intercept.
OnMove minimal — it fires on every movement.
Output needs a coroutine
HID.Press, HID.Type, and HID.Combo call Sleep() internally, so they must
run inside a coroutine context (Run, Async; OnTick is not one). Inside a
Bind action, wrap with Async. HID.Down/HID.Up are non-blocking and work
anywhere (hooks included).
Modifiers
Canonical namesLCtrl/LShift/LAlt/LWin; shorthands Ctrl/Shift/Alt/
Win/Meta/Cmd/Opt also work in combos. Read live state with
Input.GetModifiers() → { ctrl, shift, alt, win } or Input.IsDown("LShift").
Software-mode gotcha: a physical modifier the user is holding is merged
into your synthetic output (so a swallowed key re-emitted while Shift is down
still carries Shift). That’s correct for remaps, but means a macro that types
text while a modifier is held will inherit it. For output that must be clean
of physical modifiers, prefer hardware mode (the device isolates the
stream) or gate on Input.GetModifiers() first.
Timing and async
Lifecycle and cleanup
Always release what you hold. Cancel tasks, stop audio, andHID.Up any keys
you pressed with HID.Down — in both OnStop (script stopped) and OnBlur
(target window lost focus).
Targeting a specific app
Restrict a script to one process/window via the modeline. When targeted, always addOnBlur cleanup so held keys release when focus leaves.
Config and UI
Multi-file packages
When a script outgrows one file, make it a package: a folder with arebind.toml. The [package].entry (default main.luau) is what runs; other
files are libraries you require. Only ever run/start the entry, never a
library.
require paths use / or . separators and resolve <mod>.luau, <mod>.lua,
then <mod>/init.luau. A one-shot helper can exit() (alias for
Script.Exit()) when done.
Modeline reference
The-- rebind: comment at the top of a standalone script is its manifest. In a
package, rebind.toml owns these and overrides the modeline.