commit 1c75b09d0de767071cb207bf55009e3de4776a13 Author: Thomas Forgione Date: Tue Oct 17 22:59:08 2023 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c510720 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,58 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "multiview" +version = "0.1.0" +dependencies = [ + "termion", +] + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "termion" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90" +dependencies = [ + "libc", + "numtoa", + "redox_syscall", + "redox_termios", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..159b6d6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "multiview" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +termion = "2.0.1" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8c5c90f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,205 @@ +use std::env; +use std::io::{self, stdin, stdout, Write}; + +use termion::event::{Event, Key, MouseEvent}; +use termion::input::{MouseTerminal, TermRead}; +use termion::raw::IntoRawMode; +use termion::screen::IntoAlternateScreen; +use termion::terminal_size; +use termion::{clear, color, cursor, style}; + +/// A tile with a command running inside it. +#[derive(Debug)] +pub struct Tile { + /// The command that should be executed in the tile. + pub command: Vec, +} + +impl Tile { + /// Creates a new empty tile. + pub fn new(command: &[String]) -> Tile { + Tile { + command: command + .into_iter() + .map(|x| x.to_string()) + .collect::>(), + } + } +} + +/// Multiple applications running in a single terminal. +struct Multiview { + /// The stdout on which the multiview will be rendererd. + pub stdout: W, + + /// The tiles of the multiview. + pub tiles: Vec>, + + /// The coordinates of the selected tiles. + pub selected: (u16, u16), +} + +impl Multiview { + /// Creates a new multiview. + pub fn new(stdout: W, tiles: Vec>) -> io::Result> { + let mut multiview = Multiview { + stdout, + tiles, + selected: (0, 0), + }; + + write!( + multiview.stdout, + "{}{}{}┌", + clear::All, + cursor::Hide, + cursor::Goto(1, 1) + )?; + + multiview.stdout.flush()?; + + Ok(multiview) + } + + /// Helper to easily access a tile. + pub fn tile(&self, (i, j): (u16, u16)) -> &Tile { + &self.tiles[i as usize][j as usize] + } + + /// Sets the selected tile from (x, y) coordinates. + pub fn select_tile(&mut self, (x, y): (u16, u16), term_size: (u16, u16)) { + let w = term_size.0 / self.tiles[0].len() as u16; + let h = term_size.1 / self.tiles.len() as u16; + + self.selected = (y / h, x / w); + } + + /// Draws a box from (x1, y1) to (x2, y2). + pub fn rect(&mut self, (x1, y1): (u16, u16), (x2, y2): (u16, u16)) -> io::Result<()> { + write!(self.stdout, "{}┌", cursor::Goto(x1, y1))?; + + for _ in (x1 + 1)..x2 { + write!(self.stdout, "─")?; + } + + write!(self.stdout, "┐")?; + + for y in (y1 + 1)..y2 { + write!(self.stdout, "{}│", cursor::Goto(x1, y))?; + write!(self.stdout, "{}│", cursor::Goto(x2, y))?; + } + + write!(self.stdout, "{}└", cursor::Goto(x1, y2))?; + + for _ in (x1 + 1)..x2 { + write!(self.stdout, "─")?; + } + + write!(self.stdout, "┘")?; + + Ok(()) + } + + /// Clears stdout. + pub fn clear(&mut self) -> io::Result<()> { + write!(self.stdout, "{}", clear::All) + } + + /// Renders the (x, y) tile. + pub fn render_tile(&mut self, (i, j): (u16, u16), term_size: (u16, u16)) -> io::Result<()> { + let w = term_size.0 / self.tiles[0].len() as u16; + let h = term_size.1 / self.tiles.len() as u16; + + let x1 = j * w + 1; + let y1 = i * h + 1; + + let x2 = (j + 1) * w; + let y2 = (i + 1) * h; + + if self.selected == (i, j) { + write!(self.stdout, "{}", color::Green.fg_str())?; + } + self.rect((x1, y1), (x2, y2))?; + write!(self.stdout, "{}├", cursor::Goto(x1, y1 + 2))?; + + for _ in (x1 + 1)..x2 { + write!(self.stdout, "─")?; + } + + write!(self.stdout, "{}┤", cursor::Goto(x2, y1 + 2))?; + + let tile = &self.tile((i, j)); + + write!( + self.stdout, + "{}{} {}Command: {}{}", + color::Reset.fg_str(), + cursor::Goto(x1 + 1, y1 + 1), + style::Bold, + tile.command.join(" "), + style::Reset, + )?; + + Ok(()) + } + + /// Renders all the tiles of the multiview. + pub fn render(&mut self, term_size: (u16, u16)) -> io::Result<()> { + self.clear()?; + + for i in 0..self.tiles.len() { + for j in 0..self.tiles[0].len() { + self.render_tile((i as u16, j as u16), term_size)?; + } + } + + self.stdout.flush()?; + + Ok(()) + } +} + +impl Drop for Multiview { + fn drop(&mut self) { + write!(self.stdout, "{}", cursor::Show).unwrap(); + } +} + +/// Starts the multiview application. +pub fn main() -> io::Result<()> { + let args = env::args().skip(1).collect::>(); + + let tiles = args + .split(|x| x == "//") + .map(|x| x.split(|y| y == "::").map(Tile::new)) + .map(|x| x.collect::>()) + .collect::>(); + + let stdin = stdin(); + let stdout = stdout().into_raw_mode()?; + let stdout = stdout.into_alternate_screen()?; + let stdout = MouseTerminal::from(stdout); + + let term_size = terminal_size()?; + + let mut multiview = Multiview::new(stdout, tiles)?; + multiview.render(term_size)?; + + for c in stdin.events() { + let evt = c?; + match evt { + Event::Key(Key::Char('q')) => break, + + Event::Mouse(me) => match me { + MouseEvent::Press(_, x, y) => multiview.select_tile((x, y), term_size), + _ => (), + }, + + _ => {} + } + + multiview.render(term_size)?; + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..afad1dc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +fn main() { + if let Err(e) = multiview::main() { + eprintln!("An error occured: {}", e); + } +}