From c32c6b82c8f5f4be5346005b1c0f6833715716ae Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Mon, 6 Aug 2018 16:53:23 +0300 Subject: [PATCH] Refactor ui module --- ui/src/components/mod.rs | 12 +- ui/src/lib.rs | 641 +-------------------------------- ui/src/state.rs | 430 ++++++++++++++++++++++ ui/src/{ => types}/cells.rs | 0 ui/src/{ => types}/helpers.rs | 0 ui/src/types/keys.rs | 106 ++++++ ui/src/types/mod.rs | 103 ++++++ ui/src/{ => types}/position.rs | 0 8 files changed, 657 insertions(+), 635 deletions(-) create mode 100644 ui/src/state.rs rename ui/src/{ => types}/cells.rs (100%) rename ui/src/{ => types}/helpers.rs (100%) create mode 100644 ui/src/types/keys.rs create mode 100644 ui/src/types/mod.rs rename ui/src/{ => types}/position.rs (100%) diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs index dc56caaf1..c900d1e2a 100644 --- a/ui/src/components/mod.rs +++ b/ui/src/components/mod.rs @@ -26,17 +26,17 @@ */ use super::*; -pub mod mail; -pub mod notifications; -pub mod utilities; +pub mod mail; pub use mail::*; + +pub mod notifications; + +pub mod utilities; pub use self::utilities::*; -use super::cells::{CellBuffer, Color}; -use super::position::Area; -use super::{Key, UIEvent, UIEventType}; +use super::{Key, UIEvent, UIEventType}; /// The upper and lower boundary char. const HORZ_BOUNDARY: char = '─'; /// The left and right boundary char. diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 3fe6ea021..7bd9497d6 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -19,29 +19,6 @@ * along with meli. If not, see . */ -/*! - The UI crate has an Entity-Component-System design. The System part, is also the application's state, so they're both merged in the `State` struct. - - `State` owns all the Entities of the UI, which are currently plain Containers for `Component`s. In the application's main event loop, input is handed to the state in the form of `UIEvent` objects which traverse the entity graph. Components decide to handle each input or not. - - Input is received in the main loop from threads which listen on the stdin for user input, observe folders for file changes etc. The relevant struct is `ThreadEvent`. - */ - -#[macro_use] -mod position; -mod cells; -pub mod components; - -mod helpers; -pub use helpers::*; - -#[macro_use] -mod execute; -use execute::*; -use self::cells::*; -pub use self::components::*; -pub use self::position::*; - extern crate melib; extern crate mime_apps; extern crate notify_rust; @@ -50,620 +27,26 @@ extern crate chan; extern crate chan_signal; extern crate linkify; extern crate uuid; -use melib::*; -use std::collections::VecDeque; -use std::fmt; -use std::io::Write; -use std::thread; -use std::time; extern crate fnv; -use self::fnv::FnvHashMap; extern crate termion; -use termion::event::Key as TermionKey; -use termion::input::TermRead; -use termion::raw::IntoRawMode; -use termion::screen::AlternateScreen; -use termion::{clear, cursor, style}; #[macro_use] extern crate nom; -use chan::Sender; +use melib::*; +use std::collections::VecDeque; -/// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads -/// to the main process. -#[derive(Debug)] -pub enum ThreadEvent { - ThreadJoin(thread::ThreadId), - /// User input. - Input(Key), - /// A watched folder has been refreshed. - RefreshMailbox { - hash: u64, - }, - UIEvent(UIEventType), - //Decode { _ }, // For gpg2 signature check -} +#[macro_use] +mod types; +pub use types::*; -impl From for ThreadEvent { - fn from(event: RefreshEvent) -> Self { - ThreadEvent::RefreshMailbox { hash: event.hash } - } -} +#[macro_use] +mod execute; +use execute::*; -#[derive(Debug)] -pub enum ForkType { - Generic(std::process::Child), - NewDraft(File, std::process::Child), -} +pub mod state; +pub use state::*; -#[derive(Debug)] -pub enum UIEventType { - Input(Key), - ExInput(Key), - RefreshMailbox((usize, usize)), - //Quit? - Resize, - /// Force redraw. - Fork(ForkType), - ChangeMailbox(usize), - ChangeMode(UIMode), - Command(String), - Notification(String), - EditDraft(File), - Action(Action), - StatusNotification(String), - MailboxUpdate((usize, usize)), - - StartupCheck, -} - -/// An event passed from `State` to its Entities. -#[derive(Debug)] -pub struct UIEvent { - pub id: u64, - pub event_type: UIEventType, -} - -#[derive(Debug, PartialEq, Copy, Clone)] -pub enum UIMode { - Normal, - Execute, - Fork, -} - -impl fmt::Display for UIMode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - match *self { - UIMode::Normal => "NORMAL", - UIMode::Execute => "EX", - UIMode::Fork => "FORK", - } - ) - } -} - -/// An event notification that is passed to Entities for handling. -pub struct Notification { - _title: String, - _content: String, - - _timestamp: std::time::Instant, -} - -/// A context container for loaded settings, accounts, UI changes, etc. -pub struct Context { - pub accounts: Vec, - settings: Settings, - - runtime_settings: Settings, - /// Areas of the screen that must be redrawn in the next render - dirty_areas: VecDeque, - - /// Events queue that components send back to the state - replies: VecDeque, - _backends: Backends, - - input_thread: chan::Sender, -} - -impl Context { - pub fn replies(&mut self) -> Vec { - self.replies.drain(0..).collect() - } - pub fn input_thread(&mut self) -> &mut chan::Sender { - &mut self.input_thread - } -} - -/// A State object to manage and own components and entities of the UI. `State` is responsible for -/// managing the terminal and interfacing with `melib` -pub struct State { - cols: usize, - rows: usize, - - grid: CellBuffer, - stdout: Option>>, - child: Option, - pub mode: UIMode, - sender: Sender, - entities: Vec, - pub context: Context, - - startup_thread: Option>, - - threads: FnvHashMap>, -} - -impl Drop for State { - fn drop(&mut self) { - // When done, restore the defaults to avoid messing with the terminal. - write!( - self.stdout(), - "{}{}{}{}", - clear::All, - style::Reset, - cursor::Goto(1, 1), - cursor::Show - ).unwrap(); - self.flush(); - } -} - -impl State { - pub fn new(sender: Sender, input_thread: chan::Sender) -> Self { - let _stdout = std::io::stdout(); - _stdout.lock(); - let settings = Settings::new(); - let backends = Backends::new(); - let stdout = AlternateScreen::from(_stdout.into_raw_mode().unwrap()); - - let termsize = termion::terminal_size().ok(); - let termcols = termsize.map(|(w, _)| w); - let termrows = termsize.map(|(_, h)| h); - let cols = termcols.unwrap_or(0) as usize; - let rows = termrows.unwrap_or(0) as usize; - let mut accounts: Vec = settings - .accounts - .iter() - .map(|(n, a_s)| Account::new(n.to_string(), a_s.clone(), &backends)) - .collect(); - accounts.sort_by(|a, b| a.name().cmp(&b.name())); - let (startup_tx, startup_rx) = chan::async(); - let startup_thread = { - let sender = sender.clone(); - let startup_rx = startup_rx.clone(); - - thread::Builder::new() - .name("startup-thread".to_string()) - .spawn(move || { - let dur = time::Duration::from_millis(100); - loop { - chan_select! { - default => {}, - startup_rx.recv() -> _ => { - sender.send(ThreadEvent::ThreadJoin(thread::current().id())); - return; - } - } - sender.send(ThreadEvent::UIEvent(UIEventType::StartupCheck)); - thread::sleep(dur); - } - }).unwrap() - }; - let mut s = State { - cols: cols, - rows: rows, - grid: CellBuffer::new(cols, rows, Cell::with_char(' ')), - stdout: Some(stdout), - child: None, - mode: UIMode::Normal, - sender: sender, - entities: Vec::with_capacity(1), - - context: Context { - accounts: accounts, - _backends: backends, - settings: settings.clone(), - runtime_settings: settings, - dirty_areas: VecDeque::with_capacity(5), - replies: VecDeque::with_capacity(5), - - input_thread: input_thread, - }, - startup_thread: Some(startup_tx), - threads: FnvHashMap::with_capacity_and_hasher(1, Default::default()), - }; - s.threads.insert(startup_thread.thread().id(), startup_thread); - write!( - s.stdout(), - "{}{}{}", - cursor::Hide, - clear::All, - cursor::Goto(1, 1) - ).unwrap(); - s.flush(); - for account in &mut s.context.accounts { - let sender = s.sender.clone(); - account.watch(RefreshEventConsumer::new(Box::new(move |r| { - sender.send(ThreadEvent::from(r)); - }))); - } - s - } - pub fn join(&mut self, id: thread::ThreadId) { - let handle = self.threads.remove(&id).unwrap(); - handle.join().unwrap(); - - - } - pub fn finish_startup(&mut self) { - // TODO: Encode startup process with the type system if possible - if self.startup_thread.is_none() { - return; - } - { - let tx = self.startup_thread.take().unwrap(); - tx.send(true); - } - } - pub fn to_main_screen(&mut self) { - write!( - self.stdout(), - "{}{}", - termion::screen::ToMainScreen, - cursor::Show - ).unwrap(); - self.flush(); - self.stdout = None; - self.context.input_thread.send(false); - } - pub fn to_alternate_screen(&mut self) { - let s = std::io::stdout(); - s.lock(); - self.stdout = Some(AlternateScreen::from(s.into_raw_mode().unwrap())); - - write!( - self.stdout(), - "{}{}", - termion::screen::ToAlternateScreen, - cursor::Hide - ).unwrap(); - self.flush(); - } -} -impl State { - pub fn update_size(&mut self) { - let termsize = termion::terminal_size().ok(); - let termcols = termsize.map(|(w, _)| w); - let termrows = termsize.map(|(_, h)| h); - if termcols.unwrap_or(72) as usize != self.cols - || termrows.unwrap_or(120) as usize != self.rows - { - eprintln!( - "Size updated, from ({}, {}) -> ({:?}, {:?})", - self.cols, self.rows, termcols, termrows - ); - } - self.cols = termcols.unwrap_or(72) as usize; - self.rows = termrows.unwrap_or(120) as usize; - self.grid.resize(self.cols, self.rows, Cell::with_char(' ')); - - self.rcv_event(UIEvent { - id: 0, - event_type: UIEventType::Resize, - }); - } - - pub fn redraw(&mut self) { - for i in 0..self.entities.len() { - self.draw_entity(i); - } - let areas: Vec = self.context.dirty_areas.drain(0..).collect(); - /* draw each dirty area */ - for a in areas { - self.draw_area(a); - } - } - fn draw_area(&mut self, area: Area) { - let upper_left = upper_left!(area); - let bottom_right = bottom_right!(area); - - for y in get_y(upper_left)..=get_y(bottom_right) { - write!( - self.stdout(), - "{}", - cursor::Goto(get_x(upper_left) as u16 + 1, (y + 1) as u16) - ).unwrap(); - for x in get_x(upper_left)..=get_x(bottom_right) { - let c = self.grid[(x, y)]; - - if c.bg() != cells::Color::Default { - write!(self.stdout(), "{}", termion::color::Bg(c.bg().as_termion())).unwrap(); - } - if c.fg() != cells::Color::Default { - write!(self.stdout(), "{}", termion::color::Fg(c.fg().as_termion())).unwrap(); - } - write!(self.stdout(), "{}", c.ch()).unwrap(); - if c.bg() != cells::Color::Default { - write!( - self.stdout(), - "{}", - termion::color::Bg(termion::color::Reset) - ).unwrap(); - } - if c.fg() != cells::Color::Default { - write!( - self.stdout(), - "{}", - termion::color::Fg(termion::color::Reset) - ).unwrap(); - } - } - } - self.flush(); - } - pub fn render(&mut self) { - self.update_size(); - - /* draw each entity */ - for i in 0..self.entities.len() { - self.draw_entity(i); - } - let cols = self.cols; - let rows = self.rows; - - self.draw_area(((0, 0), (cols - 1, rows - 1))); - } - pub fn draw_entity(&mut self, idx: usize) { - let entity = &mut self.entities[idx]; - let upper_left = (0, 0); - let bottom_right = (self.cols - 1, self.rows - 1); - - if entity.component.is_dirty() { - entity.component.draw( - &mut self.grid, - (upper_left, bottom_right), - &mut self.context, - ); - } - } - pub fn register_entity(&mut self, entity: Entity) { - self.entities.push(entity); - } - /// Convert user commands to actions/method calls. - fn parse_command(&mut self, cmd: String) { - let result = parse_command(&cmd.as_bytes()).to_full_result(); - - if let Ok(v) = result { - self.rcv_event(UIEvent { id: 0, event_type: UIEventType::Action(v) }); - } - } - - pub fn rcv_event(&mut self, event: UIEvent) { - match event.event_type { - // Command type is handled only by State. - UIEventType::Command(cmd) => { - self.parse_command(cmd); - return; - } - UIEventType::Fork(child) => { - self.mode = UIMode::Fork; - self.child = Some(child); - self.flush(); - return; - } - UIEventType::EditDraft(mut file) => { - use std::io::Read; - use std::process::{Command, Stdio}; - let mut output = Command::new("msmtp") - .arg("-t") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("failed to execute process"); - { - let mut in_pipe = output.stdin.as_mut().unwrap(); - let mut buf = Vec::new(); - let mut f = file.file(); - - f.read_to_end(&mut buf).unwrap(); - in_pipe.write(&buf).unwrap(); - std::fs::remove_file(file.path()).unwrap(); - } - output.wait_with_output().expect("Failed to read stdout"); - - return; - } - UIEventType::Input(Key::Char('t')) => for i in 0..self.entities.len() { - self.entities[i].rcv_event( - &UIEvent { - id: 0, - event_type: UIEventType::Action(Action::MailListing( - MailListingAction::ToggleThreaded, - )), - }, - &mut self.context, - ); - }, - - _ => {} - } - /* inform each entity */ - for i in 0..self.entities.len() { - self.entities[i].rcv_event(&event, &mut self.context); - } - - if !self.context.replies.is_empty() { - let replies: Vec= self.context.replies.drain(0..).collect(); - // Pass replies to self and call count on the map iterator to force evaluation - replies.into_iter().map(|r| self.rcv_event(r)).count(); - } - } - - /// Tries to load a mailbox's content - pub fn refresh_mailbox(&mut self, account_idx: usize, folder_idx: usize) { - let flag = match &mut self.context.accounts[account_idx][folder_idx] { - Ok(_) => true, - Err(e) => { - eprintln!("error {:?}", e); - false - } - }; - if flag { - self.rcv_event(UIEvent { - id: 0, - event_type: UIEventType::RefreshMailbox((account_idx, folder_idx)), - }); - } - } - pub fn try_wait_on_child(&mut self) -> Option { - if { - match self.child { - Some(ForkType::NewDraft(_, ref mut c)) => { - let mut w = c.try_wait(); - match w { - Ok(Some(_)) => true, - Ok(None) => false, - Err(_) => { - return None; - } - } - } - Some(ForkType::Generic(ref mut c)) => { - let mut w = c.try_wait(); - match w { - Ok(Some(_)) => true, - Ok(None) => false, - Err(_) => { - return None; - } - } - } - _ => { - return None; - } - } - } { - if let Some(ForkType::NewDraft(f, _)) = std::mem::replace(&mut self.child, None) { - self.rcv_event(UIEvent { - id: 0, - event_type: UIEventType::EditDraft(f), - }); - } - return Some(true); - } - Some(false) - } - fn flush(&mut self) { - self.stdout.as_mut().map(|s| s.flush().unwrap()); - } - fn stdout(&mut self) -> &mut termion::screen::AlternateScreen> { - self.stdout.as_mut().unwrap() - } -} - -#[derive(Debug)] -pub enum Key { - /// Backspace. - Backspace, - /// Left arrow. - Left, - /// Right arrow. - Right, - /// Up arrow. - Up, - /// Down arrow. - Down, - /// Home key. - Home, - /// End key. - End, - /// Page Up key. - PageUp, - /// Page Down key. - PageDown, - /// Delete key. - Delete, - /// Insert key. - Insert, - /// Function keys. - /// - /// Only function keys 1 through 12 are supported. - F(u8), - /// Normal character. - Char(char), - /// Alt modified character. - Alt(char), - /// Ctrl modified character. - /// - /// Note that certain keys may not be modifiable with `ctrl`, due to limitations of terminals. - Ctrl(char), - /// Null byte. - Null, - /// Esc key. - Esc, -} - -impl From for Key { - fn from(k: TermionKey) -> Self { - match k { - TermionKey::Backspace => Key::Backspace, - TermionKey::Left => Key::Left, - TermionKey::Right => Key::Right, - TermionKey::Up => Key::Up, - TermionKey::Down => Key::Down, - TermionKey::Home => Key::Home, - TermionKey::End => Key::End, - TermionKey::PageUp => Key::PageUp, - TermionKey::PageDown => Key::PageDown, - TermionKey::Delete => Key::Delete, - TermionKey::Insert => Key::Insert, - TermionKey::F(u) => Key::F(u), - TermionKey::Char(c) => Key::Char(c), - TermionKey::Alt(c) => Key::Alt(c), - TermionKey::Ctrl(c) => Key::Ctrl(c), - TermionKey::Null => Key::Null, - TermionKey::Esc => Key::Esc, - _ => Key::Char(' '), - } - } -} - -/* - * If we fork (for example start $EDITOR) we want the input-thread to stop reading from stdin. The - * best way I came up with right now is to send a signal to the thread that is read in the first - * input in stdin after the fork, and then the thread kills itself. The parent process spawns a new - * input-thread when the child returns. - * - * The main loop uses try_wait_on_child() to check if child has exited. - */ -pub fn get_events( - stdin: std::io::Stdin, - mut closure: impl FnMut(Key), - mut exit: impl FnMut(), - rx: chan::Receiver, -) -> () { - for c in stdin.keys() { - chan_select! { - default => {}, - rx.recv() -> val => { - if let Some(true) = val { - exit(); - return; - } else if let Some(false) = val { - return; - } - } - - - }; - if let Ok(k) = c { - closure(Key::from(k)); - } - } -} +pub mod components; +pub use components::*; diff --git a/ui/src/state.rs b/ui/src/state.rs new file mode 100644 index 000000000..bcd9b5afd --- /dev/null +++ b/ui/src/state.rs @@ -0,0 +1,430 @@ +/*! + The UI crate has an Entity-Component-System design. The System part, is also the application's state, so they're both merged in the `State` struct. + + `State` owns all the Entities of the UI, which are currently plain Containers for `Component`s. In the application's main event loop, input is handed to the state in the form of `UIEvent` objects which traverse the entity graph. Components decide to handle each input or not. + + Input is received in the main loop from threads which listen on the stdin for user input, observe folders for file changes etc. The relevant struct is `ThreadEvent`. + */ + +use super::*; +use chan::Sender; +use fnv::FnvHashMap; +use termion::raw::IntoRawMode; +use termion::screen::AlternateScreen; +use termion::{clear, cursor, style}; +use std::io::Write; +use std::thread; +use std::time; + + +/// A context container for loaded settings, accounts, UI changes, etc. +pub struct Context { + pub accounts: Vec, + pub settings: Settings, + + pub runtime_settings: Settings, + /// Areas of the screen that must be redrawn in the next render + pub dirty_areas: VecDeque, + + /// Events queue that components send back to the state + pub replies: VecDeque, + _backends: Backends, + + input_thread: chan::Sender, +} + +impl Context { + pub fn replies(&mut self) -> Vec { + self.replies.drain(0..).collect() + } + pub fn input_thread(&mut self) -> &mut chan::Sender { + &mut self.input_thread + } +} + +/// A State object to manage and own components and entities of the UI. `State` is responsible for +/// managing the terminal and interfacing with `melib` +pub struct State { + cols: usize, + rows: usize, + + grid: CellBuffer, + stdout: Option>>, + child: Option, + pub mode: UIMode, + sender: Sender, + entities: Vec, + pub context: Context, + + startup_thread: Option>, + + threads: FnvHashMap>, +} + +impl Drop for State { + fn drop(&mut self) { + // When done, restore the defaults to avoid messing with the terminal. + write!( + self.stdout(), + "{}{}{}{}", + clear::All, + style::Reset, + cursor::Goto(1, 1), + cursor::Show + ).unwrap(); + self.flush(); + } +} + +impl State { + pub fn new(sender: Sender, input_thread: chan::Sender) -> Self { + let _stdout = std::io::stdout(); + _stdout.lock(); + let settings = Settings::new(); + let backends = Backends::new(); + let stdout = AlternateScreen::from(_stdout.into_raw_mode().unwrap()); + + let termsize = termion::terminal_size().ok(); + let termcols = termsize.map(|(w, _)| w); + let termrows = termsize.map(|(_, h)| h); + let cols = termcols.unwrap_or(0) as usize; + let rows = termrows.unwrap_or(0) as usize; + let mut accounts: Vec = settings + .accounts + .iter() + .map(|(n, a_s)| Account::new(n.to_string(), a_s.clone(), &backends)) + .collect(); + accounts.sort_by(|a, b| a.name().cmp(&b.name())); + let (startup_tx, startup_rx) = chan::async(); + let startup_thread = { + let sender = sender.clone(); + let startup_rx = startup_rx.clone(); + + thread::Builder::new() + .name("startup-thread".to_string()) + .spawn(move || { + let dur = time::Duration::from_millis(100); + loop { + chan_select! { + default => {}, + startup_rx.recv() -> _ => { + sender.send(ThreadEvent::ThreadJoin(thread::current().id())); + return; + } + } + sender.send(ThreadEvent::UIEvent(UIEventType::StartupCheck)); + thread::sleep(dur); + } + }).unwrap() + }; + let mut s = State { + cols: cols, + rows: rows, + grid: CellBuffer::new(cols, rows, Cell::with_char(' ')), + stdout: Some(stdout), + child: None, + mode: UIMode::Normal, + sender: sender, + entities: Vec::with_capacity(1), + + context: Context { + accounts: accounts, + _backends: backends, + settings: settings.clone(), + runtime_settings: settings, + dirty_areas: VecDeque::with_capacity(5), + replies: VecDeque::with_capacity(5), + + input_thread: input_thread, + }, + startup_thread: Some(startup_tx), + threads: FnvHashMap::with_capacity_and_hasher(1, Default::default()), + }; + s.threads.insert(startup_thread.thread().id(), startup_thread); + write!( + s.stdout(), + "{}{}{}", + cursor::Hide, + clear::All, + cursor::Goto(1, 1) + ).unwrap(); + s.flush(); + for account in &mut s.context.accounts { + let sender = s.sender.clone(); + account.watch(RefreshEventConsumer::new(Box::new(move |r| { + sender.send(ThreadEvent::from(r)); + }))); + } + s + } + + pub fn join(&mut self, id: thread::ThreadId) { + let handle = self.threads.remove(&id).unwrap(); + handle.join().unwrap(); + } + + pub fn finish_startup(&mut self) { + // TODO: Encode startup process with the type system if possible + if self.startup_thread.is_none() { + return; + } + { + let tx = self.startup_thread.take().unwrap(); + tx.send(true); + } + } + pub fn to_main_screen(&mut self) { + write!( + self.stdout(), + "{}{}", + termion::screen::ToMainScreen, + cursor::Show + ).unwrap(); + self.flush(); + self.stdout = None; + self.context.input_thread.send(false); + } + pub fn to_alternate_screen(&mut self) { + let s = std::io::stdout(); + s.lock(); + self.stdout = Some(AlternateScreen::from(s.into_raw_mode().unwrap())); + + write!( + self.stdout(), + "{}{}", + termion::screen::ToAlternateScreen, + cursor::Hide + ).unwrap(); + self.flush(); + } +} +impl State { + pub fn update_size(&mut self) { + let termsize = termion::terminal_size().ok(); + let termcols = termsize.map(|(w, _)| w); + let termrows = termsize.map(|(_, h)| h); + if termcols.unwrap_or(72) as usize != self.cols + || termrows.unwrap_or(120) as usize != self.rows + { + eprintln!( + "Size updated, from ({}, {}) -> ({:?}, {:?})", + self.cols, self.rows, termcols, termrows + ); + } + self.cols = termcols.unwrap_or(72) as usize; + self.rows = termrows.unwrap_or(120) as usize; + self.grid.resize(self.cols, self.rows, Cell::with_char(' ')); + + self.rcv_event(UIEvent { + id: 0, + event_type: UIEventType::Resize, + }); + } + + pub fn redraw(&mut self) { + for i in 0..self.entities.len() { + self.draw_entity(i); + } + let areas: Vec = self.context.dirty_areas.drain(0..).collect(); + /* draw each dirty area */ + for a in areas { + self.draw_area(a); + } + } + fn draw_area(&mut self, area: Area) { + let upper_left = upper_left!(area); + let bottom_right = bottom_right!(area); + + for y in get_y(upper_left)..=get_y(bottom_right) { + write!( + self.stdout(), + "{}", + cursor::Goto(get_x(upper_left) as u16 + 1, (y + 1) as u16) + ).unwrap(); + for x in get_x(upper_left)..=get_x(bottom_right) { + let c = self.grid[(x, y)]; + + if c.bg() != Color::Default { + write!(self.stdout(), "{}", termion::color::Bg(c.bg().as_termion())).unwrap(); + } + if c.fg() != Color::Default { + write!(self.stdout(), "{}", termion::color::Fg(c.fg().as_termion())).unwrap(); + } + write!(self.stdout(), "{}", c.ch()).unwrap(); + if c.bg() != Color::Default { + write!( + self.stdout(), + "{}", + termion::color::Bg(termion::color::Reset) + ).unwrap(); + } + if c.fg() != Color::Default { + write!( + self.stdout(), + "{}", + termion::color::Fg(termion::color::Reset) + ).unwrap(); + } + } + } + self.flush(); + } + pub fn render(&mut self) { + self.update_size(); + + /* draw each entity */ + for i in 0..self.entities.len() { + self.draw_entity(i); + } + let cols = self.cols; + let rows = self.rows; + + self.draw_area(((0, 0), (cols - 1, rows - 1))); + } + pub fn draw_entity(&mut self, idx: usize) { + let entity = &mut self.entities[idx]; + let upper_left = (0, 0); + let bottom_right = (self.cols - 1, self.rows - 1); + + if entity.component.is_dirty() { + entity.component.draw( + &mut self.grid, + (upper_left, bottom_right), + &mut self.context, + ); + } + } + pub fn register_entity(&mut self, entity: Entity) { + self.entities.push(entity); + } + /// Convert user commands to actions/method calls. + fn parse_command(&mut self, cmd: String) { + let result = parse_command(&cmd.as_bytes()).to_full_result(); + + if let Ok(v) = result { + self.rcv_event(UIEvent { id: 0, event_type: UIEventType::Action(v) }); + } + } + + pub fn rcv_event(&mut self, event: UIEvent) { + match event.event_type { + // Command type is handled only by State. + UIEventType::Command(cmd) => { + self.parse_command(cmd); + return; + } + UIEventType::Fork(child) => { + self.mode = UIMode::Fork; + self.child = Some(child); + self.flush(); + return; + } + UIEventType::EditDraft(mut file) => { + use std::io::Read; + use std::process::{Command, Stdio}; + let mut output = Command::new("msmtp") + .arg("-t") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute process"); + { + let mut in_pipe = output.stdin.as_mut().unwrap(); + let mut buf = Vec::new(); + let mut f = file.file(); + + f.read_to_end(&mut buf).unwrap(); + in_pipe.write(&buf).unwrap(); + std::fs::remove_file(file.path()).unwrap(); + } + output.wait_with_output().expect("Failed to read stdout"); + + return; + } + UIEventType::Input(Key::Char('t')) => for i in 0..self.entities.len() { + self.entities[i].rcv_event( + &UIEvent { + id: 0, + event_type: UIEventType::Action(Action::MailListing( + MailListingAction::ToggleThreaded, + )), + }, + &mut self.context, + ); + }, + + _ => {} + } + /* inform each entity */ + for i in 0..self.entities.len() { + self.entities[i].rcv_event(&event, &mut self.context); + } + + if !self.context.replies.is_empty() { + let replies: Vec= self.context.replies.drain(0..).collect(); + // Pass replies to self and call count on the map iterator to force evaluation + replies.into_iter().map(|r| self.rcv_event(r)).count(); + } + } + + /// Tries to load a mailbox's content + pub fn refresh_mailbox(&mut self, account_idx: usize, folder_idx: usize) { + let flag = match &mut self.context.accounts[account_idx][folder_idx] { + Ok(_) => true, + Err(e) => { + eprintln!("error {:?}", e); + false + } + }; + if flag { + self.rcv_event(UIEvent { + id: 0, + event_type: UIEventType::RefreshMailbox((account_idx, folder_idx)), + }); + } + } + pub fn try_wait_on_child(&mut self) -> Option { + if { + match self.child { + Some(ForkType::NewDraft(_, ref mut c)) => { + let mut w = c.try_wait(); + match w { + Ok(Some(_)) => true, + Ok(None) => false, + Err(_) => { + return None; + } + } + } + Some(ForkType::Generic(ref mut c)) => { + let mut w = c.try_wait(); + match w { + Ok(Some(_)) => true, + Ok(None) => false, + Err(_) => { + return None; + } + } + } + _ => { + return None; + } + } + } { + if let Some(ForkType::NewDraft(f, _)) = std::mem::replace(&mut self.child, None) { + self.rcv_event(UIEvent { + id: 0, + event_type: UIEventType::EditDraft(f), + }); + } + return Some(true); + } + Some(false) + } + fn flush(&mut self) { + self.stdout.as_mut().map(|s| s.flush().unwrap()); + } + fn stdout(&mut self) -> &mut termion::screen::AlternateScreen> { + self.stdout.as_mut().unwrap() + } +} diff --git a/ui/src/cells.rs b/ui/src/types/cells.rs similarity index 100% rename from ui/src/cells.rs rename to ui/src/types/cells.rs diff --git a/ui/src/helpers.rs b/ui/src/types/helpers.rs similarity index 100% rename from ui/src/helpers.rs rename to ui/src/types/helpers.rs diff --git a/ui/src/types/keys.rs b/ui/src/types/keys.rs new file mode 100644 index 000000000..f1b6bbf60 --- /dev/null +++ b/ui/src/types/keys.rs @@ -0,0 +1,106 @@ +use termion::event::Key as TermionKey; +use termion::input::TermRead; +use chan; +use std::io; + + +#[derive(Debug)] +pub enum Key { + /// Backspace. + Backspace, + /// Left arrow. + Left, + /// Right arrow. + Right, + /// Up arrow. + Up, + /// Down arrow. + Down, + /// Home key. + Home, + /// End key. + End, + /// Page Up key. + PageUp, + /// Page Down key. + PageDown, + /// Delete key. + Delete, + /// Insert key. + Insert, + /// Function keys. + /// + /// Only function keys 1 through 12 are supported. + F(u8), + /// Normal character. + Char(char), + /// Alt modified character. + Alt(char), + /// Ctrl modified character. + /// + /// Note that certain keys may not be modifiable with `ctrl`, due to limitations of terminals. + Ctrl(char), + /// Null byte. + Null, + /// Esc key. + Esc, +} + +impl From for Key { + fn from(k: TermionKey) -> Self { + match k { + TermionKey::Backspace => Key::Backspace, + TermionKey::Left => Key::Left, + TermionKey::Right => Key::Right, + TermionKey::Up => Key::Up, + TermionKey::Down => Key::Down, + TermionKey::Home => Key::Home, + TermionKey::End => Key::End, + TermionKey::PageUp => Key::PageUp, + TermionKey::PageDown => Key::PageDown, + TermionKey::Delete => Key::Delete, + TermionKey::Insert => Key::Insert, + TermionKey::F(u) => Key::F(u), + TermionKey::Char(c) => Key::Char(c), + TermionKey::Alt(c) => Key::Alt(c), + TermionKey::Ctrl(c) => Key::Ctrl(c), + TermionKey::Null => Key::Null, + TermionKey::Esc => Key::Esc, + _ => Key::Char(' '), + } + } +} + +/* + * If we fork (for example start $EDITOR) we want the input-thread to stop reading from stdin. The + * best way I came up with right now is to send a signal to the thread that is read in the first + * input in stdin after the fork, and then the thread kills itself. The parent process spawns a new + * input-thread when the child returns. + * + * The main loop uses try_wait_on_child() to check if child has exited. + */ +pub fn get_events( + stdin: io::Stdin, + mut closure: impl FnMut(Key), + mut exit: impl FnMut(), + rx: chan::Receiver, +) -> () { + for c in stdin.keys() { + chan_select! { + default => {}, + rx.recv() -> val => { + if let Some(true) = val { + exit(); + return; + } else if let Some(false) = val { + return; + } + } + + + }; + if let Ok(k) = c { + closure(Key::from(k)); + } + } +} diff --git a/ui/src/types/mod.rs b/ui/src/types/mod.rs new file mode 100644 index 000000000..914083768 --- /dev/null +++ b/ui/src/types/mod.rs @@ -0,0 +1,103 @@ +#[macro_use] +mod cells; +#[macro_use] +mod helpers; +#[macro_use] +mod keys; +#[macro_use] +mod position; +pub use self::cells::*; +pub use self::helpers::*; +pub use self::keys::*; +pub use self::position::*; + +use super::execute::Action; + +use melib::RefreshEvent; +use std; +use std::thread; +use std::fmt; + +/// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads +/// to the main process. +#[derive(Debug)] +pub enum ThreadEvent { + ThreadJoin(thread::ThreadId), + /// User input. + Input(Key), + /// A watched folder has been refreshed. + RefreshMailbox { + hash: u64, + }, + UIEvent(UIEventType), + //Decode { _ }, // For gpg2 signature check +} + +impl From for ThreadEvent { + fn from(event: RefreshEvent) -> Self { + ThreadEvent::RefreshMailbox { hash: event.hash } + } +} + +#[derive(Debug)] +pub enum ForkType { + Generic(std::process::Child), + NewDraft(File, std::process::Child), +} + +#[derive(Debug)] +pub enum UIEventType { + Input(Key), + ExInput(Key), + RefreshMailbox((usize, usize)), + //Quit? + Resize, + /// Force redraw. + Fork(ForkType), + ChangeMailbox(usize), + ChangeMode(UIMode), + Command(String), + Notification(String), + EditDraft(File), + Action(Action), + StatusNotification(String), + MailboxUpdate((usize, usize)), + + StartupCheck, +} + +/// An event passed from `State` to its Entities. +#[derive(Debug)] +pub struct UIEvent { + pub id: u64, + pub event_type: UIEventType, +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum UIMode { + Normal, + Execute, + Fork, +} + +impl fmt::Display for UIMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match *self { + UIMode::Normal => "NORMAL", + UIMode::Execute => "EX", + UIMode::Fork => "FORK", + } + ) + } +} + +/// An event notification that is passed to Entities for handling. +pub struct Notification { + _title: String, + _content: String, + + _timestamp: std::time::Instant, +} diff --git a/ui/src/position.rs b/ui/src/types/position.rs similarity index 100% rename from ui/src/position.rs rename to ui/src/types/position.rs