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::main_view::MainView; use crate::mail_view::MailView; pub struct MaillistView { view: ResizedView>, } impl Deref for MaillistView { type Target = TableView; fn deref(&self) -> &Self::Target { self.view.get_inner() } } impl MaillistView { pub fn create_for(database_path: PathBuf, query: &str, name: String) -> Result> { 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::>>() .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::::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(); siv.call_on_name(crate::main_view::MAIN_VIEW_NAME, move |main_view: &mut MainView| { let name = format!("{}-{}", n, mail_id); debug!("Creating MailView '{}' for {} ({}) in {}", name, mail_id, filename.display(), db_path.display()); let mv = MailView::create_for(db_path, mail_id, filename, name.clone()).unwrap(); main_view.add_tab(name, mv); }); // 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) { self.view.layout(xy) } fn needs_relayout(&self) -> bool { self.view.needs_relayout() } fn required_size(&mut self, constraint: XY) -> XY { 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) -> 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, #[getset(get = "pub")] date: String, #[getset(get = "pub")] from: String, #[getset(get = "pub")] to: String, #[getset(get = "pub")] subject: String, } impl TableViewItem 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), } } }