From 4295c93435f25adc23ecb8271ba7a82210744b1c Mon Sep 17 00:00:00 2001 From: Thomas Forgione Date: Wed, 18 Oct 2023 23:16:03 +0200 Subject: [PATCH] Spawning commands --- .gitignore | 1 + demo.sh | 13 +++++ src/lib.rs | 146 +++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 145 insertions(+), 15 deletions(-) create mode 100755 demo.sh diff --git a/.gitignore b/.gitignore index ea8c4bf..f350853 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +log.txt diff --git a/demo.sh b/demo.sh new file mode 100755 index 0000000..b7253cf --- /dev/null +++ b/demo.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +rand() { + shuf -i"$1"-"$2" -n1 +} + +iterations=$(rand 5 10) + +for i in $(seq 1 "$iterations"); do + color="\x1B[3$(rand 0 6)m" + echo -e "$color$(rand 1 100)\x1b[0m" + sleep $(rand 1 2) +done diff --git a/src/lib.rs b/src/lib.rs index 8c5c90f..3e61992 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ -use std::env; -use std::io::{self, stdin, stdout, Write}; +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}; @@ -13,16 +15,76 @@ use termion::{clear, color, cursor, style}; 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]) -> 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![], } } } @@ -66,6 +128,11 @@ impl Multiview { &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; @@ -129,6 +196,15 @@ impl Multiview { 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, @@ -136,10 +212,19 @@ impl Multiview { color::Reset.fg_str(), cursor::Goto(x1 + 1, y1 + 1), style::Bold, - tile.command.join(" "), + 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(()) } @@ -165,14 +250,34 @@ impl Drop for Multiview { } } +/// 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 == "::").map(Tile::new)) - .map(|x| x.collect::>()) + .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(); @@ -185,17 +290,28 @@ pub fn main() -> io::Result<()> { let mut multiview = Multiview::new(stdout, tiles)?; multiview.render(term_size)?; - for c in stdin.events() { - let evt = c?; - match evt { - Event::Key(Key::Char('q')) => break, + 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) => multiview.select_tile((x, y), term_size), - _ => (), - }, + 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)?;