Runtime & Drivers #

The async driver #

#[tokio::main]
async fn main() -> std::io::Result<()> {
let output = eye_declare::driver_tokio::run(app).await?;
// or, with options:
let options = RunOptions::default().keyboard(KeyboardProtocol::Enhanced);
let output = eye_declare::driver_tokio::run_with(app, options).await?;
Ok(())
}

Multiplexes terminal events, messages from spawned tasks, animation ticks, and subscriptions. Queued messages are drained in batches — one frame per burst. Returns the app's Output after ctx.exit(…); returns Output::default() if stdin closes.

The sync loop #

eye_declare::run(app) / run_with(app, options) — a blocking loop for keyboard-only apps with no executor dependency (build with default-features = false if you want to guarantee that). Apps that spawn tasks or declare subscriptions are rejected with an error directing you to the tokio driver.

Both entry points enable raw mode and bracketed paste for the duration and restore the terminal on exit, including panic unwind.

RunOptions #

optionvaluesnotes
keyboardKeyboardProtocol::Legacy (default) / EnhancedEnhanced requests the kitty protocol's disambiguated escape codes (Shift+Enter vs Enter), falling back to legacy silently where unsupported.

The struct is #[non_exhaustive]; construct with Default and the setters.

Runtime: the headless core #

Both drivers are thin shells over Runtime, which is synchronous and does no I/O — events in, escape bytes out:

let mut rt = Runtime::new(app, width, terminal_height);
let (bytes, exit) = rt.startup(); // App::init's output
let (bytes, exit) = rt.handle(input_event); // keymap → update → present
let (bytes, exit) = rt.process(msg); // update → present
let (bytes, exit) = rt.process_batch(msgs); // many updates, one present
let bytes = rt.present(); // re-present (animation tick)
let bytes = rt.resize(w, h);
let effects = rt.take_effects(); // spawned work, for the driver
let interval = rt.animation_interval(); // Some(_) while tail animates

This is the surface for headless testing and for custom drivers (another executor, a remote terminal, an event-sourced replay). A driver's obligations: write every byte returned, in order; execute or reject effects; call present on animation_interval's cadence while Some.

App lifecycle #

impl App for MyApp {
type Msg = Msg;
type Output = ExitValue; // Default required
fn init(&mut self, ctx: &mut Ctx<'_, Self>); // optional: before first frame
fn update(&mut self, msg: Msg, ctx: &mut Ctx<'_, Self>);
fn tail(&self) -> impl Element + '_;
fn keymap(&self) -> Keymap<Msg>; // optional: default empty
fn subscriptions(&self) -> Subscriptions<Msg>; // optional: default none
}

Ctx provides the effects: push(element), spawn(stream) -> Task, perform(future) -> Task, exit(output). On exit the runtime reclaims trailing blank rows and hands the cursor back to the shell directly below your final output.