//! # [Ratatui] Inline example
//! The latest version of this example is available in the [examples] folder in the repository.
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
collections::{BTreeMap, VecDeque},
time::{Duration, Instant},
use rand::distributions::{Distribution, Uniform};
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Modifier, Style},
widgets::{block, Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Frame, Terminal, TerminalOptions, Viewport,
fn main() -> Result<()> {
let mut terminal = ratatui::init_with_options(TerminalOptions {
viewport: Viewport::Inline(8),
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();
let d = downloads.next(w.id).unwrap();
let app_result = run(&mut terminal, workers, downloads, rx);
const NUM_DOWNLOADS: usize = 10;
DownloadUpdate(WorkerId, DownloadId, f64),
DownloadDone(WorkerId, DownloadId),
pending: VecDeque<Download>,
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
match self.pending.pop_front() {
started_at: Instant::now(),
struct DownloadInProgress {
tx: mpsc::Sender<Download>,
fn input_handling(tx: mpsc::Sender<Event>) {
let tick_rate = Duration::from_millis(200);
let mut last_tick = Instant::now();
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout).unwrap() {
match event::read().unwrap() {
event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
if last_tick.elapsed() >= tick_rate {
tx.send(Event::Tick).unwrap();
last_tick = Instant::now();
#[allow(clippy::cast_precision_loss, clippy::needless_pass_by_value)]
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
while let Ok(download) = worker_rx.recv() {
let mut remaining = download.size;
let wait = (remaining as u64).min(10);
thread::sleep(Duration::from_millis(wait * 10));
remaining = remaining.saturating_sub(10);
let progress = (download.size - remaining) * 100 / download.size;
tx.send(Event::DownloadUpdate(id, download.id, progress as f64))
tx.send(Event::DownloadDone(id, download.id)).unwrap();
Worker { id, tx: worker_tx }
fn downloads() -> Downloads {
let distribution = Uniform::new(0, 1000);
let mut rng = rand::thread_rng();
let pending = (0..NUM_DOWNLOADS)
let size = distribution.sample(&mut rng);
in_progress: BTreeMap::new(),
#[allow(clippy::needless_pass_by_value)]
terminal: &mut Terminal<impl Backend>,
mut downloads: Downloads,
rx: mpsc::Receiver<Event>,
terminal.draw(|frame| draw(frame, &downloads))?;
if event.code == event::KeyCode::Char('q') {
Event::DownloadUpdate(worker_id, _download_id, progress) => {
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
download.progress = progress;
Event::DownloadDone(worker_id, download_id) => {
let download = downloads.in_progress.remove(&worker_id).unwrap();
terminal.insert_before(1, |buf| {
Paragraph::new(Line::from(vec![
format!("download {download_id}"),
Style::default().add_modifier(Modifier::BOLD),
download.started_at.elapsed().as_millis()
match downloads.next(worker_id) {
Some(d) => workers[worker_id].tx.send(d).unwrap(),
if downloads.in_progress.is_empty() {
terminal.insert_before(1, |buf| {
Paragraph::new("Done !").render(buf.area, buf);
fn draw(frame: &mut Frame, downloads: &Downloads) {
let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center));
frame.render_widget(block, area);
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
let [progress_area, main] = vertical.areas(area);
let [list_area, gauge_area] = horizontal.areas(main);
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
#[allow(clippy::cast_precision_loss)]
let progress = LineGauge::default()
.filled_style(Style::default().fg(Color::Blue))
.label(format!("{done}/{NUM_DOWNLOADS}"))
.ratio(done as f64 / NUM_DOWNLOADS as f64);
frame.render_widget(progress, progress_area);
let items: Vec<ListItem> = downloads
ListItem::new(Line::from(vec![
format!(" download {:>2}", download.id),
.add_modifier(Modifier::BOLD),
download.started_at.elapsed().as_millis()
let list = List::new(items);
frame.render_widget(list, list_area);
#[allow(clippy::cast_possible_truncation)]
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
let gauge = Gauge::default()
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(download.progress / 100.0);
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
y: gauge_area.top().saturating_add(i as u16),