Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(refactor)ui: Refactoring/simplifying TUI state #8650

Merged
merged 31 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
505b0cd
Getting warmer.
anthonyshew Jun 29, 2024
4308fb9
Hoist state, handle scrolling.
anthonyshew Jun 29, 2024
9595037
Feeling close.
anthonyshew Jul 1, 2024
806c689
Begone, weird changes in packages.
anthonyshew Jul 1, 2024
431f250
Task table working sufficiently.
anthonyshew Jul 1, 2024
2d043ba
Still movin'...
anthonyshew Jul 1, 2024
8d8af95
Looking really good.
anthonyshew Jul 2, 2024
4528c06
At least it compiles.
anthonyshew Jul 2, 2024
9eaf4ed
Minor cleanups.
anthonyshew Jul 2, 2024
b64d1a8
Trivially working...
anthonyshew Jul 2, 2024
89a89bb
Correct focus on intialization.
anthonyshew Jul 2, 2024
5aba68e
Small cleanup.
anthonyshew Jul 2, 2024
1902f47
Correct focus handling.
anthonyshew Jul 2, 2024
2c12ef7
I win?
anthonyshew Jul 2, 2024
ff6f20d
Simplifying.
anthonyshew Jul 2, 2024
bf5891b
All but one test...
anthonyshew Jul 2, 2024
5fb167a
Update crates/turborepo-ui/src/tui/app.rs
anthonyshew Jul 3, 2024
f2ca89e
Answering to code review, 1.
anthonyshew Jul 3, 2024
4100d26
Feedback, 2.
anthonyshew Jul 3, 2024
ac6ea6d
Thanks eslint, very cool.
anthonyshew Jul 3, 2024
ec4791f
eslint very cool.
anthonyshew Jul 3, 2024
706ec2a
Fixing up types.
anthonyshew Jul 3, 2024
79f65a3
Move term scrolling logic to App.
anthonyshew Jul 3, 2024
39b11a6
Make clippy happy.
anthonyshew Jul 3, 2024
6e7a8a2
fix: overwrite state on task list update
chris-olszewski Jul 3, 2024
3cc43d6
Still have a bad type but trying to fix starting watch mode tasks.
anthonyshew Jul 3, 2024
dc7bdc5
fix: compile works
chris-olszewski Jul 3, 2024
f2d0f51
chore(tui): slight refactor to avoid allocations
chris-olszewski Jul 3, 2024
b2f22c8
fix clippy
chris-olszewski Jul 3, 2024
5064f8e
fix(tui): fix stdin/status setting, add tests
chris-olszewski Jul 3, 2024
be9a430
chore(tui): add test for setting status
chris-olszewski Jul 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 280 additions & 82 deletions crates/turborepo-ui/src/tui/app.rs

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions crates/turborepo-ui/src/tui/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ pub enum Event {
result: TaskResult,
},
Status {
task: String,
status: String,
},
Stop(std::sync::mpsc::SyncSender<()>),
Expand All @@ -23,7 +22,6 @@ pub enum Event {
ScrollUp,
ScrollDown,
SetStdin {
task: String,
stdin: Box<dyn std::io::Write + Send>,
},
EnterInteractive,
Expand Down
16 changes: 2 additions & 14 deletions crates/turborepo-ui/src/tui/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,27 +116,15 @@ impl TuiTask {
}

pub fn set_stdin(&self, stdin: Box<dyn std::io::Write + Send>) {
self.handle
.primary
.send(Event::SetStdin {
task: self.name.clone(),
stdin,
})
.ok();
self.handle.primary.send(Event::SetStdin { stdin }).ok();
}

pub fn status(&self, status: &str) {
// Since this will be rendered via ratatui we any ANSI escape codes will not be
// handled.
// TODO: prevent the status from having ANSI codes in this scenario
let status = console::strip_ansi_codes(status).into_owned();
self.handle
.primary
.send(Event::Status {
task: self.name.clone(),
status,
})
.ok();
self.handle.primary.send(Event::Status { status }).ok();
}
}

Expand Down
21 changes: 9 additions & 12 deletions crates/turborepo-ui/src/tui/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@

use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

use super::{event::Event, Error};
use super::{app::LayoutSections, event::Event, Error};

#[derive(Debug, Clone, Copy)]
anthonyshew marked this conversation as resolved.
Show resolved Hide resolved
pub struct InputOptions {
pub interact: bool,
pub focus: LayoutSections,
pub tty_stdin: bool,
}
/// Return any immediately available event
pub fn input(options: InputOptions) -> Result<Option<Event>, Error> {
let InputOptions {
interact,
tty_stdin,
} = options;
pub fn input(options: &InputOptions) -> Result<Option<Event>, Error> {
let InputOptions { focus, tty_stdin } = options;
// If stdin is not a tty, then we do not attempt to read from it
if !tty_stdin {
return Ok(None);
Expand All @@ -23,7 +19,7 @@
// for input
if crossterm::event::poll(Duration::from_millis(0))? {
match crossterm::event::read()? {
crossterm::event::Event::Key(k) => Ok(translate_key_event(interact, k)),
crossterm::event::Event::Key(k) => Ok(translate_key_event(&focus, k)),

Check failure on line 22 in crates/turborepo-ui/src/tui/input.rs

View workflow job for this annotation

GitHub Actions / Turborepo rust clippy

this expression creates a reference which is immediately dereferenced by the compiler
crossterm::event::Event::Mouse(m) => match m.kind {
crossterm::event::MouseEventKind::ScrollDown => Ok(Some(Event::ScrollDown)),
crossterm::event::MouseEventKind::ScrollUp => Ok(Some(Event::ScrollUp)),
Expand All @@ -37,7 +33,7 @@
}

/// Converts a crossterm key event into a TUI interaction event
fn translate_key_event(interact: bool, key_event: KeyEvent) -> Option<Event> {
fn translate_key_event(interact: &LayoutSections, key_event: KeyEvent) -> Option<Event> {
// On Windows events for releasing a key are produced
// We skip these to avoid emitting 2 events per key press.
// There is still a `Repeat` event for when a key is held that will pass through
Expand All @@ -51,12 +47,13 @@
}
// Interactive branches
KeyCode::Char('z')
if interact && key_event.modifiers == crossterm::event::KeyModifiers::CONTROL =>
if matches!(interact, LayoutSections::Pane)
&& key_event.modifiers == crossterm::event::KeyModifiers::CONTROL =>
{
Some(Event::ExitInteractive)
}
// If we're in interactive mode, convert the key event to bytes to send to stdin
_ if interact => Some(Event::Input {
_ if matches!(interact, LayoutSections::Pane) => Some(Event::Input {
bytes: encode_key(key_event),
}),
// Fall through if we aren't in interactive mode
Expand Down
2 changes: 2 additions & 0 deletions crates/turborepo-ui/src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ mod pane;
mod spinner;
mod table;
mod task;
mod term_output;

pub use app::{run_app, terminal_big_enough};
use event::{Event, TaskResult};
pub use handle::{AppReceiver, AppSender, TuiTask};
use input::{input, InputOptions};
pub use pane::TerminalPane;
pub use table::TaskTable;
pub use term_output::TerminalOutput;

#[derive(Debug, thiserror::Error)]
pub enum Error {
Expand Down
181 changes: 65 additions & 116 deletions crates/turborepo-ui/src/tui/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,40 @@
};
use tracing::debug;
use tui_term::widget::PseudoTerminal;
use turborepo_vt100 as vt100;

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

const FOOTER_TEXT_ACTIVE: &str = "Press`Ctrl-Z` to stop interacting.";
const FOOTER_TEXT_INACTIVE: &str = "Press `Enter` to interact.";

pub struct TerminalPane<W> {
tasks: BTreeMap<String, TerminalOutput<W>>,
displayed: Option<String>,
pub struct TerminalPane<'a, W> {
displayed_task: &'a String,
anthonyshew marked this conversation as resolved.
Show resolved Hide resolved
rows: u16,
cols: u16,
highlight: bool,
tasks: &'a mut BTreeMap<String, TerminalOutput<W>>,
anthonyshew marked this conversation as resolved.
Show resolved Hide resolved
}

struct TerminalOutput<W> {
rows: u16,
cols: u16,
parser: vt100::Parser,
stdin: Option<W>,
status: Option<String>,
}

impl<W> TerminalPane<W> {
pub fn new(rows: u16, cols: u16, tasks: impl IntoIterator<Item = String>) -> Self {
impl<'a, W> TerminalPane<'a, W> {
pub fn new(
rows: u16,
cols: u16,
anthonyshew marked this conversation as resolved.
Show resolved Hide resolved
highlight: bool,
tasks: &'a mut BTreeMap<String, TerminalOutput<W>>,
displayed_task: &'a String,
) -> Self {
// We trim 2 from rows and cols as we use them for borders
let rows = rows.saturating_sub(2);
let cols = cols.saturating_sub(2);
Self {
tasks: tasks
.into_iter()
.map(|name| (name, TerminalOutput::new(rows, cols, None)))
.collect(),
displayed: None,
rows,
cols,
highlight: false,
highlight,
tasks,
displayed_task,
}
}

pub fn highlight(&mut self, highlight: bool) {
self.highlight = highlight;
}

pub fn process_output(&mut self, task: &str, output: &[u8]) -> Result<(), Error> {
let task = self
.task_mut(task)
Expand All @@ -59,6 +49,13 @@
Ok(())
}

pub fn hassss_stdin(&self, task: &str) -> bool {
self.tasks
.get(task)
.map(|task| task.stdin.is_some())
.unwrap_or_default()
}
anthonyshew marked this conversation as resolved.
Show resolved Hide resolved

pub fn has_stdin(&self, task: &str) -> bool {
self.tasks
.get(task)
Expand All @@ -72,13 +69,11 @@
self.cols = cols;
if changed {
// Eagerly resize currently displayed terminal
if let Some(task_name) = self.displayed.as_deref() {
let task = self
.tasks
.get_mut(task_name)
.expect("displayed should always point to valid task");
task.resize(rows, cols);
}
let task = self
.tasks
.get_mut(self.displayed_task)
.expect("displayed should always point to valid task");
task.resize(rows, cols);
}

Ok(())
Expand All @@ -91,7 +86,7 @@
let terminal = self.task_mut(task)?;
terminal.resize(rows, cols);
}
self.displayed = Some(task.into());
self.displayed_task;

Check failure on line 89 in crates/turborepo-ui/src/tui/pane.rs

View workflow job for this annotation

GitHub Actions / Turborepo rust clippy

statement with no effect
anthonyshew marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}
Expand Down Expand Up @@ -130,8 +125,7 @@
}

fn selected(&self) -> Option<(&String, &TerminalOutput<W>)> {
let task_name = self.displayed.as_deref()?;
self.tasks.get_key_value(task_name)
self.tasks.get_key_value(self.displayed_task)
}
anthonyshew marked this conversation as resolved.
Show resolved Hide resolved

fn task_mut(&mut self, task: &str) -> Result<&mut TerminalOutput<W>, Error> {
Expand All @@ -141,7 +135,7 @@
}
}

impl<W: Write> TerminalPane<W> {
impl<'a, W: Write> TerminalPane<'a, W> {
/// Insert a stdin to be associated with a task
pub fn insert_stdin(&mut self, task_name: &str, stdin: Option<W>) -> Result<(), Error> {
let task = self.task_mut(task_name)?;
Expand All @@ -161,52 +155,7 @@
}
}

impl<W> TerminalOutput<W> {
fn new(rows: u16, cols: u16, stdin: Option<W>) -> Self {
Self {
parser: vt100::Parser::new(rows, cols, 1024),
stdin,
rows,
cols,
status: None,
}
}

fn title(&self, task_name: &str) -> String {
match self.status.as_deref() {
Some(status) => format!(" {task_name} > {status} "),
None => format!(" {task_name} > "),
}
}

fn resize(&mut self, rows: u16, cols: u16) {
if self.rows != rows || self.cols != cols {
self.parser.screen_mut().set_size(rows, cols);
}
self.rows = rows;
self.cols = cols;
}

#[tracing::instrument(skip(self))]
fn persist_screen(&self, task_name: &str) -> std::io::Result<()> {
let screen = self.parser.entire_screen();
let title = self.title(task_name);
let mut stdout = std::io::stdout().lock();
stdout.write_all("┌".as_bytes())?;
stdout.write_all(title.as_bytes())?;
stdout.write_all(b"\r\n")?;
for row in screen.rows_formatted(0, self.cols) {
stdout.write_all("│ ".as_bytes())?;
stdout.write_all(&row)?;
stdout.write_all(b"\r\n")?;
}
stdout.write_all("└────>\r\n".as_bytes())?;

Ok(())
}
}

impl<W> Widget for &TerminalPane<W> {
impl<'a, W> Widget for &TerminalPane<'a, W> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized,
Expand All @@ -229,37 +178,37 @@
}
}

#[cfg(test)]
mod test {
// Used by assert_buffer_eq
#[allow(unused_imports)]
use indoc::indoc;
use ratatui::{assert_buffer_eq, buffer::Buffer, layout::Rect};

use super::*;

#[test]
fn test_basic() {
let mut pane: TerminalPane<()> = TerminalPane::new(6, 8, vec!["foo".into()]);
pane.select("foo").unwrap();
pane.process_output("foo", b"1\r\n2\r\n3\r\n4\r\n5\r\n")
.unwrap();

let area = Rect::new(0, 0, 8, 6);
let mut buffer = Buffer::empty(area);
pane.render(area, &mut buffer);
// Reset style change of the cursor
buffer.set_style(Rect::new(1, 4, 1, 1), Style::reset());
assert_buffer_eq!(
buffer,
Buffer::with_lines(vec![
"│ foo > ",
"│3 ",
"│4 ",
"│5 ",
"│█ ",
"│Press `",
])
);
}
}
// #[cfg(test)]
// mod test {
// // Used by assert_buffer_eq
// #[allow(unused_imports)]
// use indoc::indoc;
// use ratatui::{assert_buffer_eq, buffer::Buffer, layout::Rect};
//
// use super::*;
//
// #[test]
// fn test_basic() {
// let mut pane: TerminalPane<()> = TerminalPane::new(6, 8,
// vec!["foo".into()], false); pane.select("foo").unwrap();
// pane.process_output("foo", b"1\r\n2\r\n3\r\n4\r\n5\r\n")
// .unwrap();
//
// let area = Rect::new(0, 0, 8, 6);
// let mut buffer = Buffer::empty(area);
// pane.render(area, &mut buffer);
// // Reset style change of the cursor
// buffer.set_style(Rect::new(1, 4, 1, 1), Style::reset());
// assert_buffer_eq!(
// buffer,
// Buffer::with_lines(vec![
// "│ foo > ",
// "│3 ",
// "│4 ",
// "│5 ",
// "│█ ",
// "│Press `",
// ])
// );
// }
// }
Loading
Loading