Compare commits
15 Commits
14c6db5769
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b7a5d83faa | |||
| 20757a093b | |||
| 7e4d0e05d8 | |||
| b9c31c2251 | |||
| dc950ad0d4 | |||
| 79f03b27bf | |||
| 2e771773b6 | |||
| 093845a4c6 | |||
| 6121dc4224 | |||
| e3d891c522 | |||
| ce8b4fae68 | |||
| 265208db3a | |||
| 0f4211005e | |||
| 922ba9c524 | |||
| 9802c8e740 |
60
README.md
Normal file
60
README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# multiview-rs
|
||||||
|
|
||||||
|
*Run many commands and watch all outputs in a single terminal*
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If rust is not already installed, [install rust](https://www.rust-lang.org/tools/install).
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install --git https://gitea.tforgione.fr/tforgione/multiview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Split your terminal in two rows, the first containing three columns, and the second containing one column:
|
||||||
|
```sh
|
||||||
|
multiview cmd1 :: cmd2 :: cmd3 // cmd4 :: cmd5
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Split your terminal in two columns, the first containing three rows, and the second containing one row:
|
||||||
|
```sh
|
||||||
|
multiview cmd1 // cmd2 // cmd3 :: cmd4 // cmd5
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Colors
|
||||||
|
|
||||||
|
Most well written programs will disable colors when running from multiview, in order to force them to use colors, you
|
||||||
|
can use the `unbuffer` command from the [`expect` package](https://packages.ubuntu.com/search?keywords=expect).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
multiview unbuffer cmd1 :: unbuffer cmd2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shortcuts
|
||||||
|
|
||||||
|
- `k`: kills the current tile
|
||||||
|
- `K`: kills all tiles
|
||||||
|
- `r`: restarts the current tile
|
||||||
|
- `R`: restarts all tiles
|
||||||
|
- `l`: draw a line on the current tile
|
||||||
|
- `L`: draw a line on all tiles
|
||||||
|
- `q`: quits
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
This is my attempt to rewrite [arjunmehta's multiview](https://github.com/arjunmehta/multiview) in rust.
|
||||||
|
|
||||||
|
Their version has many features that I don't use, but is missing a few things that I need:
|
||||||
|
- line wrapping: when a line is bigger than the terminal size, the end is just not displayed
|
||||||
|
- scroll: if your output has more lines than your terminal height, there is no way (to my knowledge) to scroll up
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
echo -en "0"
|
||||||
for i in `seq 1 100`; do
|
for i in `seq 1 100`; do
|
||||||
sleep 0.05s
|
sleep 0.005s
|
||||||
echo -en "\n$i"
|
echo -en "\n$i"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
width=$(stty size | cut -d ' ' -f 2)
|
|
||||||
|
|
||||||
for c in a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9; do
|
for c in a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9; do
|
||||||
for i in `seq 1 $width`; do
|
for i in `seq 1 $(stty size | cut -d ' ' -f 2)`; do
|
||||||
echo -n $c
|
echo -n $c
|
||||||
done
|
done
|
||||||
|
sleep 2s
|
||||||
done
|
done
|
||||||
|
|||||||
BIN
screenshots/col-major.png
Normal file
BIN
screenshots/col-major.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
screenshots/row-major.png
Normal file
BIN
screenshots/row-major.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
246
src/lib.rs
246
src/lib.rs
@@ -10,10 +10,12 @@ use termion::screen::IntoAlternateScreen;
|
|||||||
use termion::terminal_size;
|
use termion::terminal_size;
|
||||||
use termion::{clear, cursor};
|
use termion::{clear, cursor};
|
||||||
|
|
||||||
|
use tile::{Tile, TileBuilder};
|
||||||
|
|
||||||
pub mod tile;
|
pub mod tile;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
use tile::{Tile, TileBuilder};
|
const DELAY: Duration = Duration::from_millis(20);
|
||||||
|
|
||||||
/// Multiple applications running in a single terminal.
|
/// Multiple applications running in a single terminal.
|
||||||
struct Multiview<W: Write> {
|
struct Multiview<W: Write> {
|
||||||
@@ -29,6 +31,9 @@ struct Multiview<W: Write> {
|
|||||||
/// Whether we need to refresh the UI.
|
/// Whether we need to refresh the UI.
|
||||||
pub refresh_ui: bool,
|
pub refresh_ui: bool,
|
||||||
|
|
||||||
|
/// Whether we need to refresh the tiles.
|
||||||
|
pub refresh_tiles: bool,
|
||||||
|
|
||||||
/// Last time when the rendering was performed.
|
/// Last time when the rendering was performed.
|
||||||
pub last_render: Instant,
|
pub last_render: Instant,
|
||||||
}
|
}
|
||||||
@@ -41,6 +46,7 @@ impl<W: Write> Multiview<W> {
|
|||||||
tiles,
|
tiles,
|
||||||
selected: (0, 0),
|
selected: (0, 0),
|
||||||
refresh_ui: true,
|
refresh_ui: true,
|
||||||
|
refresh_tiles: false,
|
||||||
last_render: Instant::now(),
|
last_render: Instant::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,11 +74,17 @@ impl<W: Write> Multiview<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the selected tile from (x, y) coordinates.
|
/// Sets the selected tile from (x, y) coordinates.
|
||||||
pub fn select_tile(&mut self, (x, y): (u16, u16), term_size: (u16, u16)) {
|
pub fn select_tile(&mut self, (x, y): (u16, u16)) {
|
||||||
let w = term_size.0 / self.tiles[0].len() as u16;
|
// Ugly but working
|
||||||
let h = term_size.1 / self.tiles.len() as u16;
|
for (i, row) in self.tiles.iter().enumerate() {
|
||||||
|
for (j, tile) in row.iter().enumerate() {
|
||||||
self.selected = (y / h, x / w);
|
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;
|
self.refresh_ui = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,21 +97,31 @@ impl<W: Write> Multiview<W> {
|
|||||||
/// Renders the (x, y) tile.
|
/// Renders the (x, y) tile.
|
||||||
pub fn render_tile_content(&mut self, (i, j): (u16, u16)) -> String {
|
pub fn render_tile_content(&mut self, (i, j): (u16, u16)) -> String {
|
||||||
let tile = self.tile((i, j));
|
let tile = self.tile((i, j));
|
||||||
tile.render_content()
|
tile.render_content(self.selected == (i, j))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders all the tiles of the multiview.
|
/// Renders all the tiles of the multiview.
|
||||||
pub fn render(&mut self) -> io::Result<()> {
|
pub fn render(&mut self, force: bool) -> io::Result<()> {
|
||||||
// let now = Instant::now();
|
if !self.refresh_tiles {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// if now.duration_since(self.last_render) < Duration::from_millis(20) {
|
let now = Instant::now();
|
||||||
// return Ok(());
|
|
||||||
// }
|
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![]
|
||||||
|
};
|
||||||
|
|
||||||
// self.last_render = now;
|
|
||||||
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[i].len() {
|
||||||
if self.refresh_ui {
|
if self.refresh_ui {
|
||||||
buffer.push(self.render_tile_border((i as u16, j as u16)));
|
buffer.push(self.render_tile_border((i as u16, j as u16)));
|
||||||
}
|
}
|
||||||
@@ -108,6 +130,7 @@ impl<W: Write> Multiview<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_ui = false;
|
self.refresh_ui = false;
|
||||||
|
self.refresh_tiles = false;
|
||||||
write!(self.stdout, "{}", buffer.join(""))?;
|
write!(self.stdout, "{}", buffer.join(""))?;
|
||||||
self.stdout.flush()?;
|
self.stdout.flush()?;
|
||||||
|
|
||||||
@@ -115,15 +138,15 @@ impl<W: Write> Multiview<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls down the current selected tile.
|
/// Scrolls down the current selected tile.
|
||||||
pub fn scroll_down(&mut self) {
|
pub fn scroll_down(&mut self, step: isize) {
|
||||||
let tile = self.tile_mut(self.selected);
|
let tile = self.tile_mut(self.selected);
|
||||||
tile.scroll_down();
|
tile.scroll_down(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls up the current selected tile.
|
/// Scrolls up the current selected tile.
|
||||||
pub fn scroll_up(&mut self) {
|
pub fn scroll_up(&mut self, step: isize) {
|
||||||
let tile = self.tile_mut(self.selected);
|
let tile = self.tile_mut(self.selected);
|
||||||
tile.scroll_up();
|
tile.scroll_up(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls down to the bottom of the current selected tile.
|
/// Scrolls down to the bottom of the current selected tile.
|
||||||
@@ -150,37 +173,54 @@ impl<W: Write> Multiview<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Restarts the selected tile.
|
/// Restarts the selected tile.
|
||||||
pub fn restart(&mut self) -> io::Result<()> {
|
pub fn restart(&mut self) {
|
||||||
let tile = self.tile_mut(self.selected);
|
let tile = self.tile_mut(self.selected);
|
||||||
tile.restart()
|
tile.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restarts all tiles.
|
/// Restarts all tiles.
|
||||||
pub fn restart_all(&mut self) -> io::Result<()> {
|
pub fn restart_all(&mut self) {
|
||||||
for row in &mut self.tiles {
|
for row in &mut self.tiles {
|
||||||
for tile in row {
|
for tile in row {
|
||||||
tile.restart()?;
|
tile.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kills the selected tile.
|
/// Kills the selected tile.
|
||||||
pub fn kill(&mut self) -> io::Result<()> {
|
pub fn kill(&mut self) {
|
||||||
let tile = self.tile_mut(self.selected);
|
let tile = self.tile_mut(self.selected);
|
||||||
tile.kill()
|
tile.kill();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kills all tiles.
|
/// Kills all tiles.
|
||||||
pub fn kill_all(&mut self) -> io::Result<()> {
|
pub fn kill_all(&mut self) {
|
||||||
for row in &mut self.tiles {
|
for row in &mut self.tiles {
|
||||||
for tile in row {
|
for tile in row {
|
||||||
tile.kill()?;
|
tile.kill();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
/// 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.
|
/// Exits.
|
||||||
@@ -189,10 +229,35 @@ impl<W: Write> Multiview<W> {
|
|||||||
|
|
||||||
for row in &mut self.tiles {
|
for row in &mut self.tiles {
|
||||||
for tile in row {
|
for tile in row {
|
||||||
tile.kill().ok();
|
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> {
|
impl<W: Write> Drop for Multiview<W> {
|
||||||
@@ -202,6 +267,7 @@ impl<W: Write> Drop for Multiview<W> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An event that can be sent in channels.
|
/// An event that can be sent in channels.
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
/// An stdout line arrived.
|
/// An stdout line arrived.
|
||||||
Stdout((u16, u16), String),
|
Stdout((u16, u16), String),
|
||||||
@@ -225,10 +291,10 @@ pub enum Msg {
|
|||||||
KillAll,
|
KillAll,
|
||||||
|
|
||||||
/// Scroll up one line.
|
/// Scroll up one line.
|
||||||
ScrollUp,
|
ScrollUp(isize),
|
||||||
|
|
||||||
/// Scroll down one line.
|
/// Scroll down one line.
|
||||||
ScrollDown,
|
ScrollDown(isize),
|
||||||
|
|
||||||
/// Scroll to the top of the log.
|
/// Scroll to the top of the log.
|
||||||
ScrollFullUp,
|
ScrollFullUp,
|
||||||
@@ -236,6 +302,15 @@ pub enum Msg {
|
|||||||
/// Scroll to the bottom of the log.
|
/// Scroll to the bottom of the log.
|
||||||
ScrollFullDown,
|
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.
|
/// The program was asked to exit.
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
@@ -246,9 +321,33 @@ pub fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
let args = env::args().skip(1).collect::<Vec<_>>();
|
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
|
let tiles = args
|
||||||
.split(|x| x == "//")
|
.split(|x| x == first_split)
|
||||||
.map(|x| x.split(|y| y == "::").enumerate().collect::<Vec<_>>())
|
.map(|x| {
|
||||||
|
x.split(|y| y == second_split)
|
||||||
|
.enumerate()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, tiles)| {
|
.map(|(i, tiles)| {
|
||||||
tiles
|
tiles
|
||||||
@@ -258,22 +357,29 @@ pub fn main() -> io::Result<()> {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let term_size = terminal_size()?;
|
let mut term_size = terminal_size()?;
|
||||||
|
|
||||||
let tile_size = (
|
let col_len = tiles.len() as u16;
|
||||||
term_size.0 / tiles[0].len() as u16,
|
|
||||||
term_size.1 / tiles.len() as u16,
|
|
||||||
);
|
|
||||||
|
|
||||||
let tiles = tiles
|
let tiles = tiles
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| {
|
.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()
|
row.into_iter()
|
||||||
.map(|((i, j), tile)| {
|
.map(|((i, j), tile)| {
|
||||||
|
let (p_i, p_j) = if is_row_major { (i, j) } else { (j, i) };
|
||||||
|
|
||||||
TileBuilder::new()
|
TileBuilder::new()
|
||||||
.command(tile.into())
|
.command(tile.into())
|
||||||
.coords((i as u16, j as u16))
|
.coords((i as u16, j as u16))
|
||||||
.position((j as u16 * tile_size.0 + 1, i as u16 * tile_size.1 + 1))
|
.position((p_j as u16 * tile_size.0 + 1, p_i as u16 * tile_size.1 + 1))
|
||||||
.size(tile_size)
|
.size(tile_size)
|
||||||
.sender(sender.clone())
|
.sender(sender.clone())
|
||||||
.build()
|
.build()
|
||||||
@@ -289,7 +395,7 @@ pub fn main() -> io::Result<()> {
|
|||||||
let stdout = MouseTerminal::from(stdout);
|
let stdout = MouseTerminal::from(stdout);
|
||||||
|
|
||||||
let mut multiview = Multiview::new(stdout, tiles)?;
|
let mut multiview = Multiview::new(stdout, tiles)?;
|
||||||
multiview.render()?;
|
multiview.render(true)?;
|
||||||
|
|
||||||
for row in &mut multiview.tiles {
|
for row in &mut multiview.tiles {
|
||||||
for tile in row {
|
for tile in row {
|
||||||
@@ -306,13 +412,15 @@ pub fn main() -> io::Result<()> {
|
|||||||
Event::Key(Key::Char('R')) => sender.send(Msg::RestartAll).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::Kill).unwrap(),
|
||||||
Event::Key(Key::Char('K')) => sender.send(Msg::KillAll).unwrap(),
|
Event::Key(Key::Char('K')) => sender.send(Msg::KillAll).unwrap(),
|
||||||
Event::Key(Key::Down) => sender.send(Msg::ScrollDown).unwrap(),
|
Event::Key(Key::Char('l')) => sender.send(Msg::AddLine).unwrap(),
|
||||||
Event::Key(Key::Up) => sender.send(Msg::ScrollUp).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::End) => sender.send(Msg::ScrollFullDown).unwrap(),
|
||||||
Event::Key(Key::Home) => sender.send(Msg::ScrollFullUp).unwrap(),
|
Event::Key(Key::Home) => sender.send(Msg::ScrollFullUp).unwrap(),
|
||||||
Event::Mouse(MouseEvent::Press(p, x, y)) => match p {
|
Event::Mouse(MouseEvent::Press(p, x, y)) => match p {
|
||||||
MouseButton::WheelUp => sender.send(Msg::ScrollUp).unwrap(),
|
MouseButton::WheelUp => sender.send(Msg::ScrollUp(3)).unwrap(),
|
||||||
MouseButton::WheelDown => sender.send(Msg::ScrollDown).unwrap(),
|
MouseButton::WheelDown => sender.send(Msg::ScrollDown(3)).unwrap(),
|
||||||
MouseButton::Left => sender.send(Msg::Click(x, y)).unwrap(),
|
MouseButton::Left => sender.send(Msg::Click(x, y)).unwrap(),
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
@@ -323,26 +431,40 @@ pub fn main() -> io::Result<()> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match receiver.recv() {
|
if let Ok(msg) = receiver.recv_timeout(DELAY) {
|
||||||
Ok(Msg::Stdout(coords, line)) => multiview.push_stdout(coords, line),
|
let is_exit = msg == Msg::Exit;
|
||||||
Ok(Msg::Stderr(coords, line)) => multiview.push_stderr(coords, line),
|
multiview.manage_msg(msg)?;
|
||||||
Ok(Msg::Click(x, y)) => multiview.select_tile((x, y), term_size),
|
if is_exit {
|
||||||
Ok(Msg::ScrollDown) => multiview.scroll_down(),
|
|
||||||
Ok(Msg::Restart) => multiview.restart()?,
|
|
||||||
Ok(Msg::RestartAll) => multiview.restart_all()?,
|
|
||||||
Ok(Msg::Kill) => multiview.kill()?,
|
|
||||||
Ok(Msg::KillAll) => multiview.kill_all()?,
|
|
||||||
Ok(Msg::ScrollUp) => multiview.scroll_up(),
|
|
||||||
Ok(Msg::ScrollFullDown) => multiview.scroll_full_down(),
|
|
||||||
Ok(Msg::ScrollFullUp) => multiview.scroll_full_up(),
|
|
||||||
Ok(Msg::Exit) => {
|
|
||||||
multiview.exit();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(_) => (),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
multiview.render()?;
|
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(())
|
Ok(())
|
||||||
|
|||||||
258
src/tile.rs
258
src/tile.rs
@@ -1,7 +1,7 @@
|
|||||||
//! This module contains everything related to tiles.
|
//! This module contains everything related to tiles.
|
||||||
|
|
||||||
use std::io::{self, Read};
|
use std::io::Read;
|
||||||
use std::process::{Child, Stdio};
|
use std::process::Stdio;
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
@@ -96,7 +96,8 @@ impl TileBuilder {
|
|||||||
scroll: 0,
|
scroll: 0,
|
||||||
counting: true,
|
counting: true,
|
||||||
column_number: 0,
|
column_number: 0,
|
||||||
child: None,
|
pty: None,
|
||||||
|
sticky: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,8 +141,11 @@ pub struct Tile {
|
|||||||
/// The number of the current column.
|
/// The number of the current column.
|
||||||
pub column_number: u16,
|
pub column_number: u16,
|
||||||
|
|
||||||
/// The PTY and the child process of the command running in the tile.
|
/// The PTY of the command running in the tile.
|
||||||
pub child: Option<(Pty, Child)>,
|
pub pty: Option<Pty>,
|
||||||
|
|
||||||
|
/// Whether the tile should autoscroll.
|
||||||
|
pub sticky: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tile {
|
impl Tile {
|
||||||
@@ -161,12 +165,44 @@ impl Tile {
|
|||||||
let pty = Pty::new().unwrap();
|
let pty = Pty::new().unwrap();
|
||||||
pty.resize(pty_process::Size::new(size.1, size.0)).unwrap();
|
pty.resize(pty_process::Size::new(size.1, size.0)).unwrap();
|
||||||
|
|
||||||
let mut child = Command::new(&clone[0])
|
let child = Command::new(&clone[0])
|
||||||
.args(&clone[1..])
|
.args(&clone[1..])
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.spawn(&pty.pts().unwrap())
|
.spawn(&pty.pts().unwrap());
|
||||||
|
|
||||||
|
let mut child = match child {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
let exit_string = format!(
|
||||||
|
"{}{}Couldn't run command: {}\r{}",
|
||||||
|
style::Bold,
|
||||||
|
color::Red.fg_str(),
|
||||||
|
e,
|
||||||
|
style::Reset,
|
||||||
|
);
|
||||||
|
|
||||||
|
sender.send(Msg::Stdout(coords, exit_string)).unwrap();
|
||||||
|
|
||||||
|
let mut line = String::new();
|
||||||
|
for _ in 0..size.0 - 1 {
|
||||||
|
line.push('─');
|
||||||
|
}
|
||||||
|
|
||||||
|
sender
|
||||||
|
.send(Msg::Stdout(
|
||||||
|
coords,
|
||||||
|
format!(
|
||||||
|
"\n{}{}{}\n",
|
||||||
|
color::Red.fg_str(),
|
||||||
|
line,
|
||||||
|
color::Reset.fg_str()
|
||||||
|
),
|
||||||
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut stdout = child.stdout.take().unwrap();
|
let mut stdout = child.stdout.take().unwrap();
|
||||||
let mut stderr = child.stderr.take().unwrap();
|
let mut stderr = child.stderr.take().unwrap();
|
||||||
@@ -195,33 +231,41 @@ impl Tile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let code = child.wait().unwrap().code();
|
||||||
|
|
||||||
sender
|
sender
|
||||||
.send(Msg::Stdout(coords, String::from("\n")))
|
.send(Msg::Stdout(coords, String::from("\n")))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let code = 0;
|
let exit_string = match code {
|
||||||
|
Some(0) => format!(
|
||||||
let exit_string = format!(
|
"{}{}Command finished successfully\r{}",
|
||||||
"{}{}Command exited with return code {}\r{}",
|
|
||||||
style::Bold,
|
style::Bold,
|
||||||
if code == 0 {
|
color::Green.fg_str(),
|
||||||
color::Green.fg_str()
|
|
||||||
} else {
|
|
||||||
color::Red.fg_str()
|
|
||||||
},
|
|
||||||
code,
|
|
||||||
style::Reset,
|
style::Reset,
|
||||||
);
|
),
|
||||||
|
Some(x) => {
|
||||||
|
format!(
|
||||||
|
"{}{}Command failed with exit code {}\r{}",
|
||||||
|
style::Bold,
|
||||||
|
color::Red.fg_str(),
|
||||||
|
x,
|
||||||
|
style::Reset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
format!(
|
||||||
|
"{}{}Command was interrupted\r{}",
|
||||||
|
style::Bold,
|
||||||
|
color::Red.fg_str(),
|
||||||
|
style::Reset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
sender.send(Msg::Stdout(coords, exit_string)).unwrap();
|
sender.send(Msg::Stdout(coords, exit_string)).unwrap();
|
||||||
|
|
||||||
let mut line = String::new();
|
|
||||||
for _ in 0..size.0 - 1 {
|
|
||||||
line.push('─');
|
|
||||||
}
|
|
||||||
|
|
||||||
sender
|
sender
|
||||||
.send(Msg::Stdout(coords, format!("\n{}\n", line)))
|
.send(Msg::AddFinishLine(coords, code == Some(0)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -245,7 +289,7 @@ impl Tile {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.child = Some((pty, child));
|
self.pty = Some(pty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push content into the stdout of the tile.
|
/// Push content into the stdout of the tile.
|
||||||
@@ -275,7 +319,7 @@ impl Tile {
|
|||||||
|
|
||||||
if self.counting && !is_variation_selector {
|
if self.counting && !is_variation_selector {
|
||||||
self.column_number += 1;
|
self.column_number += 1;
|
||||||
if self.column_number == self.inner_size.0 + 1 {
|
if self.column_number == self.inner_size.0 {
|
||||||
self.stdout.push(String::new());
|
self.stdout.push(String::new());
|
||||||
self.column_number = 0;
|
self.column_number = 0;
|
||||||
}
|
}
|
||||||
@@ -289,9 +333,8 @@ impl Tile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Autoscroll whene content arrives on stdout
|
// Autoscroll whene content arrives on stdout
|
||||||
self.scroll = self.stdout.len() as isize - 1 - (self.inner_size.1 as isize);
|
if self.sticky {
|
||||||
if self.scroll < 0 {
|
self.scroll = self.max_scroll();
|
||||||
self.scroll = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +389,7 @@ impl Tile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the content of the tile.
|
/// Renders the content of the tile.
|
||||||
pub fn render_content(&self) -> String {
|
pub fn render_content(&self, selected: bool) -> String {
|
||||||
const DELETE_CHAR: char = ' ';
|
const DELETE_CHAR: char = ' ';
|
||||||
|
|
||||||
let (x, y) = self.inner_position;
|
let (x, y) = self.inner_position;
|
||||||
@@ -448,8 +491,6 @@ impl Tile {
|
|||||||
}
|
}
|
||||||
buffer.push(spaces);
|
buffer.push(spaces);
|
||||||
|
|
||||||
eprintln!("Clear {}", max_char_index);
|
|
||||||
|
|
||||||
line_index += 1;
|
line_index += 1;
|
||||||
current_char_index = 0;
|
current_char_index = 0;
|
||||||
max_char_index = 0;
|
max_char_index = 0;
|
||||||
@@ -499,6 +540,7 @@ impl Tile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if last_line_index as u16 - scroll <= h {
|
||||||
let mut spaces = format!(
|
let mut spaces = format!(
|
||||||
"{}",
|
"{}",
|
||||||
cursor::Goto(x + max_char_index, y + last_line_index as u16 - scroll)
|
cursor::Goto(x + max_char_index, y + last_line_index as u16 - scroll)
|
||||||
@@ -508,51 +550,161 @@ impl Tile {
|
|||||||
spaces.push(DELETE_CHAR);
|
spaces.push(DELETE_CHAR);
|
||||||
}
|
}
|
||||||
buffer.push(spaces);
|
buffer.push(spaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render scrollbar,thanks @gdamms
|
||||||
|
// I have no idea what this code does, I copied/pasted it from gdamms, and then modified
|
||||||
|
// some stuff so that it would look right
|
||||||
|
if last_line_index > h {
|
||||||
|
let mut subbuffer = vec![];
|
||||||
|
subbuffer.push(format!(
|
||||||
|
"{}{}{}{}",
|
||||||
|
style::Reset,
|
||||||
|
if selected { color::Green.fg_str() } else { "" },
|
||||||
|
cursor::Goto(x + w + 1, y),
|
||||||
|
"▲"
|
||||||
|
));
|
||||||
|
|
||||||
|
let bar_portion = h as f32 / self.stdout.len() as f32;
|
||||||
|
let bar_nb = f32::max(1.0, (bar_portion * (h) as f32).round()) as u16;
|
||||||
|
let max_scroll = self.stdout.len() as isize - h as isize - 1;
|
||||||
|
|
||||||
|
let (scroll_nb_bottom, scroll_nb_top) = if self.scroll > max_scroll / 2 {
|
||||||
|
let scroll_nb_bottom = (self.stdout.len() as isize - self.scroll) as u16 - h;
|
||||||
|
let scroll_nb_bottom = scroll_nb_bottom as f32 / self.stdout.len() as f32;
|
||||||
|
let scroll_nb_bottom = (scroll_nb_bottom * (h as f32)).ceil() as u16;
|
||||||
|
let scroll_nb_top = h - bar_nb - scroll_nb_bottom;
|
||||||
|
(scroll_nb_bottom, scroll_nb_top)
|
||||||
|
} else {
|
||||||
|
let scroll_nb_top = self.scroll as f32 / self.stdout.len() as f32;
|
||||||
|
let scroll_nb_top = (scroll_nb_top * (h) as f32).ceil() as u16;
|
||||||
|
let scroll_nb_bottom = h - bar_nb - scroll_nb_top;
|
||||||
|
(scroll_nb_bottom, scroll_nb_top)
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 1..=scroll_nb_top {
|
||||||
|
subbuffer.push(format!("{}{}", cursor::Goto(x + w + 1, y + i), "│"));
|
||||||
|
}
|
||||||
|
for i in scroll_nb_top + 1..=scroll_nb_top + bar_nb {
|
||||||
|
subbuffer.push(format!("{}{}", cursor::Goto(x + w + 1, y + i), "█"));
|
||||||
|
}
|
||||||
|
for i in scroll_nb_top + bar_nb + 1..=scroll_nb_top + bar_nb + scroll_nb_bottom {
|
||||||
|
subbuffer.push(format!("{}{}", cursor::Goto(x + w + 1, y + i), "│"));
|
||||||
|
}
|
||||||
|
|
||||||
|
subbuffer.push(format!("{}{}", cursor::Goto(x + w + 1, y + h), "▼"));
|
||||||
|
|
||||||
|
buffer.push(subbuffer.join(""));
|
||||||
|
}
|
||||||
|
|
||||||
buffer.push(format!("{}", style::Reset));
|
buffer.push(format!("{}", style::Reset));
|
||||||
buffer.join("")
|
buffer.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls up one line.
|
/// Returns the max scroll value.
|
||||||
pub fn scroll_up(&mut self) {
|
pub fn max_scroll(&self) -> isize {
|
||||||
if self.scroll > 0 {
|
std::cmp::max(
|
||||||
self.scroll -= 1;
|
0,
|
||||||
|
self.stdout.len() as isize - self.inner_size.1 as isize - 1,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scrolls up one line.
|
||||||
|
pub fn scroll_up(&mut self, step: isize) {
|
||||||
|
self.sticky = false;
|
||||||
|
self.scroll = std::cmp::max(0, self.scroll - step);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls down one line.
|
/// Scrolls down one line.
|
||||||
pub fn scroll_down(&mut self) {
|
pub fn scroll_down(&mut self, step: isize) {
|
||||||
if self.scroll + (self.inner_size.1 as isize) < self.stdout.len() as isize - 1 {
|
let max_scroll = self.max_scroll();
|
||||||
self.scroll += 1;
|
self.scroll = std::cmp::min(max_scroll, self.scroll + step);
|
||||||
|
if self.scroll == max_scroll {
|
||||||
|
self.sticky = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls up one line.
|
/// Scrolls up one line.
|
||||||
pub fn scroll_full_up(&mut self) {
|
pub fn scroll_full_up(&mut self) {
|
||||||
|
self.sticky = false;
|
||||||
self.scroll = 0;
|
self.scroll = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scrolls down one line.
|
/// Scrolls down one line.
|
||||||
pub fn scroll_full_down(&mut self) {
|
pub fn scroll_full_down(&mut self) {
|
||||||
self.scroll = self.stdout.len() as isize - self.inner_size.1 as isize - 1;
|
self.sticky = true;
|
||||||
if self.scroll < 0 {
|
self.scroll = self.max_scroll()
|
||||||
self.scroll = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kill the child command.
|
/// Kill the child command.
|
||||||
pub fn kill(&mut self) -> io::Result<()> {
|
pub fn kill(&mut self) {
|
||||||
if let Some((_, child)) = self.child.as_mut() {
|
self.pty = None;
|
||||||
child.kill()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restarts the child command.
|
/// Restarts the child command.
|
||||||
pub fn restart(&mut self) -> io::Result<()> {
|
pub fn restart(&mut self) {
|
||||||
self.kill()?;
|
self.kill();
|
||||||
self.start();
|
self.start();
|
||||||
Ok(())
|
}
|
||||||
|
|
||||||
|
/// Repositions the tile.
|
||||||
|
pub fn reposition(&mut self, (i, j): (u16, u16)) {
|
||||||
|
self.outer_position = (i, j);
|
||||||
|
self.inner_position = (i + 2, j + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resizes the tile.
|
||||||
|
pub fn resize(&mut self, (w, h): (u16, u16)) {
|
||||||
|
self.outer_size = (w, h);
|
||||||
|
self.inner_size = (w - 4, h - 5);
|
||||||
|
|
||||||
|
if let Some(pty) = self.pty.as_mut() {
|
||||||
|
pty.resize(pty_process::Size::new(self.inner_size.1, self.inner_size.0))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_stdout = std::mem::replace(&mut self.stdout, vec![String::new()]);
|
||||||
|
for s in old_stdout {
|
||||||
|
self.push_stdout(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a line.
|
||||||
|
pub fn add_line(&mut self) {
|
||||||
|
let mut line = String::new();
|
||||||
|
for _ in 0..self.inner_size.0 - 1 {
|
||||||
|
line.push('─');
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sender
|
||||||
|
.send(Msg::Stdout(
|
||||||
|
self.coords,
|
||||||
|
format!("\n{}{}\n", color::Reset.fg_str(), line),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a finish line, green if success or red if failure.
|
||||||
|
pub fn add_finish_line(&mut self, success: bool) {
|
||||||
|
let mut line = String::new();
|
||||||
|
for _ in 0..self.inner_size.0 - 1 {
|
||||||
|
line.push('─');
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sender
|
||||||
|
.send(Msg::Stdout(
|
||||||
|
self.coords,
|
||||||
|
format!(
|
||||||
|
"\n{}{}{}\n",
|
||||||
|
color::Reset.fg_str(),
|
||||||
|
if success {
|
||||||
|
color::Green.fg_str()
|
||||||
|
} else {
|
||||||
|
color::Red.fg_str()
|
||||||
|
},
|
||||||
|
line
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user