added many changes
+ ui updates + colon now accepting commands + message meta logic in another module
This commit is contained in:
parent
555727e007
commit
e437d95b9b
10 changed files with 157 additions and 70 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
14
notes.md
14
notes.md
|
@ -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
65
src/mail/metadata.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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<_>>();
|
||||||
|
|
|
@ -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!");
|
||||||
|
|
Loading…
Reference in a new issue