make the table look a bit nicer

This commit is contained in:
Michael Zhang 2021-03-08 18:26:18 -06:00
parent 9a58fe6d58
commit 46567c43e1
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
6 changed files with 101 additions and 54 deletions

View file

@ -45,7 +45,9 @@ use tokio_rustls::{
};
use crate::command::{Command, FetchItems, SearchCriteria};
use crate::response::{AttributeValue, MailboxData, Response, ResponseData, ResponseDone};
use crate::response::{
AttributeValue, Envelope, MailboxData, Response, ResponseData, ResponseDone,
};
pub use self::inner::{Client, ResponseStream};
@ -199,7 +201,7 @@ impl ClientAuthenticated {
}
/// Runs the UID FETCH command
pub async fn uid_fetch(&mut self, uids: &[u32]) -> Result<Vec<(u32, Vec<AttributeValue>)>> {
pub async fn uid_fetch(&mut self, uids: &[u32]) -> Result<Vec<Envelope>> {
let cmd = Command::UidFetch {
uids: uids.to_vec(),
items: FetchItems::All,
@ -210,7 +212,10 @@ impl ClientAuthenticated {
Ok(data
.into_iter()
.filter_map(|resp| match resp {
Response::Fetch(n, attrs) => Some((n, attrs)),
Response::Fetch(n, attrs) => attrs.into_iter().find_map(|attr| match attr {
AttributeValue::Envelope(envelope) => Some(envelope),
_ => None,
}),
_ => None,
})
.collect())

View file

@ -378,11 +378,15 @@ fn build_mailbox_list(pair: Pair<Rule>) -> (Vec<String>, Option<String>, String)
};
assert!(matches!(pair.as_rule(), Rule::mailbox_list_string));
let s = build_nstring(pair);
let s = build_nstring(unwrap1(pair));
pair = pairs.next().unwrap();
assert!(matches!(pair.as_rule(), Rule::mailbox));
let mailbox = build_string(pair);
let mailbox = if pair.as_str().to_lowercase() == "inbox" {
pair.as_str().to_owned()
} else {
build_astring(unwrap1(pair))
};
(flags, s, mailbox)
}
@ -413,6 +417,19 @@ where
pair.as_str().parse::<T>().unwrap()
}
fn build_astring(pair: Pair<Rule>) -> String {
assert!(matches!(pair.as_rule(), Rule::astring));
let pair_str = pair.as_str().to_owned();
let mut pairs = pair.into_inner();
let rule = pairs.peek().map(|p| p.as_rule());
if let Some(Rule::string) = rule {
let pair = pairs.next().unwrap();
build_string(pair)
} else {
pair_str
}
}
/// Wrapper around [build_string][1], except return None for the `nil` case
///
/// [1]: self::build_string
@ -434,7 +451,12 @@ fn build_string(pair: Pair<Rule>) -> String {
match pair.as_rule() {
Rule::literal => build_literal(pair),
// TODO: escaping stuff?
Rule::quoted => pair.as_str().trim_start_matches("\"").trim_end_matches("\"").replace("\\\"", "\"").to_owned(),
Rule::quoted => pair
.as_str()
.trim_start_matches("\"")
.trim_end_matches("\"")
.replace("\\\"", "\"")
.to_owned(),
_ => unreachable!(),
}
}

View file

@ -76,7 +76,9 @@ mailbox_data_recent = { number ~ sp ~ ^"RECENT" }
mailbox_data_search = { ^"SEARCH" ~ (sp ~ nz_number)* }
mailbox_list = { mailbox_list_flags ~ sp ~ mailbox_list_string ~ sp ~ mailbox }
mailbox_list_flags = { "(" ~ mbx_list_flags* ~ ")" }
mailbox_list_string = { dquote ~ quoted_char ~ dquote | nil }
// TODO: technically this should only be 1 quoted char
// mailbox_list_string = { dquote ~ quoted_char ~ dquote | nil }
mailbox_list_string = ${ nstring }
mbx_list_flags = { (mbx_list_oflag ~ sp)* ~ mbx_list_sflag ~ (sp ~ mbx_list_oflag)* | mbx_list_oflag ~ (sp ~ mbx_list_oflag)* }
mbx_list_oflag = { "\\NoInferiors" | flag_extension }
mbx_list_sflag = { "\\NoSelect" | "\\Marked" | "\\Unmarked" }

View file

@ -11,6 +11,7 @@ use panorama_imap::{
ClientBuilder, ClientConfig,
},
command::Command as ImapCommand,
response::Envelope,
};
use tokio::{
sync::mpsc::{UnboundedReceiver, UnboundedSender},
@ -37,7 +38,7 @@ pub enum MailEvent {
FolderList(Vec<String>),
/// Got the current list of messages
MessageList(Vec<String>),
MessageList(Vec<Envelope>),
}
/// Main entrypoint for the mail listener.
@ -144,11 +145,7 @@ async fn imap_main(acct: MailAccountConfig, mail2ui_tx: UnboundedSender<MailEven
let message_uids = authed.uid_search().await?;
let message_list = authed.uid_fetch(&message_uids).await?;
let mut messages = Vec::new();
for (_, attrs) in message_list {
messages.push(format!("{:?}", attrs));
}
let _ = mail2ui_tx.send(MailEvent::MessageList(messages));
let _ = mail2ui_tx.send(MailEvent::MessageList(message_list));
let mut idle_stream = authed.idle().await?;

View file

@ -1,3 +1,4 @@
use panorama_imap::response::Envelope;
use tui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
@ -8,33 +9,48 @@ use tui::{
use super::FrameType;
pub fn render_mail_tab(f: &mut FrameType, area: Rect, folders: &[String], messages: &[String]) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints([Constraint::Length(20), Constraint::Max(5000)])
.split(area);
let items = folders
.iter()
.map(|s| ListItem::new(s.to_owned()))
.collect::<Vec<_>>();
let dirlist = List::new(items)
.block(Block::default().borders(Borders::NONE))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().add_modifier(Modifier::ITALIC))
.highlight_symbol(">>");
let rows = messages
.iter()
.map(|s| Row::new(vec![s.as_ref()]))
.collect::<Vec<_>>();
let table = Table::new(rows)
.style(Style::default().fg(Color::White))
.widths(&[Constraint::Max(5000)])
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
f.render_widget(dirlist, chunks[0]);
f.render_widget(table, chunks[1]);
#[derive(Default)]
pub struct MailTabState {
pub folders: Vec<String>,
pub messages: Vec<Envelope>,
pub message_list: TableState,
}
impl MailTabState {
pub fn render(&mut self, f: &mut FrameType, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
.constraints([Constraint::Length(20), Constraint::Max(5000)])
.split(area);
// folder list
let items = self
.folders
.iter()
.map(|s| ListItem::new(s.to_owned()))
.collect::<Vec<_>>();
let dirlist = List::new(items)
.block(Block::default().borders(Borders::NONE))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().add_modifier(Modifier::ITALIC))
.highlight_symbol(">>");
// message list table
let rows = self
.messages
.iter()
.map(|s| Row::new(vec![s.subject.clone().unwrap_or_default()]))
.collect::<Vec<_>>();
let table = Table::new(rows)
.style(Style::default().fg(Color::White))
.widths(&[Constraint::Max(5000)])
.header(Row::new(vec!["Subject"]).style(Style::default().add_modifier(Modifier::BOLD)))
.highlight_style(Style::default().fg(Color::Black).bg(Color::LightBlue));
f.render_widget(dirlist, chunks[0]);
f.render_stateful_widget(table, chunks[1], &mut self.message_list);
}
}

View file

@ -25,7 +25,7 @@ use tui::{
use crate::mail::MailEvent;
use self::mail_tab::render_mail_tab;
use self::mail_tab::MailTabState;
pub(crate) type FrameType<'a, 'b> = Frame<'a, CrosstermBackend<&'b mut Stdout>>;
@ -43,8 +43,7 @@ pub async fn run_ui(
let backend = CrosstermBackend::new(&mut stdout);
let mut term = Terminal::new(backend)?;
let mut folders = Vec::<String>::new();
let mut messages = Vec::<String>::new();
let mut mail_tab = MailTabState::default();
loop {
term.draw(|f| {
@ -59,19 +58,25 @@ pub async fn run_ui(
let tabs = Tabs::new(titles);
f.render_widget(tabs, chunks[0]);
render_mail_tab(f, chunks[1], &folders, &messages);
mail_tab.render(f, chunks[1]);
// render_mail_tab(f, chunks[1], &folders, &messages);
})?;
let event = if event::poll(FRAME_DURATION)? {
let event = event::read()?;
// table.update(&event);
if let Event::Key(KeyEvent {
code: KeyCode::Char('q'),
..
}) = event
{
break;
if let Event::Key(KeyEvent { code, .. }) = event {
let selected = mail_tab.message_list.selected();
let len = mail_tab.messages.len();
let seln = selected.map(|x| if x < len - 1 { x + 1 } else { x }).unwrap_or(0);
let selp = selected.map(|x| if x > 0 { x - 1 } else { 0 }).unwrap_or(0);
match code {
KeyCode::Char('q') => break,
KeyCode::Char('j') => mail_tab.message_list.select(Some(seln)),
KeyCode::Char('k') => mail_tab.message_list.select(Some(selp)),
_ => {}
}
}
Some(event)
@ -87,10 +92,10 @@ pub async fn run_ui(
match mail_evt {
MailEvent::FolderList(new_folders) => {
folders = new_folders;
mail_tab.folders = new_folders;
}
MailEvent::MessageList(new_messages) => {
messages = new_messages;
mail_tab.messages = new_messages;
}
}
}