<!-- Source: https://medius.k4tech.net/library/clip -->
# Clip

_Preload input and let the box play it back_

Build a sequence of per-frame input with a [`ClipBuilder`](/library/clip.md#builder), hand it to the [`ClipHandle`](/library/clip.md#handle) from [`Device::clip()`](/library/clip.md#clip), and the box drains one entry per native frame into the same injection state your live [`move`](/library/move.md) and [`inject`](/library/inject.md) calls feed. Playback is box-clocked, so it carries no host scheduling jitter. It's field-generic (mouse, keyboard, and media in one clip) and backs the [`CLIP`](/native/commands/clip.md) commands.

```
  1. build a clip with ClipBuilder
       clip.move_by(10, 0)
       clip.press(Button::Left)
       clip.gap(20)
       clip.release(...)

  2. drive playback through a ClipHandle
       handle = device.clip();
       handle.append(&clip)     ──▶  copy the entries into the box's ring
       handle.start()           ──▶  box plays one entry per native frame
       handle.query_status()    ◀──  ring depth + progress + playback state
       handle.stop()            ──▶  stop (retained: rewind; streaming: flush)

  or let the box play it on a physical key, no host round-trip:
       handle.bind(ClipTrigger::new(Key::F1, Edge::Press, ClipAction::Start))
```

## clip

_Open a clip handle_

```text
fn clip(&self) -> ClipHandle
```

Returns a [`ClipHandle`](/library/clip.md#handle) bound to this box. The handle owns the append-sequence counter the box uses to spot a dropped append, so keep one handle for a clip session (top it up through that handle) rather than reopening one per append.

> **Note**
>
> Playback lives in the box's RAM: a [reboot](/library/admin.md#reboot) or [reconnect](/library/lifecycle.md#reconnect) drops the loaded clip, so re-preload after one. Its config (auto-lock, loop, retain, and the trigger set) goes too: unlike a held lock or catch subscription, a clip isn't re-asserted for you, so re-set it after a reconnect. A clip needs a cloned mouse, since its frame clock is the mouse's report tick; keyboard and media edges ride that tick.

## ClipBuilder

_Build the entry stream_

```text
fn new() -> ClipBuilder
```

Each method appends one per-frame entry, so a builder is a timeline read top to bottom. Motion is a relative delta; an edge (button, key, or media) is an [`Action`](/library/types/enums.md#action) that stays held until a later frame changes it; a `gap` is N idle frames (the box NAKs, like an idle mouse). The methods take `&mut self` and return `&mut Self`, so chain them or push in a loop; `clear()` reuses the allocation.

#### METHODS

| Method | Appends |
| --- | --- |
| `gap(frames)` | N idle frames (0 is a no-op). |
| `move_by(dx, dy)` | a cursor-motion frame. |
| `wheel(dz)` | a wheel frame. |
| `press / release / force_release(usage)` | a one-frame press, soft-release, or force-release of any [`Usage`](/library/types/enums.md#usage) (button, key, or media), like [`Device::press`](/library/inject.md#inject). |
| `edge(usage, action)` | a one-edge frame for any [`Usage`](/library/types/enums.md#usage) with an explicit [`Action`](/library/types/enums.md#action). |
| `frame(dx, dy, wheel, edges)` | a motion delta plus up to 8 [`Usage`](/library/types/enums.md#usage) / [`Action`](/library/types/enums.md#action) edges on one frame. |

`press`/`release`/`force_release` are wrappers over `edge`, which is a wrapper over `frame`. Reach for `frame` only when you need motion and edges (or several edges) on the same frame, e.g. moving while a button is held, which is how a faithful recording of "aim and hold fire" looks.

#### EXAMPLE

```rust
use medius::{ClipBuilder, Button, Key};

let mut clip = ClipBuilder::new();
for _ in 0..200 { clip.move_by(10, 0); }   // 200 box-timed frames of +10 dx
clip.press(Button::Left)                   // a click, held for 20 frames
    .gap(20)
    .release(Button::Left);
clip.press(Key::A)                         // then type 'a'
    .gap(3)
    .release(Key::A);
```

#### MOTION AND AN EDGE ON ONE FRAME

`frame` is the one builder call that fills more than a single field, so it's how a move and an edge share a frame: one entry, one wire report. Edges are sticky, so a hold is a press once then plain motion until the release.

```rust
use medius::{Action, Button, ClipBuilder};

let mut clip = ClipBuilder::new();

// move (+10, -4) AND press Left on the same frame
clip.frame(10, -4, 0, &[(Button::Left.into(), Action::Press)]);

// "aim and hold fire": press once, keep moving while held, then release
clip.frame(8, -2, 0, &[(Button::Left.into(), Action::Press)]);
for _ in 0..60 { clip.move_by(8, -2); }   // Left stays down (edges are sticky)
clip.frame(0, 0, 0, &[(Button::Left.into(), Action::SoftRelease)]);
```

## ClipHandle

_Fill the ring, configure, and drive playback_

From [`Device::clip()`](/library/clip.md#clip). Every method below is [fire-and-forget](/native/injection.md#fire-and-forget): it queues a frame and returns. The [auto-lock](/library/lock.md), loop, and retain settings and the trigger set are the clip's config; the engine verbs drive playback. [`query_status`](/library/requests.md#clip-status) reads the ring depth and playback state, and [`query_config`](/library/requests.md#clip-config) reads the config back.

#### LOAD & SETTINGS

| Method | Does |
| --- | --- |
| `append(clip: &ClipBuilder)` | Send a [`ClipBuilder`](/library/clip.md#builder)'s entries to the ring; splits a large clip into whole-entry frames with contiguous append seqs. |
| `set_autolock(scope: &[Blanket])` | Which [input groups](/library/lock.md) to lock while playing (clip-owned, released on stop). |
| `set_loop(on: bool)` | Loop playback at the clip end (retained mode only). |
| `set_retain(on: bool)` | Retain the clip so it can rewind and replay (`false` = streaming, the default). Set before the first `append`. |
| `finalize()` | Close a retained clip: fix its end so it can replay and loop. |

#### TRIGGERS (a managed set)

| Method | Does |
| --- | --- |
| `bind(trigger: [ClipTrigger](/library/types/structs.md#clip-trigger))` | Add or overwrite a binding: a physical edge drives an action on the box, no host round-trip. |
| `unbind(usage, edge: [Edge](/library/types/enums.md#edge))` | Remove the binding on that usage and edge. |
| `clear_triggers()` | Remove every binding. |

#### ENGINE VERBS

| Method | Does |
| --- | --- |
| `start()` | Rewind to the clip start and play (resume from a pause). |
| `stop()` | Stop, release held input and the clip lock; a streaming clip flushes, a retained clip rewinds and is kept. |
| `pause()` / `resume()` | Halt mid-clip keeping the cursor and held input / continue from the paused cursor. |
| `restart()` | Force a rewind and play, even mid-playback. |
| `toggle()` | Play if idle/paused, stop if playing. |
| `clear()` | Discard the loaded clip, free the ring, clear a fault. |

> **Note**
>
> A dropped append (or an overflow) leaves the clip [`ClipState::Faulted`](/library/types/enums.md#clip-state) and stops it. Recover with `clear` and rebuild, not by appending more; a faulted stream has a hole in it.

## Streaming and retained

_Drain-and-discard, or keep-and-replay_

A clip runs in one of two shapes, chosen by [`set_retain`](/library/clip.md#handle) before the first [`append`](/library/clip.md#handle).

| Aspect | Streaming (default) | Retained |
| --- | --- | --- |
| Turn on | nothing, it's the default | `set_retain(true)` before the first `append` |
| The ring | each entry is freed as it plays, so it's unbounded | entries are kept after playing, up to 64 KiB |
| Top up mid-play | yes, append to the tail in real time | append until `finalize`, then it's sealed |
| Replay | no, it plays once | yes, it rewinds and replays |
| `loop` | not available | available once `finalize`d |
| `stop` | flushes the buffer | rewinds and keeps the clip |
| Best for | open-ended or generated input | a fixed macro you replay on a trigger |

#### HOW THE RING MOVES

```
streaming (drain-and-discard)
    append ──▶ [ e4 e3 e2 ] ──▶ play ──▶ freed     (unbounded, top up forever)
                   box reclaims each entry once played; no replay

retained (keep-and-replay, up to 64 KiB)
    append ──▶ [ e0 e1 e2 e3 e4 ] ──▶ finalize ──▶ sealed
               base │──▶ play cursor ──▶ end
                    └──── start / loop rewinds to base ◀────┘
```

> **Note**
>
> `set_retain` only takes effect before the first `append`, and an `append` after `finalize` is rejected. To reload a retained clip, [`clear`](/library/clip.md#handle) it and build again.

#### EXAMPLE

```rust
// Streaming: preload, play with auto-lock, then top up in real time, pacing against free.
use medius::{Blanket, ClipState};
use std::time::Duration;

let handle = device.clip();       // device: an open Device
handle.set_autolock(&[Blanket::Aim])?;   // lock only the aim axes while playing
handle.append(&clip)?;                    // preload (clip, next_chunk: ClipBuilders you built)
handle.start()?;

loop {
    let s = handle.query_status()?;
    if s.state == ClipState::Idle { break; }           // done, or stopped
    if s.free as usize > next_chunk.as_bytes().len() {
        handle.append(&next_chunk)?;                   // stream more while there's room
    }
    std::thread::sleep(Duration::from_millis(5));
}
handle.stop()?;
```

## Triggers

_Play, stop, or toggle on a physical key_

A [`ClipTrigger`](/library/types/structs.md#clip-trigger) binds one [`Edge`](/library/types/enums.md#edge) of a usage to one [`ClipAction`](/library/types/enums.md#clip-action), so the box drives playback itself with no host round-trip.

#### ANATOMY

| Part | Is | Example |
| --- | --- | --- |
| usage | the button, key, or media the edge is on (or any of a class) | `Key::F1` |
| edge | which transition fires it: press, release, or both | `Edge::Press` |
| action | the engine verb to run | `ClipAction::Start` |

Bindings are a managed set keyed by `(usage, edge)`, like a [lock](/library/lock.md): [`bind`](/library/clip.md#handle) adds or overwrites, [`unbind`](/library/clip.md#handle) drops one, and [`clear_triggers`](/library/clip.md#handle) wipes them. A physical edge runs the one most-specific match, so a binding on `Key::F1` beats an any-key binding.

#### RECIPES

| To get | Bind |
| --- | --- |
| Hold to play | `F1 Press → Start` and `F1 Release → Stop` |
| Toggle play/stop on one key | `Side1 Press → Toggle` |
| Separate play and stop keys | `F1 Press → Start` and `F2 Press → Stop` |
| Pause, then resume | `F3 Press → Pause` and `F4 Press → Resume` |

> **Note**
>
> `.consume()` swallows the triggering edge from the game for the length of the hold, so the key drives the clip without also reaching what you're playing.

#### EXAMPLE

```rust
use medius::{Button, ClipAction, ClipTrigger, Edge, Key};

let clip = device.clip();
clip.set_retain(true)?;      // set the mode before loading
clip.append(&recording)?;
clip.finalize()?;            // close it so it can replay

// Hold-to-play: F1 down starts, F1 up stops. Consume F1 so the game never sees it.
clip.bind(ClipTrigger::new(Key::F1, Edge::Press, ClipAction::Start).consume())?;
clip.bind(ClipTrigger::new(Key::F1, Edge::Release, ClipAction::Stop).consume())?;

// Or one side-button that toggles play/stop:
clip.bind(ClipTrigger::new(Button::Side1, Edge::Press, ClipAction::Toggle))?;
```

## On AsyncDevice

_AsyncClipHandle: control fires, queries await_

[`AsyncDevice::clip()`](/library/features/async.md) returns an `AsyncClipHandle` that keeps `append`, the settings, and the engine verbs synchronous (they just queue a frame), while `query_status().await` and `query_config().await` are futures like the other queries.

#### EXAMPLE

```rust
let device = Device::find()?.into_async();
let handle = device.clip();
handle.append(&clip)?;          // sync, no await
handle.start()?;                // sync
let s = handle.query_status().await?;   // the query awaits
```
