displaying messages again
This commit is contained in:
parent
9379d06450
commit
e706a252f1
11 changed files with 257 additions and 242 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
31
src/mail/event.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>,
|
||||||
|
|
|
@ -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
90
src/ui/mail_store.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,33 +83,57 @@ 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![
|
// 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(),
|
||||||
meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(),
|
// meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(),
|
||||||
meta.from.clone(),
|
// meta.from.clone(),
|
||||||
meta.subject.clone(),
|
// meta.subject.clone(),
|
||||||
]);
|
// ]);
|
||||||
if meta.unread {
|
// if meta.unread {
|
||||||
row = row.style(
|
// row = row.style(
|
||||||
Style::default()
|
// Style::default()
|
||||||
.fg(Color::LightCyan)
|
// .fg(Color::LightCyan)
|
||||||
.add_modifier(Modifier::BOLD),
|
// .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![
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rows.push(row);
|
||||||
}
|
}
|
||||||
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) {
|
||||||
|
|
157
src/ui/mod.rs
157
src/ui/mod.rs
|
@ -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(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue