472 lines
13 KiB
Rust
472 lines
13 KiB
Rust
use std::io::{self, stdin, stdout, Write};
|
|
use std::sync::mpsc::channel;
|
|
use std::time::{Duration, Instant};
|
|
use std::{env, thread};
|
|
|
|
use termion::event::{Event, Key, MouseButton, MouseEvent};
|
|
use termion::input::{MouseTerminal, TermRead};
|
|
use termion::raw::IntoRawMode;
|
|
use termion::screen::IntoAlternateScreen;
|
|
use termion::terminal_size;
|
|
use termion::{clear, cursor};
|
|
|
|
use tile::{Tile, TileBuilder};
|
|
|
|
pub mod tile;
|
|
pub mod utils;
|
|
|
|
const DELAY: Duration = Duration::from_millis(20);
|
|
|
|
/// Multiple applications running in a single terminal.
|
|
struct Multiview<W: Write> {
|
|
/// The stdout on which the multiview will be rendererd.
|
|
pub stdout: W,
|
|
|
|
/// The tiles of the multiview.
|
|
pub tiles: Vec<Vec<Tile>>,
|
|
|
|
/// The coordinates of the selected tiles.
|
|
pub selected: (u16, u16),
|
|
|
|
/// Whether we need to refresh the UI.
|
|
pub refresh_ui: bool,
|
|
|
|
/// Whether we need to refresh the tiles.
|
|
pub refresh_tiles: bool,
|
|
|
|
/// Last time when the rendering was performed.
|
|
pub last_render: Instant,
|
|
}
|
|
|
|
impl<W: Write> Multiview<W> {
|
|
/// Creates a new multiview.
|
|
pub fn new(stdout: W, tiles: Vec<Vec<Tile>>) -> io::Result<Multiview<W>> {
|
|
let mut multiview = Multiview {
|
|
stdout,
|
|
tiles,
|
|
selected: (0, 0),
|
|
refresh_ui: true,
|
|
refresh_tiles: false,
|
|
last_render: Instant::now(),
|
|
};
|
|
|
|
write!(
|
|
multiview.stdout,
|
|
"{}{}{}",
|
|
clear::All,
|
|
cursor::Hide,
|
|
cursor::Goto(1, 1)
|
|
)?;
|
|
|
|
multiview.stdout.flush()?;
|
|
|
|
Ok(multiview)
|
|
}
|
|
|
|
/// Helper to easily access a tile.
|
|
pub fn tile(&self, (i, j): (u16, u16)) -> &Tile {
|
|
&self.tiles[i as usize][j as usize]
|
|
}
|
|
|
|
/// Helper to easily access a mut tile.
|
|
pub fn tile_mut(&mut self, (i, j): (u16, u16)) -> &mut Tile {
|
|
&mut self.tiles[i as usize][j as usize]
|
|
}
|
|
|
|
/// Sets the selected tile from (x, y) coordinates.
|
|
pub fn select_tile(&mut self, (x, y): (u16, u16)) {
|
|
// Ugly but working
|
|
for (i, row) in self.tiles.iter().enumerate() {
|
|
for (j, tile) in row.iter().enumerate() {
|
|
if tile.outer_position.0 <= x && x < tile.outer_position.0 + tile.outer_size.0 {
|
|
if tile.outer_position.1 <= y && y < tile.outer_position.1 + tile.outer_size.1 {
|
|
self.selected = (i as u16, j as u16);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.refresh_ui = true;
|
|
}
|
|
|
|
/// Renders the border and the title of a tile.
|
|
pub fn render_tile_border(&self, (i, j): (u16, u16)) -> String {
|
|
let tile = &self.tile((i, j));
|
|
tile.render_border(self.selected == ((i, j)))
|
|
}
|
|
|
|
/// Renders the (x, y) tile.
|
|
pub fn render_tile_content(&mut self, (i, j): (u16, u16)) -> String {
|
|
let tile = self.tile((i, j));
|
|
tile.render_content(self.selected == (i, j))
|
|
}
|
|
|
|
/// Renders all the tiles of the multiview.
|
|
pub fn render(&mut self, force: bool) -> io::Result<()> {
|
|
if !self.refresh_tiles {
|
|
return Ok(());
|
|
}
|
|
|
|
let now = Instant::now();
|
|
|
|
if now.duration_since(self.last_render) < DELAY && !force {
|
|
return Ok(());
|
|
}
|
|
|
|
self.last_render = now;
|
|
|
|
let mut buffer = if self.refresh_ui {
|
|
vec![format!("{}", clear::All)]
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
for i in 0..self.tiles.len() {
|
|
for j in 0..self.tiles[i].len() {
|
|
if self.refresh_ui {
|
|
buffer.push(self.render_tile_border((i as u16, j as u16)));
|
|
}
|
|
buffer.push(self.render_tile_content((i as u16, j as u16)));
|
|
}
|
|
}
|
|
|
|
self.refresh_ui = false;
|
|
self.refresh_tiles = false;
|
|
write!(self.stdout, "{}", buffer.join(""))?;
|
|
self.stdout.flush()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Scrolls down the current selected tile.
|
|
pub fn scroll_down(&mut self, step: isize) {
|
|
let tile = self.tile_mut(self.selected);
|
|
tile.scroll_down(step);
|
|
}
|
|
|
|
/// Scrolls up the current selected tile.
|
|
pub fn scroll_up(&mut self, step: isize) {
|
|
let tile = self.tile_mut(self.selected);
|
|
tile.scroll_up(step);
|
|
}
|
|
|
|
/// Scrolls down to the bottom of the current selected tile.
|
|
pub fn scroll_full_down(&mut self) {
|
|
let tile = self.tile_mut(self.selected);
|
|
tile.scroll_full_down();
|
|
}
|
|
|
|
/// Scrolls up to the top the current selected tile.
|
|
pub fn scroll_full_up(&mut self) {
|
|
let tile = self.tile_mut(self.selected);
|
|
tile.scroll_full_up();
|
|
}
|
|
|
|
/// Push a string into a tile's stdout.
|
|
pub fn push_stdout(&mut self, (i, j): (u16, u16), content: String) {
|
|
let tile = self.tile_mut((i, j));
|
|
tile.push_stdout(content);
|
|
}
|
|
|
|
/// Push a string into a tile's stderr.
|
|
pub fn push_stderr(&mut self, (i, j): (u16, u16), content: String) {
|
|
self.push_stdout((i, j), content);
|
|
}
|
|
|
|
/// Restarts the selected tile.
|
|
pub fn restart(&mut self) {
|
|
let tile = self.tile_mut(self.selected);
|
|
tile.restart();
|
|
}
|
|
|
|
/// Restarts all tiles.
|
|
pub fn restart_all(&mut self) {
|
|
for row in &mut self.tiles {
|
|
for tile in row {
|
|
tile.restart();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Kills the selected tile.
|
|
pub fn kill(&mut self) {
|
|
let tile = self.tile_mut(self.selected);
|
|
tile.kill();
|
|
}
|
|
|
|
/// Kills all tiles.
|
|
pub fn kill_all(&mut self) {
|
|
for row in &mut self.tiles {
|
|
for tile in row {
|
|
tile.kill();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Adds a line to the current tile.
|
|
pub fn add_line(&mut self) {
|
|
let tile = self.tile_mut(self.selected);
|
|
tile.add_line();
|
|
}
|
|
|
|
/// Adds a line to every tile.
|
|
pub fn add_line_all(&mut self) {
|
|
for row in &mut self.tiles {
|
|
for tile in row {
|
|
tile.add_line();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Adds a finish line to the specified tile.
|
|
pub fn add_finish_line(&mut self, coords: (u16, u16), success: bool) {
|
|
let tile = self.tile_mut(coords);
|
|
tile.add_finish_line(success);
|
|
}
|
|
|
|
/// Exits.
|
|
pub fn exit(&mut self) {
|
|
write!(self.stdout, "{}", cursor::Show).ok();
|
|
|
|
for row in &mut self.tiles {
|
|
for tile in row {
|
|
tile.kill()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Treats a message.
|
|
pub fn manage_msg(&mut self, msg: Msg) -> io::Result<()> {
|
|
self.refresh_tiles = true;
|
|
|
|
match msg {
|
|
Msg::Stdout(coords, line) => self.push_stdout(coords, line),
|
|
Msg::Stderr(coords, line) => self.push_stderr(coords, line),
|
|
Msg::Click(x, y) => self.select_tile((x, y)),
|
|
Msg::Restart => self.restart(),
|
|
Msg::RestartAll => self.restart_all(),
|
|
Msg::Kill => self.kill(),
|
|
Msg::KillAll => self.kill_all(),
|
|
Msg::ScrollDown(step) => self.scroll_down(step),
|
|
Msg::ScrollUp(step) => self.scroll_up(step),
|
|
Msg::ScrollFullDown => self.scroll_full_down(),
|
|
Msg::ScrollFullUp => self.scroll_full_up(),
|
|
Msg::AddLine => self.add_line(),
|
|
Msg::AddLineAll => self.add_line_all(),
|
|
Msg::AddFinishLine(coords, success) => self.add_finish_line(coords, success),
|
|
Msg::Exit => self.exit(),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<W: Write> Drop for Multiview<W> {
|
|
fn drop(&mut self) {
|
|
self.exit();
|
|
}
|
|
}
|
|
|
|
/// An event that can be sent in channels.
|
|
#[derive(PartialEq, Eq)]
|
|
pub enum Msg {
|
|
/// An stdout line arrived.
|
|
Stdout((u16, u16), String),
|
|
|
|
/// An stderr line arrived.
|
|
Stderr((u16, u16), String),
|
|
|
|
/// A click occured.
|
|
Click(u16, u16),
|
|
|
|
/// Restarts the selected tile.
|
|
Restart,
|
|
|
|
/// Restarts all tiles.
|
|
RestartAll,
|
|
|
|
/// Kills the selected tile.
|
|
Kill,
|
|
|
|
/// Kills all tiles.
|
|
KillAll,
|
|
|
|
/// Scroll up one line.
|
|
ScrollUp(isize),
|
|
|
|
/// Scroll down one line.
|
|
ScrollDown(isize),
|
|
|
|
/// Scroll to the top of the log.
|
|
ScrollFullUp,
|
|
|
|
/// Scroll to the bottom of the log.
|
|
ScrollFullDown,
|
|
|
|
/// Adds a line to the current tile.
|
|
AddLine,
|
|
|
|
/// Adds a line to every tile.
|
|
AddLineAll,
|
|
|
|
/// Adds the finish line to the tile.
|
|
AddFinishLine((u16, u16), bool),
|
|
|
|
/// The program was asked to exit.
|
|
Exit,
|
|
}
|
|
|
|
/// Starts the multiview application.
|
|
pub fn main() -> io::Result<()> {
|
|
let (sender, receiver) = channel();
|
|
|
|
let args = env::args().skip(1).collect::<Vec<_>>();
|
|
|
|
let mut is_row_major = true;
|
|
|
|
for arg in &args {
|
|
if arg == "//" {
|
|
is_row_major = false;
|
|
break;
|
|
}
|
|
|
|
if arg == "::" {
|
|
is_row_major = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let (first_split, second_split) = if is_row_major {
|
|
("//", "::")
|
|
} else {
|
|
("::", "//")
|
|
};
|
|
|
|
let tiles = args
|
|
.split(|x| x == first_split)
|
|
.map(|x| {
|
|
x.split(|y| y == second_split)
|
|
.enumerate()
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.enumerate()
|
|
.map(|(i, tiles)| {
|
|
tiles
|
|
.into_iter()
|
|
.map(|(j, tile)| ((i, j), tile))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut term_size = terminal_size()?;
|
|
|
|
let col_len = tiles.len() as u16;
|
|
|
|
let tiles = tiles
|
|
.into_iter()
|
|
.map(|row| {
|
|
let row_len = row.len() as u16;
|
|
|
|
let tile_size = if is_row_major {
|
|
(term_size.0 / row_len, term_size.1 / col_len)
|
|
} else {
|
|
(term_size.0 / col_len, term_size.1 / row_len)
|
|
};
|
|
|
|
row.into_iter()
|
|
.map(|((i, j), tile)| {
|
|
let (p_i, p_j) = if is_row_major { (i, j) } else { (j, i) };
|
|
|
|
TileBuilder::new()
|
|
.command(tile.into())
|
|
.coords((i as u16, j as u16))
|
|
.position((p_j as u16 * tile_size.0 + 1, p_i as u16 * tile_size.1 + 1))
|
|
.size(tile_size)
|
|
.sender(sender.clone())
|
|
.build()
|
|
.unwrap()
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let stdin = stdin();
|
|
let stdout = stdout().into_raw_mode()?;
|
|
let stdout = stdout.into_alternate_screen()?;
|
|
let stdout = MouseTerminal::from(stdout);
|
|
|
|
let mut multiview = Multiview::new(stdout, tiles)?;
|
|
multiview.render(true)?;
|
|
|
|
for row in &mut multiview.tiles {
|
|
for tile in row {
|
|
tile.start();
|
|
}
|
|
}
|
|
|
|
thread::spawn(move || {
|
|
for c in stdin.events() {
|
|
let evt = c.unwrap();
|
|
match evt {
|
|
Event::Key(Key::Char('q')) => sender.send(Msg::Exit).unwrap(),
|
|
Event::Key(Key::Char('r')) => sender.send(Msg::Restart).unwrap(),
|
|
Event::Key(Key::Char('R')) => sender.send(Msg::RestartAll).unwrap(),
|
|
Event::Key(Key::Char('k')) => sender.send(Msg::Kill).unwrap(),
|
|
Event::Key(Key::Char('K')) => sender.send(Msg::KillAll).unwrap(),
|
|
Event::Key(Key::Char('l')) => sender.send(Msg::AddLine).unwrap(),
|
|
Event::Key(Key::Char('L')) => sender.send(Msg::AddLineAll).unwrap(),
|
|
Event::Key(Key::Down) => sender.send(Msg::ScrollDown(1)).unwrap(),
|
|
Event::Key(Key::Up) => sender.send(Msg::ScrollUp(1)).unwrap(),
|
|
Event::Key(Key::End) => sender.send(Msg::ScrollFullDown).unwrap(),
|
|
Event::Key(Key::Home) => sender.send(Msg::ScrollFullUp).unwrap(),
|
|
Event::Mouse(MouseEvent::Press(p, x, y)) => match p {
|
|
MouseButton::WheelUp => sender.send(Msg::ScrollUp(3)).unwrap(),
|
|
MouseButton::WheelDown => sender.send(Msg::ScrollDown(3)).unwrap(),
|
|
MouseButton::Left => sender.send(Msg::Click(x, y)).unwrap(),
|
|
_ => (),
|
|
},
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
|
|
loop {
|
|
if let Ok(msg) = receiver.recv_timeout(DELAY) {
|
|
let is_exit = msg == Msg::Exit;
|
|
multiview.manage_msg(msg)?;
|
|
if is_exit {
|
|
break;
|
|
}
|
|
}
|
|
|
|
let new_term_size = terminal_size()?;
|
|
|
|
if term_size != new_term_size {
|
|
term_size = new_term_size;
|
|
|
|
for (i, row) in multiview.tiles.iter_mut().enumerate() {
|
|
let row_len = row.len() as u16;
|
|
|
|
let tile_size = if is_row_major {
|
|
(term_size.0 / row_len, term_size.1 / col_len)
|
|
} else {
|
|
(term_size.0 / col_len, term_size.1 / row_len)
|
|
};
|
|
|
|
for (j, tile) in row.iter_mut().enumerate() {
|
|
let (p_i, p_j) = if is_row_major { (i, j) } else { (j, i) };
|
|
tile.reposition((p_j as u16 * tile_size.0 + 1, p_i as u16 * tile_size.1 + 1));
|
|
tile.resize(tile_size);
|
|
}
|
|
}
|
|
|
|
multiview.refresh_tiles = true;
|
|
multiview.refresh_ui = true;
|
|
}
|
|
|
|
multiview.render(false)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|