summaryrefslogtreecommitdiffstats
path: root/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/views')
-rw-r--r--src/views/mail.rs122
-rw-r--r--src/views/maillist.rs240
-rw-r--r--src/views/main.rs192
-rw-r--r--src/views/mod.rs3
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;