Skip to main content

Application

Struct Application 

Source
pub struct Application<S: Send + 'static> { /* private fields */ }
Expand description

The high-level application wrapper — owns state, a view function, and a renderer.

Application is the recommended entry point for most programs. It manages the render loop, processes Handle updates, ticks lifecycle effects, and handles terminal resize.

§Two usage modes

Non-interactive — state changes flow entirely through the Handle:

let (mut app, handle) = Application::builder()
    .state(MyState::new())
    .view(my_view)
    .build()?;

tokio::spawn(async move {
    handle.update(|s| s.done = true);
    // handle dropped → app exits when effects stop
});

app.run().await?;

Interactive — terminal raw mode with event handling:

app.run_interactive(|event, state| {
    // handle keyboard/mouse events, mutate state
    ControlFlow::Continue
}).await?;

§Step API

For custom event loops or embedding, use the step methods directly:

app.update(|s| s.count += 1);
app.tick();
app.flush(&mut stdout)?;

Implementations§

Source§

impl<S: Send + 'static> Application<S>

Source

pub fn builder() -> ApplicationBuilder<S>

Create a new ApplicationBuilder.

Examples found in repository?
examples/app.rs (line 42)
41async fn main() -> io::Result<()> {
42    let (mut app, handle) = Application::builder()
43        .state(AppState {
44            messages: vec![],
45            thinking: false,
46        })
47        .view(app_view)
48        .build()?;
49
50    // All updates flow through the handle. The app manages
51    // rendering, ticking, and exits when the handle is dropped
52    // and no effects remain.
53    tokio::spawn(async move {
54        handle.update(|s| {
55            s.messages.push((
56                "Application wrapper demo".into(),
57                Style::default()
58                    .fg(Color::White)
59                    .add_modifier(Modifier::BOLD),
60            ));
61            s.messages.push((
62                "Updates flow through the Handle".into(),
63                Style::default().fg(Color::DarkGray),
64            ));
65        });
66
67        tokio::time::sleep(Duration::from_millis(800)).await;
68
69        handle.update(|s| {
70            s.messages.push((
71                "Starting background work...".into(),
72                Style::default().fg(Color::Yellow),
73            ));
74            s.thinking = true;
75        });
76
77        tokio::time::sleep(Duration::from_millis(1500)).await;
78
79        handle.update(|s| {
80            s.thinking = false;
81            s.messages.push((
82                "✓ Background work complete".into(),
83                Style::default().fg(Color::Green),
84            ));
85            s.messages.push(("".into(), Style::default()));
86        });
87
88        // handle dropped here → app exits when effects stop
89    });
90
91    app.run().await?;
92
93    println!();
94    Ok(())
95}
More examples
Hide additional examples
examples/chat.rs (line 282)
281async fn main() -> io::Result<()> {
282    let (mut app, handle) = Application::builder()
283        .state(AppState::new())
284        .view(chat_view)
285        .on_commit(|_, state: &mut AppState| {
286            state.messages.remove(0);
287        })
288        .build()?;
289
290    app.update(|_| {});
291    app.flush(&mut io::stdout())?;
292
293    let h = handle;
294    app.run_interactive(move |event, state| {
295        if let Event::Key(KeyEvent {
296            code,
297            kind: KeyEventKind::Press,
298            modifiers,
299            ..
300        }) = event
301        {
302            if modifiers.contains(KeyModifiers::CONTROL) {
303                return ControlFlow::Continue;
304            }
305
306            match code {
307                KeyCode::Char(c) => {
308                    state.input.insert(state.cursor, *c);
309                    state.cursor += c.len_utf8();
310                }
311                KeyCode::Backspace => {
312                    if state.cursor > 0 {
313                        state.cursor -= 1;
314                        state.input.remove(state.cursor);
315                    }
316                }
317                KeyCode::Left => {
318                    state.cursor = state.cursor.saturating_sub(1);
319                }
320                KeyCode::Right => {
321                    if state.cursor < state.input.len() {
322                        state.cursor += 1;
323                    }
324                }
325                KeyCode::Enter => {
326                    if !state.input.is_empty() {
327                        let text = std::mem::take(&mut state.input);
328                        state.cursor = 0;
329                        let user_id = state.next_id();
330                        state.messages.push(ChatMessage {
331                            id: user_id,
332                            kind: MessageKind::User(text),
333                        });
334
335                        let assistant_id = state.next_id();
336                        state.messages.push(ChatMessage {
337                            id: assistant_id,
338                            kind: MessageKind::Assistant {
339                                content: String::new(),
340                                done: false,
341                            },
342                        });
343
344                        let h2 = h.clone();
345                        tokio::spawn(async move {
346                            stream_response(h2, assistant_id).await;
347                        });
348                    }
349                }
350                KeyCode::Esc => {
351                    return ControlFlow::Exit;
352                }
353                _ => {}
354            }
355        }
356        ControlFlow::Continue
357    })
358    .await
359}
Source

pub async fn run(&mut self) -> Result<()>

Run the render loop.

Processes handle updates, ticks active effects, and renders automatically. Does not poll terminal events or enable raw mode. Exits when all Handles are dropped and no effects remain, or when Handle::exit is called.

This is the primary entry point for non-interactive use. All state changes flow through the Handle:

let (mut app, handle) = Application::builder()
    .state(MyState::new())
    .view(my_view)
    .build()?;

tokio::spawn(async move {
    handle.update(|s| s.message = "hello".into());
    // handle dropped → app exits when effects stop
});

app.run().await?;
Examples found in repository?
examples/app.rs (line 91)
41async fn main() -> io::Result<()> {
42    let (mut app, handle) = Application::builder()
43        .state(AppState {
44            messages: vec![],
45            thinking: false,
46        })
47        .view(app_view)
48        .build()?;
49
50    // All updates flow through the handle. The app manages
51    // rendering, ticking, and exits when the handle is dropped
52    // and no effects remain.
53    tokio::spawn(async move {
54        handle.update(|s| {
55            s.messages.push((
56                "Application wrapper demo".into(),
57                Style::default()
58                    .fg(Color::White)
59                    .add_modifier(Modifier::BOLD),
60            ));
61            s.messages.push((
62                "Updates flow through the Handle".into(),
63                Style::default().fg(Color::DarkGray),
64            ));
65        });
66
67        tokio::time::sleep(Duration::from_millis(800)).await;
68
69        handle.update(|s| {
70            s.messages.push((
71                "Starting background work...".into(),
72                Style::default().fg(Color::Yellow),
73            ));
74            s.thinking = true;
75        });
76
77        tokio::time::sleep(Duration::from_millis(1500)).await;
78
79        handle.update(|s| {
80            s.thinking = false;
81            s.messages.push((
82                "✓ Background work complete".into(),
83                Style::default().fg(Color::Green),
84            ));
85            s.messages.push(("".into(), Style::default()));
86        });
87
88        // handle dropped here → app exits when effects stop
89    });
90
91    app.run().await?;
92
93    println!();
94    Ok(())
95}
Source

pub async fn run_loop(&mut self) -> Result<()>

Run the interactive event loop with component-driven event handling.

Enables terminal raw mode and processes terminal events (keyboard, mouse, resize) through the component tree’s two-phase event dispatch (capture then bubble), including focus management and Tab cycling. Unlike run_interactive, raw terminal events are not exposed to the caller.

Use this when components handle their own events and translate them into app-domain actions via channels or other mechanisms passed through the context system.

Exits when Handle::exit is called, or when all Handles are dropped and no effects remain active. Ctrl+C exits by default (configurable via ApplicationBuilder::ctrl_c).

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()?;

// App event loop in a separate task
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?;
Source

pub async fn run_interactive( &mut self, handler: impl FnMut(&Event, &mut S) -> ControlFlow, ) -> Result<()>

Run the interactive event loop with a raw event handler.

Enables terminal raw mode and uses tokio::select! to multiplex terminal events, handle updates, and effect ticks. The handler receives terminal events and mutable state; return ControlFlow::Exit to stop. Ctrl+C exits by default (configurable via ApplicationBuilder::ctrl_c).

For most applications, prefer run_loop with the context system. Use this when you need direct access to raw terminal events.

Examples found in repository?
examples/chat.rs (lines 294-357)
281async fn main() -> io::Result<()> {
282    let (mut app, handle) = Application::builder()
283        .state(AppState::new())
284        .view(chat_view)
285        .on_commit(|_, state: &mut AppState| {
286            state.messages.remove(0);
287        })
288        .build()?;
289
290    app.update(|_| {});
291    app.flush(&mut io::stdout())?;
292
293    let h = handle;
294    app.run_interactive(move |event, state| {
295        if let Event::Key(KeyEvent {
296            code,
297            kind: KeyEventKind::Press,
298            modifiers,
299            ..
300        }) = event
301        {
302            if modifiers.contains(KeyModifiers::CONTROL) {
303                return ControlFlow::Continue;
304            }
305
306            match code {
307                KeyCode::Char(c) => {
308                    state.input.insert(state.cursor, *c);
309                    state.cursor += c.len_utf8();
310                }
311                KeyCode::Backspace => {
312                    if state.cursor > 0 {
313                        state.cursor -= 1;
314                        state.input.remove(state.cursor);
315                    }
316                }
317                KeyCode::Left => {
318                    state.cursor = state.cursor.saturating_sub(1);
319                }
320                KeyCode::Right => {
321                    if state.cursor < state.input.len() {
322                        state.cursor += 1;
323                    }
324                }
325                KeyCode::Enter => {
326                    if !state.input.is_empty() {
327                        let text = std::mem::take(&mut state.input);
328                        state.cursor = 0;
329                        let user_id = state.next_id();
330                        state.messages.push(ChatMessage {
331                            id: user_id,
332                            kind: MessageKind::User(text),
333                        });
334
335                        let assistant_id = state.next_id();
336                        state.messages.push(ChatMessage {
337                            id: assistant_id,
338                            kind: MessageKind::Assistant {
339                                content: String::new(),
340                                done: false,
341                            },
342                        });
343
344                        let h2 = h.clone();
345                        tokio::spawn(async move {
346                            stream_response(h2, assistant_id).await;
347                        });
348                    }
349                }
350                KeyCode::Esc => {
351                    return ControlFlow::Exit;
352                }
353                _ => {}
354            }
355        }
356        ControlFlow::Continue
357    })
358    .await
359}
Source

pub fn update(&mut self, f: impl FnOnce(&mut S))

Mutate application state directly. Marks dirty for rebuild.

Examples found in repository?
examples/chat.rs (line 290)
281async fn main() -> io::Result<()> {
282    let (mut app, handle) = Application::builder()
283        .state(AppState::new())
284        .view(chat_view)
285        .on_commit(|_, state: &mut AppState| {
286            state.messages.remove(0);
287        })
288        .build()?;
289
290    app.update(|_| {});
291    app.flush(&mut io::stdout())?;
292
293    let h = handle;
294    app.run_interactive(move |event, state| {
295        if let Event::Key(KeyEvent {
296            code,
297            kind: KeyEventKind::Press,
298            modifiers,
299            ..
300        }) = event
301        {
302            if modifiers.contains(KeyModifiers::CONTROL) {
303                return ControlFlow::Continue;
304            }
305
306            match code {
307                KeyCode::Char(c) => {
308                    state.input.insert(state.cursor, *c);
309                    state.cursor += c.len_utf8();
310                }
311                KeyCode::Backspace => {
312                    if state.cursor > 0 {
313                        state.cursor -= 1;
314                        state.input.remove(state.cursor);
315                    }
316                }
317                KeyCode::Left => {
318                    state.cursor = state.cursor.saturating_sub(1);
319                }
320                KeyCode::Right => {
321                    if state.cursor < state.input.len() {
322                        state.cursor += 1;
323                    }
324                }
325                KeyCode::Enter => {
326                    if !state.input.is_empty() {
327                        let text = std::mem::take(&mut state.input);
328                        state.cursor = 0;
329                        let user_id = state.next_id();
330                        state.messages.push(ChatMessage {
331                            id: user_id,
332                            kind: MessageKind::User(text),
333                        });
334
335                        let assistant_id = state.next_id();
336                        state.messages.push(ChatMessage {
337                            id: assistant_id,
338                            kind: MessageKind::Assistant {
339                                content: String::new(),
340                                done: false,
341                            },
342                        });
343
344                        let h2 = h.clone();
345                        tokio::spawn(async move {
346                            stream_response(h2, assistant_id).await;
347                        });
348                    }
349                }
350                KeyCode::Esc => {
351                    return ControlFlow::Exit;
352                }
353                _ => {}
354            }
355        }
356        ControlFlow::Continue
357    })
358    .await
359}
Source

pub fn handle_event(&mut self, event: &Event)

Forward a terminal event to the component tree (focus routing).

Source

pub fn tick(&mut self)

Advance active effects (animations, intervals).

Source

pub fn has_active(&self) -> bool

Whether any effects are active (e.g., spinner animation).

Source

pub fn state(&self) -> &S

Read-only access to the application state.

Source

pub fn is_exit_requested(&self) -> bool

Whether external code has requested exit via Handle::exit.

Source

pub fn flush(&mut self, writer: &mut impl Write) -> Result<()>

Drain pending handle updates, rebuild if dirty, render to writer. Also checks for committed scrollback if on_commit is set.

Examples found in repository?
examples/chat.rs (line 291)
281async fn main() -> io::Result<()> {
282    let (mut app, handle) = Application::builder()
283        .state(AppState::new())
284        .view(chat_view)
285        .on_commit(|_, state: &mut AppState| {
286            state.messages.remove(0);
287        })
288        .build()?;
289
290    app.update(|_| {});
291    app.flush(&mut io::stdout())?;
292
293    let h = handle;
294    app.run_interactive(move |event, state| {
295        if let Event::Key(KeyEvent {
296            code,
297            kind: KeyEventKind::Press,
298            modifiers,
299            ..
300        }) = event
301        {
302            if modifiers.contains(KeyModifiers::CONTROL) {
303                return ControlFlow::Continue;
304            }
305
306            match code {
307                KeyCode::Char(c) => {
308                    state.input.insert(state.cursor, *c);
309                    state.cursor += c.len_utf8();
310                }
311                KeyCode::Backspace => {
312                    if state.cursor > 0 {
313                        state.cursor -= 1;
314                        state.input.remove(state.cursor);
315                    }
316                }
317                KeyCode::Left => {
318                    state.cursor = state.cursor.saturating_sub(1);
319                }
320                KeyCode::Right => {
321                    if state.cursor < state.input.len() {
322                        state.cursor += 1;
323                    }
324                }
325                KeyCode::Enter => {
326                    if !state.input.is_empty() {
327                        let text = std::mem::take(&mut state.input);
328                        state.cursor = 0;
329                        let user_id = state.next_id();
330                        state.messages.push(ChatMessage {
331                            id: user_id,
332                            kind: MessageKind::User(text),
333                        });
334
335                        let assistant_id = state.next_id();
336                        state.messages.push(ChatMessage {
337                            id: assistant_id,
338                            kind: MessageKind::Assistant {
339                                content: String::new(),
340                                done: false,
341                            },
342                        });
343
344                        let h2 = h.clone();
345                        tokio::spawn(async move {
346                            stream_response(h2, assistant_id).await;
347                        });
348                    }
349                }
350                KeyCode::Esc => {
351                    return ControlFlow::Exit;
352                }
353                _ => {}
354            }
355        }
356        ControlFlow::Continue
357    })
358    .await
359}
Source

pub fn renderer(&mut self) -> &mut InlineRenderer

Access the inner InlineRenderer for advanced use.

Auto Trait Implementations§

§

impl<S> Freeze for Application<S>
where S: Freeze,

§

impl<S> !RefUnwindSafe for Application<S>

§

impl<S> !Send for Application<S>

§

impl<S> !Sync for Application<S>

§

impl<S> Unpin for Application<S>
where S: Unpin,

§

impl<S> UnsafeUnpin for Application<S>
where S: UnsafeUnpin,

§

impl<S> !UnwindSafe for Application<S>

Blanket Implementations§

Source§

impl<T, V> AddTo<DataChildren<T>> for V
where V: Into<T>,

Source§

type Handle<'a> = DataHandle where T: 'a

Handle returned after adding. Supports .key() / .width() chaining.
Source§

fn add_to(self, collector: &mut DataChildren<T>) -> DataHandle

Add this value to the collector, returning a handle for chaining .key() and .width().
Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.