Skip to content

Commit

Permalink
feat(ui): layout change (#8108)
Browse files Browse the repository at this point in the history
### Description

Change the layout of the UI display better on smaller terminals.
- Removal of task timeline in favor of spinners and icons for finished
tasks
 - Move task list to sidebar

This PR also removes persisting writer codepaths as those are no-ops now
that we aren't using an inline view.


### Testing Instructions



https://github.com/vercel/turbo/assets/4131117/10fc9f99-8772-4e03-bc83-6ae9e267f0cd



Closes TURBO-3023
  • Loading branch information
chris-olszewski committed May 9, 2024
1 parent 544a1d0 commit c3851a5
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 366 deletions.
3 changes: 2 additions & 1 deletion crates/turborepo-lib/src/task_graph/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,8 @@ impl<W: Write> TaskOutput<W> {
pub fn finish(self, use_error: bool) -> std::io::Result<Option<Vec<u8>>> {
match self {
TaskOutput::Direct(client) => client.finish(use_error),
TaskOutput::UI(client) => Ok(Some(client.finish())),
TaskOutput::UI(client) if use_error => Ok(Some(client.failed())),
TaskOutput::UI(client) => Ok(Some(client.succeeded())),
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/turborepo-ui/examples/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode},
};
use ratatui::prelude::*;
use turborepo_ui::TaskTable;
use turborepo_ui::{tui::event::TaskResult, TaskTable};

enum Event {
Tick(u64),
Expand Down Expand Up @@ -61,7 +61,7 @@ fn run_app<B: Backend>(
table.tick();
}
Event::Start(task) => table.start_task(task).unwrap(),
Event::Finish(task) => table.finish_task(task).unwrap(),
Event::Finish(task) => table.finish_task(task, TaskResult::Success).unwrap(),
Event::Up => table.previous(),
Event::Down => table.next(),
Event::Stop => break,
Expand Down
78 changes: 16 additions & 62 deletions crates/turborepo-ui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ use std::{
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Layout},
widgets::Widget,
Frame, Terminal,
};
use tracing::debug;
use tui_term::widget::PseudoTerminal;

const DEFAULT_APP_HEIGHT: u16 = 60;
const PANE_SIZE_RATIO: f32 = 3.0 / 4.0;
const FRAMERATE: Duration = Duration::from_millis(3);

Expand Down Expand Up @@ -100,11 +97,17 @@ impl<I: std::io::Write> App<I> {
/// Handle the rendering of the `App` widget based on events received by
/// `receiver`
pub fn run_app(tasks: Vec<String>, receiver: AppReceiver) -> Result<(), Error> {
let (mut terminal, app_height) = startup()?;
let mut terminal = startup()?;
let size = terminal.size()?;

let pane_height = (f32::from(app_height) * PANE_SIZE_RATIO) as u16;
let mut app: App<Box<dyn io::Write + Send>> = App::new(pane_height, size.width, tasks);
// Figure out pane width?
let task_width_hint = TaskTable::width_hint(tasks.iter().map(|s| s.as_str()));
// Want to maximize pane width
let ratio_pane_width = (f32::from(size.width) * PANE_SIZE_RATIO) as u16;
let full_task_width = size.width.saturating_sub(task_width_hint);

let mut app: App<Box<dyn io::Write + Send>> =
App::new(size.height, full_task_width.max(ratio_pane_width), tasks);

let result = run_app_inner(&mut terminal, &mut app, receiver);

Expand All @@ -125,9 +128,7 @@ fn run_app_inner<B: Backend + std::io::Write>(
let mut last_render = Instant::now();

while let Some(event) = poll(app.interact, &receiver, last_render + FRAMERATE) {
if let Some(message) = update(app, event)? {
persist_bytes(terminal, &message)?;
}
update(app, event)?;
if app.done {
break;
}
Expand All @@ -152,7 +153,7 @@ fn poll(interact: bool, receiver: &AppReceiver, deadline: Instant) -> Option<Eve
}

/// Configures terminal for rendering App
fn startup() -> io::Result<(Terminal<CrosstermBackend<Stdout>>, u16)> {
fn startup() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
crossterm::terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
crossterm::execute!(
Expand All @@ -162,9 +163,6 @@ fn startup() -> io::Result<(Terminal<CrosstermBackend<Stdout>>, u16)> {
)?;
let backend = CrosstermBackend::new(stdout);

// We need to reserve at least 1 line for writing persistent lines.
let height = DEFAULT_APP_HEIGHT.min(backend.size()?.height.saturating_sub(1));

let mut terminal = Terminal::with_options(
backend,
ratatui::TerminalOptions {
Expand All @@ -173,7 +171,7 @@ fn startup() -> io::Result<(Terminal<CrosstermBackend<Stdout>>, u16)> {
)?;
terminal.hide_cursor()?;

Ok((terminal, height))
Ok(terminal)
}

/// Restores terminal to expected state
Expand Down Expand Up @@ -214,11 +212,8 @@ fn update(
Event::Tick => {
app.table.tick();
}
Event::Log { message } => {
return Ok(Some(message));
}
Event::EndTask { task } => {
app.table.finish_task(&task)?;
Event::EndTask { task, result } => {
app.table.finish_task(&task, result)?;
}
Event::Up => {
app.previous();
Expand Down Expand Up @@ -249,50 +244,9 @@ fn update(
}

fn view<I>(app: &mut App<I>, f: &mut Frame) {
let (term_height, _) = app.term_size();
let vertical = Layout::vertical([Constraint::Min(5), Constraint::Length(term_height)]);
let (_, width) = app.term_size();
let vertical = Layout::horizontal([Constraint::Fill(1), Constraint::Length(width)]);
let [table, pane] = vertical.areas(f.size());
app.table.stateful_render(f, table);
f.render_widget(&app.pane, pane);
}

/// Write provided bytes to a section of the screen that won't get rewritten
fn persist_bytes(terminal: &mut Terminal<impl Backend>, bytes: &[u8]) -> Result<(), Error> {
let size = terminal.size()?;
let mut parser = turborepo_vt100::Parser::new(size.height, size.width, 128);
parser.process(bytes);
let screen = parser.entire_screen();
let (height, _) = screen.size();
terminal.insert_before(height as u16, |buf| {
PseudoTerminal::new(&screen).render(buf.area, buf)
})?;
Ok(())
}

#[cfg(test)]
mod test {
use ratatui::{backend::TestBackend, buffer::Buffer};

use super::*;

#[test]
fn test_persist_bytes() {
let mut term = Terminal::with_options(
TestBackend::new(10, 7),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Inline(3),
},
)
.unwrap();
persist_bytes(&mut term, b"two\r\nlines").unwrap();
term.backend().assert_buffer(&Buffer::with_lines(vec![
"two ",
"lines ",
" ",
" ",
" ",
" ",
" ",
]));
}
}
10 changes: 7 additions & 3 deletions crates/turborepo-ui/src/tui/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ pub enum Event {
},
EndTask {
task: String,
result: TaskResult,
},
Status {
task: String,
status: String,
},
Stop,
Tick,
Log {
message: Vec<u8>,
},
Up,
Down,
ScrollUp,
Expand All @@ -33,6 +31,12 @@ pub enum Event {
},
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum TaskResult {
Success,
Failure,
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
65 changes: 12 additions & 53 deletions crates/turborepo-ui/src/tui/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use std::{
time::Instant,
};

use super::Event;
use crate::LineWriter;
use super::{Event, TaskResult};

/// Struct for sending app events to TUI rendering
#[derive(Debug, Clone)]
Expand All @@ -25,17 +24,6 @@ pub struct TuiTask {
logs: Arc<Mutex<Vec<u8>>>,
}

/// Writer that will correctly render writes to the persisted part of the screen
pub struct PersistedWriter {
writer: LineWriter<PersistedWriterInner>,
}

/// Writer that will correctly render writes to the persisted part of the screen
#[derive(Debug, Clone)]
pub struct PersistedWriterInner {
handle: AppSender,
}

impl AppSender {
/// Create a new channel for sending app events.
///
Expand Down Expand Up @@ -99,11 +87,21 @@ impl TuiTask {
}

/// Mark the task as finished
pub fn finish(&self) -> Vec<u8> {
pub fn succeeded(&self) -> Vec<u8> {
self.finish(TaskResult::Success)
}

/// Mark the task as finished
pub fn failed(&self) -> Vec<u8> {
self.finish(TaskResult::Failure)
}

fn finish(&self, result: TaskResult) -> Vec<u8> {
self.handle
.primary
.send(Event::EndTask {
task: self.name.clone(),
result,
})
.ok();
self.logs.lock().expect("logs lock poisoned").clone()
Expand Down Expand Up @@ -132,20 +130,6 @@ impl TuiTask {
})
.ok();
}

/// Return a `PersistedWriter` which will properly write provided bytes to
/// a persisted section of the terminal.
///
/// Designed to be a drop in replacement for `io::stdout()`,
/// all calls such as `writeln!(io::stdout(), "hello")` should
/// pass in a PersistedWriter instead.
pub fn stdout(&self) -> PersistedWriter {
PersistedWriter {
writer: LineWriter::new(PersistedWriterInner {
handle: self.as_app().clone(),
}),
}
}
}

impl std::io::Write for TuiTask {
Expand All @@ -171,28 +155,3 @@ impl std::io::Write for TuiTask {
Ok(())
}
}

impl std::io::Write for PersistedWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.writer.write(buf)
}

fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}

impl std::io::Write for PersistedWriterInner {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let bytes = buf.to_vec();
self.handle
.primary
.send(Event::Log { message: bytes })
.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "receiver dropped"))?;
Ok(buf.len())
}

fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
8 changes: 4 additions & 4 deletions crates/turborepo-ui/src/tui/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
mod app;
mod event;
pub mod event;
mod handle;
mod input;
mod pane;
mod spinner;
mod table;
mod task;
mod task_duration;

pub use app::run_app;
use event::Event;
pub use handle::{AppReceiver, AppSender, PersistedWriterInner, TuiTask};
use event::{Event, TaskResult};
pub use handle::{AppReceiver, AppSender, TuiTask};
use input::input;
pub use pane::TerminalPane;
pub use table::TaskTable;
Expand Down
9 changes: 7 additions & 2 deletions crates/turborepo-ui/src/tui/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{

use ratatui::{
style::Style,
text::Line,
widgets::{Block, Borders, Widget},
};
use tracing::debug;
Expand All @@ -13,6 +14,9 @@ use turborepo_vt100 as vt100;

use super::{app::Direction, Error};

const FOOTER_TEXT: &str = "Use arrow keys to navigate. Press `Enter` to interact with a task and \
`Ctrl-Z` to stop interacting";

pub struct TerminalPane<W> {
tasks: BTreeMap<String, TerminalOutput<W>>,
displayed: Option<String>,
Expand Down Expand Up @@ -216,7 +220,8 @@ impl<W> Widget for &TerminalPane<W> {
let screen = task.parser.screen();
let mut block = Block::default()
.borders(Borders::ALL)
.title(task.title(task_name));
.title(task.title(task_name))
.title_bottom(Line::from(FOOTER_TEXT).centered());
if self.highlight {
block = block.border_style(Style::new().fg(ratatui::style::Color::Yellow));
}
Expand Down Expand Up @@ -254,7 +259,7 @@ mod test {
"│4 │",
"│5 │",
"│█ │",
"└──────┘",
"└Use ar┘",
])
);
}
Expand Down
Loading

0 comments on commit c3851a5

Please sign in to comment.