Skip to Content
Architecture

Architecture

Noir implements Flutter-inspired widgets, elements, and integer-cell layout in Dart, then uses OpenTUI’s native renderer for terminal output. It separates what an application declares from the state and native resources the framework must preserve. That separation lets a callback change one value while keeping the terminal session.

Widgets declare. Elements preserve identity. RenderObjects layout and record paint. The compositor talks to OpenTUI. The native layer owns FFI, memory, ABI, and binaries.

This page is for readers who extend the framework, host it, or need to know which layer owns a problem. To build an application, start with Your first app.

Follow one increment through Noir

Start with the counter’s increment method:

void _increment() => setState(() => _count++);

That call crosses the framework in a defined order:

  1. The event changes State

    _increment changes _count and marks its element dirty.

  2. The Element preserves identity

    The existing State stays attached while type, key, and position still match.

  3. Widgets declare the next tree

    build returns immutable configuration containing the new count.

  4. RenderObjects resolve the frame

    Layout and paint recording run from the render root; constraints become cell sizes, positions, and hit-test geometry.

  5. The compositor reaches OpenTUI

    Display lists update buffers; the native layer owns FFI handles and memory.

Rebuilding is scoped to the dirty elements. Layout and paint recording are not: a dirty frame walks the render tree from its root. That is a deliberate, recorded trade-off, not an optimization the framework promises to make later.

Identity is separate from configuration

A StatefulWidget is replaceable configuration. Its element owns the stable place in the tree, and that place retains the State object. If a rebuild puts the same widget type and key at the same position, Noir updates the configuration and keeps the state. During a successful replacement, Noir deactivates the old element and mounts the new one. At the end of the build pass, it unmounts inactive elements and disposes their state. The replacement’s initState can therefore run before the old state’s dispose.

Inherited dependencies also belong to the element tree. When an InheritedWidget reports a meaningful update, the dependent elements rebuild without application code maintaining a second subscription graph.

Hot reload uses the same boundary. The Dart VM swaps compatible code, Noir calls reassemble on retained state, then rebuilds, lays out, and paints. It does not rerun main() or initState().

Paint and FFI stay below widgets

A widget never calls FFI directly. A custom render object lays out and hit tests in its own layer, then records paint through PaintingContext; it does not take a Buffer as its normal paint surface. The compositor is the bridge that applies those recordings to OpenTUI buffers.

Explicit lifecycle methods remain the primary resource owner. Finalizers can limit damage after a missed cleanup, but they do not replace deterministic dispose or native handle release.

Where to go next

  • API — the four package surfaces and what each one accepts responsibility for.
  • State, identity, and ownership — the same boundary, from an application’s point of view.
  • Platform support — what the native layer and the reader’s terminal decide.
  • GOALS.md  — the durable architecture and quality bar this page summarizes.
  • CONTRIBUTING.md  — repository setup, the framework test harnesses, and drive mode.
Last updated on