Make backend folders completely agnostic (remove maildir logic from

conf)
embed
Manos Pitsidianakis 2018-08-11 18:00:21 +03:00
parent 7a6fc1ce94
commit b98a04f35b
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
28 changed files with 1472 additions and 305 deletions

View File

@ -9,35 +9,35 @@ use self::test::Bencher;
#[bench]
fn bench_threads_1(b: &mut Bencher) {
b.iter(|| {
let folder = Folder::new(String::from(""), vec![]);
let folder = Folder::new(String::from(""), String::from(""), vec![]);
MaildirType::new("").multicore(1, &folder)
});
}
#[bench]
fn bench_threads_2(b: &mut Bencher) {
b.iter(|| {
let folder = Folder::new(String::from(""), vec![]);
let folder = Folder::new(String::from(""), String::from(""), vec![]);
MaildirType::new("").multicore(2, &folder)
});
}
#[bench]
fn bench_threads_3(b: &mut Bencher) {
b.iter(|| {
let folder = Folder::new(String::from(""), vec![]);
let folder = Folder::new(String::from(""), String::from(""), vec![]);
MaildirType::new("").multicore(3, &folder)
});
}
#[bench]
fn bench_threads_4(b: &mut Bencher) {
b.iter(|| {
let folder = Folder::new(String::from(""), vec![]);
let folder = Folder::new(String::from(""), String::from(""), vec![]);
MaildirType::new("").multicore(4, &folder)
});
}
#[bench]
fn bench_threads_6(b: &mut Bencher) {
b.iter(|| {
let folder = Folder::new(String::from(""), vec![]);
let folder = Folder::new(String::from(""), String::from(""), vec![]);
MaildirType::new("").multicore(6, &folder)
});
}

View File

@ -26,53 +26,22 @@ pub mod pager;
use pager::PagerSettings;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::fs;
use std::hash::Hasher;
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Clone)]
pub struct Folder {
hash: u64,
name: String,
path: String,
children: Vec<usize>,
}
impl Folder {
pub fn new(path: String, file_name: String, children: Vec<usize>) -> Self {
let mut h = DefaultHasher::new();
h.write(&path.as_bytes());
Folder {
hash: h.finish(),
name: file_name,
path: path,
children: children,
}
}
pub fn hash(&self) -> u64 {
self.hash
}
pub fn path(&self) -> &str {
&self.path
}
pub fn name(&self) -> &str {
&self.name
}
pub fn children(&self) -> &Vec<usize> {
&self.children
}
}
#[derive(Debug, Deserialize)]
struct FileAccount {
folders: String,
pub struct FileAccount {
root_folder: String,
format: String,
sent_folder: String,
threaded: bool,
}
impl FileAccount {
pub fn folder(&self) -> &str {
&self.root_folder
}
}
#[derive(Debug, Deserialize)]
struct FileSettings {
accounts: HashMap<String, FileAccount>,
@ -82,7 +51,7 @@ struct FileSettings {
#[derive(Debug, Clone)]
pub struct AccountSettings {
name: String,
pub folders: Vec<Folder>,
root_folder: String,
format: String,
pub sent_folder: String,
pub threaded: bool,
@ -95,6 +64,9 @@ impl AccountSettings {
pub fn name(&self) -> &str {
&self.name
}
pub fn root_folder(&self) -> &str {
&self.root_folder
}
}
#[derive(Debug, Clone, Default)]
@ -126,53 +98,20 @@ impl Settings {
let mut s: HashMap<String, AccountSettings> = HashMap::new();
for (id, x) in fs.accounts {
let mut folders = Vec::new();
fn recurse_folders<P: AsRef<Path>>(folders: &mut Vec<Folder>, p: P) -> Vec<usize> {
let mut children = Vec::new();
for mut f in fs::read_dir(p).unwrap() {
for f in f.iter_mut() {
{
let path = f.path();
if path.ends_with("cur")
|| path.ends_with("new")
|| path.ends_with("tmp")
{
continue;
}
if path.is_dir() {
let path_children = recurse_folders(folders, &path);
folders.push(Folder::new(
path.to_str().unwrap().to_string(),
path.file_name().unwrap().to_str().unwrap().to_string(),
path_children,
));
children.push(folders.len() - 1);
}
}
}
}
children
let format = x.format.to_lowercase();
let sent_folder = x.sent_folder;
let threaded = x.threaded;
let root_folder = x.root_folder;
let acc = AccountSettings {
name: id.clone(),
root_folder,
format,
sent_folder,
threaded,
};
let path = PathBuf::from(&x.folders);
let path_children = recurse_folders(&mut folders, &path);
if path.is_dir() {
folders.push(Folder::new(
path.to_str().unwrap().to_string(),
path.file_name().unwrap().to_str().unwrap().to_string(),
path_children,
));
}
//folders.sort_by(|a, b| b.name.cmp(&a.name));
s.insert(
id.clone(),
AccountSettings {
name: id.clone(),
folders: folders,
format: x.format.to_lowercase(),
sent_folder: x.sent_folder.clone(),
threaded: x.threaded,
},
);
s.insert(id, acc);
}
Settings {

View File

@ -23,11 +23,11 @@
* An error object for `melib`
*/
use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use std::io;
use std::result;
use std::borrow::Cow;
use nom;

View File

@ -24,7 +24,7 @@
*/
use async::*;
use conf::{AccountSettings, Folder};
use conf::AccountSettings;
use mailbox::backends::{Backends, RefreshEventConsumer};
use mailbox::*;
use std::ops::{Index, IndexMut};
@ -47,15 +47,15 @@ pub struct Account {
}
impl Account {
pub fn new(name: String, settings: AccountSettings, backends: &Backends) -> Self {
let sent_folder = settings
.folders
pub fn new(name: String, settings: AccountSettings, map: &Backends) -> Self {
let backend = map.get(settings.format())(&settings);
let ref_folders: Vec<Folder> = backend.folders();
let mut folders: Vec<Option<Result<Mailbox>>> = Vec::with_capacity(ref_folders.len());
let mut workers: Vec<Worker> = Vec::new();
let sent_folder = ref_folders
.iter()
.position(|x| *x.path() == settings.sent_folder);
let mut folders = Vec::with_capacity(settings.folders.len());
let mut workers = Vec::new();
let backend = backends.get(settings.format());
for f in &settings.folders {
.position(|x: &Folder| x.name() == settings.sent_folder);
for f in ref_folders {
folders.push(None);
let mut handle = backend.get(&f);
workers.push(Some(handle));
@ -73,14 +73,14 @@ impl Account {
}
}
pub fn watch(&self, r: RefreshEventConsumer) -> () {
self.backend.watch(r, &self.settings.folders[..]);
self.backend.watch(r).unwrap();
}
/* This doesn't represent the number of correctly parsed mailboxes though */
pub fn len(&self) -> usize {
self.folders.len()
}
pub fn list_folders(&self) -> Vec<Folder> {
self.settings.folders.clone()
self.backend.folders()
}
pub fn name(&self) -> &str {
&self.name
@ -89,7 +89,8 @@ impl Account {
&mut self.workers
}
fn load_mailbox(&mut self, index: usize, envelopes: Result<Vec<Envelope>>) -> () {
let folder = &self.settings.folders[index];
let folders = self.backend.folders();
let folder = &folders[index];
if self.sent_folder.is_some() {
let id = self.sent_folder.unwrap();
if id == index {
@ -105,7 +106,7 @@ impl Account {
)
}
};
let sent_path = &self.settings.folders[id];
let sent_path = &folders[id];
if sent[0].is_none() {
sent[0] = Some(Mailbox::new(sent_path, &None, envelopes.clone()));
}

View File

@ -19,10 +19,10 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*
use async::*;
use conf::Folder;
use error::Result;
use mailbox::backends::{MailBackend, RefreshEventConsumer};
use mailbox::backends::{MailBackend, RefreshEventConsumer, Folder};
use mailbox::email::Envelope;
/// `BackendOp` implementor for Imap
@ -35,7 +35,6 @@ impl ImapOp {
}
}
/*
impl BackendOp for ImapOp {
fn description(&self) -> String {
@ -53,7 +52,7 @@ impl BackendOp for ImapOp {
fn fetch_flags(&self) -> Flag {
unimplemented!();
}
}*/
}
/// Imap backend
#[derive(Debug)]
@ -72,4 +71,4 @@ impl ImapType {
pub fn new(_path: &str) -> Self {
ImapType {}
}
}
}*/

View File

@ -20,10 +20,11 @@
*/
use async::*;
use conf::Folder;
use conf::AccountSettings;
use error::{MeliError, Result};
use mailbox::backends::{
BackendOp, BackendOpGenerator, MailBackend, RefreshEvent, RefreshEventConsumer,
BackendFolder, BackendOp, BackendOpGenerator, Folder, MailBackend, RefreshEvent,
RefreshEventConsumer,
};
use mailbox::email::parser;
use mailbox::email::{Envelope, Flag};
@ -43,7 +44,7 @@ use memmap::{Mmap, Protection};
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::Hasher;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
/// `BackendOp` implementor for Maildir
#[derive(Debug, Default)]
@ -152,38 +153,42 @@ impl BackendOp for MaildirOp {
/// Maildir backend https://cr.yp.to/proto/maildir.html
#[derive(Debug)]
pub struct MaildirType {
folders: Vec<MaildirFolder>,
path: String,
idx: (usize, usize),
}
impl MailBackend for MaildirType {
fn folders(&self) -> Vec<Folder> {
self.folders.iter().map(|f| f.clone()).collect()
}
fn get(&self, folder: &Folder) -> Async<Result<Vec<Envelope>>> {
self.multicore(4, folder)
}
fn watch(&self, sender: RefreshEventConsumer, folders: &[Folder]) -> () {
let folders = folders.to_vec();
fn watch(&self, sender: RefreshEventConsumer) -> Result<()> {
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap();
for f in &self.folders {
if f.is_valid().is_err() {
continue;
}
eprintln!("watching {:?}", f);
let mut p = PathBuf::from(&f.path);
p.push("cur");
watcher.watch(&p, RecursiveMode::NonRecursive).unwrap();
p.pop();
p.push("new");
watcher.watch(&p, RecursiveMode::NonRecursive).unwrap();
}
thread::Builder::new()
.name("folder watch".to_string())
.spawn(move || {
let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap();
for f in folders {
if MaildirType::is_valid(&f).is_err() {
continue;
}
eprintln!("watching {}", f.path());
let mut p = PathBuf::from(&f.path());
p.push("cur");
watcher.watch(&p, RecursiveMode::NonRecursive).unwrap();
p.pop();
p.push("new");
watcher.watch(&p, RecursiveMode::NonRecursive).unwrap();
}
// Move `watcher` in the closure's scope so that it doesn't get dropped.
let _watcher = watcher;
loop {
match rx.recv() {
Ok(event) => match event {
DebouncedEvent::Create(mut pathbuf) | DebouncedEvent::Remove(mut pathbuf) => {
DebouncedEvent::Create(mut pathbuf)
| DebouncedEvent::Remove(mut pathbuf) => {
let path = if pathbuf.is_dir() {
if pathbuf.ends_with("cur") | pathbuf.ends_with("new") {
pathbuf.pop();
@ -207,45 +212,76 @@ impl MailBackend for MaildirType {
Err(e) => eprintln!("watch error: {:?}", e),
}
}
})
.unwrap();
})?;
Ok(())
}
}
impl MaildirType {
pub fn new(path: &str, idx: (usize, usize)) -> Self {
MaildirType {
path: path.to_string(),
idx: idx,
}
}
fn is_valid(f: &Folder) -> Result<()> {
let path = f.path();
let mut p = PathBuf::from(path);
for d in &["cur", "new", "tmp"] {
p.push(d);
if !p.is_dir() {
return Err(MeliError::new(format!(
"{} is not a valid maildir folder",
path
)));
pub fn new(f: &AccountSettings) -> Self {
let mut folders: Vec<MaildirFolder> = Vec::new();
fn recurse_folders<P: AsRef<Path>>(folders: &mut Vec<MaildirFolder>, p: P) -> Vec<usize> {
let mut children = Vec::new();
for mut f in fs::read_dir(p).unwrap() {
for f in f.iter_mut() {
{
let path = f.path();
if path.ends_with("cur") || path.ends_with("new") || path.ends_with("tmp") {
continue;
}
if path.is_dir() {
let path_children = recurse_folders(folders, &path);
if let Ok(f) = MaildirFolder::new(
path.to_str().unwrap().to_string(),
path.file_name().unwrap().to_str().unwrap().to_string(),
path_children,
) {
folders.push(f);
children.push(folders.len() - 1);
}
}
}
}
}
children
};
let path = PathBuf::from(f.root_folder());
let path_children = recurse_folders(&mut folders, &path);
if path.is_dir() {
if let Ok(f) = MaildirFolder::new(
path.to_str().unwrap().to_string(),
path.file_name().unwrap().to_str().unwrap().to_string(),
path_children,
) {
folders.push(f);
}
p.pop();
}
Ok(())
MaildirType {
folders,
path: f.root_folder().to_string(),
}
}
fn owned_folder_idx(&self, folder: &Folder) -> usize {
for (idx, f) in self.folders.iter().enumerate() {
if f.hash() == folder.hash() {
return idx;
}
}
unreachable!()
}
pub fn multicore(&self, cores: usize, folder: &Folder) -> Async<Result<Vec<Envelope>>> {
let mut w = AsyncBuilder::new();
let handle = {
let tx = w.tx();
// TODO: Avoid clone
let folder = folder.clone();
let folder: &MaildirFolder = &self.folders[self.owned_folder_idx(folder)];
let path = folder.path().to_string();
let name = format!("parsing {:?}", folder.name());
thread::Builder::new()
.name(format!("parsing {:?}", folder))
.name(name)
.spawn(move || {
MaildirType::is_valid(&folder)?;
let path = folder.path();
let mut path = PathBuf::from(path);
path.push("cur");
let iter = path.read_dir()?;
@ -310,3 +346,64 @@ impl MaildirType {
w.build(handle)
}
}
#[derive(Debug, Default)]
pub struct MaildirFolder {
hash: u64,
name: String,
path: String,
children: Vec<usize>,
}
impl MaildirFolder {
pub fn new(path: String, file_name: String, children: Vec<usize>) -> Result<Self> {
let mut h = DefaultHasher::new();
h.write(&path.as_bytes());
let ret = MaildirFolder {
hash: h.finish(),
name: file_name,
path: path,
children: children,
};
ret.is_valid()?;
Ok(ret)
}
pub fn path(&self) -> &str {
&self.path
}
fn is_valid(&self) -> Result<()> {
let path = self.path();
let mut p = PathBuf::from(path);
for d in &["cur", "new", "tmp"] {
p.push(d);
if !p.is_dir() {
return Err(MeliError::new(format!(
"{} is not a valid maildir folder",
path
)));
}
p.pop();
}
Ok(())
}
}
impl BackendFolder for MaildirFolder {
fn hash(&self) -> u64 {
self.hash
}
fn name(&self) -> &str {
&self.name
}
fn children(&self) -> &Vec<usize> {
&self.children
}
fn clone(&self) -> Folder {
Box::new(MaildirFolder {
hash: self.hash,
name: self.name.clone(),
path: self.path.clone(),
children: self.children.clone(),
})
}
}

View File

@ -23,10 +23,10 @@
* https://wiki2.dovecot.org/MailboxFormat/mbox
*/
/*
use async::*;
use conf::Folder;
use error::Result;
use mailbox::backends::{MailBackend, RefreshEventConsumer};
use mailbox::backends::{MailBackend, RefreshEventConsumer, Folder};
use mailbox::email::Envelope;
/// `BackendOp` implementor for Mbox
@ -39,7 +39,6 @@ impl MboxOp {
}
}
/*
impl BackendOp for MboxOp {
fn description(&self) -> String {
unimplemented!();
@ -60,7 +59,6 @@ impl BackendOp for MboxOp {
unimplemented!()
}
}
*/
/// Mbox backend
#[derive(Debug)]
@ -80,3 +78,4 @@ impl MboxType {
MboxType {}
}
}
*/

View File

@ -23,22 +23,25 @@ pub mod maildir;
pub mod mbox;
use async::*;
use conf::Folder;
use conf::AccountSettings;
use error::Result;
use mailbox::backends::imap::ImapType;
//use mailbox::backends::imap::ImapType;
//use mailbox::backends::mbox::MboxType;
use mailbox::backends::maildir::MaildirType;
use mailbox::backends::mbox::MboxType;
use mailbox::email::{Envelope, Flag};
use std::fmt;
use std::fmt::Debug;
extern crate fnv;
use self::fnv::FnvHashMap;
use std;
pub type BackendCreator = Box<Fn(&AccountSettings) -> Box<MailBackend>>;
/// A hashmap containing all available mail backends.
/// An abstraction over any available backends.
pub struct Backends {
map: FnvHashMap<std::string::String, Box<Fn() -> Box<MailBackend>>>,
map: FnvHashMap<std::string::String, Box<Fn() -> BackendCreator>>,
}
impl Backends {
@ -48,20 +51,20 @@ impl Backends {
};
b.register(
"maildir".to_string(),
Box::new(|| Box::new(MaildirType::new("", (0, 0)))),
Box::new(|| Box::new(|f| Box::new(MaildirType::new(f)))),
);
b.register("mbox".to_string(), Box::new(|| Box::new(MboxType::new(""))));
b.register("imap".to_string(), Box::new(|| Box::new(ImapType::new(""))));
//b.register("mbox".to_string(), Box::new(|| Box::new(MboxType::new(""))));
//b.register("imap".to_string(), Box::new(|| Box::new(ImapType::new(""))));
b
}
pub fn get(&self, key: &str) -> Box<MailBackend> {
pub fn get(&self, key: &str) -> BackendCreator {
if !self.map.contains_key(key) {
panic!("{} is not a valid mail backend", key);
}
self.map[key]()
}
pub fn register(&mut self, key: String, backend: Box<Fn() -> Box<MailBackend>>) -> () {
pub fn register(&mut self, key: String, backend: Box<Fn() -> BackendCreator>) -> () {
if self.map.contains_key(&key) {
panic!("{} is an already registered backend", key);
}
@ -90,8 +93,8 @@ impl RefreshEventConsumer {
}
pub trait MailBackend: ::std::fmt::Debug {
fn get(&self, folder: &Folder) -> Async<Result<Vec<Envelope>>>;
fn watch(&self, sender: RefreshEventConsumer, folders: &[Folder]) -> ();
//fn new(folders: &Vec<String>) -> Box<Self>;
fn watch(&self, sender: RefreshEventConsumer) -> Result<()>;
fn folders(&self) -> Vec<Folder>;
//login function
}
@ -168,3 +171,37 @@ impl fmt::Debug for BackendOpGenerator {
write!(f, "BackendOpGenerator: {}", op.description())
}
}
pub trait BackendFolder: Debug {
fn hash(&self) -> u64;
fn name(&self) -> &str;
fn clone(&self) -> Folder;
fn children(&self) -> &Vec<usize>;
}
#[derive(Debug)]
struct DummyFolder {
v: Vec<usize>,
}
impl BackendFolder for DummyFolder {
fn hash(&self) -> u64 {
0
}
fn name(&self) -> &str {
""
}
fn clone(&self) -> Folder {
folder_default()
}
fn children(&self) -> &Vec<usize> {
&self.v
}
}
pub fn folder_default() -> Folder {
Box::new(DummyFolder {
v: Vec::with_capacity(0),
})
}
pub type Folder = Box<BackendFolder>;

View File

@ -20,7 +20,7 @@ impl Default for Charset {
}
}
impl<'a> From<&'a[u8]> for Charset {
impl<'a> From<&'a [u8]> for Charset {
fn from(b: &'a [u8]) -> Self {
// TODO: Case insensitivity
match b {
@ -69,9 +69,10 @@ pub enum ContentType {
impl Default for ContentType {
fn default() -> Self {
ContentType::Text{ charset: Charset::UTF8 }
ContentType::Text {
charset: Charset::UTF8,
}
}
}
impl Display for ContentType {
@ -128,4 +129,3 @@ pub enum ContentTransferEncoding {
QuotedPrintable,
Other { tag: Vec<u8> },
}

View File

@ -18,10 +18,10 @@
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::fmt;
use std::str;
use data_encoding::BASE64_MIME;
use mailbox::email::parser;
use std::fmt;
use std::str;
pub use mailbox::email::attachment_types::*;
@ -44,7 +44,14 @@ impl fmt::Debug for AttachmentType {
match self {
AttachmentType::Data { .. } => write!(f, "AttachmentType::Data {{ .. }}"),
AttachmentType::Text { .. } => write!(f, "AttachmentType::Text {{ .. }}"),
AttachmentType::Multipart { of_type, subattachments } => write!(f, "AttachmentType::Multipart {{ of_type: {:?},\nsubattachments: {:?} }}", of_type, subattachments),
AttachmentType::Multipart {
of_type,
subattachments,
} => write!(
f,
"AttachmentType::Multipart {{ of_type: {:?},\nsubattachments: {:?} }}",
of_type, subattachments
),
}
}
}
@ -96,7 +103,7 @@ impl fmt::Display for AttachmentType {
impl AttachmentBuilder {
pub fn new(content: &[u8]) -> Self {
AttachmentBuilder {
content_type: (Default::default() , ContentSubType::Plain),
content_type: (Default::default(), ContentSubType::Plain),
content_transfer_encoding: ContentTransferEncoding::_7Bit,
raw: content.to_vec(),
}
@ -124,7 +131,9 @@ impl AttachmentBuilder {
self.content_type.0 = Default::default();
for (n, v) in params {
if n.eq_ignore_ascii_case(b"charset") {
self.content_type.0 = ContentType::Text { charset: Charset::from(v) };
self.content_type.0 = ContentType::Text {
charset: Charset::from(v),
};
break;
}
}
@ -168,7 +177,7 @@ impl AttachmentBuilder {
fn decode(&self) -> Vec<u8> {
// TODO merge this and standalone decode() function
let charset = match self.content_type.0 {
ContentType::Text{ charset: c } => c,
ContentType::Text { charset: c } => c,
_ => Default::default(),
};
@ -180,13 +189,17 @@ impl AttachmentBuilder {
ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_bytes(&self.raw)
.to_full_result()
.unwrap(),
ContentTransferEncoding::_7Bit
| ContentTransferEncoding::_8Bit
| ContentTransferEncoding::Other { .. } => self.raw.to_vec(),
ContentTransferEncoding::_7Bit
| ContentTransferEncoding::_8Bit
| ContentTransferEncoding::Other { .. } => self.raw.to_vec(),
};
let decoded_result = parser::decode_charset(&bytes, charset);
decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| &self.raw).to_vec()
decoded_result
.as_ref()
.map(|v| v.as_bytes())
.unwrap_or_else(|_| &self.raw)
.to_vec()
}
pub fn build(self) -> Attachment {
let attachment_type = match self.content_type.0 {
@ -197,7 +210,9 @@ impl AttachmentBuilder {
let multipart_type = match self.content_type.1 {
ContentSubType::Other { ref tag } => match &tag[..] {
b"mixed" | b"Mixed" | b"MIXED" => MultipartType::Mixed,
b"alternative" | b"Alternative" | b"ALTERNATIVE" => MultipartType::Alternative,
b"alternative" | b"Alternative" | b"ALTERNATIVE" => {
MultipartType::Alternative
}
b"digest" | b"Digest" | b"DIGEST" => MultipartType::Digest,
_ => MultipartType::Unsupported { tag: tag.clone() },
},
@ -261,14 +276,15 @@ impl AttachmentBuilder {
}
}
impl fmt::Display for Attachment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.attachment_type {
AttachmentType::Data { .. } => {
write!(f, "Data attachment of type {}", self.mime_type())
}
AttachmentType::Text { .. } => write!(f, "Text attachment of type {}", self.mime_type()),
AttachmentType::Text { .. } => {
write!(f, "Text attachment of type {}", self.mime_type())
}
AttachmentType::Multipart {
of_type: ref multipart_type,
subattachments: ref sub_att_vec,
@ -276,10 +292,16 @@ impl fmt::Display for Attachment {
write!(
f,
"{} attachment with {} subs",
self.mime_type(), sub_att_vec.len()
self.mime_type(),
sub_att_vec.len()
)
} else {
write!(f, "{} attachment with {} subs", self.mime_type(), sub_att_vec.len())
write!(
f,
"{} attachment with {} subs",
self.mime_type(),
sub_att_vec.len()
)
},
}
}
@ -340,7 +362,7 @@ impl Attachment {
for a in sub_att_vec {
count_recursive(a, ret);
}
},
}
}
}
@ -374,7 +396,7 @@ fn decode_rec_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<
return filter(a);
}
match a.attachment_type {
AttachmentType::Data { .. } => { Vec::new()},
AttachmentType::Data { .. } => Vec::new(),
AttachmentType::Text { .. } => decode_helper(a, filter),
AttachmentType::Multipart {
of_type: ref multipart_type,
@ -405,7 +427,7 @@ fn decode_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<u8>>
}
let charset = match a.content_type.0 {
ContentType::Text{ charset: c } => c,
ContentType::Text { charset: c } => c,
_ => Default::default(),
};
@ -424,7 +446,11 @@ fn decode_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<u8>>
if a.content_type().0.is_text() {
let decoded_result = parser::decode_charset(&bytes, charset);
decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| a.bytes()).to_vec()
decoded_result
.as_ref()
.map(|v| v.as_bytes())
.unwrap_or_else(|_| a.bytes())
.to_vec()
} else {
bytes.to_vec()
}

View File

@ -203,6 +203,33 @@ bitflags! {
}
}
#[derive(Debug, Clone, Default)]
pub struct EnvelopeBuilder {
from: Option<Vec<Address>>,
to: Vec<Address>,
body: Option<Attachment>,
in_reply_to: Option<MessageID>,
flags: Flag,
}
impl EnvelopeBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn build(self) -> Envelope {
unimplemented!();
/*
* 1. Check for date. Default is now
* 2.
Envelope {
*/
}
}
/// `Envelope` represents all the data of an email we need to know.
///
/// Attachments (the email's body) is parsed on demand with `body`.

View File

@ -166,7 +166,7 @@ named!(pub attachment<(std::vec::Vec<(&[u8], &[u8])>, &[u8])>,
/* TODO: make a map of encodings and decoding functions so that they can be reused and easily
* extended */
use encoding::all::{ISO_8859_1, ISO_8859_2, ISO_8859_7, WINDOWS_1253, WINDOWS_1252, GBK};
use encoding::all::{ISO_8859_1, ISO_8859_2, ISO_8859_7, WINDOWS_1252, WINDOWS_1253, GBK};
fn encoded_word(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
if input.len() < 5 {
@ -266,33 +266,15 @@ fn encoded_word(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
pub fn decode_charset(s: &[u8], charset: Charset) -> Result<String> {
match charset {
Charset::UTF8 | Charset::Ascii => {
Ok(String::from_utf8(s.to_vec()).unwrap())
}
Charset::ISO8859_7 => {
Ok(ISO_8859_7.decode(s, DecoderTrap::Strict)?)
}
Charset::ISO8859_1 => {
Ok(ISO_8859_1.decode(s, DecoderTrap::Strict)?)
}
Charset::ISO8859_2 => {
Ok(ISO_8859_2.decode(s, DecoderTrap::Strict)?)
}
Charset::GBK => {
Ok(GBK.decode(s, DecoderTrap::Strict)?)
}
Charset::Windows1252 => {
Ok(WINDOWS_1252.decode(s, DecoderTrap::Strict)?)
},
Charset::Windows1253 => {
Ok(WINDOWS_1253.decode(s, DecoderTrap::Strict)?)
},
Charset::GB2312 => {
unimplemented!()
},
Charset::UTF16 => {
unimplemented!()
},
Charset::UTF8 | Charset::Ascii => Ok(String::from_utf8(s.to_vec()).unwrap()),
Charset::ISO8859_7 => Ok(ISO_8859_7.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_1 => Ok(ISO_8859_1.decode(s, DecoderTrap::Strict)?),
Charset::ISO8859_2 => Ok(ISO_8859_2.decode(s, DecoderTrap::Strict)?),
Charset::GBK => Ok(GBK.decode(s, DecoderTrap::Strict)?),
Charset::Windows1252 => Ok(WINDOWS_1252.decode(s, DecoderTrap::Strict)?),
Charset::Windows1253 => Ok(WINDOWS_1253.decode(s, DecoderTrap::Strict)?),
Charset::GB2312 => unimplemented!(),
Charset::UTF16 => unimplemented!(),
}
}
@ -322,12 +304,10 @@ named!(
pub quoted_printable_bytes<Vec<u8>>,
many0!(alt_complete!(
preceded!(quoted_printable_soft_break, quoted_printable_byte) |
preceded!(quoted_printable_soft_break, le_u8)
| quoted_printable_byte | le_u8
preceded!(quoted_printable_soft_break, le_u8) | quoted_printable_byte | le_u8
))
);
named!(
encoded_word_list<Vec<u8>>,
ws!(do_parse!(
@ -397,7 +377,10 @@ fn display_addr(input: &[u8]) -> IResult<&[u8], Address> {
IResult::Done(rest, raw) => {
display_name.length = raw.find(b"<").unwrap().saturating_sub(1);
address_spec.offset = display_name.length + 2;
address_spec.length = raw.len().saturating_sub(display_name.length).saturating_sub(3);
address_spec.length = raw
.len()
.saturating_sub(display_name.length)
.saturating_sub(3);
IResult::Done(
rest,
Address::Mailbox(MailboxAddress {

View File

@ -30,18 +30,16 @@ pub use self::email::*;
/* Mail backends. Currently only maildir is supported */
pub mod backends;
use error::Result;
use mailbox::backends::MailBackend;
use mailbox::backends::{folder_default, Folder, MailBackend};
pub mod accounts;
pub use mailbox::accounts::Account;
pub mod thread;
pub use mailbox::thread::{build_threads, Container};
use conf::Folder;
use std::option::Option;
/// `Mailbox` represents a folder of mail.
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct Mailbox {
pub folder: Folder,
pub collection: Vec<Envelope>,
@ -49,10 +47,21 @@ pub struct Mailbox {
pub threads: Vec<Container>,
}
impl Clone for Mailbox {
fn clone(&self) -> Self {
Mailbox {
folder: self.folder.clone(),
collection: self.collection.clone(),
threaded_collection: self.threaded_collection.clone(),
threads: self.threads.clone(),
}
}
}
impl Mailbox {
pub fn new_dummy() -> Self {
Mailbox {
folder: Folder::default(),
folder: folder_default(),
collection: Vec::with_capacity(0),
threaded_collection: Vec::with_capacity(0),
threads: Vec::with_capacity(0),
@ -67,7 +76,7 @@ impl Mailbox {
collection.sort_by(|a, b| a.date().cmp(&b.date()));
let (threads, threaded_collection) = build_threads(&mut collection, sent_folder);
Ok(Mailbox {
folder: folder.clone(),
folder: (*folder).clone(),
collection: collection,
threads: threads,
threaded_collection: threaded_collection,

View File

@ -59,9 +59,7 @@ fn make_input_thread(
sx.send(ThreadEvent::Input(k));
},
|| {
sx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode(
UIMode::Fork,
)));
sx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode(UIMode::Fork)));
},
&rx,
)
@ -104,9 +102,10 @@ fn main() {
let b = Entity {
component: Box::new(listing),
};
let window = Entity {
component: Box::new(VSplit::new(menu, b, 90, true)),
};
let mut tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true))]));
tabs.add_component(Box::new(Composer {}));
let window = Entity { component: tabs };
let status_bar = Entity {
component: Box::new(StatusBar::new(window)),
};

View File

@ -1,22 +1,21 @@
#![cfg(feature = "python")]
use pyo3::prelude::*;
#[pymodinit(pythonmeli)]
fn pythonmeli(py: Python, m: &PyModule) -> PyResult<()> {
// pyo3 aware function. All of our python interface could be declared in a separate module.
// Note that the `#[pyfn()]` annotation automatically converts the arguments from
// Python objects to Rust values; and the Rust return value back into a Python object.
#[pyfn(m, "sum_as_string")]
fn sum_as_string_py(_py: Python, a:i64, b:i64) -> PyResult<String> {
let out = sum_as_string(a, b);
Ok(out)
fn sum_as_string_py(_py: Python, a: i64, b: i64) -> PyResult<String> {
let out = sum_as_string(a, b);
Ok(out)
}
Ok(())
}
// logic implemented as a normal rust function
fn sum_as_string(a:i64, b:i64) -> String {
fn sum_as_string(a: i64, b: i64) -> String {
format!("{}", a + b).to_string()
}

View File

@ -0,0 +1,45 @@
/*
* 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/>.
*/
use super::*;
pub struct Composer {}
impl fmt::Display for Composer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
write!(f, "compose")
}
}
impl Component for Composer {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
clear_area(grid, area);
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, event: &UIEvent, context: &mut Context) {}
fn is_dirty(&self) -> bool {
true
}
fn set_dirty(&mut self) {}
}

View File

@ -0,0 +1,679 @@
/*
* 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/>.
*/
use super::*;
const MAX_COLS: usize = 500;
/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the thread's content in a
/// `ThreadView`.
pub struct CompactMailListing {
/// (x, y, z): x is accounts, y is folders, z is index inside a folder.
cursor_pos: (usize, usize, usize),
new_cursor_pos: (usize, usize, usize),
length: usize,
sort: (SortField, SortOrder),
//subsort: (SortField, SortOrder),
/// Cache current view.
content: CellBuffer,
/// If we must redraw on next redraw event
dirty: bool,
/// If `self.view` exists or not.
unfocused: bool,
view: Option<ThreadView>,
}
impl Default for CompactMailListing {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for CompactMailListing {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "mail")
}
}
impl CompactMailListing {
pub fn new() -> Self {
let content = CellBuffer::new(0, 0, Cell::with_char(' '));
CompactMailListing {
cursor_pos: (0, 1, 0),
new_cursor_pos: (0, 0, 0),
length: 0,
sort: (SortField::Date, SortOrder::Desc),
//subsort: (SortField::Date, SortOrder::Asc),
content: content,
dirty: true,
unfocused: false,
view: None,
}
}
/// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context) {
self.dirty = true;
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
// Inform State that we changed the current folder view.
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::RefreshMailbox((self.cursor_pos.0, self.cursor_pos.1)),
});
// Get mailbox as a reference.
//
loop {
// TODO: Show progress visually
if let Ok(()) = context.accounts[self.cursor_pos.0].status(self.cursor_pos.1) {
break;
}
}
let mailbox = &mut context.accounts[self.cursor_pos.0][self.cursor_pos.1]
.as_ref()
.unwrap();
self.length = mailbox.threads.len();
let mut content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
if self.length == 0 {
write_string_to_grid(
&format!("Folder `{}` is empty.", mailbox.folder.name()),
&mut content,
Color::Default,
Color::Default,
((0, 0), (MAX_COLS - 1, 0)),
true,
);
self.content = content;
return;
}
// TODO: Fix the threaded hell and refactor stuff into seperate functions and/or modules.
let mut indentations: Vec<bool> = Vec::with_capacity(6);
let mut thread_idx = 0; // needed for alternate thread colors
/* Draw threaded view. */
let mut local_collection: Vec<usize> = mailbox.threaded_collection.clone();
let threads: &Vec<Container> = &mailbox.threads;
local_collection.sort_by(|a, b| match self.sort {
(SortField::Date, SortOrder::Desc) => {
mailbox.thread(*b).date().cmp(&mailbox.thread(*a).date())
}
(SortField::Date, SortOrder::Asc) => {
mailbox.thread(*a).date().cmp(&mailbox.thread(*b).date())
}
(SortField::Subject, SortOrder::Desc) => {
let a = mailbox.thread(*a);
let b = mailbox.thread(*b);
let ma = &mailbox.collection[*a.message().as_ref().unwrap()];
let mb = &mailbox.collection[*b.message().as_ref().unwrap()];
ma.subject().cmp(&mb.subject())
}
(SortField::Subject, SortOrder::Asc) => {
let a = mailbox.thread(*a);
let b = mailbox.thread(*b);
let ma = &mailbox.collection[*a.message().as_ref().unwrap()];
let mb = &mailbox.collection[*b.message().as_ref().unwrap()];
mb.subject().cmp(&ma.subject())
}
});
let mut iter = local_collection.iter().enumerate().peekable();
let len = mailbox
.threaded_collection
.len()
.to_string()
.chars()
.count();
/* This is just a desugared for loop so that we can use .peek() */
while let Some((idx, i)) = iter.next() {
let container = &threads[*i];
let indentation = container.indentation();
if indentation == 0 {
thread_idx += 1;
}
assert!(container.has_message());
match iter.peek() {
Some(&(_, x)) if threads[*x].indentation() == indentation => {
indentations.pop();
indentations.push(true);
}
_ => {
indentations.pop();
indentations.push(false);
}
}
if container.has_sibling() {
indentations.pop();
indentations.push(true);
}
let envelope: &Envelope = &mailbox.collection[container.message().unwrap()];
let fg_color = if !envelope.is_seen() {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if !envelope.is_seen() {
Color::Byte(251)
} else if thread_idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
};
let (x, _) = write_string_to_grid(
&CompactMailListing::make_thread_entry(
envelope,
idx,
indentation,
container,
&indentations,
len,
),
&mut content,
fg_color,
bg_color,
((0, idx), (MAX_COLS - 1, idx)),
false,
);
for x in x..MAX_COLS {
content[(x, idx)].set_ch(' ');
content[(x, idx)].set_bg(bg_color);
}
match iter.peek() {
Some(&(_, x)) if threads[*x].indentation() > indentation => {
indentations.push(false);
}
Some(&(_, x)) if threads[*x].indentation() < indentation => {
for _ in 0..(indentation - threads[*x].indentation()) {
indentations.pop();
}
}
_ => {}
}
}
self.content = content;
}
fn highlight_line_self(&mut self, idx: usize, context: &Context) {
let threaded = context.accounts[self.cursor_pos.0]
.runtime_settings
.threaded;
let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1]
.as_ref()
.unwrap();
let envelope: &Envelope = if threaded {
let i = mailbox.threaded_mail(idx);
&mailbox.collection[i]
} else {
&mailbox.collection[idx]
};
let fg_color = if !envelope.is_seen() {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if !envelope.is_seen() {
Color::Byte(251)
} else if idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
};
change_colors(
&mut self.content,
((0, idx), (MAX_COLS - 1, idx)),
fg_color,
bg_color,
);
}
fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
let threaded = context.accounts[self.cursor_pos.0]
.runtime_settings
.threaded;
let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1]
.as_ref()
.unwrap();
let envelope: &Envelope = if threaded {
let i = mailbox.threaded_mail(idx);
&mailbox.collection[i]
} else {
&mailbox.collection[idx]
};
let fg_color = if !envelope.is_seen() {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if self.cursor_pos.2 == idx {
Color::Byte(246)
} else if !envelope.is_seen() {
Color::Byte(251)
} else if idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
};
change_colors(grid, area, fg_color, bg_color);
}
/// Draw the list of `Envelope`s.
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.cursor_pos.1 != self.new_cursor_pos.1 {
self.refresh_mailbox(context);
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.length == 0 {
clear_area(grid, area);
copy_area(grid, &self.content, area, ((0, 0), (MAX_COLS - 1, 0)));
context.dirty_areas.push_back(area);
return;
}
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
let prev_page_no = (self.cursor_pos.2).wrapping_div(rows);
let page_no = (self.new_cursor_pos.2).wrapping_div(rows);
let top_idx = page_no * rows;
/* If cursor position has changed, remove the highlight from the previous position and
* apply it in the new one. */
if self.cursor_pos.2 != self.new_cursor_pos.2 && prev_page_no == page_no {
let old_cursor_pos = self.cursor_pos;
self.cursor_pos = self.new_cursor_pos;
for idx in &[old_cursor_pos.2, self.new_cursor_pos.2] {
if *idx >= self.length {
continue; //bounds check
}
let new_area = (
set_y(upper_left, get_y(upper_left) + (*idx % rows)),
set_y(bottom_right, get_y(upper_left) + (*idx % rows)),
);
self.highlight_line(grid, new_area, *idx, context);
context.dirty_areas.push_back(new_area);
}
return;
} else if self.cursor_pos != self.new_cursor_pos {
self.cursor_pos = self.new_cursor_pos;
}
/* Page_no has changed, so draw new page */
copy_area(
grid,
&self.content,
area,
((0, top_idx), (MAX_COLS - 1, self.length)),
);
self.highlight_line(
grid,
(
set_y(upper_left, get_y(upper_left) + (self.cursor_pos.2 % rows)),
set_y(bottom_right, get_y(upper_left) + (self.cursor_pos.2 % rows)),
),
self.cursor_pos.2,
context,
);
context.dirty_areas.push_back(area);
}
fn make_thread_entry(
envelope: &Envelope,
idx: usize,
indent: usize,
container: &Container,
indentations: &[bool],
idx_width: usize,
) -> String {
let has_sibling = container.has_sibling();
let has_parent = container.has_parent();
let show_subject = container.show_subject();
let mut s = format!(
"{}{}{} ",
idx,
" ".repeat(idx_width + 2 - (idx.to_string().chars().count())),
CompactMailListing::format_date(&envelope)
);
for i in 0..indent {
if indentations.len() > i && indentations[i] {
s.push('│');
} else {
s.push(' ');
}
if i > 0 {
s.push(' ');
}
}
if indent > 0 {
if has_sibling && has_parent {
s.push('├');
} else if has_sibling {
s.push('┬');
} else {
s.push('└');
}
s.push('─');
s.push('>');
}
if show_subject {
s.push_str(&format!("{:.85}", envelope.subject()));
}
let attach_count = envelope.body().count_attachments();
if attach_count > 1 {
s.push_str(&format!(" {}", attach_count - 1));
}
s
}
fn format_date(envelope: &Envelope) -> String {
let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(envelope.date());
let now: std::time::Duration = std::time::SystemTime::now().duration_since(d).unwrap();
match now.as_secs() {
n if n < 10 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(8)),
n if n < 24 * 60 * 60 => format!("{} hours ago{}", n / (60 * 60), " ".repeat(7)),
n if n < 4 * 24 * 60 * 60 => {
format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9))
}
_ => envelope.datetime().format("%Y-%m-%d %H:%M:%S").to_string(),
}
}
}
impl Component for CompactMailListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.unfocused {
if !self.is_dirty() {
return;
}
self.dirty = false;
/* Draw the entire list */
self.draw_list(grid, area, context);
} else {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.length == 0 && self.dirty {
clear_area(grid, area);
context.dirty_areas.push_back(area);
}
/* Render the mail body in a pager, basically copy what HSplit does */
let total_rows = get_y(bottom_right) - get_y(upper_left);
let pager_ratio = context.runtime_settings.pager.pager_ratio;
let bottom_entity_rows = (pager_ratio * total_rows) / 100;
if bottom_entity_rows > total_rows {
clear_area(grid, area);
context.dirty_areas.push_back(area);
return;
}
/* Mark message as read */
let idx = self.cursor_pos.2;
let must_highlight = {
if self.length == 0 {
false
} else {
let threaded = context.accounts[self.cursor_pos.0]
.runtime_settings
.threaded;
let mailbox = &mut context.accounts[self.cursor_pos.0][self.cursor_pos.1]
.as_mut()
.unwrap();
let envelope: &mut Envelope = if threaded {
let i = mailbox.threaded_mail(idx);
&mut mailbox.collection[i]
} else {
&mut mailbox.collection[idx]
};
if !envelope.is_seen() {
envelope.set_seen().unwrap();
true
} else {
false
}
}
};
if must_highlight {
self.highlight_line_self(idx, context);
}
let mid = get_y(upper_left) + total_rows - bottom_entity_rows;
self.draw_list(
grid,
(
upper_left,
(get_x(bottom_right), get_y(upper_left) + mid - 1),
),
context,
);
if self.length == 0 {
self.dirty = false;
return;
}
{
/* TODO: Move the box drawing business in separate functions */
if get_x(upper_left) > 0 && grid[(get_x(upper_left) - 1, mid)].ch() == VERT_BOUNDARY
{
grid[(get_x(upper_left) - 1, mid)].set_ch(LIGHT_VERTICAL_AND_RIGHT);
}
for i in get_x(upper_left)..=get_x(bottom_right) {
grid[(i, mid)].set_ch('─');
}
context
.dirty_areas
.push_back((set_y(upper_left, mid), set_y(bottom_right, mid)));
}
// TODO: Make headers view configurable
if !self.dirty {
if let Some(v) = self.view.as_mut() {
v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context);
}
return;
}
self.view = Some(ThreadView::new(Vec::new()));
self.view.as_mut().unwrap().draw(
grid,
(set_y(upper_left, mid + 1), bottom_right),
context,
);
self.dirty = false;
}
}
fn process_event(&mut self, event: &UIEvent, context: &mut Context) {
match event.event_type {
UIEventType::Input(Key::Up) => {
if self.cursor_pos.2 > 0 {
self.new_cursor_pos.2 -= 1;
self.dirty = true;
}
}
UIEventType::Input(Key::Down) => {
if self.length > 0 && self.new_cursor_pos.2 < self.length - 1 {
self.new_cursor_pos.2 += 1;
self.dirty = true;
}
}
UIEventType::Input(Key::Char('\n')) if !self.unfocused => {
self.unfocused = true;
self.dirty = true;
}
UIEventType::Input(Key::Char('m')) if !self.unfocused => {
use std::process::{Command, Stdio};
/* Kill input thread so that spawned command can be sole receiver of stdin */
{
/* I tried thread::park() here but for some reason it never blocked and always
* returned. Spinlocks are also useless because you have to keep the mutex
* guard alive til the child process exits, which requires some effort.
*
* The only problem with this approach is tht the user has to send some input
* in order for the input-thread to wake up and realise it should kill itself.
*
* I tried writing to stdin/tty manually but for some reason rustty didn't
* acknowledge it.
*/
/*
* tx sends to input-thread and it kills itself.
*/
let tx = context.input_thread();
tx.send(true);
}
let mut f = create_temp_file(&new_draft(context), None);
//let mut f = Box::new(std::fs::File::create(&dir).unwrap());
// TODO: check exit status
let mut output = Command::new("vim")
.arg("+/^$")
.arg(&f.path())
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.spawn()
.expect("failed to execute process");
/*
* Main loop will wait on children and when they reap them the loop spawns a new
* input-thread
*/
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::Fork(ForkType::NewDraft(f, output)),
});
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::ChangeMode(UIMode::Fork),
});
return;
}
UIEventType::Input(Key::Char('i')) if self.unfocused => {
self.unfocused = false;
self.dirty = true;
self.view = None;
}
UIEventType::Input(Key::Char(k @ 'J')) | UIEventType::Input(Key::Char(k @ 'K')) => {
let folder_length = context.accounts[self.cursor_pos.0].len();
let accounts_length = context.accounts.len();
match k {
'J' if folder_length > 0 => {
if self.new_cursor_pos.1 < folder_length - 1 {
self.new_cursor_pos.1 = self.cursor_pos.1 + 1;
self.dirty = true;
self.refresh_mailbox(context);
} else if accounts_length > 0 && self.new_cursor_pos.0 < accounts_length - 1
{
self.new_cursor_pos.0 = self.cursor_pos.0 + 1;
self.new_cursor_pos.1 = 0;
self.dirty = true;
self.refresh_mailbox(context);
}
}
'K' => {
if self.cursor_pos.1 > 0 {
self.new_cursor_pos.1 = self.cursor_pos.1 - 1;
self.dirty = true;
self.refresh_mailbox(context);
} else if self.cursor_pos.0 > 0 {
self.new_cursor_pos.0 = self.cursor_pos.0 - 1;
self.new_cursor_pos.1 = 0;
self.dirty = true;
self.refresh_mailbox(context);
}
}
_ => {}
}
}
UIEventType::Input(Key::Char(k @ 'h')) | UIEventType::Input(Key::Char(k @ 'l')) => {
let accounts_length = context.accounts.len();
match k {
'h' if accounts_length > 0 && self.new_cursor_pos.0 < accounts_length - 1 => {
self.new_cursor_pos.0 = self.cursor_pos.0 + 1;
self.new_cursor_pos.1 = 0;
self.dirty = true;
self.refresh_mailbox(context);
}
'l' if self.cursor_pos.0 > 0 => {
self.new_cursor_pos.0 = self.cursor_pos.0 - 1;
self.new_cursor_pos.1 = 0;
self.dirty = true;
self.refresh_mailbox(context);
}
_ => {}
}
}
UIEventType::RefreshMailbox(_) => {
self.dirty = true;
self.view = None;
}
UIEventType::MailboxUpdate((ref idxa, ref idxf)) => {
if *idxa == self.new_cursor_pos.1 && *idxf == self.new_cursor_pos.0 {
self.refresh_mailbox(context);
self.dirty = true;
}
}
UIEventType::ChangeMode(UIMode::Normal) => {
self.dirty = true;
}
UIEventType::Resize => {
self.dirty = true;
}
UIEventType::Action(ref action) => match action {
Action::MailListing(MailListingAction::ToggleThreaded) => {
context.accounts[self.cursor_pos.0]
.runtime_settings
.threaded = !context.accounts[self.cursor_pos.0]
.runtime_settings
.threaded;
self.refresh_mailbox(context);
self.dirty = true;
return;
}
Action::ViewMailbox(idx) => {
self.new_cursor_pos.1 = *idx;
self.dirty = true;
self.refresh_mailbox(context);
return;
}
Action::Sort(field, order) => {
self.sort = (field.clone(), order.clone());
self.dirty = true;
self.refresh_mailbox(context);
return;
}
_ => {}
},
_ => {}
}
if let Some(ref mut v) = self.view {
v.process_event(event, context);
}
}
fn is_dirty(&self) -> bool {
self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
}
fn set_dirty(&mut self) {
self.dirty = true;
}
}

View File

@ -21,6 +21,9 @@
use super::*;
mod compact;
pub use self::compact::*;
const MAX_COLS: usize = 500;
/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a
@ -31,7 +34,7 @@ pub struct MailListing {
new_cursor_pos: (usize, usize, usize),
length: usize,
sort: (SortField, SortOrder),
subsort: (SortField, SortOrder),
//subsort: (SortField, SortOrder),
/// Cache current view.
content: CellBuffer,
/// If we must redraw on next redraw event
@ -47,6 +50,12 @@ impl Default for MailListing {
}
}
impl fmt::Display for MailListing {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "mail")
}
}
impl MailListing {
/// Helper function to format entry strings for MailListing */
/* TODO: Make this configurable */
@ -66,7 +75,7 @@ impl MailListing {
new_cursor_pos: (0, 0, 0),
length: 0,
sort: (SortField::Date, SortOrder::Desc),
subsort: (SortField::Date, SortOrder::Asc),
//subsort: (SortField::Date, SortOrder::Asc),
content: content,
dirty: true,
unfocused: false,
@ -728,4 +737,7 @@ impl Component for MailListing {
fn is_dirty(&self) -> bool {
self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
}
fn set_dirty(&mut self) {
self.dirty = true;
}
}

View File

@ -22,11 +22,14 @@
/*! Entities that handle Mail specific functions.
*/
use super::*;
use melib::backends::Folder;
pub mod listing;
pub mod view;
pub use listing::*;
pub mod view;
pub use view::*;
mod compose;
pub use self::compose::*;
#[derive(Debug)]
struct AccountMenuEntry {
@ -43,6 +46,13 @@ pub struct AccountMenu {
cursor: Option<(usize, usize)>,
}
impl fmt::Display for AccountMenu {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
write!(f, "menu")
}
}
impl AccountMenu {
pub fn new(accounts: &[Account]) -> Self {
let accounts = accounts
@ -53,8 +63,10 @@ impl AccountMenu {
index: i,
entries: {
let mut entries = Vec::with_capacity(a.len());
for (idx, acc) in a.list_folders().iter().enumerate() {
entries.push((idx, acc.clone()));
let mut idx = 0;
for acc in a.list_folders() {
entries.push((idx, acc));
idx += 1;
}
entries
},
@ -187,12 +199,7 @@ impl AccountMenu {
);
if highlight && idx > 1 && self.cursor.unwrap().1 == idx - 2 {
change_colors(
grid,
((x, y), (get_x(bottom_right), y)),
color_fg,
color_bg,
);
change_colors(grid, ((x, y), (get_x(bottom_right), y)), color_fg, color_bg);
} else {
change_colors(grid, ((x, y), set_y(bottom_right, y)), color_fg, color_bg);
}
@ -240,4 +247,7 @@ impl Component for AccountMenu {
fn is_dirty(&self) -> bool {
self.dirty
}
fn set_dirty(&mut self) {
self.dirty = true;
}
}

View File

@ -54,6 +54,13 @@ impl HtmlView {
}
}
impl fmt::Display for HtmlView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
write!(f, "view")
}
}
impl Component for HtmlView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
self.pager.draw(grid, area, context);
@ -90,4 +97,5 @@ impl Component for HtmlView {
fn is_dirty(&self) -> bool {
self.pager.is_dirty()
}
fn set_dirty(&mut self) {}
}

View File

@ -24,8 +24,9 @@ use linkify::{Link, LinkFinder};
use std::process::{Command, Stdio};
mod html;
pub use self::html::*;
mod thread;
pub use self::thread::*;
use mime_apps::query_default_app;
@ -59,6 +60,13 @@ pub struct MailView {
cmd_buf: String,
}
impl fmt::Display for MailView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
write!(f, "view mail")
}
}
impl MailView {
pub fn new(
coordinates: (usize, usize, usize),
@ -80,23 +88,34 @@ impl MailView {
fn attachment_to_text(&self, body: Attachment) -> String {
let finder = LinkFinder::new();
let body_text = if body.content_type().0.is_text() && body.content_type().1.is_html() {
let mut s = String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n");
let mut s =
String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n");
s.extend(
String::from_utf8_lossy(&decode(&body, Some(Box::new(|a: &Attachment| {
use std::io::Write;
use std::process::{Command, Stdio};
String::from_utf8_lossy(&decode(
&body,
Some(Box::new(|a: &Attachment| {
use std::io::Write;
use std::process::{Command, Stdio};
let raw = decode(a, None);
let mut html_filter = Command::new("w3m")
.args(&["-I", "utf-8", "-T", "text/html"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start html filter process");
let raw = decode(a, None);
let mut html_filter = Command::new("w3m")
.args(&["-I", "utf-8", "-T", "text/html"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start html filter process");
html_filter.stdin.as_mut().unwrap().write_all(&raw).expect("Failed to write to w3m stdin");
html_filter.wait_with_output().unwrap().stdout
})))).into_owned().chars());
html_filter
.stdin
.as_mut()
.unwrap()
.write_all(&raw)
.expect("Failed to write to w3m stdin");
html_filter.wait_with_output().unwrap().stdout
})),
)).into_owned()
.chars(),
);
s
} else {
String::from_utf8_lossy(&decode_rec(&body, None)).into()
@ -105,13 +124,14 @@ impl MailView {
ViewMode::Normal | ViewMode::Subview => {
let mut t = body_text.to_string();
if body.count_attachments() > 1 {
t = body.attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
t = body
.attachments()
.iter()
.enumerate()
.fold(t, |mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
});
}
t
}
@ -131,13 +151,14 @@ impl MailView {
t.insert_str(l.start() + offset, &format!("[{}]", lidx));
}
if body.count_attachments() > 1 {
t = body.attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
t = body
.attachments()
.iter()
.enumerate()
.fold(t, |mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
});
}
t
}
@ -190,9 +211,7 @@ impl Component for MailView {
let (envelope_idx, y): (usize, usize) = {
let accounts = &mut context.accounts;
let threaded = accounts[self.coordinates.0]
.runtime_settings
.threaded;
let threaded = accounts[self.coordinates.0].runtime_settings.threaded;
let mailbox = &mut accounts[self.coordinates.0][self.coordinates.1]
.as_ref()
.unwrap();
@ -291,12 +310,15 @@ impl Component for MailView {
let body = envelope.body();
match self.mode {
ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
self.subview = Some(Box::new(HtmlView::new(decode(&body.attachments()[aidx], None))));
},
self.subview = Some(Box::new(HtmlView::new(decode(
&body.attachments()[aidx],
None,
))));
}
ViewMode::Normal if body.is_html() => {
self.subview = Some(Box::new(HtmlView::new(decode(&body, None))));
self.mode = ViewMode::Subview;
},
}
_ => {
let buf = {
let text = self.attachment_to_text(body);
@ -309,7 +331,7 @@ impl Component for MailView {
self.pager.as_mut().map(|p| p.cursor_pos())
};
self.pager = Some(Pager::from_buf(&buf, cursor_pos));
},
}
};
self.dirty = false;
}
@ -351,9 +373,7 @@ impl Component for MailView {
{
let accounts = &mut context.accounts;
let threaded = accounts[self.coordinates.0]
.runtime_settings
.threaded;
let threaded = accounts[self.coordinates.0].runtime_settings.threaded;
let mailbox = &mut accounts[self.coordinates.0][self.coordinates.1]
.as_ref()
.unwrap();
@ -424,9 +444,7 @@ impl Component for MailView {
self.cmd_buf.clear();
let url = {
let accounts = &mut context.accounts;
let threaded = accounts[self.coordinates.0]
.runtime_settings
.threaded;
let threaded = accounts[self.coordinates.0].runtime_settings.threaded;
let mailbox = &mut accounts[self.coordinates.0][self.coordinates.1]
.as_ref()
.unwrap();
@ -482,4 +500,7 @@ impl Component for MailView {
|| self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
|| self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
}
fn set_dirty(&mut self) {
self.dirty = true;
}
}

View File

@ -0,0 +1,101 @@
/*
* 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/>.
*/
use super::*;
use std::io::Write;
use std::process::{Command, Stdio};
pub struct ThreadView {
pager: Pager,
bytes: Vec<u8>,
}
impl ThreadView {
pub fn new(bytes: Vec<u8>) -> Self {
let mut html_filter = Command::new("w3m")
.args(&["-I", "utf-8", "-T", "text/html"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start html filter process");
html_filter
.stdin
.as_mut()
.unwrap()
.write_all(&bytes)
.expect("Failed to write to w3m stdin");
let mut display_text =
String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n");
display_text.push_str(&String::from_utf8_lossy(
&html_filter.wait_with_output().unwrap().stdout,
));
let buf = MailView::plain_text_to_buf(&display_text, true);
let pager = Pager::from_buf(&buf, None);
ThreadView { pager, bytes }
}
}
impl fmt::Display for ThreadView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
write!(f, "view thread")
}
}
impl Component for ThreadView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
self.pager.draw(grid, area, context);
}
fn process_event(&mut self, event: &UIEvent, context: &mut Context) {
match event.event_type {
UIEventType::Input(Key::Char('v')) => {
// TODO: Optional filter that removes outgoing resource requests (images and
// scripts)
let binary = query_default_app("text/html");
if let Ok(binary) = binary {
let mut p = create_temp_file(&self.bytes, None);
Command::new(&binary)
.arg(p.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap_or_else(|_| panic!("Failed to start {}", binary.display()));
context.temp_files.push(p);
} else {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusNotification(format!(
"Couldn't find a default application for html files."
)),
});
}
return;
}
_ => {}
}
self.pager.process_event(event, context);
}
fn is_dirty(&self) -> bool {
self.pager.is_dirty()
}
fn set_dirty(&mut self) {}
}

View File

@ -35,6 +35,10 @@ pub mod notifications;
pub mod utilities;
pub use self::utilities::*;
use std::fmt;
use std::fmt::Display;
use std::ops::Deref;
use super::{Key, UIEvent, UIEventType};
/// The upper and lower boundary char.
const HORZ_BOUNDARY: char = '─';
@ -65,6 +69,14 @@ pub struct Entity {
pub component: Box<Component>, // more than one?
}
impl Deref for Entity {
type Target = Box<Component>;
fn deref(&self) -> &Box<Component> {
&self.component
}
}
impl Entity {
/// Pass events to child component.
pub fn rcv_event(&mut self, event: &UIEvent, context: &mut Context) {
@ -75,12 +87,13 @@ impl Entity {
/// Types implementing this Trait can draw on the terminal and receive events.
/// If a type wants to skip drawing if it has not changed anything, it can hold some flag in its
/// fields (eg self.dirty = false) and act upon that in their `draw` implementation.
pub trait Component {
pub trait Component: Display {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);
fn process_event(&mut self, event: &UIEvent, context: &mut Context);
fn is_dirty(&self) -> bool {
true
}
fn set_dirty(&mut self);
}
// TODO: word break.

View File

@ -29,6 +29,13 @@ use super::*;
/// Passes notifications to the OS using the XDG specifications.
pub struct XDGNotifications {}
impl fmt::Display for XDGNotifications {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
write!(f, "")
}
}
impl Component for XDGNotifications {
fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {}
fn process_event(&mut self, event: &UIEvent, _context: &mut Context) {
@ -41,4 +48,5 @@ impl Component for XDGNotifications {
.unwrap();
}
}
fn set_dirty(&mut self) {}
}

View File

@ -31,6 +31,13 @@ pub struct HSplit {
ratio: usize, // bottom/whole height * 100
}
impl fmt::Display for HSplit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
self.top.fmt(f)
}
}
impl HSplit {
pub fn new(top: Entity, bottom: Entity, ratio: usize, show_divider: bool) -> Self {
HSplit {
@ -79,6 +86,10 @@ impl Component for HSplit {
fn is_dirty(&self) -> bool {
self.top.component.is_dirty() || self.bottom.component.is_dirty()
}
fn set_dirty(&mut self) {
self.top.component.set_dirty();
self.bottom.component.set_dirty();
}
}
/// A vertically split in half container.
@ -90,6 +101,13 @@ pub struct VSplit {
ratio: usize, // right/(container width) * 100
}
impl fmt::Display for VSplit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display focused entity
self.right.fmt(f)
}
}
impl VSplit {
pub fn new(left: Entity, right: Entity, ratio: usize, show_divider: bool) -> Self {
VSplit {
@ -155,6 +173,10 @@ impl Component for VSplit {
fn is_dirty(&self) -> bool {
self.left.component.is_dirty() || self.right.component.is_dirty()
}
fn set_dirty(&mut self) {
self.left.component.set_dirty();
self.right.component.set_dirty();
}
}
/// A pager for text.
@ -168,6 +190,13 @@ pub struct Pager {
content: CellBuffer,
}
impl fmt::Display for Pager {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display info
write!(f, "pager")
}
}
impl Pager {
pub fn from_string(mut text: String, context: &mut Context, cursor_pos: Option<usize>) -> Self {
let pager_filter: Option<&String> = context.settings.pager.filter.as_ref();
@ -325,6 +354,9 @@ impl Component for Pager {
fn is_dirty(&self) -> bool {
self.dirty
}
fn set_dirty(&mut self) {
self.dirty = true;
}
}
/// Status bar.
@ -338,6 +370,13 @@ pub struct StatusBar {
dirty: bool,
}
impl fmt::Display for StatusBar {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display info
write!(f, "status bar")
}
}
impl StatusBar {
pub fn new(container: Entity) -> Self {
StatusBar {
@ -497,6 +536,9 @@ impl Component for StatusBar {
fn is_dirty(&self) -> bool {
self.dirty || self.container.component.is_dirty()
}
fn set_dirty(&mut self) {
self.dirty = true;
}
}
// A box with a text content.
@ -510,11 +552,17 @@ impl TextBox {
}
}
impl fmt::Display for TextBox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display info
write!(f, "text box")
}
}
impl Component for TextBox {
fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {}
fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) {
return;
}
fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) {}
fn set_dirty(&mut self) {}
}
pub struct Progress {
@ -552,6 +600,13 @@ impl Progress {
}
}
impl fmt::Display for Progress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display info
write!(f, "progress bar")
}
}
impl Component for Progress {
fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {
unimplemented!()
@ -559,4 +614,97 @@ impl Component for Progress {
fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) {
return;
}
fn set_dirty(&mut self) {}
}
pub struct Tabbed {
children: Vec<Box<Component>>,
cursor_pos: usize,
}
impl Tabbed {
pub fn new(children: Vec<Box<Component>>) -> Self {
Tabbed {
children,
cursor_pos: 0,
}
}
fn draw_tabs(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let mut x = get_x(upper_left!(area));
let mut y: usize = get_y(upper_left!(area));
for (idx, c) in self.children.iter().enumerate() {
let (fg, bg) = if idx == self.cursor_pos {
(Color::Default, Color::Default)
} else {
(Color::Byte(15), Color::Byte(8))
};
let (x_, _y_) = write_string_to_grid(
&format!(" {} ", c),
grid,
fg,
bg,
(set_x(upper_left!(area), x), bottom_right!(area)),
false,
);
x = x_ + 1;
if y != _y_ {
break;
}
y = _y_;
}
let (cols, _) = grid.size();
let cslice: &mut [Cell] = grid;
for c in cslice[(y * cols) + x..(y * cols) + cols].iter_mut() {
c.set_bg(Color::Byte(7));
}
context.dirty_areas.push_back(area);
}
pub fn add_component(&mut self, new: Box<Component>) {
self.children.push(new);
}
}
impl fmt::Display for Tabbed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display info
write!(f, "tabs")
}
}
impl Component for Tabbed {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.children.len() > 1 {
self.draw_tabs(
grid,
(
upper_left!(area),
set_x(upper_left!(area), get_x(bottom_right!(area))),
),
context,
);
let y = get_y(upper_left!(area));
self.children[self.cursor_pos].draw(
grid,
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
context,
);
} else {
self.children[self.cursor_pos].draw(grid, area, context);
}
}
fn process_event(&mut self, event: &UIEvent, context: &mut Context) {
match &event.event_type {
UIEventType::Input(Key::Char('T')) => {
self.cursor_pos = (self.cursor_pos + 1) % self.children.len();
self.children[self.cursor_pos].set_dirty();
return;
}
_ => {}
}
self.children[self.cursor_pos].process_event(event, context);
}
fn is_dirty(&self) -> bool {
self.children[self.cursor_pos].is_dirty()
}
fn set_dirty(&mut self) {}
}

View File

@ -0,0 +1 @@

View File

@ -50,7 +50,7 @@ pub struct Context {
/// Events queue that components send back to the state
pub replies: VecDeque<UIEvent>,
_backends: Backends,
backends: Backends,
input_thread: chan::Sender<bool>,
pub temp_files: Vec<File>,
@ -103,8 +103,8 @@ 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 settings = Settings::new();
let stdout = AlternateScreen::from(_stdout.into_raw_mode().unwrap());
let termsize = termion::terminal_size().ok();
@ -155,7 +155,7 @@ impl State<std::io::Stdout> {
accounts,
mailbox_hashes: FnvHashMap::with_capacity_and_hasher(1, Default::default()),
_backends: backends,
backends,
settings: settings.clone(),
runtime_settings: settings,
dirty_areas: VecDeque::with_capacity(5),
@ -178,7 +178,7 @@ impl State<std::io::Stdout> {
).unwrap();
s.flush();
for (x, account) in s.context.accounts.iter_mut().enumerate() {
for (y, folder) in account.settings.folders.iter().enumerate() {
for (y, folder) in account.backend.folders().iter().enumerate() {
s.context.mailbox_hashes.insert(folder.hash(), (x, y));
}
let sender = s.sender.clone();
@ -463,8 +463,7 @@ impl<W: Write> State<W> {
_ => {
return None;
}
}
{
} {
if let Some(ForkType::NewDraft(f, _)) = std::mem::replace(&mut self.child, None) {
self.rcv_event(UIEvent {
id: 0,
@ -476,7 +475,9 @@ impl<W: Write> State<W> {
Some(false)
}
fn flush(&mut self) {
if let Some(s) = self.stdout.as_mut() { s.flush().unwrap(); }
if let Some(s) = self.stdout.as_mut() {
s.flush().unwrap();
}
}
fn stdout(&mut self) -> &mut termion::screen::AlternateScreen<termion::raw::RawTerminal<W>> {
self.stdout.as_mut().unwrap()

View File

@ -20,9 +20,9 @@
*/
use std;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::fs::OpenOptions;
use uuid::Uuid;
@ -39,7 +39,12 @@ impl Drop for File {
impl File {
pub fn file(&mut self) -> std::fs::File {
OpenOptions::new().read(true).write(true).create(true).open(&self.path).unwrap()
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&self.path)
.unwrap()
}
pub fn path(&self) -> &PathBuf {