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-smtp",
|
||||
"parking_lot",
|
||||
"quoted_printable",
|
||||
"serde",
|
||||
"structopt",
|
||||
"tokio 1.2.0",
|
||||
|
@ -1852,6 +1853,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"quoted_printable",
|
||||
"tokio 1.2.0",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
|
@ -2099,6 +2101,12 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quoted_printable"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b080c5db639b292ac79cbd34be0cfc5d36694768d8341109634d90b86930e2"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
|
|
|
@ -37,6 +37,7 @@ tui = { version = "0.14.0", default-features = false, features = ["crossterm"] }
|
|||
webpki-roots = "0.21.0"
|
||||
xdg = "2.2.0"
|
||||
downcast-rs = "1.2.0"
|
||||
quoted_printable = "0.4.2"
|
||||
|
||||
[dependencies.panorama-imap]
|
||||
path = "imap"
|
||||
|
|
|
@ -21,6 +21,7 @@ parking_lot = "0.11.1"
|
|||
# pest_derive = { path = "../../pest/derive" }
|
||||
pest = { 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-rustls = "0.22.0"
|
||||
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
|
||||
---
|
||||
|
||||
|
|
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
|
||||
|
||||
mod metadata;
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::{
|
||||
future::FutureExt,
|
||||
|
@ -22,6 +24,8 @@ use tokio_stream::wrappers::WatchStream;
|
|||
|
||||
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)
|
||||
#[derive(Debug)]
|
||||
#[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 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()));
|
||||
|
||||
// TODO: make this happen concurrently with the main loop?
|
||||
|
|
|
@ -32,12 +32,20 @@ impl HandlesInput for ColonPrompt {
|
|||
let mut b = [0; 2];
|
||||
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 => {
|
||||
let mut new_len = self.value.len();
|
||||
if new_len > 0 {
|
||||
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::fmt::Debug;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
atomic::{AtomicBool, AtomicI8, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
|
@ -31,13 +31,15 @@ pub enum InputResult {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BaseInputHandler(pub Arc<AtomicBool>);
|
||||
pub struct BaseInputHandler(pub Arc<AtomicBool>, pub Arc<AtomicI8>);
|
||||
|
||||
impl HandlesInput for BaseInputHandler {
|
||||
fn handle_key(&mut self, term: TermType, evt: KeyEvent) -> Result<InputResult> {
|
||||
let KeyEvent { code, .. } = evt;
|
||||
match code {
|
||||
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(':') => {
|
||||
let colon_prompt = Box::new(ColonPrompt::init(term));
|
||||
return Ok(InputResult::Push(colon_prompt));
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::{
|
||||
atomic::{AtomicI8, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use chrono::{DateTime, Datelike, Duration, Local};
|
||||
use chrono_humanize::HumanTime;
|
||||
|
@ -11,6 +15,8 @@ use tui::{
|
|||
widgets::*,
|
||||
};
|
||||
|
||||
use crate::mail::EmailMetadata;
|
||||
|
||||
use super::FrameType;
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -20,13 +26,7 @@ pub struct MailTabState {
|
|||
pub message_map: HashMap<u32, EmailMetadata>,
|
||||
pub messages: Vec<Envelope>,
|
||||
pub message_list: TableState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EmailMetadata {
|
||||
pub date: DateTime<Local>,
|
||||
pub from: String,
|
||||
pub subject: String,
|
||||
pub change: Arc<AtomicI8>,
|
||||
}
|
||||
|
||||
fn humanize_timestamp(date: DateTime<Local>) -> String {
|
||||
|
@ -78,6 +78,22 @@ impl MailTabState {
|
|||
.constraints([Constraint::Length(20), Constraint::Max(5000)])
|
||||
.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
|
||||
let items = self
|
||||
.folders
|
||||
|
@ -86,23 +102,31 @@ impl MailTabState {
|
|||
.collect::<Vec<_>>();
|
||||
|
||||
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))
|
||||
.highlight_style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
.highlight_symbol(">>");
|
||||
|
||||
// message list table
|
||||
let rows = self
|
||||
let mut metas = self
|
||||
.message_uids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let meta = self.message_map.get(id);
|
||||
.filter_map(|id| self.message_map.get(id))
|
||||
.collect::<Vec<_>>();
|
||||
metas.sort_by_key(|m| m.date);
|
||||
let rows = metas
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|meta| {
|
||||
Row::new(vec![
|
||||
"".to_owned(),
|
||||
id.to_string(),
|
||||
meta.map(|m| humanize_timestamp(m.date)).unwrap_or_default(),
|
||||
meta.map(|m| m.from.clone()).unwrap_or_default(),
|
||||
meta.map(|m| m.subject.clone()).unwrap_or_default(),
|
||||
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(),
|
||||
])
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
|
|
@ -34,11 +34,11 @@ use tui::{
|
|||
Frame, Terminal,
|
||||
};
|
||||
|
||||
use crate::mail::MailEvent;
|
||||
use crate::mail::{EmailMetadata, MailEvent};
|
||||
|
||||
use self::colon_prompt::ColonPrompt;
|
||||
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 TermType<'a, 'b> = &'b mut Terminal<CrosstermBackend<&'a mut Stdout>>;
|
||||
|
@ -60,8 +60,10 @@ pub async fn run_ui(
|
|||
|
||||
// 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()))];
|
||||
let mut input_states: Vec<Box<dyn HandlesInput>> = vec![Box::new(BaseInputHandler(
|
||||
should_exit.clone(),
|
||||
mail_tab.change.clone(),
|
||||
))];
|
||||
|
||||
while !should_exit.load(Ordering::Relaxed) {
|
||||
term.draw(|f| {
|
||||
|
@ -76,7 +78,7 @@ pub async fn run_ui(
|
|||
.split(f.size());
|
||||
|
||||
// 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);
|
||||
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)
|
||||
} else {
|
||||
None
|
||||
|
@ -161,37 +148,10 @@ pub async fn run_ui(
|
|||
MailEvent::MessageList(new_messages) => mail_tab.messages = new_messages,
|
||||
MailEvent::MessageUids(new_uids) => mail_tab.message_uids = new_uids,
|
||||
|
||||
MailEvent::UpdateUid(_, attrs) => {
|
||||
let mut uid = None;
|
||||
let mut date = None;
|
||||
let mut from = String::new();
|
||||
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::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!");
|
||||
|
|
Loading…
Reference in a new issue