Refactor ui module

embed
Manos Pitsidianakis 2018-08-06 16:53:23 +03:00
parent 00200aedb6
commit c32c6b82c8
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
8 changed files with 657 additions and 635 deletions

View File

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

View File

@ -19,29 +19,6 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*!
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<RefreshEvent> 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<Account>,
settings: Settings,
runtime_settings: Settings,
/// Areas of the screen that must be redrawn in the next render
dirty_areas: VecDeque<Area>,
/// Events queue that components send back to the state
replies: VecDeque<UIEvent>,
_backends: Backends,
input_thread: chan::Sender<bool>,
}
impl Context {
pub fn replies(&mut self) -> Vec<UIEvent> {
self.replies.drain(0..).collect()
}
pub fn input_thread(&mut self) -> &mut chan::Sender<bool> {
&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<W: Write> {
cols: usize,
rows: usize,
grid: CellBuffer,
stdout: Option<termion::screen::AlternateScreen<termion::raw::RawTerminal<W>>>,
child: Option<ForkType>,
pub mode: UIMode,
sender: Sender<ThreadEvent>,
entities: Vec<Entity>,
pub context: Context,
startup_thread: Option<chan::Sender<bool>>,
threads: FnvHashMap<thread::ThreadId, thread::JoinHandle<()>>,
}
impl<W: Write> Drop for State<W> {
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<std::io::Stdout> {
pub fn new(sender: Sender<ThreadEvent>, input_thread: chan::Sender<bool>) -> 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<Account> = 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<W: Write> State<W> {
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<Area> = 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<UIEvent>= 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<bool> {
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<termion::raw::RawTerminal<W>> {
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<TermionKey> 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<bool>,
) -> () {
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::*;

430
ui/src/state.rs 100644
View File

@ -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<Account>,
pub settings: Settings,
pub runtime_settings: Settings,
/// Areas of the screen that must be redrawn in the next render
pub dirty_areas: VecDeque<Area>,
/// Events queue that components send back to the state
pub replies: VecDeque<UIEvent>,
_backends: Backends,
input_thread: chan::Sender<bool>,
}
impl Context {
pub fn replies(&mut self) -> Vec<UIEvent> {
self.replies.drain(0..).collect()
}
pub fn input_thread(&mut self) -> &mut chan::Sender<bool> {
&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<W: Write> {
cols: usize,
rows: usize,
grid: CellBuffer,
stdout: Option<termion::screen::AlternateScreen<termion::raw::RawTerminal<W>>>,
child: Option<ForkType>,
pub mode: UIMode,
sender: Sender<ThreadEvent>,
entities: Vec<Entity>,
pub context: Context,
startup_thread: Option<chan::Sender<bool>>,
threads: FnvHashMap<thread::ThreadId, thread::JoinHandle<()>>,
}
impl<W: Write> Drop for State<W> {
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<std::io::Stdout> {
pub fn new(sender: Sender<ThreadEvent>, input_thread: chan::Sender<bool>) -> 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<Account> = 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<W: Write> State<W> {
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<Area> = 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<UIEvent>= 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<bool> {
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<termion::raw::RawTerminal<W>> {
self.stdout.as_mut().unwrap()
}
}

View File

@ -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<TermionKey> 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<bool>,
) -> () {
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));
}
}
}

103
ui/src/types/mod.rs 100644
View File

@ -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<RefreshEvent> 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,
}