Compare commits

..

15 Commits

Author SHA1 Message Date
b7a5d83faa Also resize pty 2023-11-06 23:05:32 +01:00
20757a093b Update readme 2023-11-06 23:02:35 +01:00
7e4d0e05d8 Resize will approximately work 2023-11-06 23:02:12 +01:00
b9c31c2251 Update README.md 2023-11-06 11:36:34 +01:00
dc950ad0d4 Fix bug, adds readme 2023-11-06 11:33:15 +01:00
79f03b27bf Row-major or col-major, no need for exact grid 2023-11-06 11:08:28 +01:00
2e771773b6 Beautiful lines 2023-10-26 22:48:01 +02:00
093845a4c6 Autoscroll only when at bottom 2023-10-26 22:40:12 +02:00
6121dc4224 Better scroll, fix exit code 2023-10-26 22:36:07 +02:00
e3d891c522 Beautiful scrollbars 2023-10-26 20:52:31 +02:00
ce8b4fae68 Fix wrong line wrap 2023-10-26 15:07:16 +02:00
265208db3a Fix autoscroll 2023-10-26 12:07:00 +02:00
0f4211005e Fix bug in frame rate limit 2023-10-26 11:55:31 +02:00
922ba9c524 Remove useless eprintln 2023-10-26 11:41:16 +02:00
9802c8e740 Fix bad rendering of border 2023-10-26 11:40:20 +02:00
7 changed files with 463 additions and 129 deletions

60
README.md Normal file
View File

@@ -0,0 +1,60 @@
# multiview-rs
*Run many commands and watch all outputs in a single terminal*
![multiview preview](/screenshots/row-major.png)
## 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
```
![multiview row major preview](/screenshots/row-major.png)
Split your terminal in two columns, the first containing three rows, and the second containing one row:
```sh
multiview cmd1 // cmd2 // cmd3 :: cmd4 // cmd5
```
![multiview col major preview](/screenshots/col-major.png)
## 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

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
screenshots/row-major.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -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(())

View File

@@ -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();
} }
} }