Skip to content

Show it while data loads

The hourglass keeps no state. The app keeps one thing, the Instant the wait started, and builds a fresh Hourglass from started.elapsed() every frame. Two rules make it look right:

  • Redraw on a timer while it shows, about every 50 ms, not only on input: the frames change every 200 to 430 ms and the shimmer moves faster.
  • Keep the same start for the whole wait. A new Instant restarts the glass at the top; a redraw at any moment with the same start lands on the same picture.

This app loads its data on a thread, shows the hourglass until the data arrives, then shows the data. Esc or q quits at any time.

use std::{
io,
sync::mpsc,
thread,
time::{Duration, Instant},
};
use pito_hourglass::Hourglass;
use ratatui::{
DefaultTerminal, Frame,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
style::{Color, Modifier, Style},
widgets::Paragraph,
};
fn main() -> io::Result<()> {
ratatui::run(run)
}
fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
let (done, loaded) = mpsc::channel();
thread::spawn(move || {
thread::sleep(Duration::from_secs(5));
let _ = done.send(String::from("42 rows loaded"));
});
let started = Instant::now();
let mut data = None;
loop {
if data.is_none() {
data = loaded.try_recv().ok();
}
terminal.draw(|frame| draw(frame, started, data.as_deref()))?;
if event::poll(Duration::from_millis(50))?
&& let Event::Key(key) = event::read()?
&& key.kind == KeyEventKind::Press
&& matches!(key.code, KeyCode::Esc | KeyCode::Char('q'))
{
return Ok(());
}
}
}
fn draw(frame: &mut Frame, started: Instant, data: Option<&str>) {
match data {
Some(data) => frame.render_widget(Paragraph::new(data), frame.area()),
None => frame.render_widget(
Hourglass::new(started.elapsed())
.label("Loading the data...")
.hint(Some("esc stops waiting"))
.glass(Style::new().fg(Color::Yellow))
.accent(Style::new().fg(Color::Magenta))
.muted(Style::new().fg(Color::DarkGray).add_modifier(Modifier::DIM)),
frame.area(),
),
}
}

event::poll with a 50 ms timeout is the redraw timer: it returns early on a key, and otherwise the loop draws the next picture. The crossterm it uses is ratatui’s own (ratatui::crossterm, ratatui’s default crossterm feature); pito-hourglass itself brings no backend.

The widget draws into whatever Rect it’s given and centres itself there, so it can fill a pane while the rest of the screen stays live:

use std::time::Instant;
use pito_hourglass::Hourglass;
use ratatui::{
Frame,
layout::{Constraint, Layout},
widgets::{Block, Paragraph},
};
fn draw(frame: &mut Frame, started: Instant) {
let [list, detail] =
Layout::horizontal([Constraint::Length(30), Constraint::Fill(1)]).areas(frame.area());
frame.render_widget(Paragraph::new("inbox\narchive").block(Block::bordered()), list);
let pane = Block::bordered().title("Message");
let inner = pane.inner(detail);
frame.render_widget(pane, detail);
frame.render_widget(Hourglass::new(started.elapsed()).label("Opening..."), inner);
}

What it draws when the pane is small is in Small areas.

Hourglass is a 96-byte Copy value and a Widget both by value and by reference, so an app can build it once a frame and render &hourglass if it needs the value again afterwards.