Medius - Rust LibraryClip

Clip

Preload input and let the box play it back

Build a sequence of per-frame input with a ClipBuilder, hand it to the ClipHandle from Device::clip(), and the box drains it into the same injection state move and inject feed.

Playback is box-clocked, so it carries no host scheduling jitter. One clip covers mouse, keyboard, and media. Backs the CLIP 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
fn clip(&self) -> ClipHandle

No round-trip

Returns a ClipHandle bound to this box. Keep one handle per clip session and top it up through it: the handle owns the append-sequence counter the box uses to spot a dropped append.

A reboot or reconnect drops the clip and its config (auto-lock, loop, retain, ride, triggers); nothing is re-asserted, so re-preload and re-set after one. A clip needs a cloned mouse: its frame clock is the mouse's report tick, which keyboard and media edges ride.

ClipBuilder

Build the entry stream
fn new() -> ClipBuilder

No round-trip

Each method appends one per-frame entry, so a builder is a timeline read top to bottom. Motion is a relative delta; an edge is an Action that stays held until a later frame changes it; a gap NAKs like an idle mouse.

MethodAppends
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 (button, key, or media), like Device::press.
edge(usage, action)a one-edge frame for any Usage with an explicit Action.
frame(dx, dy, wheel, edges)a motion delta plus up to 8 Usage / Action edges on one frame.

They take &mut self and return &mut Self, so chain them or push in a loop; clear() reuses the allocation. press/release/force_release wrap edge, which wraps frame. Reach for frame only when motion and edges share one frame.

EXAMPLE
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 fills more than one field, so a move and an edge share one entry and one wire report.

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)]);

// 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(). Every method below is fire-and-forget: it queues a frame and returns. query_status reads the ring depth and playback state; query_config reads the config back.

LOAD & SETTINGS
MethodDoes
append(clip: &ClipBuilder)Send a ClipBuilder's entries to the ring; splits a large clip into whole-entry frames with contiguous append seqs.
set_autolock(scope: &[Blanket])Which input groups 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.
set_ride(on: bool)Make the clip's motion wait for a real move under movement riding (false = the box's own clock, the default). Changeable mid-playback.
finalize()Close a retained clip: fix its end so it can replay and loop.
TRIGGERS (a managed set)
MethodDoes
bind(trigger: ClipTrigger)Add or overwrite a binding: a physical edge drives an action on the box, no host round-trip.
unbind(usage, edge: Edge)Remove the binding on that usage and edge.
clear_triggers()Remove every binding.
ENGINE VERBS
MethodDoes
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.

A dropped append (or an overflow) leaves the clip ClipState::Faulted and stops it. Recover with clear and rebuild, not by appending more; a faulted stream is missing entries.

Streaming and retained

Drain-and-discard, or keep-and-replay

A clip runs in one of two shapes, chosen by set_retain before the first append.

AspectStreaming (default)Retained
Turn onnothing, it's the defaultset_retain(true) before the first append
The ringeach entry is freed as it plays, so it's unboundedentries are kept after playing, up to 64 KiB
Top up mid-playyes, append to the tail in real timeappend until finalize, then it's sealed
Replayno, it plays onceyes, it rewinds and replays
loopnot availableavailable once finalized
stopflushes the bufferrewinds and keeps the clip
Best foropen-ended or generated inputa 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 ◀────┘

set_retain only takes effect before the first append, and an append after finalize is rejected. To reload a retained clip, clear it and build again.

EXAMPLE
// 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 X and Y 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 runs one ClipAction on the box when a physical Edge fires, with no host round-trip.

ANATOMY
PartIsExample
usagethe button, key, or media the edge is on (or any of a class)Key::F1
edgewhich transition fires it: press, release, or bothEdge::Press
actionthe engine verb to runClipAction::Start

Bindings are a managed set keyed by (usage, edge), like a lock. A physical edge runs the one most-specific match, so a binding on Key::F1 resolves before an any-key one.

RECIPES
To getBind
Hold to playF1 Press → Start and F1 Release → Stop
Toggle play/stop on one keySide1 Press → Toggle
Separate play and stop keysF1 Press → Start and F2 Press → Stop
Pause, then resumeF3 Press → Pause and F4 Press → Resume

.consume() locks the trigger usage while it stays active. It applies to the press edge only, so a Edge::Release binding carries the flag with no effect.

EXAMPLE
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. The press binding's consume locks F1
// for the whole hold, so the release binding does not need one.
clip.bind(ClipTrigger::new(Key::F1, Edge::Press, ClipAction::Start).consume())?;
clip.bind(ClipTrigger::new(Key::F1, Edge::Release, ClipAction::Stop))?;

// 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() returns an AsyncClipHandle that keeps append, the settings, and the engine verbs synchronous; query_status().await and query_config().await are futures like the other queries.

EXAMPLE
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