Skip to content
This repository has been archived by the owner on Aug 6, 2023. It is now read-only.

Commit

Permalink
feat!(terminal): inline viewport
Browse files Browse the repository at this point in the history
  • Loading branch information
fdehau committed Nov 1, 2021
1 parent ca68bae commit 0187ec4
Show file tree
Hide file tree
Showing 10 changed files with 599 additions and 103 deletions.
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ default = ["termion"]
curses = ["easycurses", "pancurses"]

[dependencies]
tracing = "0.1"
bitflags = "1.3"
cassowary = "0.3"
unicode-segmentation = "1.2"
Expand All @@ -33,6 +34,7 @@ pancurses = { version = "0.16.1", optional = true, features = ["win32a"] }
serde = { version = "1", "optional" = true, features = ["derive"]}

[dev-dependencies]
tracing-subscriber = "0.2"
rand = "0.8"
argh = "0.1"

Expand Down Expand Up @@ -125,3 +127,8 @@ required-features = ["crossterm"]
name = "curses_demo"
path = "examples/curses_demo.rs"
required-features = ["curses"]

[[example]]
name = "inline"
path = "examples/inline.rs"
required-features = ["crossterm"]
303 changes: 303 additions & 0 deletions examples/inline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
use rand::distributions::{Distribution, Uniform};
use std::{
collections::{BTreeMap, VecDeque},
error::Error,
io,
sync::mpsc,
thread,
time::{Duration, Instant},
};
use tracing::{event, span, Level};
use tui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
symbols,
text::{Span, Spans},
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
Terminal, TerminalOptions, ViewportVariant,
};

const NUM_DOWNLOADS: usize = 10;

type DownloadId = usize;
type WorkerId = usize;

enum Event {
Input(crossterm::event::KeyEvent),
Tick,
Resize,
DownloadUpdate(WorkerId, DownloadId, f64),
DownloadDone(WorkerId, DownloadId),
}

struct Downloads {
pending: VecDeque<Download>,
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
}

impl Downloads {
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
match self.pending.pop_front() {
Some(d) => {
self.in_progress.insert(
worker_id,
DownloadInProgress {
id: d.id,
started_at: Instant::now(),
progress: 0.0,
},
);
Some(d)
}
None => None,
}
}
}

struct DownloadInProgress {
id: DownloadId,
started_at: Instant,
progress: f64,
}

struct Download {
id: DownloadId,
size: usize,
}

struct Worker {
id: WorkerId,
tx: mpsc::Sender<Download>,
}

fn main() -> Result<(), Box<dyn Error>> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_writer(io::stderr)
.init();

crossterm::terminal::enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: ViewportVariant::Inline(8),
},
)?;

let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
let mut downloads = downloads();

for w in &workers {
let d = downloads.next(w.id).unwrap();
w.tx.send(d).unwrap();
}

let mut redraw = true;
loop {
if redraw {
ui(&mut terminal);
}
redraw = true;

let span = span!(Level::INFO, "recv");
let _guard = span.enter();
match rx.recv()? {
Event::Input(event) => {
if event.code == crossterm::event::KeyCode::Char('q') {
break;
}
}
Event::Resize => {
event!(Level::INFO, "resize");
terminal.resize()?;
}
Event::Tick => {
event!(Level::INFO, "tick");
}
Event::DownloadUpdate(worker_id, download_id, progress) => {
event!(
Level::INFO,
worker_id,
download_id,
progress,
"download update"
);
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
download.progress = progress;
redraw = false
}
Event::DownloadDone(worker_id, download_id) => {
event!(Level::INFO, worker_id, download_id, "download done");
let download = downloads.in_progress.remove(&worker_id).unwrap();
terminal.insert_before(1, |buf| {
Paragraph::new(Spans::from(vec![
Span::from("Finished "),
Span::styled(
format!("download {}", download_id),
Style::default().add_modifier(Modifier::BOLD),
),
Span::from(format!(
" in {}ms",
download.started_at.elapsed().as_millis()
)),
]))
.render(area, buf);
})?;
match downloads.next(worker_id) {
Some(d) => workers[worker_id].tx.send(d).unwrap(),
None => {
if downloads.in_progress.is_empty() {
terminal.insert_before(1, |area, buffer| {
Paragraph::new("Done !").render(area, buffer);
})?;
break;
}
}
};
}
};
}

crossterm::terminal::disable_raw_mode()?;
terminal.clear()?;

Ok(())
}

fn input_handling(tx: mpsc::Sender<Event>) {
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout).unwrap() {
match crossterm::event::read().unwrap() {
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
_ => {}
};
}
if last_tick.elapsed() >= tick_rate {
tx.send(Event::Tick).unwrap();
last_tick = Instant::now();
}
}
});
}

fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
(0..4)
.map(|id| {
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
let tx = tx.clone();
thread::spawn(move || {
while let Ok(download) = worker_rx.recv() {
let mut remaining = download.size;
while remaining > 0 {
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))
.unwrap();
}
tx.send(Event::DownloadDone(id, download.id)).unwrap();
}
});
Worker { id, tx: worker_tx }
})
.collect()
}

fn downloads() -> Downloads {
let distribution = Uniform::new(0, 1000);
let mut rng = rand::thread_rng();
let pending = (0..NUM_DOWNLOADS)
.map(|id| {
let size = distribution.sample(&mut rng);
Download { id, size }
})
.collect();
Downloads {
pending,
in_progress: BTreeMap::new(),
}
}

fn ui(terminal: &mut Terminal<CrosstermBackend>) {
terminal.draw(|f| {
let size = f.size();

let block = Block::default()
.title("Progress")
.title_alignment(Alignment::Center);
f.render_widget(block, size);

let chunks = Layout::default()
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
.margin(1)
.split(size);

// total progress
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
let progress = LineGauge::default()
.gauge_style(Style::default().fg(Color::Blue))
.label(format!("{}/{}", done, NUM_DOWNLOADS))
.ratio(done as f64 / NUM_DOWNLOADS as f64);
f.render_widget(progress, chunks[0]);

let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
.split(chunks[1]);

// in progress downloads
let items: Vec<ListItem> = downloads
.in_progress
.iter()
.map(|(worker_id, download)| {
ListItem::new(Spans::from(vec![
Span::raw(symbols::DOT),
Span::styled(
format!(" download {:>2}", download.id),
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
),
Span::raw(format!(
" ({}ms)",
download.started_at.elapsed().as_millis()
)),
]))
})
.collect();
let list = List::new(items);
f.render_widget(list, chunks[0]);

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 chunks[1].top().saturating_add(i as u16) > size.bottom() {
continue;
}
f.render_widget(
gauge,
Rect {
x: chunks[1].left(),
y: chunks[1].top().saturating_add(i as u16),
width: chunks[1].width,
height: 1,
},
);
}
})?;
}
Loading

0 comments on commit 0187ec4

Please sign in to comment.