Skip to Content
InternalsRuntime Loop

Runtime Loop

In many UI systems, the main loop looks like an implementation detail:

  • read events
  • render
  • flush output

In Ansiq, the runtime loop is not a hidden plumbing layer. It is a real architectural boundary.

Why? Because Ansiq is not just drawing a tree. It has to coordinate:

  • terminal events
  • app messages
  • reactive flushes
  • rerender decisions
  • layout and patch emission

If those concerns are not gathered into one clear loop, they start to fight each other.

Why the runtime loop matters as a design object

Look at what Ansiq has to coordinate today:

  • input events
  • async task messages
  • dirty scopes produced by the reactive system
  • focus and routing
  • subtree replacement
  • partial relayout
  • invalidated region tracking
  • terminal patch emission

That means the runtime loop is not:

“render whenever something changes”

It is much closer to:

“collect several sources of change and reduce them into one predictable screen update pipeline”

First, separate lifecycle from the per-update loop

One of the easiest things to blur together in Ansiq is:

  • the App lifecycle
  • the runtime’s per-update loop

They are related, but they are not the same thing.

App lifecycle

At a high level, a Ansiq app usually goes through:

  1. create the App
  2. enter the terminal session
  3. call mount(...)
  4. perform the first render(...)
  5. enter the main loop
  6. keep reacting to input, messages, and reactive dirtiness
  7. exit and restore terminal state

As a rough timeline:

create app -> enter terminal session -> mount(...) -> first render(...) -> main runtime loop -> exit / restore terminal

The runtime loop per update

The loop itself is more fine-grained. It repeatedly coordinates:

  • input events
  • async messages
  • reactive flushes
  • rerender decisions
  • relayout
  • redraw / diff / patch emission

So lifecycle answers:

what stages does the app go through over its lifetime?

While the runtime loop answers:

when something changes, how do we turn that into one stable terminal update?

What that pipeline looks like

Today, the core Ansiq runtime chain can be summarized as:

input / async messages / reactive flush -> dirty scope collection -> subtree rerender or full rerender -> partial relayout -> invalidated regions -> framebuffer diff -> terminal patch emission
The runtime loop update chain
1input / async messages / reactive flush
2dirty scope collection
3subtree rerender or full rerender
4partial relayout
5invalidated regions
6framebuffer diff
7terminal patch emission

The value of this chain is not just performance. It is boundary clarity.

What mount, render, and update each mean

mount(...)

mount(...) is for work that should exist as soon as the app starts, for example:

  • starting background sampling
  • opening long-lived connections
  • scheduling initial messages

It does not render UI. It establishes long-lived runtime conditions.

render(...)

render(...) is responsible for:

  • describing the current UI tree
  • exposing component boundaries
  • performing reactive reads for this render pass

It does not draw the terminal directly, and it does not decide how patches are emitted.

update(...)

update(...) is responsible for:

  • consuming messages
  • mutating app state
  • deciding whether more async work should begin

It does not directly:

  • patch the terminal
  • relayout the tree
  • own the low-level focus implementation

The shortest possible summary is:

mount establishes long-lived background conditions, update consumes facts and changes state, and render describes the current UI.

Why this logic should not be spread around the system

Without a strong runtime loop, the most common failure mode is that different parts of the system start deciding redraw behavior independently:

  • input handling decides when to repaint
  • widgets mutate global state directly
  • surface decides viewport changes in isolation
  • app logic quietly triggers extra rerenders

This can still appear to work in a small app. But once you add:

  • async work
  • multiple panes
  • subtree replacement
  • history / scrollback

it becomes extremely difficult to debug.

What the Ansiq runtime loop is responsible for

From the perspective of the loop, the runtime has three major jobs:

1. Collect all sources of change

That includes:

  • keyboard input
  • async messages
  • reactive dirty scopes

2. Decide the smallest acceptable update

That includes:

  • full rerender vs dirty subtree replacement
  • which ancestors must be relaid out
  • which regions only need redraw

3. Commit the update into the terminal

That includes:

  • full-frame patches
  • region diffs
  • cursor patches

This is why the runtime loop cannot be treated as “just an event loop detail”. It defines the update model of the whole framework.

What a real update looks like

Suppose the user types one more character into an input while a background task also delivers Message::Chunk.

The runtime loop should not behave like “whoever arrives first triggers an ad-hoc redraw”. It should behave more like this:

  1. receive the input event and async message
  2. route the key and see whether a widget consumes it
  3. forward the message into update(...)
  4. run flush_reactivity() and collect dirty scopes
  5. decide between full rerender and subtree replacement
  6. perform partial relayout
  7. compute invalidated regions
  8. diff framebuffers
  9. emit terminal patches

The most important value here is not only speed. It is:

  • the order is explicit
  • the ownership is explicit
  • bugs can be traced to a specific layer

A key design principle

One of Ansiq’s most important architectural rules is:

  • the reactive system decides what became dirty
  • the runtime decides how the screen should update

The runtime loop is exactly where those two worlds meet.

It connects the reactive graph on one side and the terminal surface on the other. If that boundary becomes blurry, Ansiq quickly turns into an unstable mix of several UI models at once.

Where the runtime loop sits in the system
1the reactive graph reports dirty work
2the UI tree provides the current structure
3the runtime loop decides one update path
4surface / terminal session receives that plan
5terminal patches are emitted

What to read in the source

If you want to map this page to implementation, start here:

  • ansiq-runtime
    • run.rs
    • engine.rs
  • ansiq-surface
    • session.rs

When reading, the important question is not “what does every function do?”, but rather:

  • where events enter
  • where dirty work is collected
  • where rerender is decided
  • where patches are emitted

The conclusion of this page

In Ansiq, the runtime loop is not hidden plumbing.

It is a real architectural object because it ties together:

  • reactive updates
  • tree updates
  • partial layout
  • terminal patch emission

into one unified, debuggable, documentable pipeline.

If you want to understand why the loop does not immediately imply “rerender everything”, the next page to read is Reactive Graph vs UI Tree.

Last updated on