Expand description
Declarative inline TUI rendering for Rust, built on Ratatui.
eye_declare provides a React-like component model for terminal UIs that render inline — content grows downward into the terminal’s native scrollback rather than taking over the full screen. This makes it ideal for CLI tools, AI assistants, build systems, and interactive prompts where earlier output should remain visible.
§Quick start
use eye_declare::{element, Application, Elements, Spinner, TextBlock};
struct State { messages: Vec<String>, loading: bool }
fn view(state: &State) -> Elements {
element! {
#(for (i, msg) in state.messages.iter().enumerate() {
TextBlock(key: format!("msg-{i}"), lines: vec![msg.clone().into()])
})
#(if state.loading {
Spinner(key: "loading", label: "Thinking...")
})
}
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let (mut app, handle) = Application::builder()
.state(State { messages: vec![], loading: true })
.view(view)
.build()?;
tokio::spawn(async move {
handle.update(|s| s.messages.push("Hello!".into()));
handle.update(|s| s.loading = false);
});
app.run().await
}§Core concepts
-
Component— The trait all UI elements implement. Props live on&self(immutable, set by the parent); internal state lives in the associatedStatetype, managed by the framework viaTrackedfor automatic dirty detection. -
Elements— A list of component descriptions returned by view functions. The framework reconciles the new list against the existing tree, preserving state for reused nodes. -
element!— A JSX-like proc macro for buildingElementsdeclaratively. Supports props, children, keys, conditionals (#(if ...)), loops (#(for ...)), and splicing pre-builtElements(#(expr)). -
Application— Owns your state and manages the render loop.Handlelets you send state updates from any thread or async task. -
InlineRenderer— The lower-level rendering engine for when you need direct control over the render loop (sync code, embedding, custom event loops).
§Built-in components
| Component | Description |
|---|---|
TextBlock | Styled text with display-time word wrapping |
Spinner | Animated spinner with auto-tick via lifecycle hooks |
Markdown | Headings, bold, italic, inline code, code blocks, lists |
VStack | Vertical container — children stack top-to-bottom |
HStack | Horizontal container with WidthConstraint-based layout |
§Layout
Vertical stacking is the default. HStack provides horizontal layout where
children declare their width via WidthConstraint::Fixed or WidthConstraint::Fill.
Components can declare Insets for border/padding chrome — children render inside
the inset area while the component draws its chrome in the full area.
§Lifecycle hooks
Components declare effects in Component::lifecycle using the Hooks API:
Hooks::use_interval— periodic callback (e.g., animation)Hooks::use_mount— fires once after the component is builtHooks::use_unmount— fires when the component is removedHooks::use_autofocus— request focus on mountHooks::use_focus_scope— confine Tab cycling to this subtreeHooks::provide_context— make a value available to descendantsHooks::use_context— read a value provided by an ancestor
§Context
The context system lets ancestor components provide values to their
descendants without prop-drilling. Register root-level contexts via
ApplicationBuilder::with_context, or have components provide
them via Hooks::provide_context in their lifecycle method.
Descendants read context values with Hooks::use_context.
This is commonly used with Application::run_loop to give
components access to an app-domain event channel:
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
let (mut app, handle) = Application::builder()
.state(MyState::default())
.view(my_view)
.with_context(tx)
.build()?;
let h = handle.clone();
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
AppEvent::Submit(val) => h.update(|s| s.result = val),
AppEvent::Quit => { h.exit(); break; }
}
}
});
app.run_loop().await?;§Feature flags
| Flag | Default | Description |
|---|---|---|
macros | yes | Enables the element! proc macro via eye_declare_macros |
Re-exports§
pub use app::Application;pub use app::ApplicationBuilder;pub use app::CommittedElement;pub use app::ControlFlow;pub use app::CtrlCBehavior;pub use app::Handle;pub use app::KeyboardProtocol;pub use children::AddTo;pub use children::ChildCollector;pub use children::ComponentWithSlot;pub use children::DataHandle;pub use children::SpliceInto;pub use component::Column;pub use component::Component;pub use component::EventResult;pub use component::HStack;pub use component::Tracked;pub use component::VStack;pub use components::markdown::Markdown;pub use components::markdown::MarkdownState;pub use components::spinner::Spinner;pub use components::spinner::SpinnerState;pub use components::text::Line;pub use components::text::Span;pub use components::text::TextBlock;pub use element::ElementHandle;pub use element::Elements;pub use hooks::Hooks;pub use inline::InlineRenderer;pub use insets::Insets;
Modules§
- app
- Application wrapper, builder, handle, and control flow types.
- children
- Traits and types for the
element!macro’s child collection system. - component
- The
Componenttrait and built-in container types (VStack,HStack,Column). - components
- Built-in components:
TextBlock,Spinner, andMarkdown. Built-in components shipped with eye_declare. - element
- The
Elementslist andElementHandlefor building component trees. - hooks
- Lifecycle hooks for declaring component effects.
- inline
- The
InlineRenderer— low-level inline rendering engine. - insets
- The
Insetstype for declaring content padding and border chrome.
Macros§
- element
- Declarative element tree macro.
- impl_
slot_ children - Implement
ChildCollectorfor a component so it accepts slot children in theelement!macro.
Structs§
- NodeId
- Opaque handle identifying a node in the component tree.
Enums§
- Layout
- Layout direction for a container’s children.
- Width
Constraint - How a child claims horizontal space inside an
HStack.