use std::io::{self, stdin, stdout, BufRead, BufReader, Write}; use std::process::{Command, Stdio}; use std::sync::mpsc::{channel, Sender}; use std::{env, thread}; use termion::event::{Event, Key, MouseEvent}; use termion::input::{MouseTerminal, TermRead}; use termion::raw::IntoRawMode; use termion::screen::IntoAlternateScreen; use termion::terminal_size; use termion::{clear, color, cursor, style}; /// A tile with a command running inside it. #[derive(Debug)] pub struct Tile { /// The command that should be executed in the tile. pub command: Vec, /// Content of the command's stdout and stderr. /// /// We put both stdout and stderr here to avoid dealing with order between stdout and stderr. pub stdout: Vec, } impl Tile { /// Creates a new empty tile. pub fn new(command: &[String], i: u16, j: u16, sender: Sender) -> Tile { let command = command .into_iter() .map(|x| x.to_string()) .collect::>(); let clone = command.clone(); thread::spawn(move || { let mut child = Command::new(&clone[0]) .args(&clone[1..]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let stdout = child.stdout.take().unwrap(); let reader = BufReader::new(stdout); let mut lines = reader.lines(); loop { match lines.next() { Some(Ok(line)) => { sender.send(Msg::Stdout(i, j, line)).unwrap(); } Some(Err(_)) => { break; } None => break, } } sender.send(Msg::Stdout(i, j, String::new())).unwrap(); let code = child.wait().unwrap().code().unwrap(); let exit_string = format!( "{}{}Command exited with return code {}{}{}", style::Bold, if code == 0 { color::Green.fg_str() } else { color::Red.fg_str() }, code, style::Reset, color::Reset.fg_str() ); sender.send(Msg::Stdout(i, j, exit_string)).unwrap(); }); Tile { command: command .into_iter() .map(|x| x.to_string()) .collect::>(), stdout: vec![], } } } /// Multiple applications running in a single terminal. struct Multiview { /// The stdout on which the multiview will be rendererd. pub stdout: W, /// The tiles of the multiview. pub tiles: Vec>, /// The coordinates of the selected tiles. pub selected: (u16, u16), } impl Multiview { /// Creates a new multiview. pub fn new(stdout: W, tiles: Vec>) -> io::Result> { let mut multiview = Multiview { stdout, tiles, selected: (0, 0), }; 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), term_size: (u16, u16)) { let w = term_size.0 / self.tiles[0].len() as u16; let h = term_size.1 / self.tiles.len() as u16; self.selected = (y / h, x / w); } /// Draws a box from (x1, y1) to (x2, y2). pub fn rect(&mut self, (x1, y1): (u16, u16), (x2, y2): (u16, u16)) -> io::Result<()> { write!(self.stdout, "{}┌", cursor::Goto(x1, y1))?; for _ in (x1 + 1)..x2 { write!(self.stdout, "─")?; } write!(self.stdout, "┐")?; for y in (y1 + 1)..y2 { write!(self.stdout, "{}│", cursor::Goto(x1, y))?; write!(self.stdout, "{}│", cursor::Goto(x2, y))?; } write!(self.stdout, "{}└", cursor::Goto(x1, y2))?; for _ in (x1 + 1)..x2 { write!(self.stdout, "─")?; } write!(self.stdout, "┘")?; Ok(()) } /// Clears stdout. pub fn clear(&mut self) -> io::Result<()> { write!(self.stdout, "{}", clear::All) } /// Renders the (x, y) tile. pub fn render_tile(&mut self, (i, j): (u16, u16), term_size: (u16, u16)) -> io::Result<()> { let w = term_size.0 / self.tiles[0].len() as u16; let h = term_size.1 / self.tiles.len() as u16; let x1 = j * w + 1; let y1 = i * h + 1; let x2 = (j + 1) * w; let y2 = (i + 1) * h; if self.selected == (i, j) { write!(self.stdout, "{}", color::Green.fg_str())?; } self.rect((x1, y1), (x2, y2))?; write!(self.stdout, "{}├", cursor::Goto(x1, y1 + 2))?; for _ in (x1 + 1)..x2 { write!(self.stdout, "─")?; } write!(self.stdout, "{}┤", cursor::Goto(x2, y1 + 2))?; let tile = &self.tile((i, j)); let command_str = tile.command.join(" "); // TODO: find a way to avoid this copy let lines = tile .stdout .iter() .map(|x| x.to_string()) .enumerate() .collect::>(); write!( self.stdout, "{}{} {}Command: {}{}", color::Reset.fg_str(), cursor::Goto(x1 + 1, y1 + 1), style::Bold, command_str, style::Reset, )?; for (line_index, line) in lines { write!( self.stdout, "{}{}", cursor::Goto(x1 + 2, y1 + 3 + line_index as u16), line )?; } Ok(()) } /// Renders all the tiles of the multiview. pub fn render(&mut self, term_size: (u16, u16)) -> io::Result<()> { self.clear()?; for i in 0..self.tiles.len() { for j in 0..self.tiles[0].len() { self.render_tile((i as u16, j as u16), term_size)?; } } self.stdout.flush()?; Ok(()) } } impl Drop for Multiview { fn drop(&mut self) { write!(self.stdout, "{}", cursor::Show).unwrap(); } } /// An event that can be sent in channels. pub enum Msg { /// An stdout line arrived. Stdout(u16, u16, String), /// A click occured. Click(u16, u16), /// 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::>(); let tiles = args .split(|x| x == "//") .map(|x| x.split(|y| y == "::").enumerate().collect::>()) .enumerate() .map(|(i, tiles)| { tiles .into_iter() .map(|(j, tile)| Tile::new(tile, i as u16, j as u16, sender.clone())) .collect::>() }) .collect::>(); let stdin = stdin(); let stdout = stdout().into_raw_mode()?; let stdout = stdout.into_alternate_screen()?; let stdout = MouseTerminal::from(stdout); let term_size = terminal_size()?; let mut multiview = Multiview::new(stdout, tiles)?; multiview.render(term_size)?; 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::Mouse(me) => match me { MouseEvent::Press(_, x, y) => sender.send(Msg::Click(x, y)).unwrap(), _ => (), }, _ => {} } } }); loop { match receiver.recv() { Ok(Msg::Stdout(i, j, line)) => multiview.tile_mut((i, j)).stdout.push(line), Ok(Msg::Click(x, y)) => multiview.select_tile((x, y), term_size), Ok(Msg::Exit) => break, Err(_) => (), } multiview.render(term_size)?; } Ok(()) }