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>
impl<S: Send + 'static> Application<S>
Sourcepub fn builder() -> ApplicationBuilder<S>
pub fn builder() -> ApplicationBuilder<S>
Create a new ApplicationBuilder.
Examples found in repository?
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
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}Sourcepub async fn run(&mut self) -> Result<()>
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?
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}Sourcepub async fn run_loop(&mut self) -> Result<()>
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?;Sourcepub async fn run_interactive(
&mut self,
handler: impl FnMut(&Event, &mut S) -> ControlFlow,
) -> Result<()>
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?
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}Sourcepub fn update(&mut self, f: impl FnOnce(&mut S))
pub fn update(&mut self, f: impl FnOnce(&mut S))
Mutate application state directly. Marks dirty for rebuild.
Examples found in repository?
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}Sourcepub fn handle_event(&mut self, event: &Event)
pub fn handle_event(&mut self, event: &Event)
Forward a terminal event to the component tree (focus routing).
Sourcepub fn has_active(&self) -> bool
pub fn has_active(&self) -> bool
Whether any effects are active (e.g., spinner animation).
Sourcepub fn is_exit_requested(&self) -> bool
pub fn is_exit_requested(&self) -> bool
Whether external code has requested exit via Handle::exit.
Sourcepub fn flush(&mut self, writer: &mut impl Write) -> Result<()>
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?
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}Sourcepub fn renderer(&mut self) -> &mut InlineRenderer
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 Vwhere
V: Into<T>,
impl<T, V> AddTo<DataChildren<T>> for Vwhere
V: Into<T>,
Source§type Handle<'a> = DataHandle
where
T: 'a
type Handle<'a> = DataHandle where T: 'a
.key() / .width() chaining.Source§fn add_to(self, collector: &mut DataChildren<T>) -> DataHandle
fn add_to(self, collector: &mut DataChildren<T>) -> DataHandle
.key() and .width().Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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