displaying messages again

This commit is contained in:
Michael Zhang 2021-03-20 04:35:41 -05:00
parent 9379d06450
commit e706a252f1
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
11 changed files with 257 additions and 242 deletions

View file

@ -303,9 +303,9 @@ where
} else if let Some((tag, cmd, cmd_tx)) = curr_cmd.as_mut() { } else if let Some((tag, cmd, cmd_tx)) = curr_cmd.as_mut() {
// we got a response from the server for this command, so send it over the // we got a response from the server for this command, so send it over the
// channel // channel
debug!("sending {:?} to tag {}", resp, tag); // debug!("sending {:?} to tag {}", resp, tag);
let res = cmd_tx.send(resp); let res = cmd_tx.send(resp);
debug!("res1: {:?}", res); // debug!("res1: {:?}", res);
} }
} }
} }

View file

@ -13,7 +13,7 @@ impl Decoder for ImapCodec {
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> { fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
let s = std::str::from_utf8(src)?; let s = std::str::from_utf8(src)?;
trace!("codec parsing {:?}", s); // trace!("codec parsing {:?}", s);
match parse_streamed_response(s) { match parse_streamed_response(s) {
Ok((resp, len)) => { Ok((resp, len)) => {
src.advance(len); src.advance(len);

View file

@ -4,6 +4,9 @@ use anyhow::Result;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use tokio::fs::{self, File, OpenOptions}; use tokio::fs::{self, File, OpenOptions};
/// A Maildir, as defined by [Daniel J. Bernstein][1].
///
/// [1]: https://cr.yp.to/proto/maildir.html
pub struct Maildir { pub struct Maildir {
path: PathBuf, path: PathBuf,
} }

View file

@ -23,9 +23,12 @@ use super::{MailCommand, MailEvent};
/// The main sequence of steps for the IMAP thread to follow /// The main sequence of steps for the IMAP thread to follow
pub async fn imap_main( pub async fn imap_main(
acct_name: impl AsRef<str>,
acct: MailAccountConfig, acct: MailAccountConfig,
mail2ui_tx: UnboundedSender<MailEvent>, mail2ui_tx: UnboundedSender<MailEvent>,
) -> Result<()> { ) -> Result<()> {
let acct_name = acct_name.as_ref().to_owned();
// loop ensures that the connection is retried after it dies // loop ensures that the connection is retried after it dies
loop { loop {
let builder: ClientConfig = ClientBuilder::default() let builder: ClientConfig = ClientBuilder::default()
@ -68,17 +71,20 @@ pub async fn imap_main(
loop { loop {
let folder_list = authed.list().await?; let folder_list = authed.list().await?;
debug!("mailbox list: {:?}", folder_list); debug!("mailbox list: {:?}", folder_list);
let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list)); let _ = mail2ui_tx.send(MailEvent::FolderList(acct_name.clone(), folder_list));
let message_uids = authed.uid_search().await?; let message_uids = authed.uid_search().await?;
let message_uids = message_uids.into_iter().take(30).collect::<Vec<_>>(); let message_uids = message_uids.into_iter().take(30).collect::<Vec<_>>();
let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone())); let _ = mail2ui_tx.send(MailEvent::MessageUids(
acct_name.clone(),
message_uids.clone(),
));
// TODO: make this happen concurrently with the main loop? // TODO: make this happen concurrently with the main loop?
let mut message_list = authed.uid_fetch(&message_uids).await.unwrap(); let mut message_list = authed.uid_fetch(&message_uids).await.unwrap();
while let Some((uid, attrs)) = message_list.next().await { while let Some((uid, attrs)) = message_list.next().await {
let evt = MailEvent::UpdateUid(uid, attrs); let evt = MailEvent::UpdateUid(acct_name.clone(), uid, attrs);
debug!("sent {:?}", evt); // debug!("sent {:?}", evt);
mail2ui_tx.send(evt); mail2ui_tx.send(evt);
} }
@ -110,13 +116,16 @@ pub async fn imap_main(
let message_uids = authed.uid_search().await?; let message_uids = authed.uid_search().await?;
let message_uids = let message_uids =
message_uids.into_iter().take(20).collect::<Vec<_>>(); message_uids.into_iter().take(20).collect::<Vec<_>>();
let _ = mail2ui_tx.send(MailEvent::MessageUids(message_uids.clone())); let _ = mail2ui_tx.send(MailEvent::MessageUids(
acct_name.clone(),
message_uids.clone(),
));
// TODO: make this happen concurrently with the main loop? // TODO: make this happen concurrently with the main loop?
let mut message_list = authed.uid_fetch(&message_uids).await.unwrap(); let mut message_list = authed.uid_fetch(&message_uids).await.unwrap();
while let Some((uid, attrs)) = message_list.next().await { while let Some((uid, attrs)) = message_list.next().await {
let evt = MailEvent::UpdateUid(uid, attrs); let evt = MailEvent::UpdateUid(acct_name.clone(), uid, attrs);
debug!("sent {:?}", evt); // debug!("sent {:?}", evt);
mail2ui_tx.send(evt); mail2ui_tx.send(evt);
} }

31
src/mail/event.rs Normal file
View file

@ -0,0 +1,31 @@
use panorama_imap::response::{AttributeValue, Envelope};
/// Possible events returned from the server that should be sent to the UI
#[derive(Debug)]
#[non_exhaustive]
pub enum MailEvent {
/// Got the list of folders
FolderList(String, Vec<String>),
/// A list of the UIDs in the current mail view
MessageUids(String, Vec<u32>),
/// Update the given UID with the given attribute list
UpdateUid(String, u32, Vec<AttributeValue>),
/// New message came in with given UID
NewUid(String, u32),
}
impl MailEvent {
/// Retrieves the account name that this event is associated with
pub fn acct_name(&self) -> &str {
use MailEvent::*;
match self {
FolderList(name, _)
| MessageUids(name, _)
| UpdateUid(name, _, _)
| NewUid(name, _) => name,
}
}
}

View file

@ -4,7 +4,7 @@ use chrono::{DateTime, Local};
use panorama_imap::response::*; use panorama_imap::response::*;
/// A record that describes the metadata of an email as it appears in the UI list /// A record that describes the metadata of an email as it appears in the UI list
#[derive(Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct EmailMetadata { pub struct EmailMetadata {
/// UID if the message has one /// UID if the message has one
pub uid: Option<u32>, pub uid: Option<u32>,

View file

@ -1,6 +1,7 @@
//! Mail //! Mail
mod client; mod client;
mod event;
mod metadata; mod metadata;
use anyhow::Result; use anyhow::Result;
@ -25,6 +26,7 @@ use tokio_stream::wrappers::WatchStream;
use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod}; use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod};
pub use self::event::MailEvent;
pub use self::metadata::EmailMetadata; pub use self::metadata::EmailMetadata;
/// Command sent to the mail thread by something else (i.e. UI) /// Command sent to the mail thread by something else (i.e. UI)
@ -38,26 +40,6 @@ pub enum MailCommand {
Raw(ImapCommand), Raw(ImapCommand),
} }
/// Possible events returned from the server that should be sent to the UI
#[derive(Debug)]
#[non_exhaustive]
pub enum MailEvent {
/// Got the list of folders
FolderList(Vec<String>),
/// Got the current list of messages
MessageList(Vec<Envelope>),
/// A list of the UIDs in the current mail view
MessageUids(Vec<u32>),
/// Update the given UID with the given attribute list
UpdateUid(u32, Vec<AttributeValue>),
/// New message came in with given UID
NewUid(u32),
}
/// Main entrypoint for the mail listener. /// Main entrypoint for the mail listener.
pub async fn run_mail( pub async fn run_mail(
mut config_watcher: ConfigWatcher, mut config_watcher: ConfigWatcher,
@ -90,7 +72,7 @@ pub async fn run_mail(
// this loop is to make sure accounts are restarted on error // this loop is to make sure accounts are restarted on error
loop { loop {
match client::imap_main(acct.clone(), mail2ui_tx.clone()).await { match client::imap_main(&acct_name, acct.clone(), mail2ui_tx.clone()).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("IMAP Error: {}", err); error!("IMAP Error: {}", err);

90
src/ui/mail_store.rs Normal file
View file

@ -0,0 +1,90 @@
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use crate::mail::{EmailMetadata, MailEvent};
/// UI's view of the currently-known mail-related state of all accounts.
#[derive(Clone, Debug, Default)]
pub struct MailStore {
accounts: Arc<RwLock<HashMap<String, Arc<RwLock<MailAccountState>>>>>,
}
impl MailStore {
pub fn handle_mail_event(&self, evt: MailEvent) {
let acct_name = evt.acct_name().to_owned();
{
let accounts = self.accounts.read();
let contains_key = accounts.contains_key(&acct_name);
std::mem::drop(accounts);
if !contains_key {
let mut accounts = self.accounts.write();
accounts.insert(
acct_name.clone(),
Arc::new(RwLock::new(MailAccountState::default())),
);
}
}
let accounts = self.accounts.read();
if let Some(lock) = accounts.get(&acct_name) {
let mut state = lock.write();
state.update(evt);
}
}
pub fn iter_accts(&self) -> Vec<String> {
self.accounts.read().keys().cloned().collect()
}
pub fn folders_of(&self, acct_name: impl AsRef<str>) -> Option<Vec<String>> {
let accounts = self.accounts.read();
let lock = accounts.get(acct_name.as_ref())?;
let state = lock.read();
Some(state.folders.clone())
}
pub fn messages_of(&self, acct_name: impl AsRef<str>) -> Option<Vec<EmailMetadata>> {
let accounts = self.accounts.read();
let lock = accounts.get(acct_name.as_ref())?;
let state = lock.read();
let mut msgs = Vec::new();
for uid in state.message_uids.iter() {
if let Some(meta) = state.message_map.get(uid) {
msgs.push(meta.clone());
}
}
Some(msgs)
}
}
#[derive(Debug, Default)]
pub struct MailAccountState {
pub folders: Vec<String>,
pub message_uids: Vec<u32>,
pub message_map: HashMap<u32, EmailMetadata>,
}
impl MailAccountState {
pub fn update(&mut self, evt: MailEvent) {
match evt {
MailEvent::FolderList(_, new_folders) => self.folders = new_folders,
MailEvent::MessageUids(_, new_uids) => self.message_uids = new_uids,
MailEvent::UpdateUid(_, uid, attrs) => {
let meta = EmailMetadata::from_attrs(attrs);
let uid = meta.uid.unwrap_or(uid);
self.message_map.insert(uid, meta);
}
MailEvent::NewUid(_, uid) => {
debug!("new msg!");
self.message_uids.push(uid);
}
_ => {}
}
// debug!("mail store updated! {:?}", self);
}
}

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{ use std::sync::{
atomic::{AtomicI8, Ordering}, atomic::{AtomicI8, AtomicU32, Ordering},
Arc, Arc,
}; };
@ -19,15 +19,13 @@ use tui::{
use crate::mail::EmailMetadata; use crate::mail::EmailMetadata;
use super::{FrameType, HandlesInput, InputResult, TermType, Window, UI}; use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, UI};
#[derive(Default, Debug)] #[derive(Debug)]
pub struct MailView { pub struct MailView {
pub folders: Vec<String>, pub mail_store: MailStore,
pub message_uids: Vec<u32>,
pub message_map: HashMap<u32, EmailMetadata>,
pub messages: Vec<Envelope>,
pub message_list: TableState, pub message_list: TableState,
pub selected: Arc<AtomicU32>,
pub change: Arc<AtomicI8>, pub change: Arc<AtomicI8>,
} }
@ -61,16 +59,23 @@ impl Window for MailView {
.constraints([Constraint::Length(20), Constraint::Max(5000)]) .constraints([Constraint::Length(20), Constraint::Max(5000)])
.split(area); .split(area);
let accts = self.mail_store.iter_accts();
// folder list // folder list
let items = self let mut items = vec![];
.folders for acct in accts.iter() {
.iter() let result = self.mail_store.folders_of(acct);
.map(|s| ListItem::new(s.to_owned())) if let Some(folders) = result {
.collect::<Vec<_>>(); items.push(ListItem::new(acct.to_owned()));
for folder in folders {
items.push(ListItem::new(format!(" {}", folder)));
}
}
}
let dirlist = List::new(items) let dirlist = List::new(items)
.block(Block::default().borders(Borders::NONE).title(Span::styled( .block(Block::default().borders(Borders::NONE).title(Span::styled(
"ur mom", "hellosu",
Style::default().add_modifier(Modifier::BOLD), Style::default().add_modifier(Modifier::BOLD),
))) )))
.style(Style::default().fg(Color::White)) .style(Style::default().fg(Color::White))
@ -78,16 +83,39 @@ impl Window for MailView {
.highlight_symbol(">>"); .highlight_symbol(">>");
// message list table // message list table
let mut metas = self // let mut metas = self
.message_uids // .message_uids
.iter() // .iter()
.filter_map(|id| self.message_map.get(id)) // .filter_map(|id| self.message_map.get(id))
.collect::<Vec<_>>(); // .collect::<Vec<_>>();
metas.sort_by_key(|m| m.date); // metas.sort_by_key(|m| m.date);
let rows = metas // let rows = metas
.iter() // .iter()
.rev() // .rev()
.map(|meta| { // .map(|meta| {
// let mut row = Row::new(vec![
// String::from(if meta.unread { "\u{2b24}" } else { "" }),
// meta.uid.map(|u| u.to_string()).unwrap_or_default(),
// meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(),
// meta.from.clone(),
// meta.subject.clone(),
// ]);
// if meta.unread {
// row = row.style(
// Style::default()
// .fg(Color::LightCyan)
// .add_modifier(Modifier::BOLD),
// );
// }
// row
// })
// .collect::<Vec<_>>();
let mut rows = vec![];
for acct in accts.iter() {
let result = self.mail_store.messages_of(acct);
if let Some(messages) = result {
for meta in messages {
let mut row = Row::new(vec![ let mut row = Row::new(vec![
String::from(if meta.unread { "\u{2b24}" } else { "" }), String::from(if meta.unread { "\u{2b24}" } else { "" }),
meta.uid.map(|u| u.to_string()).unwrap_or_default(), meta.uid.map(|u| u.to_string()).unwrap_or_default(),
@ -102,9 +130,10 @@ impl Window for MailView {
.add_modifier(Modifier::BOLD), .add_modifier(Modifier::BOLD),
); );
} }
row rows.push(row);
}) }
.collect::<Vec<_>>(); }
}
let table = Table::new(rows) let table = Table::new(rows)
.style(Style::default().fg(Color::White)) .style(Style::default().fg(Color::White))
.widths(&[ .widths(&[
@ -144,32 +173,41 @@ fn humanize_timestamp(date: DateTime<Local>) -> String {
} }
impl MailView { impl MailView {
pub fn move_down(&mut self) { pub fn new(mail_store: MailStore) -> Self {
if self.message_uids.is_empty() { MailView {
return; mail_store,
} message_list: TableState::default(),
let len = self.message_uids.len(); selected: Arc::new(AtomicU32::default()),
if let Some(selected) = self.message_list.selected() { change: Arc::new(AtomicI8::default()),
if selected + 1 < len {
self.message_list.select(Some(selected + 1));
}
} else {
self.message_list.select(Some(0));
} }
} }
pub fn move_down(&mut self) {
// if self.message_uids.is_empty() {
// return;
// }
// let len = self.message_uids.len();
// if let Some(selected) = self.message_list.selected() {
// if selected + 1 < len {
// self.message_list.select(Some(selected + 1));
// }
// } else {
// self.message_list.select(Some(0));
// }
}
pub fn move_up(&mut self) { pub fn move_up(&mut self) {
if self.message_uids.is_empty() { // if self.message_uids.is_empty() {
return; // return;
} // }
let len = self.message_uids.len(); // let len = self.message_uids.len();
if let Some(selected) = self.message_list.selected() { // if let Some(selected) = self.message_list.selected() {
if selected >= 1 { // if selected >= 1 {
self.message_list.select(Some(selected - 1)); // self.message_list.select(Some(selected - 1));
} // }
} else { // } else {
self.message_list.select(Some(len - 1)); // self.message_list.select(Some(len - 1));
} // }
} }
pub fn update(&mut self) { pub fn update(&mut self) {

View file

@ -3,6 +3,7 @@
mod colon_prompt; mod colon_prompt;
mod input; mod input;
mod keybinds; mod keybinds;
mod mail_store;
mod mail_view; mod mail_view;
mod messages; mod messages;
mod windows; mod windows;
@ -41,6 +42,7 @@ use crate::mail::{EmailMetadata, MailEvent};
use self::colon_prompt::ColonPrompt; use self::colon_prompt::ColonPrompt;
use self::input::{BaseInputHandler, HandlesInput, InputResult}; use self::input::{BaseInputHandler, HandlesInput, InputResult};
use self::mail_store::MailStore;
use self::mail_view::MailView; use self::mail_view::MailView;
pub(crate) use self::messages::*; pub(crate) use self::messages::*;
use self::windows::*; use self::windows::*;
@ -64,14 +66,17 @@ pub async fn run_ui2(
let should_exit = Arc::new(AtomicBool::new(false)); let should_exit = Arc::new(AtomicBool::new(false));
let mail_store = MailStore::default();
let mut ui = UI { let mut ui = UI {
should_exit: should_exit.clone(), should_exit: should_exit.clone(),
window_layout: WindowLayout::default(), window_layout: WindowLayout::default(),
windows: HashMap::new(), windows: HashMap::new(),
page_names: HashMap::new(), page_names: HashMap::new(),
mail_store: mail_store.clone(),
}; };
ui.open_window(MailView::default()); ui.open_window(MailView::new(mail_store));
// let mut input_states: Vec<Box<dyn HandlesInput>> = vec![]; // let mut input_states: Vec<Box<dyn HandlesInput>> = vec![];
@ -118,6 +123,7 @@ pub struct UI {
window_layout: WindowLayout, window_layout: WindowLayout,
windows: HashMap<LayoutId, Box<dyn Window>>, windows: HashMap<LayoutId, Box<dyn Window>>,
page_names: HashMap<PageId, String>, page_names: HashMap<PageId, String>,
mail_store: MailStore,
} }
impl UI { impl UI {
@ -201,153 +207,6 @@ impl UI {
} }
fn process_mail_event(&mut self, evt: MailEvent) { fn process_mail_event(&mut self, evt: MailEvent) {
debug!("received mail event: {:?}", evt); self.mail_store.handle_mail_event(evt);
} }
} }
/// Main entrypoint for the UI
pub async fn run_ui(
mut stdout: Stdout,
exit_tx: mpsc::Sender<()>,
mut mail2ui_rx: mpsc::UnboundedReceiver<MailEvent>,
) -> Result<()> {
execute!(stdout, cursor::Hide, terminal::EnterAlternateScreen)?;
terminal::enable_raw_mode()?;
let backend = CrosstermBackend::new(&mut stdout);
let mut term = Terminal::new(backend)?;
let mut mail_tab = MailView::default();
// state stack for handling inputs
let should_exit = Arc::new(AtomicBool::new(false));
let mut input_states: Vec<Box<dyn HandlesInput>> = vec![Box::new(BaseInputHandler(
should_exit.clone(),
mail_tab.change.clone(),
))];
let mut window_layout = WindowLayout::default();
let mut page_names = HashMap::new();
// TODO: have this be configured thru the settings?
let (mail_id, mail_page) = window_layout.new_page();
page_names.insert(mail_page, "Email");
while !should_exit.load(Ordering::Relaxed) {
term.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([
Constraint::Length(1),
Constraint::Max(5000),
Constraint::Length(1),
])
.split(f.size());
// this is the title bar
// let titles = vec!["email"].into_iter().map(Spans::from).collect();
let titles = window_layout
.list_pages()
.iter()
.filter_map(|id| page_names.get(id))
.map(|s| Spans::from(*s))
.collect();
let tabs = Tabs::new(titles);
f.render_widget(tabs, chunks[0]);
// this is the main mail tab
// mail_tab.render(f, chunks[1]);
// this is the status bar
if let Some(last_state) = input_states.last() {
let downcasted = last_state.downcast_ref::<ColonPrompt>();
match downcasted {
Some(colon_prompt) => {
let status = Block::default().title(vec![
Span::styled(":", Style::default().fg(Color::Gray)),
Span::raw(&colon_prompt.value),
]);
f.render_widget(status, chunks[2]);
f.set_cursor(colon_prompt.value.len() as u16 + 1, chunks[2].y);
}
None => {
let status = Paragraph::new("hellosu");
f.render_widget(status, chunks[2]);
}
};
}
})?;
let event = if event::poll(FRAME_DURATION)? {
let event = event::read()?;
// table.update(&event);
if let Event::Key(evt) = event {
// handle states in the state stack
// although this is written in a for loop, every case except one should break
let mut should_pop = false;
if let Some(input_state) = input_states.last_mut() {
match input_state.handle_key(&mut term, evt)? {
InputResult::Ok => {}
InputResult::Push(state) => {
input_states.push(state);
}
InputResult::Pop => {
should_pop = true;
}
}
}
if should_pop {
input_states.pop();
}
}
Some(event)
} else {
None
};
select! {
mail_evt = mail2ui_rx.recv().fuse() => {
debug!("received mail event: {:?}", mail_evt);
// TODO: handle case that channel is closed later
let mail_evt = mail_evt.unwrap();
match mail_evt {
MailEvent::FolderList(new_folders) => mail_tab.folders = new_folders,
MailEvent::MessageList(new_messages) => mail_tab.messages = new_messages,
MailEvent::MessageUids(new_uids) => mail_tab.message_uids = new_uids,
MailEvent::UpdateUid(uid, attrs) => {
let meta = EmailMetadata::from_attrs(attrs);
let uid = meta.uid.unwrap_or(uid);
mail_tab.message_map.insert(uid, meta);
}
MailEvent::NewUid(uid) => {
debug!("new msg!");
mail_tab.message_uids.push(uid);
}
_ => {}
}
}
// approx 60fps
_ = time::sleep(FRAME_DURATION).fuse() => {}
}
}
mem::drop(term);
execute!(
stdout,
style::ResetColor,
cursor::Show,
terminal::LeaveAlternateScreen
)?;
terminal::disable_raw_mode()?;
exit_tx.send(()).await?;
debug!("sent exit");
Ok(())
}

View file

@ -18,6 +18,9 @@ pub trait Window: HandlesInput {
fn draw_inactive(&mut self, f: FrameType, area: Rect, ui: &UI) { fn draw_inactive(&mut self, f: FrameType, area: Rect, ui: &UI) {
self.draw(f, area, ui); self.draw(f, area, ui);
} }
/// Update function
fn update(&mut self) {}
} }
downcast_rs::impl_downcast!(Window); downcast_rs::impl_downcast!(Window);