ansiq-runtime
ansiq-runtime is the runtime entry point for Ansiq. It owns:
- the
Applifecycle RuntimeHandleas the control surface for messages, tasks, history, and focus- the main loop, dirty scope collection, and localized rerender
- focus and input routing
If you have not read the conceptual pages yet, start with:
The App trait
pub trait App {
type Message: Send + 'static;
fn mount(&mut self, _handle: &RuntimeHandle<Self::Message>) {}
fn render(&mut self, cx: &mut ViewCtx<'_, Self::Message>) -> Element<Self::Message>;
fn update(&mut self, message: Self::Message, handle: &RuntimeHandle<Self::Message>);
fn on_unhandled_key(&mut self, _key: Key, _handle: &RuntimeHandle<Self::Message>) -> bool {
false
}
}mount(...)
Call timing:
- after the terminal session is entered
- before the first
render(...)
Good uses:
- start background tasks
- emit initial messages
- establish long-lived samplers or connections
Bad uses:
- manually patching the terminal
- treating it as a rendering hook
render(...)
render(...) is responsible for:
- describing the current UI tree
- establishing reactive reads for that render
- exposing component boundaries and continuity keys
It does not:
- write terminal patches directly
- decide relayout or damage tracking
update(...)
update(...) consumes Message and turns it into app state changes.
The core boundary is:
async task -> handle.emit(Message) -> update(...) -> state
on_unhandled_key(...)
If a key is:
- not consumed by the focused widget
- and not consumed by runtime-level focus navigation
it can still bubble back to the app through on_unhandled_key(...).
This is a good place for:
- global shortcuts
Escapeto cancel- mode switches
- command entry
fn on_unhandled_key(
&mut self,
key: Key,
handle: &RuntimeHandle<Self::Message>,
) -> bool {
if key == Key::Esc {
let _ = handle.emit(Message::Cancel);
return true;
}
false
}RuntimeHandle
RuntimeHandle is the main control surface between the app and the runtime.
Messages and tasks
handle.emit(Message::Refresh)?;
let task_handle = handle.clone();
handle.spawn(async move {
let _ = task_handle.emit(Message::Loaded(data));
});Most important methods:
emit(message): send a message back into the appspawn(future): start background workquit(): request that the main loop exits
Important rule:
- do not mutate UI signals directly inside spawned tasks
- the stable path is always
emit -> update -> state
History and scrollback
handle.commit_history("completed turn".to_string())?;
handle.commit_history_block(block)?;Use these when:
- a completed turn should move into scrollback
- the live viewport should stay reserved for the active session
Focus scopes
handle.trap_focus_in("composer-panel")?;
handle.clear_focus_scope()?;Use focus scopes for:
- modals
- overlays
- panels that should trap
Tabtraversal inside a subtree
These APIs are keyed by continuity keys, not temporary node ids.
Minimal runtime entry
use ansiq_runtime::run_app;
#[tokio::main]
async fn main() -> std::io::Result<()> {
run_app(MyApp::default()).await
}If you also need viewport control, use run_app_with_policy(...), covered on:
Last updated on