Cleaning
This commit is contained in:
parent
12df99ad84
commit
7b6b7992ab
4
demo.sh
4
demo.sh
|
@ -7,7 +7,7 @@ rand() {
|
||||||
iterations=$(rand 5 10)
|
iterations=$(rand 5 10)
|
||||||
|
|
||||||
for i in $(seq 1 "$iterations"); do
|
for i in $(seq 1 "$iterations"); do
|
||||||
color="\x1B[3$(rand 0 6)m"
|
color="\x1B[3$(rand 1 6)m"
|
||||||
echo -e "$color$(rand 1 100)\x1b[0m"
|
echo -e "$color$(rand 1 100)\x1b[0m"
|
||||||
sleep $(rand 1 2)
|
sleep 0.$(rand 1 2)
|
||||||
done
|
done
|
||||||
|
|
515
src/lib.rs
515
src/lib.rs
|
@ -1,17 +1,45 @@
|
||||||
use std::io::{self, stdin, stdout, Read, Write};
|
use std::io::{self, stdin, stdout, Write};
|
||||||
use std::process::Stdio;
|
use std::sync::mpsc::channel;
|
||||||
use std::sync::mpsc::{channel, Sender};
|
|
||||||
use std::{env, thread};
|
use std::{env, thread};
|
||||||
|
|
||||||
use pty_process::blocking::Command;
|
|
||||||
use pty_process::blocking::Pty;
|
|
||||||
|
|
||||||
use termion::event::{Event, Key, MouseButton, MouseEvent};
|
use termion::event::{Event, Key, MouseButton, MouseEvent};
|
||||||
use termion::input::{MouseTerminal, TermRead};
|
use termion::input::{MouseTerminal, TermRead};
|
||||||
use termion::raw::IntoRawMode;
|
use termion::raw::IntoRawMode;
|
||||||
use termion::screen::IntoAlternateScreen;
|
use termion::screen::IntoAlternateScreen;
|
||||||
use termion::terminal_size;
|
use termion::terminal_size;
|
||||||
use termion::{clear, color, cursor, style};
|
use termion::{clear, cursor};
|
||||||
|
|
||||||
|
pub mod tile;
|
||||||
|
|
||||||
|
use tile::{Tile, TileBuilder};
|
||||||
|
|
||||||
|
/// Draws a box from (x1, y1) to (x2, y2).
|
||||||
|
pub fn rect((x1, y1): (u16, u16), (x2, y2): (u16, u16)) -> String {
|
||||||
|
let mut buffer = vec![];
|
||||||
|
|
||||||
|
buffer.push(format!("{}┌", cursor::Goto(x1, y1)));
|
||||||
|
|
||||||
|
for _ in (x1 + 1)..x2 {
|
||||||
|
buffer.push(format!("─"));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(format!("┐"));
|
||||||
|
|
||||||
|
for y in (y1 + 1)..y2 {
|
||||||
|
buffer.push(format!("{}│", cursor::Goto(x1, y)));
|
||||||
|
buffer.push(format!("{}│", cursor::Goto(x2, y)));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(format!("{}└", cursor::Goto(x1, y2)));
|
||||||
|
|
||||||
|
for _ in (x1 + 1)..x2 {
|
||||||
|
buffer.push(format!("─"));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(format!("┘"));
|
||||||
|
|
||||||
|
buffer.join("")
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the length of a string containing colors and styles.
|
/// Returns the length of a string containing colors and styles.
|
||||||
pub fn str_len(s: &str) -> u16 {
|
pub fn str_len(s: &str) -> u16 {
|
||||||
|
@ -109,173 +137,6 @@ pub fn sub_str<'a>(s: &'a str, start: u16, end: u16) -> &'a str {
|
||||||
&s[real_start..real_end]
|
&s[real_start..real_end]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use termion::color;
|
|
||||||
|
|
||||||
use crate::str_len;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_str_len_1() {
|
|
||||||
let string = format!(
|
|
||||||
"{}Hello{} {}World{}",
|
|
||||||
color::Red.fg_str(),
|
|
||||||
color::Reset.fg_str(),
|
|
||||||
color::Green.fg_str(),
|
|
||||||
color::Reset.fg_str(),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(str_len(&string), 11);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<String>,
|
|
||||||
|
|
||||||
/// 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: String,
|
|
||||||
|
|
||||||
/// The cursor where stdout should write.
|
|
||||||
///
|
|
||||||
/// If None, stdout should push at the end of the string.
|
|
||||||
pub cursor: Option<usize>,
|
|
||||||
|
|
||||||
/// The number of chars in stdout.
|
|
||||||
pub len: usize,
|
|
||||||
|
|
||||||
/// The sender for the communication with the multiview.
|
|
||||||
pub sender: Sender<Msg>,
|
|
||||||
|
|
||||||
/// Coordinates of the tile.
|
|
||||||
pub coords: (u16, u16),
|
|
||||||
|
|
||||||
/// The number of lines that the stdout is scrolled.
|
|
||||||
pub scroll: isize,
|
|
||||||
|
|
||||||
/// The number of lines that stdout will print.
|
|
||||||
pub max_scroll: isize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Tile {
|
|
||||||
/// Creates a new empty tile.
|
|
||||||
pub fn new(command: &[String], i: u16, j: u16, sender: Sender<Msg>) -> Tile {
|
|
||||||
Tile {
|
|
||||||
command: command
|
|
||||||
.into_iter()
|
|
||||||
.map(|x| x.to_string())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
stdout: String::new(),
|
|
||||||
len: 0,
|
|
||||||
cursor: None,
|
|
||||||
sender,
|
|
||||||
coords: (i, j),
|
|
||||||
scroll: 0,
|
|
||||||
max_scroll: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts the commands.
|
|
||||||
pub fn start(&mut self, width: u16, height: u16) {
|
|
||||||
let command = self
|
|
||||||
.command
|
|
||||||
.iter()
|
|
||||||
.map(|x| x.to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let coords = self.coords;
|
|
||||||
let clone = command.clone();
|
|
||||||
let sender = self.sender.clone();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
let pty = Pty::new().unwrap();
|
|
||||||
pty.resize(pty_process::Size::new(height - 4, width - 4))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut child = Command::new(&clone[0])
|
|
||||||
.args(&clone[1..])
|
|
||||||
.stdout(Stdio::piped())
|
|
||||||
.stderr(Stdio::piped())
|
|
||||||
.spawn(&pty.pts().unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut stdout = child.stdout.take().unwrap();
|
|
||||||
let mut stderr = child.stderr.take().unwrap();
|
|
||||||
let stderr_sender = sender.clone();
|
|
||||||
|
|
||||||
let coords = coords;
|
|
||||||
|
|
||||||
thread::spawn(move || loop {
|
|
||||||
let mut buffer = [0; 4096];
|
|
||||||
let result = stderr.read(&mut buffer);
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(0) => break,
|
|
||||||
|
|
||||||
Ok(n) => {
|
|
||||||
stderr_sender
|
|
||||||
.send(Msg::Stderr(
|
|
||||||
coords.0,
|
|
||||||
coords.1,
|
|
||||||
String::from_utf8_lossy(&buffer[0..n]).to_string(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let mut buffer = [0; 4096];
|
|
||||||
let result = stdout.read(&mut buffer);
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(0) => break,
|
|
||||||
|
|
||||||
Ok(n) => {
|
|
||||||
sender
|
|
||||||
.send(Msg::Stderr(
|
|
||||||
coords.0,
|
|
||||||
coords.1,
|
|
||||||
String::from_utf8_lossy(&buffer[0..n]).to_string(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sender
|
|
||||||
.send(Msg::Stdout(coords.0, coords.1, String::from("\n")))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let code = child.wait().unwrap().code().unwrap();
|
|
||||||
|
|
||||||
let exit_string = format!(
|
|
||||||
"{}{}Command exited with return code {}{}\n",
|
|
||||||
style::Bold,
|
|
||||||
if code == 0 {
|
|
||||||
color::Green.fg_str()
|
|
||||||
} else {
|
|
||||||
color::Red.fg_str()
|
|
||||||
},
|
|
||||||
code,
|
|
||||||
style::Reset,
|
|
||||||
);
|
|
||||||
|
|
||||||
sender
|
|
||||||
.send(Msg::Stdout(coords.0, coords.1, exit_string))
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Multiple applications running in a single terminal.
|
/// Multiple applications running in a single terminal.
|
||||||
struct Multiview<W: Write> {
|
struct Multiview<W: Write> {
|
||||||
/// The stdout on which the multiview will be rendererd.
|
/// The stdout on which the multiview will be rendererd.
|
||||||
|
@ -333,191 +194,27 @@ impl<W: Write> Multiview<W> {
|
||||||
self.refresh_ui = true;
|
self.refresh_ui = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draws a box from (x1, y1) to (x2, y2).
|
|
||||||
pub fn rect(&self, (x1, y1): (u16, u16), (x2, y2): (u16, u16)) -> String {
|
|
||||||
let mut buffer = vec![];
|
|
||||||
|
|
||||||
buffer.push(format!("{}┌", cursor::Goto(x1, y1)));
|
|
||||||
|
|
||||||
for _ in (x1 + 1)..x2 {
|
|
||||||
buffer.push(format!("─"));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push(format!("┐"));
|
|
||||||
|
|
||||||
for y in (y1 + 1)..y2 {
|
|
||||||
buffer.push(format!("{}│", cursor::Goto(x1, y)));
|
|
||||||
buffer.push(format!("{}│", cursor::Goto(x2, y)));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push(format!("{}└", cursor::Goto(x1, y2)));
|
|
||||||
|
|
||||||
for _ in (x1 + 1)..x2 {
|
|
||||||
buffer.push(format!("─"));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push(format!("┘"));
|
|
||||||
|
|
||||||
buffer.join("")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders the border and the title of a tile.
|
/// Renders the border and the title of a tile.
|
||||||
pub fn first_render_tile(&self, (i, j): (u16, u16), term_size: (u16, u16)) -> String {
|
pub fn render_tile_border(&self, (i, j): (u16, u16)) -> String {
|
||||||
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;
|
|
||||||
|
|
||||||
let tile = &self.tile((i, j));
|
let tile = &self.tile((i, j));
|
||||||
let command_str = tile.command.join(" ");
|
tile.render_border(self.selected == ((i, j)))
|
||||||
|
|
||||||
let mut buffer = vec![];
|
|
||||||
|
|
||||||
let max_title_len =
|
|
||||||
(term_size.0 / self.tiles[0].len() as u16) - 4 - "Command: ".len() as u16;
|
|
||||||
|
|
||||||
let command_str = if command_str.len() > max_title_len as usize {
|
|
||||||
format!(
|
|
||||||
"{}...",
|
|
||||||
&command_str[0 as usize..max_title_len as usize - 3]
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
command_str
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer.push(format!(
|
|
||||||
"{}{} {}Command: {}{}{}",
|
|
||||||
color::Reset.fg_str(),
|
|
||||||
cursor::Goto(x1 + 1, y1 + 1),
|
|
||||||
style::Bold,
|
|
||||||
command_str,
|
|
||||||
style::Reset,
|
|
||||||
cursor::Goto(x1 + 2, y1 + 3),
|
|
||||||
));
|
|
||||||
|
|
||||||
if self.selected == (i, j) {
|
|
||||||
buffer.push(format!("{}", color::Green.fg_str()));
|
|
||||||
}
|
|
||||||
buffer.push(self.rect((x1, y1), (x2, y2)));
|
|
||||||
buffer.push(format!("{}├", cursor::Goto(x1, y1 + 2)));
|
|
||||||
|
|
||||||
for _ in (x1 + 1)..x2 {
|
|
||||||
buffer.push(format!("─"));
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push(format!("{}┤", cursor::Goto(x2, y1 + 2)));
|
|
||||||
|
|
||||||
buffer.join("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the (x, y) tile.
|
/// Renders the (x, y) tile.
|
||||||
pub fn render_tile(&mut self, (i, j): (u16, u16), term_size: (u16, u16)) -> String {
|
pub fn render_tile_content(&mut self, (i, j): (u16, u16)) -> String {
|
||||||
let w = term_size.0 / self.tiles[0].len() as u16;
|
let tile = self.tile((i, j));
|
||||||
let h = term_size.1 / self.tiles.len() as u16;
|
tile.render_content()
|
||||||
|
|
||||||
let x1 = j * w + 1;
|
|
||||||
let y1 = i * h + 1;
|
|
||||||
|
|
||||||
let tile = &self.tile((i, j));
|
|
||||||
|
|
||||||
let mut buffer = vec![];
|
|
||||||
|
|
||||||
let mut counting = true;
|
|
||||||
let mut line_index = 0;
|
|
||||||
let mut current_char_index = 0;
|
|
||||||
let scroll = tile.scroll as u16;
|
|
||||||
|
|
||||||
buffer.push(format!("{}", cursor::Goto(x1 + 2, y1 + 3)));
|
|
||||||
|
|
||||||
for c in tile.stdout.chars() {
|
|
||||||
if c == '\x1b' {
|
|
||||||
counting = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
match c {
|
|
||||||
'\n' => {
|
|
||||||
line_index += 1;
|
|
||||||
let old_current_char_index = current_char_index;
|
|
||||||
current_char_index = 0;
|
|
||||||
|
|
||||||
if line_index >= scroll && line_index < h + scroll - 4 {
|
|
||||||
if old_current_char_index < w {
|
|
||||||
let mut spaces = String::new();
|
|
||||||
for _ in old_current_char_index..w - 3 {
|
|
||||||
spaces.push(' ');
|
|
||||||
}
|
|
||||||
buffer.push(spaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push(format!(
|
|
||||||
"{}",
|
|
||||||
cursor::Goto(x1 + 2, y1 + 3 + line_index as u16 - scroll)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
if counting {
|
|
||||||
current_char_index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_char_index == w - 3 {
|
|
||||||
line_index += 1;
|
|
||||||
current_char_index = 1;
|
|
||||||
|
|
||||||
if line_index >= scroll && line_index < h + scroll - 4 {
|
|
||||||
buffer.push(format!(
|
|
||||||
"{}",
|
|
||||||
cursor::Goto(x1 + 2, y1 + 3 + line_index as u16 - scroll)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if line_index >= scroll && line_index < h + scroll - 4 {
|
|
||||||
buffer.push(format!("{}", c));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == 'm' {
|
|
||||||
counting = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_char_index == 0 {
|
|
||||||
let mut spaces = format!("{}", cursor::Goto(x1 + 2, y1 + h - 2));
|
|
||||||
for _ in 0..w - 3 {
|
|
||||||
spaces.push(' ');
|
|
||||||
}
|
|
||||||
buffer.push(spaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tile = self.tile_mut((i, j));
|
|
||||||
if tile.max_scroll != line_index as isize {
|
|
||||||
tile.max_scroll = line_index as isize;
|
|
||||||
tile.scroll = tile.max_scroll - h as isize + 5;
|
|
||||||
if tile.scroll < 0 {
|
|
||||||
tile.scroll = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push(format!("{}", style::Reset));
|
|
||||||
buffer.join("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders all the tiles of the multiview.
|
/// Renders all the tiles of the multiview.
|
||||||
pub fn render(&mut self, term_size: (u16, u16)) -> io::Result<()> {
|
pub fn render(&mut self) -> io::Result<()> {
|
||||||
let mut buffer = vec![];
|
let mut buffer = vec![];
|
||||||
for i in 0..self.tiles.len() {
|
for i in 0..self.tiles.len() {
|
||||||
for j in 0..self.tiles[0].len() {
|
for j in 0..self.tiles[0].len() {
|
||||||
if self.refresh_ui {
|
if self.refresh_ui {
|
||||||
buffer.push(self.first_render_tile((i as u16, j as u16), term_size));
|
buffer.push(self.render_tile_border((i as u16, j as u16)));
|
||||||
}
|
}
|
||||||
buffer.push(self.render_tile((i as u16, j as u16), term_size));
|
buffer.push(self.render_tile_content((i as u16, j as u16)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,83 +241,7 @@ impl<W: Write> Multiview<W> {
|
||||||
/// Push a string into a tile's stdout.
|
/// Push a string into a tile's stdout.
|
||||||
pub fn push_stdout(&mut self, (i, j): (u16, u16), content: String) {
|
pub fn push_stdout(&mut self, (i, j): (u16, u16), content: String) {
|
||||||
let tile = self.tile_mut((i, j));
|
let tile = self.tile_mut((i, j));
|
||||||
|
tile.push_stdout(content);
|
||||||
let mut clear_line_counter = 0;
|
|
||||||
|
|
||||||
for c in content.chars() {
|
|
||||||
// Check if we're running into \x1b[K
|
|
||||||
clear_line_counter = match (c, clear_line_counter) {
|
|
||||||
('\x1b', _) => 1,
|
|
||||||
('[', 1) => 2,
|
|
||||||
('K', 2) => 3,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
match (clear_line_counter, tile.cursor) {
|
|
||||||
(3, Some(cursor)) => {
|
|
||||||
// Find the size of the string until the next '\n' or end
|
|
||||||
let mut counter = 0;
|
|
||||||
loop {
|
|
||||||
counter += 1;
|
|
||||||
|
|
||||||
// TODO fix utf8
|
|
||||||
if tile.stdout.len() <= counter + cursor
|
|
||||||
|| &tile.stdout[cursor + counter..cursor + counter + 1] == "\n"
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tile.stdout
|
|
||||||
.replace_range((cursor - 2)..(cursor + counter), "");
|
|
||||||
tile.len -= 2 + counter;
|
|
||||||
tile.cursor = None;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == '\r' {
|
|
||||||
// Set cursor at the right place
|
|
||||||
let mut index = tile.len;
|
|
||||||
let mut reverse = tile.stdout.chars().rev();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match reverse.next() {
|
|
||||||
Some('\n') | None => break,
|
|
||||||
_ => index -= 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tile.cursor = Some(index);
|
|
||||||
} else {
|
|
||||||
let new_cursor = match tile.cursor {
|
|
||||||
Some(index) => {
|
|
||||||
if c == '\n' {
|
|
||||||
tile.stdout.push(c);
|
|
||||||
tile.len += 1;
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
// TODO fix utf8
|
|
||||||
tile.stdout.replace_range(index..index + 1, &c.to_string());
|
|
||||||
if index + 1 == tile.len {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(index + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None => {
|
|
||||||
tile.stdout.push(c);
|
|
||||||
tile.len += 1;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tile.cursor = new_cursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a string into a tile's stderr.
|
/// Push a string into a tile's stderr.
|
||||||
|
@ -638,10 +259,10 @@ impl<W: Write> Drop for Multiview<W> {
|
||||||
/// An event that can be sent in channels.
|
/// An event that can be sent in channels.
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
/// An stdout line arrived.
|
/// An stdout line arrived.
|
||||||
Stdout(u16, u16, String),
|
Stdout((u16, u16), String),
|
||||||
|
|
||||||
/// An stderr line arrived.
|
/// An stderr line arrived.
|
||||||
Stderr(u16, u16, String),
|
Stderr((u16, u16), String),
|
||||||
|
|
||||||
/// A click occured.
|
/// A click occured.
|
||||||
Click(u16, u16),
|
Click(u16, u16),
|
||||||
|
@ -669,7 +290,32 @@ pub fn main() -> io::Result<()> {
|
||||||
.map(|(i, tiles)| {
|
.map(|(i, tiles)| {
|
||||||
tiles
|
tiles
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(j, tile)| Tile::new(tile, i as u16, j as u16, sender.clone()))
|
.map(|(j, tile)| ((i, j), tile))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let term_size = terminal_size()?;
|
||||||
|
|
||||||
|
let tile_size = (
|
||||||
|
term_size.0 / tiles[0].len() as u16,
|
||||||
|
term_size.1 / tiles.len() as u16,
|
||||||
|
);
|
||||||
|
|
||||||
|
let tiles = tiles
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
row.into_iter()
|
||||||
|
.map(|((i, j), tile)| {
|
||||||
|
TileBuilder::new()
|
||||||
|
.command(tile.into())
|
||||||
|
.coords((i as u16, j as u16))
|
||||||
|
.position((j as u16 * tile_size.0 + 1, i as u16 * tile_size.1 + 1))
|
||||||
|
.size(tile_size)
|
||||||
|
.sender(sender.clone())
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -679,19 +325,12 @@ pub fn main() -> io::Result<()> {
|
||||||
let stdout = stdout.into_alternate_screen()?;
|
let stdout = stdout.into_alternate_screen()?;
|
||||||
let stdout = MouseTerminal::from(stdout);
|
let stdout = MouseTerminal::from(stdout);
|
||||||
|
|
||||||
let term_size = terminal_size()?;
|
|
||||||
|
|
||||||
let mut multiview = Multiview::new(stdout, tiles)?;
|
let mut multiview = Multiview::new(stdout, tiles)?;
|
||||||
multiview.render(term_size)?;
|
multiview.render()?;
|
||||||
|
|
||||||
let tile_size = (
|
|
||||||
term_size.0 / multiview.tiles[0].len() as u16,
|
|
||||||
term_size.1 / multiview.tiles.len() as u16,
|
|
||||||
);
|
|
||||||
|
|
||||||
for row in &mut multiview.tiles {
|
for row in &mut multiview.tiles {
|
||||||
for tile in row {
|
for tile in row {
|
||||||
tile.start(tile_size.0, tile_size.1);
|
tile.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -716,8 +355,8 @@ pub fn main() -> io::Result<()> {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match receiver.recv() {
|
match receiver.recv() {
|
||||||
Ok(Msg::Stdout(i, j, line)) => multiview.push_stdout((i, j), line),
|
Ok(Msg::Stdout(coords, line)) => multiview.push_stdout(coords, line),
|
||||||
Ok(Msg::Stderr(i, j, line)) => multiview.push_stderr((i, j), line),
|
Ok(Msg::Stderr(coords, line)) => multiview.push_stderr(coords, line),
|
||||||
Ok(Msg::Click(x, y)) => multiview.select_tile((x, y), term_size),
|
Ok(Msg::Click(x, y)) => multiview.select_tile((x, y), term_size),
|
||||||
Ok(Msg::ScrollDown) => multiview.scroll_down(),
|
Ok(Msg::ScrollDown) => multiview.scroll_down(),
|
||||||
Ok(Msg::ScrollUp) => multiview.scroll_up(),
|
Ok(Msg::ScrollUp) => multiview.scroll_up(),
|
||||||
|
@ -725,7 +364,7 @@ pub fn main() -> io::Result<()> {
|
||||||
Err(_) => (),
|
Err(_) => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
multiview.render(term_size)?;
|
multiview.render()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -0,0 +1,445 @@
|
||||||
|
//! This module contains everything related to tiles.
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
use std::process::Stdio;
|
||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use pty_process::blocking::Command;
|
||||||
|
use pty_process::blocking::Pty;
|
||||||
|
|
||||||
|
use termion::{color, cursor, style};
|
||||||
|
|
||||||
|
use crate::{rect, Msg};
|
||||||
|
|
||||||
|
/// A helper to build tiles.
|
||||||
|
pub struct TileBuilder {
|
||||||
|
/// The command that the tile will run.
|
||||||
|
pub command: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// The coordinates of the tile.
|
||||||
|
pub coords: Option<(u16, u16)>,
|
||||||
|
|
||||||
|
/// The top left corner of the tile.
|
||||||
|
pub position: Option<(u16, u16)>,
|
||||||
|
|
||||||
|
/// The size of the tile.
|
||||||
|
pub size: Option<(u16, u16)>,
|
||||||
|
|
||||||
|
/// The sender to communicate with the main view.
|
||||||
|
pub sender: Option<Sender<Msg>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TileBuilder {
|
||||||
|
/// Creates an empty tile builder.
|
||||||
|
pub fn new() -> TileBuilder {
|
||||||
|
TileBuilder {
|
||||||
|
command: None,
|
||||||
|
coords: None,
|
||||||
|
position: None,
|
||||||
|
size: None,
|
||||||
|
sender: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the command of the tile.
|
||||||
|
pub fn command(self, command: Vec<String>) -> TileBuilder {
|
||||||
|
let mut s = self;
|
||||||
|
s.command = Some(command);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the coordinates of the tile.
|
||||||
|
pub fn coords(self, coords: (u16, u16)) -> TileBuilder {
|
||||||
|
let mut s = self;
|
||||||
|
s.coords = Some(coords);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the position of the tile.
|
||||||
|
pub fn position(self, position: (u16, u16)) -> TileBuilder {
|
||||||
|
let mut s = self;
|
||||||
|
s.position = Some(position);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the size of the tile.
|
||||||
|
pub fn size(self, size: (u16, u16)) -> TileBuilder {
|
||||||
|
let mut s = self;
|
||||||
|
s.size = Some(size);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the sender of the tile.
|
||||||
|
pub fn sender(self, sender: Sender<Msg>) -> TileBuilder {
|
||||||
|
let mut s = self;
|
||||||
|
s.sender = Some(sender);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the tile.
|
||||||
|
pub fn build(self) -> Option<Tile> {
|
||||||
|
let (x, y) = self.position?;
|
||||||
|
let (w, h) = self.size?;
|
||||||
|
|
||||||
|
Some(Tile {
|
||||||
|
command: self.command?,
|
||||||
|
coords: self.coords?,
|
||||||
|
outer_position: (x, y),
|
||||||
|
inner_position: (x + 2, y + 3),
|
||||||
|
outer_size: (w, h),
|
||||||
|
inner_size: (w - 4, h - 5),
|
||||||
|
sender: self.sender?,
|
||||||
|
stdout: String::new(),
|
||||||
|
cursor: None,
|
||||||
|
len: 0,
|
||||||
|
scroll: 0,
|
||||||
|
number_lines: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String>,
|
||||||
|
|
||||||
|
/// 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: String,
|
||||||
|
|
||||||
|
/// The cursor where stdout should write.
|
||||||
|
///
|
||||||
|
/// If None, stdout should push at the end of the string.
|
||||||
|
pub cursor: Option<usize>,
|
||||||
|
|
||||||
|
/// The number of chars in stdout.
|
||||||
|
pub len: usize,
|
||||||
|
|
||||||
|
/// The sender for the communication with the multiview.
|
||||||
|
pub sender: Sender<Msg>,
|
||||||
|
|
||||||
|
/// Coordinates of the tile.
|
||||||
|
pub coords: (u16, u16),
|
||||||
|
|
||||||
|
/// Top left corner of the tile.
|
||||||
|
pub outer_position: (u16, u16),
|
||||||
|
|
||||||
|
/// Top left corner of the content of the tile.
|
||||||
|
pub inner_position: (u16, u16),
|
||||||
|
|
||||||
|
/// Size of the tile.
|
||||||
|
pub outer_size: (u16, u16),
|
||||||
|
|
||||||
|
/// Size of the inside of the tile.
|
||||||
|
pub inner_size: (u16, u16),
|
||||||
|
|
||||||
|
/// The number of lines that the stdout is scrolled.
|
||||||
|
pub scroll: isize,
|
||||||
|
|
||||||
|
/// The number of lines that stdout will print.
|
||||||
|
pub number_lines: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tile {
|
||||||
|
/// Starts the commands.
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
let command = self
|
||||||
|
.command
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let coords = self.coords;
|
||||||
|
let clone = command.clone();
|
||||||
|
let size = self.inner_size;
|
||||||
|
let sender = self.sender.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let pty = Pty::new().unwrap();
|
||||||
|
pty.resize(pty_process::Size::new(size.1, size.0)).unwrap();
|
||||||
|
|
||||||
|
let mut child = Command::new(&clone[0])
|
||||||
|
.args(&clone[1..])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn(&pty.pts().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut stdout = child.stdout.take().unwrap();
|
||||||
|
let mut stderr = child.stderr.take().unwrap();
|
||||||
|
let stderr_sender = sender.clone();
|
||||||
|
|
||||||
|
let coords = coords;
|
||||||
|
|
||||||
|
thread::spawn(move || loop {
|
||||||
|
let mut buffer = [0; 4096];
|
||||||
|
let result = stderr.read(&mut buffer);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(0) => break,
|
||||||
|
|
||||||
|
Ok(n) => {
|
||||||
|
stderr_sender
|
||||||
|
.send(Msg::Stderr(
|
||||||
|
coords,
|
||||||
|
String::from_utf8_lossy(&buffer[0..n]).to_string(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut buffer = [0; 4096];
|
||||||
|
let result = stdout.read(&mut buffer);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(0) => break,
|
||||||
|
|
||||||
|
Ok(n) => {
|
||||||
|
sender
|
||||||
|
.send(Msg::Stderr(
|
||||||
|
coords,
|
||||||
|
String::from_utf8_lossy(&buffer[0..n]).to_string(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sender
|
||||||
|
.send(Msg::Stdout(coords, String::from("\n")))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let code = child.wait().unwrap().code().unwrap();
|
||||||
|
|
||||||
|
let exit_string = format!(
|
||||||
|
"{}{}Command exited with return code {}{}\n",
|
||||||
|
style::Bold,
|
||||||
|
if code == 0 {
|
||||||
|
color::Green.fg_str()
|
||||||
|
} else {
|
||||||
|
color::Red.fg_str()
|
||||||
|
},
|
||||||
|
code,
|
||||||
|
style::Reset,
|
||||||
|
);
|
||||||
|
|
||||||
|
sender.send(Msg::Stdout(coords, exit_string)).unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push content into the stdout of the tile.
|
||||||
|
pub fn push_stdout(&mut self, content: String) {
|
||||||
|
let mut clear_line_counter = 0;
|
||||||
|
|
||||||
|
for c in content.chars() {
|
||||||
|
// Check if we're running into \x1b[K
|
||||||
|
clear_line_counter = match (c, clear_line_counter) {
|
||||||
|
('\x1b', _) => 1,
|
||||||
|
('[', 1) => 2,
|
||||||
|
('K', 2) => 3,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
match (clear_line_counter, self.cursor) {
|
||||||
|
(3, Some(cursor)) => {
|
||||||
|
// Find the size of the string until the next '\n' or end
|
||||||
|
let mut counter = 0;
|
||||||
|
loop {
|
||||||
|
counter += 1;
|
||||||
|
|
||||||
|
// TODO fix utf8
|
||||||
|
if self.stdout.len() <= counter + cursor
|
||||||
|
|| &self.stdout[cursor + counter..cursor + counter + 1] == "\n"
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stdout
|
||||||
|
.replace_range((cursor - 2)..(cursor + counter), "");
|
||||||
|
self.len -= 2 + counter;
|
||||||
|
self.cursor = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '\r' {
|
||||||
|
// Set cursor at the right place
|
||||||
|
let mut index = self.len;
|
||||||
|
let mut reverse = self.stdout.chars().rev();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reverse.next() {
|
||||||
|
Some('\n') | None => break,
|
||||||
|
_ => index -= 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cursor = Some(index);
|
||||||
|
} else {
|
||||||
|
let new_cursor = match self.cursor {
|
||||||
|
Some(index) => {
|
||||||
|
if c == '\n' {
|
||||||
|
self.stdout.push(c);
|
||||||
|
self.len += 1;
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// TODO fix utf8
|
||||||
|
self.stdout.replace_range(index..index + 1, &c.to_string());
|
||||||
|
if index + 1 == self.len {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
self.stdout.push(c);
|
||||||
|
self.len += 1;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.cursor = new_cursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the borders of the tile.
|
||||||
|
pub fn render_border(&self, selected: bool) -> String {
|
||||||
|
let (x, y) = self.outer_position;
|
||||||
|
let (w, h) = self.outer_size;
|
||||||
|
|
||||||
|
let command_str = self.command.join(" ");
|
||||||
|
|
||||||
|
let mut buffer = vec![];
|
||||||
|
|
||||||
|
let max_title_len = self.inner_size.1 - "Command: ".len() as u16;
|
||||||
|
|
||||||
|
let command_str = if command_str.len() > max_title_len as usize {
|
||||||
|
format!(
|
||||||
|
"{}...",
|
||||||
|
&command_str[0 as usize..max_title_len as usize - 3]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
command_str
|
||||||
|
};
|
||||||
|
|
||||||
|
buffer.push(format!(
|
||||||
|
"{}{} {}Command: {}{}{}",
|
||||||
|
color::Reset.fg_str(),
|
||||||
|
cursor::Goto(x + 1, y + 1),
|
||||||
|
style::Bold,
|
||||||
|
command_str,
|
||||||
|
style::Reset,
|
||||||
|
cursor::Goto(x + 2, y + 3),
|
||||||
|
));
|
||||||
|
|
||||||
|
if selected {
|
||||||
|
buffer.push(format!("{}", color::Green.fg_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(rect((x, y), (x + w - 1, y + h - 1)));
|
||||||
|
buffer.push(format!("{}├", cursor::Goto(x, y + 2)));
|
||||||
|
|
||||||
|
for _ in (x + 1)..(x + w) {
|
||||||
|
buffer.push(format!("─"));
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(format!("{}┤", cursor::Goto(x + w - 1, y + 2)));
|
||||||
|
|
||||||
|
buffer.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the content of the tile.
|
||||||
|
pub fn render_content(&self) -> String {
|
||||||
|
let (x, y) = self.inner_position;
|
||||||
|
let (w, h) = self.inner_size;
|
||||||
|
|
||||||
|
let mut buffer = vec![];
|
||||||
|
|
||||||
|
let mut counting = true;
|
||||||
|
let mut line_index = 0;
|
||||||
|
let mut current_char_index = 0;
|
||||||
|
let scroll = self.scroll as u16;
|
||||||
|
|
||||||
|
buffer.push(format!("{}", cursor::Goto(x, y)));
|
||||||
|
|
||||||
|
for c in self.stdout.chars() {
|
||||||
|
if c == '\x1b' {
|
||||||
|
counting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match c {
|
||||||
|
'\n' => {
|
||||||
|
line_index += 1;
|
||||||
|
let old_current_char_index = current_char_index;
|
||||||
|
current_char_index = 0;
|
||||||
|
|
||||||
|
if line_index >= scroll && line_index < h + scroll {
|
||||||
|
if old_current_char_index < w {
|
||||||
|
let mut spaces = String::new();
|
||||||
|
for _ in old_current_char_index..w {
|
||||||
|
spaces.push(' ');
|
||||||
|
}
|
||||||
|
buffer.push(spaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(format!(
|
||||||
|
"{}",
|
||||||
|
cursor::Goto(x, y + line_index as u16 - scroll)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
if counting {
|
||||||
|
current_char_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_char_index == w {
|
||||||
|
line_index += 1;
|
||||||
|
current_char_index = 1;
|
||||||
|
|
||||||
|
if line_index >= scroll && line_index < h + scroll {
|
||||||
|
buffer.push(format!(
|
||||||
|
"{}",
|
||||||
|
cursor::Goto(x, y + line_index as u16 - scroll)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if line_index >= scroll && line_index < h + scroll {
|
||||||
|
buffer.push(format!("{}", c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == 'm' {
|
||||||
|
counting = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_char_index == 0 {
|
||||||
|
let mut spaces = format!("{}", cursor::Goto(x, y + h));
|
||||||
|
for _ in 0..w {
|
||||||
|
spaces.push(' ');
|
||||||
|
}
|
||||||
|
buffer.push(spaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.push(format!("{}", style::Reset));
|
||||||
|
buffer.join("")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue