Quickstart
Six copy-paste templates. Each picture is one frame from running that example. The guide is Getting started.
Minimal
cargo run --example template_minimal
A titled box. Press q to quit.
// Copyright (c) 2026 Moonbase 2090 LLC
// SPDX-License-Identifier: MIT
#![doc = include_str!("../README.md")]
use std::env;
use std::io::{self, Write};
use lunatui_core::{ClusterTable, Grid, Rect, Style};
use lunatui_runtime::{App, ControlFlow, RunOptions, is_quit, render_once, run};
use lunatui_term::Event;
use lunatui_theme::{Theme, Token, Variant};
use lunatui_widgets::{Block, Paragraph, RenderCx, Widget};
const WIDTH: u16 = 48;
const HEIGHT: u16 = 10;
struct Hello;
impl App for Hello {
fn update(&mut self, event: &Event) -> ControlFlow {
if is_quit(event) {
ControlFlow::Break
} else {
ControlFlow::Continue
}
}
fn render(&mut self, grid: &mut Grid, area: Rect, clusters: &mut ClusterTable) {
let theme = Theme::builtin(Variant::Dark);
let mut cx = RenderCx::new(grid, clusters, &theme);
let text = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface0));
let accent = Style::new()
.fg(cx.token(Token::Accent))
.bg(cx.token(Token::Surface0));
let block = Block::bordered()
.title(" hello ")
.style(text)
.border_style(accent)
.title_style(accent);
block.render(area, &mut cx);
Paragraph::new("Hello. Press q to quit.")
.style(text)
.render(block.inner(area), &mut cx);
}
}
fn main() -> io::Result<()> {
let mut app = Hello;
if env::args().any(|arg| arg == "--snapshot") {
let (grid, clusters) = render_once(&mut app, WIDTH, HEIGHT);
io::stdout().write_all(grid.display_text(&clusters).as_bytes())?;
return Ok(());
}
run(app, RunOptions::new(WIDTH, HEIGHT))
}
List
cargo run --example template_list
Up and down move the selection.
// Copyright (c) 2026 Moonbase 2090 LLC
// SPDX-License-Identifier: MIT
#![doc = include_str!("../README.md")]
use std::env;
use std::io::{self, Write};
use lunatui_core::{ClusterTable, Grid, Rect, Style};
use lunatui_runtime::{App, ControlFlow, RunOptions, is_quit, render_once, run};
use lunatui_term::{Event, KeyCode};
use lunatui_theme::{Theme, Token, Variant};
use lunatui_widgets::{Block, List, Paragraph, RenderCx, VecSource, Widget};
const WIDTH: u16 = 42;
const HEIGHT: u16 = 14;
const ITEMS: [&str; 5] = ["Calendar", "Gauge", "Table", "Sparkline", "Form"];
struct Screen {
selected: usize,
}
impl Screen {
fn new() -> Self {
Self { selected: 1 }
}
fn move_selection(&mut self, later: bool) {
if later {
self.selected = self
.selected
.saturating_add(1)
.min(ITEMS.len().saturating_sub(1));
} else {
self.selected = self.selected.saturating_sub(1);
}
}
fn source() -> VecSource {
let mut source = VecSource::new();
for item in ITEMS {
source.push_row(vec![String::from(item)]);
}
source
}
}
impl App for Screen {
fn update(&mut self, event: &Event) -> ControlFlow {
if is_quit(event) {
return ControlFlow::Break;
}
if let Event::Key(key) = event {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.move_selection(false),
KeyCode::Down | KeyCode::Char('j') => self.move_selection(true),
_ => {}
}
}
ControlFlow::Continue
}
fn render(&mut self, grid: &mut Grid, area: Rect, clusters: &mut ClusterTable) {
let theme = Theme::builtin(Variant::Dark);
let mut cx = RenderCx::new(grid, clusters, &theme);
let text = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface0));
let accent = Style::new()
.fg(cx.token(Token::Accent))
.bg(cx.token(Token::Surface0));
let raised = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface1));
Block::new().style(text).render(area, &mut cx);
let list_area = Rect::new(0, 0, area.width, area.height.saturating_sub(1));
let block = Block::bordered()
.title(" items ")
.style(text)
.border_style(accent)
.title_style(accent);
block.render(list_area, &mut cx);
List::new(Self::source())
.selected(Some(self.selected))
.style(text)
.highlight_style(raised)
.highlight_symbol("> ", accent)
.render(block.inner(list_area), &mut cx);
let hint = Rect::new(
1,
area.height.saturating_sub(1),
area.width.saturating_sub(2),
1,
);
Paragraph::new("Up and down move the row. q quits.")
.style(text)
.render(hint, &mut cx);
}
}
fn main() -> io::Result<()> {
let mut app = Screen::new();
if env::args().any(|arg| arg == "--snapshot") {
let (grid, clusters) = render_once(&mut app, WIDTH, HEIGHT);
io::stdout().write_all(grid.display_text(&clusters).as_bytes())?;
return Ok(());
}
run(app, RunOptions::new(WIDTH, HEIGHT))
}
Form
cargo run --example template_form
Tab moves focus. Typed keys edit the focused field.
// Copyright (c) 2026 Moonbase 2090 LLC
// SPDX-License-Identifier: MIT
#![doc = include_str!("../README.md")]
use std::env;
use std::io::{self, Write};
use lunatui_core::{ClusterTable, Grid, Rect, Style};
use lunatui_runtime::{App, ControlFlow, RunOptions, is_quit, render_once, run};
use lunatui_term::{Event, KeyCode};
use lunatui_theme::{Theme, Token, Variant};
use lunatui_widgets::{Block, Form, FormField, RenderCx, TextInput, Widget};
const WIDTH: u16 = 56;
const HEIGHT: u16 = 12;
struct Screen {
name: String,
role: String,
focus: usize,
}
impl Screen {
fn new() -> Self {
Self {
name: String::from("Ada"),
role: String::new(),
focus: 0,
}
}
fn form(&self) -> Form {
Form::new([
FormField::new("Name").required(),
FormField::new("Role").hint("shown on the card"),
])
.focus(Some(self.focus))
}
fn field_mut(&mut self) -> &mut String {
if self.focus == 0 {
&mut self.name
} else {
&mut self.role
}
}
}
impl App for Screen {
fn update(&mut self, event: &Event) -> ControlFlow {
if is_quit(event) {
return ControlFlow::Break;
}
if let Event::Key(key) = event {
match key.code {
KeyCode::Tab | KeyCode::Down => {
if let Some(next) = self.form().step(Some(self.focus), true) {
self.focus = next;
}
}
KeyCode::Up => {
if let Some(next) = self.form().step(Some(self.focus), false) {
self.focus = next;
}
}
KeyCode::Backspace => {
self.field_mut().pop();
}
KeyCode::Char(ch) if ch.is_ascii_graphic() => {
let field = self.field_mut();
if field.len() < 24 {
field.push(ch);
}
}
_ => {}
}
}
ControlFlow::Continue
}
fn render(&mut self, grid: &mut Grid, area: Rect, clusters: &mut ClusterTable) {
let theme = Theme::builtin(Variant::Dark);
let mut cx = RenderCx::new(grid, clusters, &theme);
let text = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface0));
let accent = Style::new()
.fg(cx.token(Token::Accent))
.bg(cx.token(Token::Surface0));
let block = Block::bordered()
.title(" account ")
.style(text)
.border_style(accent)
.title_style(accent);
block.render(area, &mut cx);
let inner = block.inner(area);
let form = self.form();
form.render(inner, &mut cx);
for field in form.layout(inner) {
let value = if field.index == 0 {
self.name.as_str()
} else {
self.role.as_str()
};
if field.input.width == 0 || field.input.height == 0 {
continue;
}
TextInput::new(value, value.len())
.show_cursor(field.index == self.focus)
.style(text)
.field_style(text)
.render(field.input, &mut cx);
}
}
}
fn main() -> io::Result<()> {
let mut app = Screen::new();
if env::args().any(|arg| arg == "--snapshot") {
let (grid, clusters) = render_once(&mut app, WIDTH, HEIGHT);
io::stdout().write_all(grid.display_text(&clusters).as_bytes())?;
return Ok(());
}
run(app, RunOptions::new(WIDTH, HEIGHT))
}
Dashboard
cargo run --example template_dashboard
A sparkline, a gauge, and a table. The samples advance while it runs.
// Copyright (c) 2026 Moonbase 2090 LLC
// SPDX-License-Identifier: MIT
#![doc = include_str!("../README.md")]
use std::env;
use std::io::{self, Write};
use std::time::Instant;
use lunatui_core::{ClusterTable, Grid, Rect, Style};
use lunatui_layout::Constraint;
use lunatui_runtime::{App, ControlFlow, RunOptions, is_quit, render_once, run};
use lunatui_term::Event;
use lunatui_theme::{Theme, Token, Variant};
use lunatui_widgets::{Block, Gauge, RenderCx, Sparkline, Table, VecSource, Widget};
const WIDTH: u16 = 72;
const HEIGHT: u16 = 22;
struct Screen {
started: Instant,
}
impl Screen {
fn new() -> Self {
Self {
started: Instant::now(),
}
}
fn tick(&self) -> u64 {
u64::try_from(self.started.elapsed().as_millis() / 200).unwrap_or(0)
}
fn samples(&self) -> [u64; 48] {
const TRAFFIC: [u64; 48] = [
14, 15, 16, 18, 22, 27, 24, 19, 17, 16, 18, 21, 29, 36, 33, 26, 22, 19, 18, 17, 16, 18,
20, 23, 21, 19, 17, 15, 16, 19, 25, 31, 28, 23, 20, 18, 17, 16, 18, 22, 26, 24, 21, 19,
17, 16, 15, 14,
];
let shift = usize::try_from(self.tick() % 48).unwrap_or(0);
let mut samples = [0; 48];
for (index, sample) in samples.iter_mut().enumerate() {
let src = (index + shift) % TRAFFIC.len();
*sample = TRAFFIC.get(src).copied().unwrap_or(0);
}
samples
}
fn ratio(&self) -> f32 {
let step = u8::try_from(self.tick() % 8).unwrap_or(0);
0.25 + f32::from(step) * 0.08
}
fn table() -> VecSource {
let mut source = VecSource::new();
for (service, state, latency) in [
("api", "ok", "12 ms"),
("worker", "warn", "40 ms"),
("edge", "ok", "9 ms"),
] {
source.push_row(vec![
String::from(service),
String::from(state),
String::from(latency),
]);
}
source
}
}
impl App for Screen {
fn update(&mut self, event: &Event) -> ControlFlow {
if is_quit(event) {
ControlFlow::Break
} else {
ControlFlow::Continue
}
}
fn render(&mut self, grid: &mut Grid, area: Rect, clusters: &mut ClusterTable) {
let theme = Theme::builtin(Variant::Dark);
let mut cx = RenderCx::new(grid, clusters, &theme);
let text = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface0));
let accent = Style::new()
.fg(cx.token(Token::Accent))
.bg(cx.token(Token::Surface0));
let warn = Style::new()
.fg(cx.token(Token::Warn))
.bg(cx.token(Token::Surface0));
let raised = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface1));
let rows = lunatui_layout::Layout::vertical([
Constraint::Length(8),
Constraint::Length(3),
Constraint::Fill(1),
])
.split(area);
let Some(spark_area) = rows.segments.first().copied() else {
return;
};
let Some(gauge_area) = rows.segments.get(1).copied() else {
return;
};
let Some(table_area) = rows.segments.get(2).copied() else {
return;
};
let spark = Block::bordered()
.title(" requests ")
.style(text)
.border_style(accent)
.title_style(accent);
spark.render(spark_area, &mut cx);
let samples = self.samples();
Sparkline::new(&samples)
.style(accent)
.render(spark.inner(spark_area), &mut cx);
let gauge = Block::bordered()
.title(" capacity ")
.style(text)
.border_style(accent)
.title_style(accent);
gauge.render(gauge_area, &mut cx);
Gauge::new(self.ratio())
.gauge_style(accent)
.track_style(text)
.render(gauge.inner(gauge_area), &mut cx);
let table = Block::bordered()
.title(" services ")
.style(text)
.border_style(accent)
.title_style(accent);
table.render(table_area, &mut cx);
let selected = usize::try_from(self.tick() % 3).unwrap_or(0);
Table::new(
Self::table(),
vec![
Constraint::Length(12),
Constraint::Length(8),
Constraint::Fill(1),
],
)
.header([("service", text), ("state", text), ("latency", text)])
.selected(Some(selected))
.style(text)
.highlight_style(if selected == 1 { warn } else { raised })
.render(table.inner(table_area), &mut cx);
}
fn needs_animation_frames(&self) -> bool {
true
}
}
fn main() -> io::Result<()> {
let mut app = Screen::new();
if env::args().any(|arg| arg == "--snapshot") {
let (grid, clusters) = render_once(&mut app, WIDTH, HEIGHT);
io::stdout().write_all(grid.display_text(&clusters).as_bytes())?;
return Ok(());
}
run(app, RunOptions::new(WIDTH, HEIGHT))
}
Popover
cargo run --example template_popover
A panel beside an anchor. Enter toggles it.
// Copyright (c) 2026 Moonbase 2090 LLC
// SPDX-License-Identifier: MIT
#![doc = include_str!("../README.md")]
use std::env;
use std::io::{self, Write};
use lunatui_core::{ClusterTable, Grid, Rect, Style};
use lunatui_runtime::{App, ControlFlow, RunOptions, is_quit, render_once, run};
use lunatui_term::{Event, KeyCode};
use lunatui_theme::{Theme, Token, Variant};
use lunatui_widgets::{Block, Paragraph, Popover, RenderCx, Size, Widget};
const WIDTH: u16 = 56;
const HEIGHT: u16 = 16;
struct Screen {
open: bool,
}
impl Screen {
fn new() -> Self {
Self { open: true }
}
}
impl App for Screen {
fn update(&mut self, event: &Event) -> ControlFlow {
if is_quit(event) {
return ControlFlow::Break;
}
if let Event::Key(key) = event {
match key.code {
KeyCode::Enter => self.open = !self.open,
KeyCode::Esc => self.open = false,
_ => {}
}
}
ControlFlow::Continue
}
fn render(&mut self, grid: &mut Grid, area: Rect, clusters: &mut ClusterTable) {
let theme = Theme::builtin(Variant::Dark);
let mut cx = RenderCx::new(grid, clusters, &theme);
let text = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface0));
let accent = Style::new()
.fg(cx.token(Token::Accent))
.bg(cx.token(Token::Surface0));
Block::new().style(text).render(area, &mut cx);
let anchor = Rect::new(2, 1, 16, 3);
let button = Block::bordered()
.title(" views ")
.style(text)
.border_style(accent)
.title_style(accent);
button.render(anchor, &mut cx);
if self.open {
Popover::new(Paragraph::new("Open items\nAssigned to me\nClosed"), anchor)
.title(" filters ")
.size(Size::new(20, 5))
.style(text)
.border_style(accent)
.title_style(accent)
.render(area, &mut cx);
}
let hint = Rect::new(
2,
area.height.saturating_sub(2),
area.width.saturating_sub(4),
1,
);
Paragraph::new("Enter toggles the panel. q quits.")
.style(text)
.render(hint, &mut cx);
}
}
fn main() -> io::Result<()> {
let mut app = Screen::new();
if env::args().any(|arg| arg == "--snapshot") {
let (grid, clusters) = render_once(&mut app, WIDTH, HEIGHT);
io::stdout().write_all(grid.display_text(&clusters).as_bytes())?;
return Ok(());
}
run(app, RunOptions::new(WIDTH, HEIGHT))
}
Layout
cargo run --example template_layout
Header, sidebar, body, and a status bar.
// Copyright (c) 2026 Moonbase 2090 LLC
// SPDX-License-Identifier: MIT
#![doc = include_str!("../README.md")]
use std::env;
use std::io::{self, Write};
use lunatui_core::{ClusterTable, Grid, Rect, Style};
use lunatui_layout::{Constraint, Layout};
use lunatui_runtime::{App, ControlFlow, RunOptions, is_quit, render_once, run};
use lunatui_term::Event;
use lunatui_theme::{Theme, Token, Variant};
use lunatui_widgets::{Block, List, Paragraph, RenderCx, StatusBar, VecSource, Widget};
const WIDTH: u16 = 72;
const HEIGHT: u16 = 22;
struct Screen;
impl Screen {
fn nav() -> VecSource {
let mut source = VecSource::new();
for item in ["Templates", "Gallery", "Docs"] {
source.push_row(vec![String::from(item)]);
}
source
}
}
impl App for Screen {
fn update(&mut self, event: &Event) -> ControlFlow {
if is_quit(event) {
ControlFlow::Break
} else {
ControlFlow::Continue
}
}
fn render(&mut self, grid: &mut Grid, area: Rect, clusters: &mut ClusterTable) {
let theme = Theme::builtin(Variant::Dark);
let mut cx = RenderCx::new(grid, clusters, &theme);
let text = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface0));
let accent = Style::new()
.fg(cx.token(Token::Accent))
.bg(cx.token(Token::Surface0));
let raised = Style::new()
.fg(cx.token(Token::TextPrimary))
.bg(cx.token(Token::Surface1));
let rows = Layout::vertical([
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(1),
])
.split(area);
let Some(header) = rows.segments.first().copied() else {
return;
};
let Some(middle) = rows.segments.get(1).copied() else {
return;
};
let Some(status) = rows.segments.get(2).copied() else {
return;
};
let columns =
Layout::horizontal([Constraint::Length(22), Constraint::Fill(1)]).split(middle);
let Some(sidebar) = columns.segments.first().copied() else {
return;
};
let Some(body) = columns.segments.get(1).copied() else {
return;
};
let header_block = Block::bordered()
.title(" header ")
.style(text)
.border_style(accent)
.title_style(accent);
header_block.render(header, &mut cx);
Paragraph::new("Quickstart")
.style(text)
.render(header_block.inner(header), &mut cx);
let side_block = Block::bordered()
.title(" sidebar ")
.style(text)
.border_style(accent)
.title_style(accent);
side_block.render(sidebar, &mut cx);
List::new(Self::nav())
.selected(Some(0))
.style(text)
.highlight_style(raised)
.render(side_block.inner(sidebar), &mut cx);
let body_block = Block::bordered()
.title(" body ")
.style(text)
.border_style(accent)
.title_style(accent);
body_block.render(body, &mut cx);
Paragraph::new("The body fills the space beside the sidebar.")
.style(text)
.render(body_block.inner(body), &mut cx);
StatusBar::new(" ready")
.style(raised)
.left([("templates", accent)])
.render(status, &mut cx);
}
}
fn main() -> io::Result<()> {
let mut app = Screen;
if env::args().any(|arg| arg == "--snapshot") {
let (grid, clusters) = render_once(&mut app, WIDTH, HEIGHT);
io::stdout().write_all(grid.display_text(&clusters).as_bytes())?;
return Ok(());
}
run(app, RunOptions::new(WIDTH, HEIGHT))
}