From 09eafd0f3ff0c015b70b857c989ead9286f0d6f1 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 1 Mar 2021 02:20:21 -0600 Subject: [PATCH] gonna just plop tui.rs in here --- Cargo.lock | 85 ++++++++++-------- Cargo.toml | 5 +- src/lib.rs | 4 +- src/ui/events.rs | 95 ++++++++++++++++++++ src/ui/mod.rs | 149 ++++++++++--------------------- src/{ui => ui_custom}/drawbuf.rs | 0 src/ui_custom/mod.rs | 121 +++++++++++++++++++++++++ src/{ui => ui_custom}/table.rs | 0 src/{ui => ui_custom}/tabs.rs | 0 src/{ui => ui_custom}/widget.rs | 0 10 files changed, 315 insertions(+), 144 deletions(-) create mode 100644 src/ui/events.rs rename src/{ui => ui_custom}/drawbuf.rs (100%) create mode 100644 src/ui_custom/mod.rs rename src/{ui => ui_custom}/table.rs (100%) rename src/{ui => ui_custom}/tabs.rs (100%) rename src/{ui => ui_custom}/widget.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 5a9a6bf..f5d7894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,6 +279,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cast5" version = "0.8.0" @@ -434,31 +440,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "crossterm" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" -dependencies = [ - "bitflags 1.2.1", - "crossterm_winapi", - "lazy_static", - "libc", - "mio", - "parking_lot", - "signal-hook", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" -dependencies = [ - "winapi", -] - [[package]] name = "curve25519-dalek" version = "3.0.2" @@ -1189,6 +1170,12 @@ dependencies = [ "libc", ] +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + [[package]] name = "objc" version = "0.2.7" @@ -1250,7 +1237,6 @@ dependencies = [ "async-trait", "cfg-if 1.0.0", "chrono", - "crossterm", "fern", "format-bytes", "futures", @@ -1265,11 +1251,13 @@ dependencies = [ "rustls-connector", "serde", "structopt", + "termion", "tokio", "tokio-rustls", "tokio-stream", "tokio-util", "toml", + "tui", "webpki-roots", "xdg", ] @@ -1634,6 +1622,15 @@ dependencies = [ "bitflags 1.2.1", ] +[[package]] +name = "redox_termios" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +dependencies = [ + "redox_syscall 0.2.5", +] + [[package]] name = "redox_users" version = "0.3.5" @@ -1899,17 +1896,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "signal-hook" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" -dependencies = [ - "libc", - "mio", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.3.0" @@ -2087,6 +2073,18 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "termion" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +dependencies = [ + "libc", + "numtoa", + "redox_syscall 0.2.5", + "redox_termios", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -2215,6 +2213,19 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "tui" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ced152a8e9295a5b168adc254074525c17ac4a83c90b2716274cc38118bddc9" +dependencies = [ + "bitflags 1.2.1", + "cassowary", + "termion", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "twofish" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 2921577..c91abd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,11 +12,11 @@ license = "GPL-3.0-or-later" members = ["imap", "smtp"] [dependencies] +# crossterm = "0.19.0" anyhow = "1.0.38" async-trait = "0.1.42" cfg-if = "1.0.0" chrono = "0.4.19" -crossterm = "0.19.0" fern = { version = "0.6.0", features = ["colored"] } format-bytes = "0.2.0" futures = "0.3.13" @@ -34,8 +34,11 @@ tokio-rustls = "0.22.0" tokio-stream = { version = "0.1.3", features = ["sync"] } tokio-util = { version = "0.6.3", features = ["full"] } toml = "0.5.8" +# tui = { version = "0.14.0", default-features = false, features = ["crossterm"] } webpki-roots = "0.21.0" xdg = "2.2.0" +tui = "0.14.0" +termion = "1.5.6" [dependencies.panorama-imap] path = "imap" diff --git a/src/lib.rs b/src/lib.rs index e231bc0..7704034 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,8 +8,8 @@ #[macro_use] extern crate anyhow; -#[macro_use] -extern crate crossterm; +// #[macro_use] +// extern crate crossterm; #[macro_use] extern crate format_bytes; #[macro_use] diff --git a/src/ui/events.rs b/src/ui/events.rs new file mode 100644 index 0000000..333096a --- /dev/null +++ b/src/ui/events.rs @@ -0,0 +1,95 @@ +use std::io; +use std::sync::mpsc; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::thread; +use std::time::Duration; + +use termion::event::Key; +use termion::input::TermRead; + +pub enum Event { + Input(I), + Tick, +} + +/// A small event handler that wrap termion input and tick events. Each event +/// type is handled in its own thread and returned to a common `Receiver` +pub struct Events { + rx: mpsc::Receiver>, + input_handle: thread::JoinHandle<()>, + ignore_exit_key: Arc, + tick_handle: thread::JoinHandle<()>, +} + +#[derive(Debug, Clone, Copy)] +pub struct Config { + pub exit_key: Key, + pub tick_rate: Duration, +} + +impl Default for Config { + fn default() -> Config { + Config { + exit_key: Key::Char('q'), + tick_rate: Duration::from_millis(250), + } + } +} + +impl Events { + pub fn new() -> Events { + Events::with_config(Config::default()) + } + + pub fn with_config(config: Config) -> Events { + let (tx, rx) = mpsc::channel(); + let ignore_exit_key = Arc::new(AtomicBool::new(false)); + let input_handle = { + let tx = tx.clone(); + let ignore_exit_key = ignore_exit_key.clone(); + thread::spawn(move || { + let stdin = io::stdin(); + for evt in stdin.keys() { + if let Ok(key) = evt { + if let Err(err) = tx.send(Event::Input(key)) { + eprintln!("{}", err); + return; + } + if !ignore_exit_key.load(Ordering::Relaxed) && key == config.exit_key { + return; + } + } + } + }) + }; + let tick_handle = { + thread::spawn(move || loop { + if tx.send(Event::Tick).is_err() { + break; + } + thread::sleep(config.tick_rate); + }) + }; + Events { + rx, + ignore_exit_key, + input_handle, + tick_handle, + } + } + + pub fn next(&self) -> Result, mpsc::RecvError> { + self.rx.recv() + } + + pub fn disable_exit_key(&mut self) { + self.ignore_exit_key.store(true, Ordering::Relaxed); + } + + pub fn enable_exit_key(&mut self) { + self.ignore_exit_key.store(false, Ordering::Relaxed); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 0236839..db100f0 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,121 +1,62 @@ -//! UI module +//! UI library -mod drawbuf; -mod table; -mod tabs; -mod widget; +mod events; -use std::fmt::Debug; -use std::io::{Stdout, Write}; +use std::io::Stdout; use std::time::Duration; use anyhow::Result; -use chrono::Local; -use crossterm::{ - cursor::{self, MoveTo}, - event::{self, Event, KeyCode, KeyEvent}, - style::{self, Color, SetBackgroundColor, SetForegroundColor}, - terminal::{self, Clear, ClearType}, +use termion::{event::Key, screen::AlternateScreen}; +use tokio::sync::mpsc; +use tui::{ + backend::TermionBackend, + layout::{Constraint, Direction, Layout}, + widgets::{Block, Borders, Widget}, + Terminal, }; -use tokio::time; -use crate::ExitSender; +use self::events::{Config, Event, Events}; -use self::drawbuf::DrawBuf; -use self::table::Table; -use self::tabs::Tabs; -use self::widget::Widget; +/// Main entrypoint for the UI +pub async fn run_ui(stdout: Stdout, exit_tx: mpsc::Sender<()>) -> Result<()> { + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; -const FRAME_DURATION: Duration = Duration::from_millis(20); - -/// Type alias for the screen object we're drawing to -pub type Screen = Stdout; - -/// Rectangle -#[derive(Copy, Clone, Debug)] -#[allow(missing_docs)] -pub struct Rect { - pub x: u16, - pub y: u16, - pub w: u16, - pub h: u16, -} - -impl Rect { - /// Construct a new rectangle from (x, y) and (w, h) - pub fn new(x: u16, y: u16, w: u16, h: u16) -> Self { - Rect { x, y, w, h } - } -} - -/// UI entrypoint. -pub async fn run_ui(mut w: Stdout, exit: ExitSender) -> Result<()> { - execute!(w, cursor::Hide, terminal::EnterAlternateScreen)?; - terminal::enable_raw_mode()?; - - let (term_width, term_height) = terminal::size()?; - let bounds = Rect::new(0, 0, term_width, term_height); - let mut drawbuf = DrawBuf::new(bounds); - - let mut table = Table::default(); - table.push_row(vec!["ur mom Lol!".to_owned()]); - table.push_row(vec!["hek".to_owned()]); - - let mut tabs = Tabs::new(); - tabs.add_tab("Mail", table); + let events = Events::with_config(Config { + tick_rate: Duration::from_millis(17), + ..Config::default() + }); loop { - queue!( - w, - SetBackgroundColor(Color::Reset), - SetForegroundColor(Color::Reset), - Clear(ClearType::All), - MoveTo(0, 0), - )?; + terminal.draw(|f| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Percentage(10), + Constraint::Percentage(80), + Constraint::Percentage(10), + ] + .as_ref(), + ) + .split(f.size()); + let block = Block::default().title("Block").borders(Borders::ALL); + f.render_widget(block, chunks[0]); + let block = Block::default().title("Block 2").borders(Borders::ALL); + f.render_widget(block, chunks[1]); + })?; - // let now = Local::now(); - // println!("time {}", now); - - let (term_width, term_height) = terminal::size()?; - let bounds = Rect::new(5, 5, term_width - 10, term_height - 10); - // table.draw(&mut w, bounds)?; - // tabs.draw(&mut w, bounds)?; - drawbuf.draw(&mut w)?; - w.flush()?; - - // approx 60fps - time::sleep(FRAME_DURATION).await; - - // check to see if there's even an event this frame. otherwise, just keep going - let event = if event::poll(FRAME_DURATION)? { - let event = event::read()?; - // table.update(&event); - - if let Event::Key(KeyEvent { - code: KeyCode::Char('q'), - .. - }) = event - { - break; + if let Event::Input(input) = events.next()? { + match input { + Key::Char('q') => { + break; + } + _ => {} } - - Some(event) - } else { - None - }; - - tabs.update(event); + } } - execute!( - w, - style::ResetColor, - cursor::Show, - terminal::LeaveAlternateScreen - )?; - terminal::disable_raw_mode()?; - - exit.send(()).await?; - debug!("sent exit"); Ok(()) } diff --git a/src/ui/drawbuf.rs b/src/ui_custom/drawbuf.rs similarity index 100% rename from src/ui/drawbuf.rs rename to src/ui_custom/drawbuf.rs diff --git a/src/ui_custom/mod.rs b/src/ui_custom/mod.rs new file mode 100644 index 0000000..0236839 --- /dev/null +++ b/src/ui_custom/mod.rs @@ -0,0 +1,121 @@ +//! UI module + +mod drawbuf; +mod table; +mod tabs; +mod widget; + +use std::fmt::Debug; +use std::io::{Stdout, Write}; +use std::time::Duration; + +use anyhow::Result; +use chrono::Local; +use crossterm::{ + cursor::{self, MoveTo}, + event::{self, Event, KeyCode, KeyEvent}, + style::{self, Color, SetBackgroundColor, SetForegroundColor}, + terminal::{self, Clear, ClearType}, +}; +use tokio::time; + +use crate::ExitSender; + +use self::drawbuf::DrawBuf; +use self::table::Table; +use self::tabs::Tabs; +use self::widget::Widget; + +const FRAME_DURATION: Duration = Duration::from_millis(20); + +/// Type alias for the screen object we're drawing to +pub type Screen = Stdout; + +/// Rectangle +#[derive(Copy, Clone, Debug)] +#[allow(missing_docs)] +pub struct Rect { + pub x: u16, + pub y: u16, + pub w: u16, + pub h: u16, +} + +impl Rect { + /// Construct a new rectangle from (x, y) and (w, h) + pub fn new(x: u16, y: u16, w: u16, h: u16) -> Self { + Rect { x, y, w, h } + } +} + +/// UI entrypoint. +pub async fn run_ui(mut w: Stdout, exit: ExitSender) -> Result<()> { + execute!(w, cursor::Hide, terminal::EnterAlternateScreen)?; + terminal::enable_raw_mode()?; + + let (term_width, term_height) = terminal::size()?; + let bounds = Rect::new(0, 0, term_width, term_height); + let mut drawbuf = DrawBuf::new(bounds); + + let mut table = Table::default(); + table.push_row(vec!["ur mom Lol!".to_owned()]); + table.push_row(vec!["hek".to_owned()]); + + let mut tabs = Tabs::new(); + tabs.add_tab("Mail", table); + + loop { + queue!( + w, + SetBackgroundColor(Color::Reset), + SetForegroundColor(Color::Reset), + Clear(ClearType::All), + MoveTo(0, 0), + )?; + + // let now = Local::now(); + // println!("time {}", now); + + let (term_width, term_height) = terminal::size()?; + let bounds = Rect::new(5, 5, term_width - 10, term_height - 10); + // table.draw(&mut w, bounds)?; + // tabs.draw(&mut w, bounds)?; + drawbuf.draw(&mut w)?; + w.flush()?; + + // approx 60fps + time::sleep(FRAME_DURATION).await; + + // check to see if there's even an event this frame. otherwise, just keep going + let event = if event::poll(FRAME_DURATION)? { + let event = event::read()?; + // table.update(&event); + + if let Event::Key(KeyEvent { + code: KeyCode::Char('q'), + .. + }) = event + { + break; + } + + Some(event) + } else { + None + }; + + tabs.update(event); + } + + execute!( + w, + style::ResetColor, + cursor::Show, + terminal::LeaveAlternateScreen + )?; + terminal::disable_raw_mode()?; + + exit.send(()).await?; + debug!("sent exit"); + Ok(()) +} diff --git a/src/ui/table.rs b/src/ui_custom/table.rs similarity index 100% rename from src/ui/table.rs rename to src/ui_custom/table.rs diff --git a/src/ui/tabs.rs b/src/ui_custom/tabs.rs similarity index 100% rename from src/ui/tabs.rs rename to src/ui_custom/tabs.rs diff --git a/src/ui/widget.rs b/src/ui_custom/widget.rs similarity index 100% rename from src/ui/widget.rs rename to src/ui_custom/widget.rs