2018-08-07 15:01:15 +03:00
|
|
|
/*
|
|
|
|
* meli - ui crate.
|
|
|
|
*
|
|
|
|
* Copyright 2017-2018 Manos Pitsidianakis
|
|
|
|
*
|
|
|
|
* This file is part of meli.
|
|
|
|
*
|
|
|
|
* meli is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* meli is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2018-08-06 22:20:34 +03:00
|
|
|
/*! The application's state.
|
|
|
|
|
2018-08-06 16:53:23 +03:00
|
|
|
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::*;
|
2018-08-16 16:32:47 +03:00
|
|
|
use chan::{Receiver, Sender};
|
2018-08-06 16:53:23 +03:00
|
|
|
use fnv::FnvHashMap;
|
|
|
|
use std::io::Write;
|
2018-08-16 21:20:53 +03:00
|
|
|
use std::result;
|
2018-08-23 15:36:52 +03:00
|
|
|
use std::thread;
|
2018-08-06 16:53:23 +03:00
|
|
|
use std::time;
|
2018-08-07 15:01:15 +03:00
|
|
|
use termion::raw::IntoRawMode;
|
|
|
|
use termion::screen::AlternateScreen;
|
|
|
|
use termion::{clear, cursor, style};
|
2018-08-06 16:53:23 +03:00
|
|
|
|
2018-09-04 01:49:29 +03:00
|
|
|
type StateStdout = termion::screen::AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>;
|
|
|
|
|
2018-08-16 16:32:47 +03:00
|
|
|
struct InputHandler {
|
|
|
|
rx: Receiver<bool>,
|
|
|
|
tx: Sender<bool>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InputHandler {
|
|
|
|
fn restore(&self, tx: Sender<ThreadEvent>) {
|
|
|
|
let stdin = std::io::stdin();
|
|
|
|
let rx = self.rx.clone();
|
|
|
|
thread::Builder::new()
|
|
|
|
.name("input-thread".to_string())
|
|
|
|
.spawn(move || {
|
|
|
|
get_events(
|
|
|
|
stdin,
|
|
|
|
|k| {
|
|
|
|
tx.send(ThreadEvent::Input(k));
|
|
|
|
},
|
|
|
|
|| {
|
|
|
|
tx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode(UIMode::Fork)));
|
|
|
|
},
|
|
|
|
&rx,
|
2018-08-23 15:36:52 +03:00
|
|
|
)
|
2018-08-16 16:32:47 +03:00
|
|
|
})
|
2018-08-23 15:36:52 +03:00
|
|
|
.unwrap();
|
2018-08-16 16:32:47 +03:00
|
|
|
}
|
|
|
|
fn kill(&self) {
|
|
|
|
self.tx.send(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-06 16:53:23 +03:00
|
|
|
/// A context container for loaded settings, accounts, UI changes, etc.
|
|
|
|
pub struct Context {
|
|
|
|
pub accounts: Vec<Account>,
|
2018-08-08 10:41:25 +03:00
|
|
|
mailbox_hashes: FnvHashMap<u64, (usize, usize)>,
|
2018-08-06 16:53:23 +03:00
|
|
|
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>,
|
2018-08-16 16:32:47 +03:00
|
|
|
sender: Sender<ThreadEvent>,
|
|
|
|
receiver: Receiver<ThreadEvent>,
|
|
|
|
input: InputHandler,
|
2018-08-06 16:53:23 +03:00
|
|
|
|
2018-08-08 20:58:15 +03:00
|
|
|
pub temp_files: Vec<File>,
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Context {
|
|
|
|
pub fn replies(&mut self) -> Vec<UIEvent> {
|
|
|
|
self.replies.drain(0..).collect()
|
|
|
|
}
|
2018-08-16 16:32:47 +03:00
|
|
|
pub fn input_kill(&self) {
|
|
|
|
self.input.kill();
|
|
|
|
}
|
|
|
|
pub fn restore_input(&self) {
|
|
|
|
self.input.restore(self.sender.clone());
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
2018-08-16 21:20:53 +03:00
|
|
|
pub fn account_status(&mut self, idx_a: usize, idx_m: usize) -> result::Result<bool, usize> {
|
|
|
|
let s = self.accounts[idx_a].status(idx_m)?;
|
2018-08-23 15:36:52 +03:00
|
|
|
match s {
|
|
|
|
LoadMailboxResult::New(event) => {
|
|
|
|
eprintln!("setting up notification");
|
|
|
|
let (idx_a, idx_m) = self.mailbox_hashes[&event.folder];
|
|
|
|
let subjects = {
|
|
|
|
let mut ret = Vec::with_capacity(event.index.len());
|
|
|
|
eprintln!("index is {:?}", &event.index);
|
|
|
|
for &i in &event.index {
|
|
|
|
ret.push(
|
|
|
|
self.accounts[idx_a][idx_m].as_ref().unwrap().collection[i].subject(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
ret
|
|
|
|
};
|
|
|
|
self.replies.push_back(UIEvent {
|
|
|
|
id: 0,
|
|
|
|
event_type: UIEventType::Notification(format!(
|
|
|
|
"Update in {}/{}, indexes {:?}",
|
|
|
|
self.accounts[idx_a].name(),
|
|
|
|
self.accounts[idx_a][idx_m].as_ref().unwrap().folder.name(),
|
|
|
|
subjects
|
|
|
|
)),
|
|
|
|
});
|
|
|
|
Ok(true)
|
|
|
|
}
|
|
|
|
LoadMailboxResult::Loaded => Ok(true),
|
|
|
|
LoadMailboxResult::Refresh => Ok(false),
|
2018-08-16 21:20:53 +03:00
|
|
|
}
|
|
|
|
}
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/// A State object to manage and own components and entities of the UI. `State` is responsible for
|
|
|
|
/// managing the terminal and interfacing with `melib`
|
2018-09-04 01:49:29 +03:00
|
|
|
pub struct State {
|
2018-08-06 16:53:23 +03:00
|
|
|
cols: usize,
|
|
|
|
rows: usize,
|
|
|
|
|
|
|
|
grid: CellBuffer,
|
2018-09-04 01:49:29 +03:00
|
|
|
stdout: Option<StateStdout>,
|
2018-08-06 16:53:23 +03:00
|
|
|
child: Option<ForkType>,
|
|
|
|
pub mode: UIMode,
|
|
|
|
entities: Vec<Entity>,
|
|
|
|
pub context: Context,
|
|
|
|
|
|
|
|
startup_thread: Option<chan::Sender<bool>>,
|
|
|
|
|
2018-08-13 09:25:48 +03:00
|
|
|
threads: FnvHashMap<thread::ThreadId, (chan::Sender<bool>, thread::JoinHandle<()>)>,
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
|
2018-09-04 01:49:29 +03:00
|
|
|
impl Drop for State {
|
2018-08-06 16:53:23 +03:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-04 01:49:29 +03:00
|
|
|
impl Default for State {
|
2018-08-23 15:36:52 +03:00
|
|
|
fn default() -> Self {
|
|
|
|
Self::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-04 01:49:29 +03:00
|
|
|
impl State {
|
2018-08-16 16:32:47 +03:00
|
|
|
pub fn new() -> Self {
|
|
|
|
/* Create a channel to communicate with other threads. The main process is the sole receiver.
|
|
|
|
* */
|
|
|
|
let (sender, receiver) = chan::sync(::std::mem::size_of::<ThreadEvent>());
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Create async channel to block the input-thread if we need to fork and stop it from reading
|
|
|
|
* stdin, see get_events() for details
|
|
|
|
* */
|
|
|
|
let input_thread = chan::async();
|
2018-08-06 16:53:23 +03:00
|
|
|
let _stdout = std::io::stdout();
|
|
|
|
_stdout.lock();
|
|
|
|
let backends = Backends::new();
|
2018-08-11 18:00:21 +03:00
|
|
|
let settings = Settings::new();
|
2018-08-06 16:53:23 +03:00
|
|
|
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()
|
2018-08-19 14:08:20 +03:00
|
|
|
.map(|(n, a_s)| Account::new(n.to_string(), a_s.clone(), &backends))
|
2018-08-06 16:53:23 +03:00
|
|
|
.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()));
|
2018-08-13 09:25:48 +03:00
|
|
|
break;
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
sender.send(ThreadEvent::UIEvent(UIEventType::StartupCheck));
|
|
|
|
thread::sleep(dur);
|
|
|
|
}
|
2018-08-13 09:25:48 +03:00
|
|
|
startup_rx.recv();
|
|
|
|
return;
|
2018-08-07 15:01:15 +03:00
|
|
|
})
|
|
|
|
.unwrap()
|
2018-08-06 16:53:23 +03:00
|
|
|
};
|
|
|
|
let mut s = State {
|
2018-08-07 15:01:15 +03:00
|
|
|
cols,
|
|
|
|
rows,
|
2018-08-06 16:53:23 +03:00
|
|
|
grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
|
|
|
|
stdout: Some(stdout),
|
|
|
|
child: None,
|
|
|
|
mode: UIMode::Normal,
|
|
|
|
entities: Vec::with_capacity(1),
|
|
|
|
|
|
|
|
context: Context {
|
2018-08-07 15:01:15 +03:00
|
|
|
accounts,
|
2018-08-08 10:41:25 +03:00
|
|
|
mailbox_hashes: FnvHashMap::with_capacity_and_hasher(1, Default::default()),
|
|
|
|
|
2018-08-06 16:53:23 +03:00
|
|
|
settings: settings.clone(),
|
|
|
|
runtime_settings: settings,
|
|
|
|
dirty_areas: VecDeque::with_capacity(5),
|
|
|
|
replies: VecDeque::with_capacity(5),
|
2018-08-08 20:58:15 +03:00
|
|
|
temp_files: Vec::new(),
|
2018-08-06 16:53:23 +03:00
|
|
|
|
2018-08-16 16:32:47 +03:00
|
|
|
sender,
|
|
|
|
receiver,
|
|
|
|
input: InputHandler {
|
|
|
|
rx: input_thread.1,
|
|
|
|
tx: input_thread.0,
|
|
|
|
},
|
2018-08-06 16:53:23 +03:00
|
|
|
},
|
2018-08-13 09:25:48 +03:00
|
|
|
startup_thread: Some(startup_tx.clone()),
|
2018-08-06 16:53:23 +03:00
|
|
|
threads: FnvHashMap::with_capacity_and_hasher(1, Default::default()),
|
|
|
|
};
|
2018-08-23 15:36:52 +03:00
|
|
|
s.threads.insert(
|
|
|
|
startup_thread.thread().id(),
|
|
|
|
(startup_tx.clone(), startup_thread),
|
|
|
|
);
|
2018-08-06 16:53:23 +03:00
|
|
|
write!(
|
|
|
|
s.stdout(),
|
|
|
|
"{}{}{}",
|
|
|
|
cursor::Hide,
|
|
|
|
clear::All,
|
|
|
|
cursor::Goto(1, 1)
|
2018-08-07 15:01:15 +03:00
|
|
|
).unwrap();
|
2018-08-06 16:53:23 +03:00
|
|
|
s.flush();
|
2018-08-16 21:20:53 +03:00
|
|
|
eprintln!("DEBUG: inserting mailbox hashes:");
|
2018-08-08 10:41:25 +03:00
|
|
|
for (x, account) in s.context.accounts.iter_mut().enumerate() {
|
2018-08-11 18:00:21 +03:00
|
|
|
for (y, folder) in account.backend.folders().iter().enumerate() {
|
2018-08-16 21:20:53 +03:00
|
|
|
eprintln!("{:?}", folder);
|
2018-08-08 10:41:25 +03:00
|
|
|
s.context.mailbox_hashes.insert(folder.hash(), (x, y));
|
|
|
|
}
|
2018-08-16 16:32:47 +03:00
|
|
|
let sender = s.context.sender.clone();
|
2018-08-06 16:53:23 +03:00
|
|
|
account.watch(RefreshEventConsumer::new(Box::new(move |r| {
|
|
|
|
sender.send(ThreadEvent::from(r));
|
|
|
|
})));
|
|
|
|
}
|
2018-08-16 16:32:47 +03:00
|
|
|
s.restore_input();
|
2018-08-06 16:53:23 +03:00
|
|
|
s
|
|
|
|
}
|
2018-08-11 19:19:30 +03:00
|
|
|
/*
|
|
|
|
* When we receive a folder hash from a watcher thread,
|
|
|
|
* we match the hash to the index of the mailbox, request a reload
|
|
|
|
* and startup a thread to remind us to poll it every now and then till it's finished.
|
|
|
|
*/
|
|
|
|
pub fn hash_to_folder(&mut self, hash: u64) {
|
2018-08-16 21:20:53 +03:00
|
|
|
if let Some(&(idxa, idxm)) = self.context.mailbox_hashes.get(&hash) {
|
|
|
|
self.context.accounts[idxa].reload(idxm);
|
|
|
|
let (startup_tx, startup_rx) = chan::async();
|
|
|
|
let startup_thread = {
|
|
|
|
let sender = self.context.sender.clone();
|
|
|
|
let startup_rx = startup_rx.clone();
|
2018-08-11 19:19:30 +03:00
|
|
|
|
2018-08-16 21:20:53 +03:00
|
|
|
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::UIEvent(UIEventType::MailboxUpdate((idxa,idxm))));
|
|
|
|
sender.send(ThreadEvent::ThreadJoin(thread::current().id()));
|
|
|
|
return;
|
|
|
|
}
|
2018-08-11 19:19:30 +03:00
|
|
|
}
|
2018-08-16 21:20:53 +03:00
|
|
|
sender.send(ThreadEvent::UIEvent(UIEventType::StartupCheck));
|
|
|
|
thread::sleep(dur);
|
2018-08-11 19:19:30 +03:00
|
|
|
}
|
2018-08-16 21:20:53 +03:00
|
|
|
})
|
2018-08-23 15:36:52 +03:00
|
|
|
.expect("Failed to spawn startup-thread in hash_to_folder()")
|
2018-08-16 21:20:53 +03:00
|
|
|
};
|
|
|
|
self.startup_thread = Some(startup_tx.clone());
|
|
|
|
self.threads
|
|
|
|
.insert(startup_thread.thread().id(), (startup_tx, startup_thread));
|
|
|
|
} else {
|
2018-08-23 15:36:52 +03:00
|
|
|
eprintln!(
|
|
|
|
"BUG: mailbox with hash {} not found in mailbox_hashes.",
|
|
|
|
hash
|
|
|
|
);
|
2018-08-16 21:20:53 +03:00
|
|
|
}
|
2018-08-08 10:41:25 +03:00
|
|
|
}
|
2018-08-06 16:53:23 +03:00
|
|
|
|
2018-08-06 22:20:34 +03:00
|
|
|
/// If an owned thread returns a `ThreadEvent::ThreadJoin` event to `State` then it must remove
|
|
|
|
/// the thread from its list and `join` it.
|
2018-08-06 16:53:23 +03:00
|
|
|
pub fn join(&mut self, id: thread::ThreadId) {
|
2018-08-13 09:25:48 +03:00
|
|
|
let (tx, handle) = self.threads.remove(&id).unwrap();
|
|
|
|
tx.send(true);
|
2018-08-06 16:53:23 +03:00
|
|
|
handle.join().unwrap();
|
|
|
|
}
|
|
|
|
|
2018-08-06 22:20:34 +03:00
|
|
|
/// If startup has finished, inform startup thread that it doesn't need to tick us with startup
|
|
|
|
/// check reminders and let it die.
|
2018-08-06 16:53:23 +03:00
|
|
|
pub fn finish_startup(&mut self) {
|
|
|
|
// TODO: Encode startup process with the type system if possible
|
|
|
|
if self.startup_thread.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
{
|
2018-08-07 15:01:15 +03:00
|
|
|
let tx = self.startup_thread.take().unwrap();
|
2018-08-06 16:53:23 +03:00
|
|
|
tx.send(true);
|
|
|
|
}
|
|
|
|
}
|
2018-08-06 22:20:34 +03:00
|
|
|
|
|
|
|
/// Switch back to the terminal's main screen (The command line the user sees before opening
|
|
|
|
/// the application)
|
2018-08-07 15:01:15 +03:00
|
|
|
pub fn switch_to_main_screen(&mut self) {
|
2018-08-06 16:53:23 +03:00
|
|
|
write!(
|
|
|
|
self.stdout(),
|
|
|
|
"{}{}",
|
|
|
|
termion::screen::ToMainScreen,
|
|
|
|
cursor::Show
|
|
|
|
).unwrap();
|
|
|
|
self.flush();
|
|
|
|
self.stdout = None;
|
2018-08-16 16:32:47 +03:00
|
|
|
self.context.input.kill();
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
2018-08-07 15:01:15 +03:00
|
|
|
pub fn switch_to_alternate_screen(&mut self) {
|
2018-08-06 16:53:23 +03:00
|
|
|
let s = std::io::stdout();
|
|
|
|
s.lock();
|
|
|
|
self.stdout = Some(AlternateScreen::from(s.into_raw_mode().unwrap()));
|
|
|
|
|
|
|
|
write!(
|
|
|
|
self.stdout(),
|
2018-08-30 14:48:18 +03:00
|
|
|
"{}{}{}{}",
|
2018-08-06 16:53:23 +03:00
|
|
|
termion::screen::ToAlternateScreen,
|
2018-08-30 14:48:18 +03:00
|
|
|
cursor::Hide,
|
|
|
|
clear::All,
|
|
|
|
cursor::Goto(1, 1)
|
2018-08-06 16:53:23 +03:00
|
|
|
).unwrap();
|
|
|
|
self.flush();
|
|
|
|
}
|
2018-09-04 01:49:29 +03:00
|
|
|
|
2018-08-16 16:32:47 +03:00
|
|
|
pub fn receiver(&self) -> Receiver<ThreadEvent> {
|
|
|
|
self.context.receiver.clone()
|
|
|
|
}
|
|
|
|
pub fn restore_input(&mut self) {
|
|
|
|
self.context.restore_input();
|
|
|
|
}
|
|
|
|
|
2018-08-06 22:20:34 +03:00
|
|
|
/// On `SIGWNICH` the `State` redraws itself according to the new terminal size.
|
2018-08-06 16:53:23 +03:00
|
|
|
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,
|
|
|
|
});
|
2018-08-10 12:00:27 +03:00
|
|
|
|
|
|
|
// Invalidate dirty areas.
|
|
|
|
self.context.dirty_areas.clear();
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
|
2018-08-06 22:20:34 +03:00
|
|
|
/// Force a redraw for all dirty components.
|
2018-08-06 16:53:23 +03:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2018-08-06 22:20:34 +03:00
|
|
|
|
|
|
|
/// Draw only a specific `area` on the screen.
|
2018-08-06 16:53:23 +03:00
|
|
|
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();
|
|
|
|
}
|
2018-08-06 22:20:34 +03:00
|
|
|
|
|
|
|
/// Draw the entire screen from scratch.
|
2018-08-06 16:53:23 +03:00
|
|
|
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.
|
2018-08-07 15:01:15 +03:00
|
|
|
fn parse_command(&mut self, cmd: &str) {
|
2018-08-06 16:53:23 +03:00
|
|
|
let result = parse_command(&cmd.as_bytes()).to_full_result();
|
|
|
|
|
|
|
|
if let Ok(v) = result {
|
2018-08-07 15:01:15 +03:00
|
|
|
self.rcv_event(UIEvent {
|
|
|
|
id: 0,
|
|
|
|
event_type: UIEventType::Action(v),
|
|
|
|
});
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-06 22:20:34 +03:00
|
|
|
/// The application's main loop sends `UIEvents` to state via this method.
|
2018-08-06 16:53:23 +03:00
|
|
|
pub fn rcv_event(&mut self, event: UIEvent) {
|
|
|
|
match event.event_type {
|
|
|
|
// Command type is handled only by State.
|
|
|
|
UIEventType::Command(cmd) => {
|
2018-08-07 15:01:15 +03:00
|
|
|
self.parse_command(&cmd);
|
2018-08-06 16:53:23 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
UIEventType::Fork(child) => {
|
|
|
|
self.mode = UIMode::Fork;
|
|
|
|
self.child = Some(child);
|
2018-09-04 01:49:29 +03:00
|
|
|
if let Some(ForkType::Finished) = self.child {
|
|
|
|
/*
|
|
|
|
* Fork has finished in the past.
|
|
|
|
* We're back in the AlternateScreen, but the cursor is reset to Shown, so fix
|
|
|
|
* it.
|
|
|
|
*/
|
|
|
|
write!(self.stdout(), "{}", cursor::Hide,).unwrap();
|
|
|
|
self.flush();
|
|
|
|
}
|
2018-08-06 16:53:23 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
UIEventType::EditDraft(mut file) => {
|
2018-08-10 11:06:47 +03:00
|
|
|
eprintln!("edit draft event");
|
2018-08-06 16:53:23 +03:00
|
|
|
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();
|
2018-08-07 15:01:15 +03:00
|
|
|
in_pipe.write_all(&buf).unwrap();
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
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,
|
2018-09-04 01:49:29 +03:00
|
|
|
event_type: UIEventType::Action(Action::Listing(
|
|
|
|
ListingAction::ToggleThreaded,
|
2018-08-06 16:53:23 +03:00
|
|
|
)),
|
|
|
|
},
|
|
|
|
&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() {
|
2018-08-07 15:01:15 +03:00
|
|
|
let replies: Vec<UIEvent> = self.context.replies.drain(0..).collect();
|
2018-08-06 16:53:23 +03:00
|
|
|
// 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)),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2018-08-06 22:20:34 +03:00
|
|
|
|
2018-08-06 16:53:23 +03:00
|
|
|
pub fn try_wait_on_child(&mut self) -> Option<bool> {
|
2018-09-04 01:49:29 +03:00
|
|
|
let should_return_flag = match self.child {
|
2018-08-07 15:01:15 +03:00
|
|
|
Some(ForkType::NewDraft(_, ref mut c)) => {
|
|
|
|
let mut w = c.try_wait();
|
|
|
|
match w {
|
|
|
|
Ok(Some(_)) => true,
|
|
|
|
Ok(None) => false,
|
|
|
|
Err(_) => {
|
|
|
|
return None;
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
}
|
2018-08-07 15:01:15 +03:00
|
|
|
}
|
|
|
|
Some(ForkType::Generic(ref mut c)) => {
|
|
|
|
let mut w = c.try_wait();
|
|
|
|
match w {
|
|
|
|
Ok(Some(_)) => true,
|
|
|
|
Ok(None) => false,
|
|
|
|
Err(_) => {
|
|
|
|
return None;
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-09-04 01:49:29 +03:00
|
|
|
Some(ForkType::Finished) => {
|
|
|
|
/* Fork has already finished */
|
|
|
|
std::mem::replace(&mut self.child, None);
|
|
|
|
return None;
|
|
|
|
}
|
2018-08-07 15:01:15 +03:00
|
|
|
_ => {
|
|
|
|
return None;
|
|
|
|
}
|
2018-09-04 01:49:29 +03:00
|
|
|
};
|
|
|
|
if should_return_flag {
|
2018-08-06 16:53:23 +03:00
|
|
|
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) {
|
2018-08-11 18:00:21 +03:00
|
|
|
if let Some(s) = self.stdout.as_mut() {
|
|
|
|
s.flush().unwrap();
|
|
|
|
}
|
2018-08-06 16:53:23 +03:00
|
|
|
}
|
2018-09-04 01:49:29 +03:00
|
|
|
fn stdout(&mut self) -> &mut StateStdout {
|
2018-08-06 16:53:23 +03:00
|
|
|
self.stdout.as_mut().unwrap()
|
|
|
|
}
|
|
|
|
}
|