> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rebind.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> Run a function when a script loads, unloads, ticks, or sees a key, mouse or controller input.

`rb.on` runs a function when something happens to the script or to the input
it sees: the script loading or unloading, its window gaining or losing focus, a
tick, a key, a mouse movement or a controller stick. A script reaches for it to set up
state at start, clean up at stop, or react to input as it arrives.

This script counts key and button presses without blocking any of them and prints the
total when it unloads:

```luau theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
-- rebind: min_sdk=3.9.0 name=example version=1.0.0
local presses = 0

rb.on("start", function()
  print("counting presses")
end)

rb.on("input_down", function(key, device)
  presses += 1
end)

rb.on("stop", function()
  print("presses:", presses)
end)
```

`rb.on(event: Event, fn: (...any) -> boolean?) -> EventHandle`

Adds a handler for an event and returns an [EventHandle](https://docs.rebind.gg/reference/sdk/handles.md) whose `stop`
removes it. A script may add several handlers for one event; they run in the
order they were added. A handler that raises stops the handlers after it for
that event.

Errors: an unknown name raises `rb.on: unknown event '<name>'. Valid events:`
followed by the list; a handler that is not a function raises
`rb.on: the handler must be a function`.

## Lifecycle

| Event | Handler arguments | Fires when |
| - | - | - |
| `"start"` | none | the script has loaded |
| `"stop"` | none | the script is about to unload |
| `"focus"` | `window` | a targeted script's window gained focus |
| `"blur"` | `window` | a targeted script's window lost focus; `window` is the window that now has focus |
| `"tick"` | `dt_ms` | the script's tick is due |

`window` is `{ title, process, x, y, width, height }`.

Top-level code runs first, when the file loads; `start` follows. A script with no
[targeting](https://docs.rebind.gg/reference/sdk/modeline.md) is active from then on. A targeted script whose target is
focused at load gets `focus` right after `start`; otherwise it waits inactive.
While a script is inactive it gets no `tick`, and its timers and coroutines wait.
Keys and buttons a waiting coroutine holds through `rb.hid` stay held in
whatever window has focus. Stop the task and release them in a `blur` handler:

```luau theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
-- rebind: min_sdk=3.9.0 name=example version=1.0.0 permissions=none process=notepad.exe
local task
rb.bind("F9", function()
  task = rb.run(function()
    rb.hid.down("W")
    rb.sleep(2000)
    rb.hid.up("W")
  end)
end)

rb.on("blur", function()
  if task then
    task:stop()
  end
  rb.hid.up("W")
end)
```

The engine ticks 8,000 times a second. `tick` runs every `8000 / tick_rate`
engine ticks (integer division), so the default `tick_rate=1000` runs it every
8th tick. `dt_ms` is the real time since this script's previous `tick`, in
fractional milliseconds, and `0` on the first. Timers and coroutines are
serviced on every engine tick; HTTP, WebSocket and UDP servers every 8th.

An error raised in `start`, `focus`, `blur` or `tick` stops the script with that
error. An error in `stop` is logged.

This handler counts ticks and removes itself after the 1,000th:

```luau theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
-- rebind: min_sdk=3.9.0 name=example version=1.0.0
local ticks = 0
local handle
handle = rb.on("tick", function(dt_ms)
  ticks += 1
  if ticks == 1000 then
    handle:stop()
  end
end)
```

## Input events

| Event | Handler arguments | Fires when |
| - | - | - |
| `"down"` | `key, device` | a key or mouse button was pressed |
| `"up"` | `key, duration_ms, device` | a key or mouse button was released |
| `"move"` | `dx, dy, device` | the mouse moved |
| `"scroll"` | `delta, device` | the wheel scrolled vertically |
| `"axis"` | `axis, value, device` | a controller stick or trigger moved |

`key` is a name from the [Key reference](https://docs.rebind.gg/reference/sdk/keys.md); `device` is described
under [Devices](#devices). Controller buttons arrive as `down` and `up` with
names such as `"PadA"`; see [Controllers](https://docs.rebind.gg/reference/sdk/keys.md#controllers).

A handler returns `false` to block the event. Any other return, or none, lets it
through. Active scripts see an event in [`z_index`](https://docs.rebind.gg/reference/sdk/modeline) order, highest
first. Within one script, its [binds](https://docs.rebind.gg/reference/sdk/bind.md) are tried first, then its handlers.
The first `false`, or a bind that takes the key, ends the event: lower scripts do
not see it. An error in an input handler is logged, and the event continues as
if that script had not blocked it.

**Exclusive capture.** A `down` or `up` handler can block any key, so while a
script with one is active, Rebind intercepts every key and re-emits the ones that
pass. Use
[`rb.bind`](https://docs.rebind.gg/reference/sdk/bind) to take only the keys you name, or declare
[`key_block=false`](https://docs.rebind.gg/reference/sdk/modeline) to observe keys without intercepting them.

**Mouse movement.** A `move` handler and `mouse_block=true` need hardware mode.
In software mode Rebind refuses to start a script with either, and an autostart
script waits for hardware mode; an `rb.on("move", ...)` that runs in software
mode raises `rb.on: a move handler needs hardware mode`. Without `mouse_block=true`, movement is
forwarded at once and `move` handlers run when the engine is free; their return
value is ignored.

**Controller axes.** An `axis` handler runs once for each stick or trigger that
changed, at most once per controller report. `axis` is `"LeftX"`, `"LeftY"`,
`"RightX"`, `"RightY"` (-1 to 1, up and left negative) or `"LT"`, `"RT"` (0 to
1\). Returning `false` keeps that value off Rebind Link's joypad; the other axes
of the same report still pass. A value a handler swallows while the
controller had already moved that axis returns the axis to rest on the joypad.
Controllers are captured only in hardware mode. An `axis` handler needs
`min_sdk=3.11.0`.

## Observer events

Every key, button, move and scroll is also delivered as an observer event to
every active script, before the blocking handlers run. `input_move` follows the
same `mouse_block` rule as `move`. Observers cannot block: their return value is
ignored. A script that uses only observers takes no exclusive capture.

| Event | Handler arguments |
| - | - |
| `"input_down"` | `key, device` |
| `"input_up"` | `key, duration_ms, device` |
| `"input_move"` | `dx, dy, device` |
| `"input_scroll"` | `delta, device` |

Controller axes have no observer event. A script that watches sticks uses `axis`
and returns nothing; a higher `z_index` script whose `axis` handler returns
`false` hides that value from it.

## Devices

Input handlers, observers, and `rb.bind` callbacks receive the device the input
came from:

| Field | Type | Meaning |
| - | - | - |
| `id` | string | stable on this computer across replugs on the same port and restarts; differs between computers |
| `name` | string | the device's product name |
| `nickname` | string? | the nickname set in **Peripherals**, or `nil` |
| `kind` | string | `"keyboard"`, `"mouse"`, `"gamepad"`, `"joystick"` or `"other"` |
| `vid`, `pid` | string | USB vendor and product id |
| `source` | string | `"vm"`: captured in hardware mode |
| `exact` | boolean | `false` while identical devices without serial numbers may share this id |

`device` is `nil` in software mode, where the source cannot tell devices
apart.

[`rb.input.devices()`](https://docs.rebind.gg/reference/sdk/input) lists the devices Rebind has identified, one
entry per `id` (see `exact`), with these fields; it needs `min_sdk=3.12.0`. A device that is both a
keyboard and a mouse lists as its keyboard. A
device attached a moment ago may not be listed yet, so call it when you need the
answer rather than once at start. From a terminal:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
rebind exec 'for _, d in rb.input.devices() do print(d.id, d.nickname, d.name) end'
```

## Not dispatched

`rb.on` accepts `"scroll_h"` (`delta`), `"reload"` (no arguments) and `"error"`
(`message`), but Rebind never dispatches them. A handler for one
raises no error and never runs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.