diff options
Diffstat (limited to 'src/views')
-rw-r--r-- | src/views/mail.rs | 122 | ||||
-rw-r--r-- | src/views/maillist.rs | 240 | ||||
-rw-r--r-- | src/views/main.rs | 192 | ||||
-rw-r--r-- | src/views/mod.rs | 3 |
4 files changed, 557 insertions, 0 deletions
diff --git a/src/views/mail.rs b/src/views/mail.rs new file mode 100644 index 0000000..93c781a --- /dev/null +++ b/src/views/mail.rs @@ -0,0 +1,122 @@ +use std::path::PathBuf; +use anyhow::Result; +use anyhow::Error; + +use cursive::Printer; +use cursive::Rect; +use cursive::View; +use cursive::XY; +use cursive::direction::Direction; +use cursive::event::Event; +use cursive::event::EventResult; +use cursive::view::Nameable; +use cursive::view::Selector; +use cursive::view::Scrollable; +use cursive::views::NamedView; +use cursive::views::TextView; +use cursive::views::LinearLayout; +use cursive::views::ScrollView; +use result_inspect::ResultInspect; +use mailparse::ParsedMail; + +pub struct MailView { + view: ScrollView<LinearLayout>, +} + +impl MailView { + + pub fn create_for(database_path: PathBuf, id: String, mailfile: PathBuf, name: String) -> Result<NamedView<Self>> { + let query = format!("id:{}", id); + let view = notmuch::Database::open(&database_path, notmuch::DatabaseMode::ReadOnly)? + .create_query(&query)? + .search_messages()? + .map(|msg| { + debug!("Constructing textview for '{}'", msg.filename().display()); + let buf = std::fs::read(msg.filename())?; + debug!("Found {} bytes from {}", buf.len(), msg.filename().display()); + + let parsed = mailparse::parse_mail(buf)?; + MailView::parsed_mail_to_list_of_textviews(&parsed) + }) + .fold(Ok(LinearLayout::vertical()), |ll: Result<_>, views: Result<Vec<TextView>>| { + ll.and_then(|mut l| { + views?.into_iter().for_each(|e| l.add_child(e)); + Ok(l) + }) + })?; + + let view = if view.len() == 0 { + debug!("Falling back to mailfile parsing"); + LinearLayout::vertical().child({ + std::fs::read(&mailfile) + .map_err(Error::from) + .and_then(|b| String::from_utf8(b).map_err(Error::from)) + .inspect(|s| debug!("Found {} bytes from {}", s.bytes().len(), mailfile.display())) + .map(TextView::new)? + }) + } else { + view + }; + + Ok(MailView { view: view.scrollable() }.with_name(name)) + } + + fn parsed_mail_to_list_of_textviews<'a>(pm: &'a ParsedMail) -> Result<Vec<TextView>> { + fn collect_into<'a>(v: &mut Vec<TextView>, pm: &'a ParsedMail) -> Result<()> { + v.push(pm.get_body().map(TextView::new)?); + + pm.subparts + .iter() + .map(|subp| collect_into(v, subp)) + .collect::<Result<Vec<_>>>() + .map(|_| ()) + } + + let mut vec = Vec::new(); + collect_into(&mut vec, pm).map(|_| vec) + } + +} + +impl View for MailView { + fn draw(&self, printer: &Printer) { + self.view.draw(printer) + } + + fn layout(&mut self, xy: XY<usize>) { + self.view.layout(xy) + } + + fn needs_relayout(&self) -> bool { + self.view.needs_relayout() + } + + fn required_size(&mut self, constraint: XY<usize>) -> XY<usize> { + self.view.required_size(constraint) + } + + fn on_event(&mut self, e: Event) -> EventResult { + self.view.on_event(e) + } + + fn call_on_any<'a>(&mut self, s: &Selector, tpl: &'a mut (dyn FnMut(&mut (dyn View + 'static)) + 'a)) { + self.view.call_on_any(s, tpl); + } + + fn focus_view(&mut self, s: &Selector) -> Result<(), ()> { + self.view.focus_view(s) + } + + fn take_focus(&mut self, source: Direction) -> bool { + self.view.take_focus(source) + } + + fn important_area(&self, view_size: XY<usize>) -> Rect { + self.view.important_area(view_size) + } + + fn type_name(&self) -> &'static str { + self.view.type_name() + } + +} diff --git a/src/views/maillist.rs b/src/views/maillist.rs new file mode 100644 index 0000000..dc13bf3 --- /dev/null +++ b/src/views/maillist.rs @@ -0,0 +1,240 @@ +use std::path::PathBuf; +use std::ops::Deref; + +use anyhow::Result; +use anyhow::Context; +use cursive::Cursive; +use cursive::Printer; +use cursive::Rect; +use cursive::View; +use cursive::XY; +use cursive::direction::Direction; +use cursive::view::Nameable; +use cursive::view::Selector; +use cursive::event::Event; +use cursive::event::EventResult; +use cursive::views::NamedView; +use cursive_table_view::TableView; +use cursive_table_view::TableViewItem; +use chrono::naive::NaiveDateTime; +use notmuch::Message; +use notmuch::MessageOwner; +use getset::Getters; +use cursive::views::ResizedView; +use cursive::view::SizeConstraint; + +use crate::views::main::MainView; +use crate::views::mail::MailView; + +pub struct MaillistView { + view: ResizedView<TableView<MailListingData, MailListingColumn>>, +} + +impl Deref for MaillistView { + type Target = TableView<MailListingData, MailListingColumn>; + fn deref(&self) -> &Self::Target { + self.view.get_inner() + } +} + +impl MaillistView { + pub fn create_for(database_path: PathBuf, query: &str, name: String) -> Result<NamedView<Self>> { + debug!("Getting '{}' from '{}'", query, database_path.display()); + + fn get_header_field_save<'o, O: MessageOwner + 'o>(msg: &Message<'o, O>, field: &str) -> String { + match msg.header(field) { + Err(e) => { + error!("Failed getting '{}' of '{}': {}", field, msg.id(), e); + String::from("---") + }, + + Ok(None) => format!("No Value for {}", field), + Ok(Some(f)) => f.to_string(), + } + } + + let items = notmuch::Database::open(&database_path, notmuch::DatabaseMode::ReadOnly) + .context(format!("Opening database {}", database_path.display()))? + .create_query(query) + .context("Creating the search query")? + .search_messages() + .context(format!("Searching for messages with '{}'", query))? + .map(|msg| { + let mail_id = msg.id().to_string(); + let filename = msg.filename(); + let tags = msg.tags().collect(); + let date = NaiveDateTime::from_timestamp_opt(msg.date(), 0) + .map(|ndt| ndt.to_string()) + .ok_or_else(|| { + error!("Failed to parse timestamp: {}", msg.date()); + anyhow!("Failed to parse timestamp: {}", msg.date()) + }) + .context(format!("Getting the date of message {}", msg.id()))?; + + let from = get_header_field_save(&msg, "From"); + let to = get_header_field_save(&msg, "To"); + let subject = get_header_field_save(&msg, "Subject"); + + Ok(MailListingData { + mail_id, + filename, + tags, + date, + from, + to, + subject, + }) + }) + .collect::<Result<Vec<_>>>() + .context(format!("Creating MaillinglistView for '{}' on {}", query, database_path.display()))?; + + debug!("Found {} entries", items.len()); + let n = name.clone(); + let db_path = database_path.clone(); + let view = TableView::<MailListingData, MailListingColumn>::new() + .column(MailListingColumn::Date, "Date", |c| c.width(20)) + .column(MailListingColumn::Tags, "Tags", |c| c.width(20)) + .column(MailListingColumn::From, "From", |c| c) + .column(MailListingColumn::To, "To", |c| c) + .column(MailListingColumn::Subject, "Subject", |c| c) + .default_column(MailListingColumn::Date) + .items(items) + .selected_item(0) + .on_submit(move |siv: &mut Cursive, row: usize, _: usize| { + let (mail_id, filename) = siv.call_on_name(&n, move |table: &mut MaillistView| { + table.view + .get_inner_mut() + .borrow_item(row) + .map(|data| { + debug!("Opening: {:?}", data); + (data.mail_id.clone(), data.filename.clone()) + }) + }) + .unwrap() + .unwrap(); + + debug!("Showing mail {}", mail_id); + + // Why do I have to do this? This is UGLY! + let n = n.clone(); + let db_path = db_path.clone(); + let name = format!("{}-{}", n, mail_id); + let mv = MailView::create_for(db_path, mail_id, filename, name).unwrap(); + + siv.call_on_name(crate::views::main::MAIN_MUX_NAME, move |mux: &mut cursive_multiplex::Mux| { + mux.add_right_of(mv, mux.root().build().unwrap()); + }); + + // use the mail ID to get the whole thread and open it as a table item + }); + + Ok(MaillistView { view: ResizedView::new(SizeConstraint::Full, SizeConstraint::Full, view )}.with_name(name)) + } + + pub fn borrow_item(&mut self, idx: usize) -> Option<&MailListingData> { + self.view.get_inner_mut().borrow_item(idx) + } +} + +impl View for MaillistView { + fn draw(&self, printer: &Printer) { + self.view.draw(printer) + } + + fn layout(&mut self, xy: XY<usize>) { + self.view.layout(xy) + } + + fn needs_relayout(&self) -> bool { + self.view.needs_relayout() + } + + fn required_size(&mut self, constraint: XY<usize>) -> XY<usize> { + self.view.required_size(constraint) + } + + fn on_event(&mut self, e: Event) -> EventResult { + self.view.on_event(e) + } + + fn call_on_any<'a>(&mut self, s: &Selector, tpl: &'a mut (dyn FnMut(&mut (dyn View + 'static)) + 'a)) { + self.view.call_on_any(s, tpl); + } + + fn focus_view(&mut self, s: &Selector) -> Result<(), ()> { + self.view.focus_view(s) + } + + fn take_focus(&mut self, source: Direction) -> bool { + self.view.take_focus(source) + } + + fn important_area(&self, view_size: XY<usize>) -> Rect { + self.view.important_area(view_size) + } + + fn type_name(&self) -> &'static str { + self.view.type_name() + } + +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub enum MailListingColumn { + Date, + Tags, + From, + To, + Subject, +} + +#[derive(Clone, Debug, Getters)] +pub struct MailListingData { + #[getset(get = "pub")] + mail_id: String, + + #[getset(get = "pub")] + filename: PathBuf, + + #[getset(get = "pub")] + tags: Vec<String>, + + #[getset(get = "pub")] + date: String, + + #[getset(get = "pub")] + from: String, + + #[getset(get = "pub")] + to: String, + + #[getset(get = "pub")] + subject: String, +} + +impl TableViewItem<MailListingColumn> for MailListingData { + + fn to_column(&self, column: MailListingColumn) -> String { + match column { + MailListingColumn::Date => self.date.clone(), + MailListingColumn::Tags => self.tags.join(", "), + MailListingColumn::From => self.from.clone(), + MailListingColumn::To => self.to.clone(), + MailListingColumn::Subject => self.subject.clone(), + } + } + + fn cmp(&self, other: &Self, column: MailListingColumn) -> std::cmp::Ordering + where Self: Sized + { + match column { + MailListingColumn::Date => self.date.cmp(&other.date), + MailListingColumn::Tags => self.tags.cmp(&other.tags), + MailListingColumn::From => self.from.cmp(&other.from), + MailListingColumn::To => self.to.cmp(&other.to), + MailListingColumn::Subject => self.subject.cmp(&other.subject), + } + } + +} + diff --git a/src/views/main.rs b/src/views/main.rs new file mode 100644 index 0000000..af9dcea --- /dev/null +++ b/src/views/main.rs @@ -0,0 +1,192 @@ +use anyhow::Result; +use cursive::Cursive; +use cursive::Printer; +use cursive::Rect; +use cursive::View; +use cursive::XY; +use cursive::direction::Direction; +use cursive::event::Callback; +use cursive::event::Event; +use cursive::event::EventResult; +use cursive::view::Nameable; +use cursive::view::Selector; +use cursive::views::Dialog; +use cursive::views::EditView; +use cursive::views::NamedView; +use cursive::views::ResizedView; +use cursive::traits::Resizable; +use cursive_multiplex::Mux; +use cursive::view::SizeConstraint; +use cursive::event::Key; +use getset::{Getters, MutGetters}; + +use crate::bindings::BindingCaller; +use crate::bindings::Bindings; +use crate::configuration::Configuration; +use crate::views::mail::MailView; +use crate::views::maillist::MailListingData; +use crate::views::maillist::MaillistView; + +pub const MAIN_VIEW_NAME: &'static str = "main_view"; +pub const MAIN_MUX_NAME: &'static str = "main_mux"; +pub const MAIN_MAIL_LIST_NAME: &'static str = "main_mail_list"; + +#[derive(Getters, MutGetters)] +pub struct MainView { + config: Configuration, + muxroot: cursive_multiplex::Id, + + #[getset(get = "pub", get_mut = "pub")] + tabs: cursive_tabs::TabPanel<String>, + + bindings: Bindings, + bindings_caller: Option<ResizedView<NamedView<BindingCaller>>>, +} + +impl View for MainView { + fn draw(&self, printer: &Printer) { + self.tabs.draw(printer); + if let Some(caller) = self.bindings_caller.as_ref() { + caller.draw(printer); + } + } + + fn layout(&mut self, xy: XY<usize>) { + self.tabs.layout(xy) + } + + fn needs_relayout(&self) -> bool { + self.tabs.needs_relayout() + } + + fn required_size(&mut self, constraint: XY<usize>) -> XY<usize> { + self.tabs.required_size(constraint) + } + + fn on_event(&mut self, e: Event) -> EventResult { + debug!("Received event: {:?}", e); + match self.bindings_caller.as_mut() { + Some(caller) => match e { + Event::Key(Key::Esc) => { + self.bindings_caller = None; + debug!("Escape. Resetting bindings caller."); + EventResult::Consumed(None) + }, + other => { + debug!("Forwarding event to bindings caller"); + caller.on_event(other) + }, + }, + None => match e { + Event::Char(':') => { + debug!(": -> Constructing bindings caller."); + self.bindings_caller = Some({ + self.bindings.caller() + .with_name(crate::bindings::BINDINGS_CALLER) + .resized(SizeConstraint::Full, SizeConstraint::AtLeast(5)) + }); + EventResult::Consumed(None) + }, + other => { + debug!("Forwarding event to tabs"); + self.tabs.on_event(other) + }, + } + } + } + + fn call_on_any<'a>(&mut self, s: &Selector, tpl: &'a mut (dyn FnMut(&mut (dyn View + 'static)) + 'a)) { + self.tabs.call_on_any(s, tpl); + } + + fn focus_view(&mut self, s: &Selector) -> Result<(), ()> { + self.tabs.focus_view(s) + } + + fn take_focus(&mut self, source: Direction) -> bool { + self.tabs.take_focus(source) + } + + fn important_area(&self, view_size: XY<usize>) -> Rect { + self.tabs.important_area(view_size) + } + + fn type_name(&self) -> &'static str { + self.tabs.type_name() + } + +} + +impl MainView { + pub fn new(config: Configuration) -> Result<NamedView<Self>> { + let mut tab = cursive_multiplex::Mux::new(); + let muxroot = tab.root().build().unwrap(); + + { + let dbpath = config.notmuch_database_path(); + let dbquery = config.notmuch_default_query(); + let mlname = MAIN_MAIL_LIST_NAME.to_string(); + let view = MaillistView::create_for(dbpath.to_path_buf(), dbquery, mlname)?; + + let _ = tab.add_right_of(view, muxroot); + } + + let tabs = cursive_tabs::TabPanel::default() + .with_bar_alignment(cursive_tabs::Align::Start) + .with_bar_placement(cursive_tabs::Placement::HorizontalTop) + .with_tab(config.notmuch_default_query().clone(), tab.with_name(MAIN_MUX_NAME)); + + let bindings = crate::bindings::get_bindings(); + + Ok(MainView { config, muxroot, tabs, bindings, bindings_caller: None }.with_name(MAIN_VIEW_NAME)) + } + + pub fn add_tab<T: View>(&mut self, id: String, view: T) { + self.tabs.add_tab(id, view) + } + + pub fn config(&self) -> &Configuration { + &self.config + } + + pub fn add_notmuch_query_layer(siv: &mut Cursive) { + use crate::util::dialog_for; + use crate::util::error_dialog_for; + + let edit_view = EditView::new() + .on_submit(move |siv: &mut Cursive, query: &str| { + siv.call_on_name(MAIN_VIEW_NAME, move |main_view: &mut MainView| { + main_view.config().notmuch_database_path().clone() + }) + .map(|dbpath| { + let t = MaillistView::create_for(dbpath.to_path_buf(), query, query.to_string())? + .full_screen() + .with_name(format!("{}-view", query)); + + siv.call_on_name(MAIN_VIEW_NAME, move |main_view: &mut MainView| { + main_view.add_tab(query.to_string(), t); + }); + + siv.pop_layer(); + Ok(()) + }) + .unwrap_or_else(|| { + siv.pop_layer(); + siv.add_layer(dialog_for("Failed to get database connection set up")); + Ok(()) + }) + .unwrap_or_else(|e: anyhow::Error| { + siv.pop_layer(); + siv.add_layer(error_dialog_for(e)) + }); + }) + .with_name("query"); + + siv.add_layer({ + Dialog::around(edit_view) + .title("Query") + .min_width(80) + }) + } + +} diff --git a/src/views/mod.rs b/src/views/mod.rs new file mode 100644 index 0000000..9053bf1 --- /dev/null +++ b/src/views/mod.rs @@ -0,0 +1,3 @@ +pub mod maillist; +pub mod mail; +pub mod main; |