Skip to content

Commit

Permalink
feat: task table widget (#7470)
Browse files Browse the repository at this point in the history
### Description

Implementation of the "task table" component of our new UI. The table is
comprised of the task names and the task status which displays an ASCII
representation of when a task started and finished.

### Testing Instructions

Added some unit testing for stateful parts of task table and the task
duration rendering. I held off on the unit testing of the task table
rendering as I expect us to change styling a bit.

Added example binary that fakes some task execution, this if meant to
take place of unit test for rendering. Please play around with it!


https://github.com/vercel/turbo/assets/4131117/03ced974-d676-425e-b7a4-edacfab2a4a2



Closes TURBO-2419
  • Loading branch information
chris-olszewski committed Feb 27, 2024
1 parent 3c37b2a commit 8defac2
Show file tree
Hide file tree
Showing 9 changed files with 825 additions and 7 deletions.
132 changes: 125 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/turborepo-ui/Cargo.toml
Expand Up @@ -7,6 +7,7 @@ license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dev-dependencies]
anyhow = { workspace = true }
indoc.workspace = true
tempfile = { workspace = true }
test-case = { workspace = true }

Expand All @@ -16,8 +17,10 @@ workspace = true
[dependencies]
atty = { workspace = true }
console = { workspace = true }
crossterm = "0.26.1"
indicatif = { workspace = true }
lazy_static = { workspace = true }
ratatui = "0.26.1"
thiserror = { workspace = true }
tracing = { workspace = true }
turbopath = { workspace = true }
Expand Down
131 changes: 131 additions & 0 deletions crates/turborepo-ui/examples/table.rs
@@ -0,0 +1,131 @@
use std::{error::Error, io, sync::mpsc, time::Duration};

use crossterm::{
event::{KeyCode, KeyModifiers},
terminal::{disable_raw_mode, enable_raw_mode},
};
use ratatui::prelude::*;
use turborepo_ui::TaskTable;

enum Event {
Tick(u64),
Start(&'static str),
Finish(&'static str),
Up,
Down,
Stop,
}

fn main() -> Result<(), Box<dyn Error>> {
enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);

let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(8),
},
)?;

let (tx, rx) = mpsc::sync_channel(1);
let input_tx = tx.clone();
// Thread forwards user input
let input = std::thread::spawn(move || handle_input(input_tx));
// Thread simulates starting/finishing of tasks
let events = std::thread::spawn(move || send_events(tx));

let table = TaskTable::new((0..6).map(|i| format!("task_{i}")));

run_app(&mut terminal, table, rx)?;

events.join().expect("event thread panicked");
input.join().expect("input thread panicked")?;

// restore terminal
disable_raw_mode()?;
terminal.show_cursor()?;
println!();

Ok(())
}

fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut table: TaskTable,
rx: mpsc::Receiver<Event>,
) -> io::Result<()> {
while let Ok(event) = rx.recv() {
match event {
Event::Tick(_) => {
table.tick();
}
Event::Start(task) => table.start_task(task).unwrap(),
Event::Finish(task) => table.finish_task(task).unwrap(),
Event::Up => table.previous(),
Event::Down => table.next(),
Event::Stop => break,
}
terminal.draw(|f| table.stateful_render(f))?;
}

Ok(())
}

fn send_events(tx: mpsc::SyncSender<Event>) {
let mut events = vec![
Event::Start("task_0"),
Event::Start("task_1"),
Event::Tick(10),
Event::Start("task_2"),
Event::Tick(30),
Event::Start("task_3"),
Event::Finish("task_2"),
Event::Tick(30),
Event::Start("task_4"),
Event::Finish("task_0"),
Event::Tick(10),
Event::Finish("task_1"),
Event::Start("task_5"),
Event::Tick(30),
Event::Finish("task_3"),
Event::Finish("task_4"),
Event::Tick(50),
Event::Finish("task_5"),
Event::Stop,
];
events.reverse();
while let Some(event) = events.pop() {
if let Event::Tick(ticks) = event {
std::thread::sleep(Duration::from_millis(50 * ticks));
}
if tx.send(event).is_err() {
break;
}
}
}

fn handle_input(tx: mpsc::SyncSender<Event>) -> std::io::Result<()> {
loop {
if crossterm::event::poll(Duration::from_millis(10))? {
let event = crossterm::event::read()?;
if let crossterm::event::Event::Key(key_event) = event {
if let Some(event) = match key_event.code {
KeyCode::Up => Some(Event::Up),
KeyCode::Down => Some(Event::Down),
KeyCode::Char('c') if key_event.modifiers == KeyModifiers::CONTROL => {
Some(Event::Stop)
}
_ => None,
} {
if tx.send(event).is_err() {
break;
}
}
}
} else if tx.send(Event::Tick(0)).is_err() {
break;
}
}
Ok(())
}

0 comments on commit 8defac2

Please sign in to comment.