added many changes

+ ui updates
+ colon now accepting commands
+ message meta logic in another module
This commit is contained in:
Michael Zhang 2021-03-10 04:46:26 -06:00
parent 555727e007
commit e437d95b9b
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
10 changed files with 157 additions and 70 deletions

8
Cargo.lock generated
View file

@ -1825,6 +1825,7 @@ dependencies = [
"panorama-imap", "panorama-imap",
"panorama-smtp", "panorama-smtp",
"parking_lot", "parking_lot",
"quoted_printable",
"serde", "serde",
"structopt", "structopt",
"tokio 1.2.0", "tokio 1.2.0",
@ -1852,6 +1853,7 @@ dependencies = [
"parking_lot", "parking_lot",
"pest", "pest",
"pest_derive", "pest_derive",
"quoted_printable",
"tokio 1.2.0", "tokio 1.2.0",
"tokio-rustls", "tokio-rustls",
"tokio-util", "tokio-util",
@ -2099,6 +2101,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "quoted_printable"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.7.3" version = "0.7.3"

View file

@ -37,6 +37,7 @@ tui = { version = "0.14.0", default-features = false, features = ["crossterm"] }
webpki-roots = "0.21.0" webpki-roots = "0.21.0"
xdg = "2.2.0" xdg = "2.2.0"
downcast-rs = "1.2.0" downcast-rs = "1.2.0"
quoted_printable = "0.4.2"
[dependencies.panorama-imap] [dependencies.panorama-imap]
path = "imap" path = "imap"

View file

@ -21,6 +21,7 @@ parking_lot = "0.11.1"
# pest_derive = { path = "../../pest/derive" } # pest_derive = { path = "../../pest/derive" }
pest = { git = "https://github.com/iptq/pest", rev = "6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" } pest = { git = "https://github.com/iptq/pest", rev = "6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" }
pest_derive = { git = "https://github.com/iptq/pest", rev = "6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" } pest_derive = { git = "https://github.com/iptq/pest", rev = "6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" }
quoted_printable = "0.4.2"
tokio = { version = "1.1.1", features = ["full"] } tokio = { version = "1.1.1", features = ["full"] }
tokio-rustls = "0.22.0" tokio-rustls = "0.22.0"
tokio-util = { version = "0.6.3" } tokio-util = { version = "0.6.3" }

View file

@ -1,3 +1,17 @@
design ideas
---
- instead of dumb search with `/`, have like an omnibar with recency info built in?
- this requires some kind of cache and text search
- tmux-like windows
- maybe some of the familiar commands? `<C-b %>` for split for ex,
- gluon for scripting language
- hook into some global keybinds/hooks struct
- transparent self-updates?? this could work with some kind of deprecation scheme for the config files
- for ex: v1 has `{ x: Int }`, v2 has `{ [deprecated] x: Int, x2: Float }` and v3 has `{ x2: Float }`
this means v1 -> v2 upgrade can be done automatically but because there are _any_ pending deprecated values being used
it's not allowed to automatically upgrade to v3
imap routine imap routine
--- ---

65
src/mail/metadata.rs Normal file
View file

@ -0,0 +1,65 @@
use chrono::{DateTime, Local};
use panorama_imap::response::*;
/// A record that describes the metadata of an email as it appears in the UI list
#[derive(Debug, Default)]
pub struct EmailMetadata {
/// UID if the message has one
pub uid: Option<u32>,
/// Whether or not this message is unread
pub unread: Option<bool>,
/// Date
pub date: Option<DateTime<Local>>,
/// Sender
pub from: String,
/// Subject
pub subject: String,
}
impl EmailMetadata {
/// Construct an EmailMetadata from a list of attributes retrieved from the server
pub fn from_attrs(attrs: Vec<AttributeValue>) -> Self {
let mut meta = EmailMetadata::default();
for attr in attrs {
match attr {
AttributeValue::Uid(new_uid) => meta.uid = Some(new_uid),
AttributeValue::InternalDate(new_date) => {
meta.date = Some(new_date.with_timezone(&Local));
}
AttributeValue::Envelope(Envelope {
subject: new_subject,
from: new_from,
..
}) => {
if let Some(new_from) = new_from {
meta.from = new_from
.iter()
.filter_map(|addr| addr.name.to_owned())
.collect::<Vec<_>>()
.join(", ");
}
if let Some(new_subject) = new_subject {
// TODO: probably shouldn't insert quoted-printable here
// but this is just the most convenient for it to look right at the moment
// there's probably some header that indicates it's quoted-printable
// MIME?
use quoted_printable::ParseMode;
let new_subject =
quoted_printable::decode(new_subject.as_bytes(), ParseMode::Robust)
.unwrap();
let new_subject = String::from_utf8(new_subject).unwrap();
meta.subject = new_subject;
}
}
_ => {}
}
}
meta
}
}

View file

@ -1,5 +1,7 @@
//! Mail //! Mail
mod metadata;
use anyhow::Result; use anyhow::Result;
use futures::{ use futures::{
future::FutureExt, future::FutureExt,
@ -22,6 +24,8 @@ use tokio_stream::wrappers::WatchStream;
use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod}; use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod};
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)
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
@ -156,7 +160,7 @@ async fn imap_main(acct: MailAccountConfig, mail2ui_tx: UnboundedSender<MailEven
let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list)); let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list));
let message_uids = authed.uid_search().await?; let message_uids = authed.uid_search().await?;
let message_uids = message_uids.into_iter().take(20).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(message_uids.clone()));
// TODO: make this happen concurrently with the main loop? // TODO: make this happen concurrently with the main loop?

View file

@ -32,12 +32,20 @@ impl HandlesInput for ColonPrompt {
let mut b = [0; 2]; let mut b = [0; 2];
self.value += c.encode_utf8(&mut b); self.value += c.encode_utf8(&mut b);
} }
KeyCode::Enter => {
let cmd = self.value.clone();
self.value.clear();
debug!("executing colon command: {:?}", cmd);
return Ok(InputResult::Pop);
}
KeyCode::Backspace => { KeyCode::Backspace => {
let mut new_len = self.value.len(); let mut new_len = self.value.len();
if new_len > 0 { if new_len > 0 {
new_len -= 1; new_len -= 1;
self.value.truncate(new_len);
} else {
return Ok(InputResult::Pop);
} }
self.value.truncate(new_len);
} }
_ => {} _ => {}
} }

View file

@ -1,7 +1,7 @@
use std::any::Any; use std::any::Any;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::{ use std::sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, AtomicI8, Ordering},
Arc, Arc,
}; };
@ -31,13 +31,15 @@ pub enum InputResult {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct BaseInputHandler(pub Arc<AtomicBool>); pub struct BaseInputHandler(pub Arc<AtomicBool>, pub Arc<AtomicI8>);
impl HandlesInput for BaseInputHandler { impl HandlesInput for BaseInputHandler {
fn handle_key(&mut self, term: TermType, evt: KeyEvent) -> Result<InputResult> { fn handle_key(&mut self, term: TermType, evt: KeyEvent) -> Result<InputResult> {
let KeyEvent { code, .. } = evt; let KeyEvent { code, .. } = evt;
match code { match code {
KeyCode::Char('q') => self.0.store(true, Ordering::Relaxed), KeyCode::Char('q') => self.0.store(true, Ordering::Relaxed),
KeyCode::Char('j') => self.1.store(1, Ordering::Relaxed),
KeyCode::Char('k') => self.1.store(-1, Ordering::Relaxed),
KeyCode::Char(':') => { KeyCode::Char(':') => {
let colon_prompt = Box::new(ColonPrompt::init(term)); let colon_prompt = Box::new(ColonPrompt::init(term));
return Ok(InputResult::Push(colon_prompt)); return Ok(InputResult::Push(colon_prompt));

View file

@ -1,4 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{
atomic::{AtomicI8, Ordering},
Arc,
};
use chrono::{DateTime, Datelike, Duration, Local}; use chrono::{DateTime, Datelike, Duration, Local};
use chrono_humanize::HumanTime; use chrono_humanize::HumanTime;
@ -11,6 +15,8 @@ use tui::{
widgets::*, widgets::*,
}; };
use crate::mail::EmailMetadata;
use super::FrameType; use super::FrameType;
#[derive(Default)] #[derive(Default)]
@ -20,13 +26,7 @@ pub struct MailTabState {
pub message_map: HashMap<u32, EmailMetadata>, pub message_map: HashMap<u32, EmailMetadata>,
pub messages: Vec<Envelope>, pub messages: Vec<Envelope>,
pub message_list: TableState, pub message_list: TableState,
} pub change: Arc<AtomicI8>,
#[derive(Debug)]
pub struct EmailMetadata {
pub date: DateTime<Local>,
pub from: String,
pub subject: String,
} }
fn humanize_timestamp(date: DateTime<Local>) -> String { fn humanize_timestamp(date: DateTime<Local>) -> String {
@ -78,6 +78,22 @@ impl MailTabState {
.constraints([Constraint::Length(20), Constraint::Max(5000)]) .constraints([Constraint::Length(20), Constraint::Max(5000)])
.split(area); .split(area);
// make the change
if self
.change
.compare_exchange(-1, 0, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
self.move_up();
}
if self
.change
.compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
self.move_down();
}
// folder list // folder list
let items = self let items = self
.folders .folders
@ -86,23 +102,31 @@ impl MailTabState {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let dirlist = List::new(items) let dirlist = List::new(items)
.block(Block::default().borders(Borders::NONE)) .block(Block::default().borders(Borders::NONE).title(Span::styled(
"ur mom",
Style::default().add_modifier(Modifier::BOLD),
)))
.style(Style::default().fg(Color::White)) .style(Style::default().fg(Color::White))
.highlight_style(Style::default().add_modifier(Modifier::ITALIC)) .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
.highlight_symbol(">>"); .highlight_symbol(">>");
// message list table // message list table
let rows = self let mut metas = self
.message_uids .message_uids
.iter() .iter()
.map(|id| { .filter_map(|id| self.message_map.get(id))
let meta = self.message_map.get(id); .collect::<Vec<_>>();
metas.sort_by_key(|m| m.date);
let rows = metas
.iter()
.rev()
.map(|meta| {
Row::new(vec![ Row::new(vec![
"".to_owned(), "".to_owned(),
id.to_string(), meta.uid.map(|u| u.to_string()).unwrap_or_default(),
meta.map(|m| humanize_timestamp(m.date)).unwrap_or_default(), meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(),
meta.map(|m| m.from.clone()).unwrap_or_default(), meta.from.clone(),
meta.map(|m| m.subject.clone()).unwrap_or_default(), meta.subject.clone(),
]) ])
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -34,11 +34,11 @@ use tui::{
Frame, Terminal, Frame, Terminal,
}; };
use crate::mail::MailEvent; 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_tab::{EmailMetadata, MailTabState}; use self::mail_tab::MailTabState;
pub(crate) type FrameType<'a, 'b> = Frame<'a, CrosstermBackend<&'b mut Stdout>>; pub(crate) type FrameType<'a, 'b> = Frame<'a, CrosstermBackend<&'b mut Stdout>>;
pub(crate) type TermType<'a, 'b> = &'b mut Terminal<CrosstermBackend<&'a mut Stdout>>; pub(crate) type TermType<'a, 'b> = &'b mut Terminal<CrosstermBackend<&'a mut Stdout>>;
@ -60,8 +60,10 @@ pub async fn run_ui(
// state stack for handling inputs // state stack for handling inputs
let should_exit = Arc::new(AtomicBool::new(false)); let should_exit = Arc::new(AtomicBool::new(false));
let mut input_states: Vec<Box<dyn HandlesInput>> = let mut input_states: Vec<Box<dyn HandlesInput>> = vec![Box::new(BaseInputHandler(
vec![Box::new(BaseInputHandler(should_exit.clone()))]; should_exit.clone(),
mail_tab.change.clone(),
))];
while !should_exit.load(Ordering::Relaxed) { while !should_exit.load(Ordering::Relaxed) {
term.draw(|f| { term.draw(|f| {
@ -76,7 +78,7 @@ pub async fn run_ui(
.split(f.size()); .split(f.size());
// this is the title bar // this is the title bar
let titles = vec!["OSU mail"].into_iter().map(Spans::from).collect(); let titles = vec!["panorama mail"].into_iter().map(Spans::from).collect();
let tabs = Tabs::new(titles); let tabs = Tabs::new(titles);
f.render_widget(tabs, chunks[0]); f.render_widget(tabs, chunks[0]);
@ -130,21 +132,6 @@ pub async fn run_ui(
} }
} }
// if let Event::Key(KeyEvent { code, .. }) = event {
// match code {
// // KeyCode::Char('q') => break,
// KeyCode::Char('j') => mail_tab.move_down(),
// KeyCode::Char('k') => mail_tab.move_up(),
// KeyCode::Char(':') => {
// let rect = term.size()?;
// term.set_cursor(1, rect.height - 1)?;
// term.show_cursor()?;
// colon_prompt = Some(ColonPrompt::default());
// }
// _ => {}
// }
// }
Some(event) Some(event)
} else { } else {
None None
@ -161,37 +148,10 @@ pub async fn run_ui(
MailEvent::MessageList(new_messages) => mail_tab.messages = new_messages, MailEvent::MessageList(new_messages) => mail_tab.messages = new_messages,
MailEvent::MessageUids(new_uids) => mail_tab.message_uids = new_uids, MailEvent::MessageUids(new_uids) => mail_tab.message_uids = new_uids,
MailEvent::UpdateUid(_, attrs) => { MailEvent::UpdateUid(uid, attrs) => {
let mut uid = None; let meta = EmailMetadata::from_attrs(attrs);
let mut date = None; let uid = meta.uid.unwrap_or(uid);
let mut from = String::new(); mail_tab.message_map.insert(uid, meta);
let mut subject = String::new();
for attr in attrs {
match attr {
AttributeValue::Uid(new_uid) => uid = Some(new_uid),
AttributeValue::InternalDate(new_date) => {
date = Some(new_date.with_timezone(&Local));
}
AttributeValue::Envelope(Envelope {
subject: new_subject, from: new_from, ..
}) => {
if let Some(new_from) = new_from {
from = new_from.iter()
.filter_map(|addr| addr.name.to_owned())
.collect::<Vec<_>>()
.join(", ");
}
if let Some(new_subject) = new_subject {
subject = new_subject;
}
}
_ => {}
}
}
if let (Some(uid), Some(date)) = (uid, date) {
let meta = EmailMetadata {date ,from, subject };
mail_tab.message_map.insert(uid, meta);
}
} }
MailEvent::NewUid(uid) => { MailEvent::NewUid(uid) => {
debug!("new msg!"); debug!("new msg!");