From 508934c50c38a17acce505e6a8d39391d6fc11d2 Mon Sep 17 00:00:00 2001 From: FliegendeWurst <2012gdwu+github@posteo.de> Date: Wed, 7 Apr 2021 16:03:34 +0200 Subject: [PATCH] browse: basic config system --- Cargo.lock | 66 ++++++++++++++++++++++++- Cargo.toml | 12 +++-- src/bin/browse.rs | 42 +++++++++++++++- src/lib.rs | 120 +++++++++++++++++++++++++++++++++++++++------- 4 files changed, 217 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 782665d..f52e739 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,16 @@ dependencies = [ "syn 0.11.11", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -450,18 +460,25 @@ dependencies = [ "chrono", "cursive", "cursive_tree_view", + "directories-next", "imap", "itertools", + "log", "maildir", "mailparse", "mailproc", "mime2ext", "moins", + "once_cell", + "parking_lot", "petgraph", "rusqlite", "rustls-connector", "rustyline", + "serde", + "serde_derive", "subprocess", + "toml", ] [[package]] @@ -474,6 +491,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if", +] + [[package]] name = "itertools" version = "0.10.0" @@ -526,13 +552,23 @@ checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714" [[package]] name = "libsqlite3-sys" version = "0.22.0" -source = "git+https://github.com/rusqlite/rusqlite?branch=master#ddf69f749a67fdb673837ebb3c881a522fd7f638" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f6332d94daa84478d55a6aa9dbb3b305ed6500fb0cb9400cb9e1525d0e0e188" dependencies = [ "cc", "pkg-config", "vcpkg", ] +[[package]] +name = "lock_api" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.14" @@ -733,6 +769,31 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + [[package]] name = "petgraph" version = "0.5.1" @@ -852,7 +913,8 @@ dependencies = [ [[package]] name = "rusqlite" version = "0.25.0" -source = "git+https://github.com/rusqlite/rusqlite?branch=master#ddf69f749a67fdb673837ebb3c881a522fd7f638" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48381bf52627e7b0e02c4c0e4c0c88fc1cf2228a2eb7461d9499b1372399f1da" dependencies = [ "bitflags", "fallible-iterator", diff --git a/Cargo.toml b/Cargo.toml index 074c76d..448c044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,7 @@ mailparse = "0.13.2" rustls-connector = "0.13.1" ascii_table = { git = "https://gitlab.com/arnekeller/ascii-table.git", branch = "master" } chrono = "0.4.19" -# remove when 0.24.3 is released -rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch = "master", features = ["bundled"] } +rusqlite = { version = "0.25.0", features = ["bundled"] } rustyline = "8.0.0" moins = { git = "https://github.com/FliegendeWurst/moins", branch = "master" } anyhow = "1.0.40" @@ -26,6 +25,13 @@ mime2ext = "0.1.2" petgraph = "0.5.1" cursive = { version = "0.16.3", default-features = false, features = ["termion-backend"] } cursive_tree_view = { git = "https://github.com/FliegendeWurst/cursive_tree_view.git", branch = "master" } +directories-next = "2.0.0" +serde_derive = "1.0.25" +serde = "1.0.25" +toml = "0.5.8" +once_cell = "1.7.2" +parking_lot = "0.11.1" +log = "0.4.14" [profile.release] -overflow-checks = true +overflow-checks = true # useful when debugging diff --git a/src/bin/browse.rs b/src/bin/browse.rs index 6153a3d..84c2a38 100644 --- a/src/bin/browse.rs +++ b/src/bin/browse.rs @@ -3,17 +3,21 @@ use std::{array::IntoIter, cell::RefCell, cmp, collections::{HashMap, HashSet}, env, fmt::Display, io, sync::{Arc, Mutex}}; use cursive::{Cursive, CursiveExt}; +use cursive::event::{Event, Key}; use cursive::traits::Identifiable; -use cursive::view::{Scrollable, SizeConstraint}; -use cursive::views::{LinearLayout, ResizedView, TextView}; +use cursive::view::{Scrollable, SizeConstraint, View}; +use cursive::views::{Button, Checkbox, LinearLayout, OnEventView, ResizedView, TextView}; use cursive_tree_view::{Placement, TreeEntry, TreeView}; use inboxid::*; use io::Write; use itertools::Itertools; +use log::error; use mailparse::ParsedMail; +use parking_lot::RwLock; use petgraph::{EdgeDirection, graph::{DiGraph, NodeIndex}, visit::{Dfs, IntoNodeReferences}}; fn main() -> Result<()> { + load_config(); let sink = Arc::new(Mutex::new(Vec::new())); std::io::set_output_capture(Some(sink.clone())); let result = std::panic::catch_unwind(|| { @@ -238,6 +242,40 @@ fn show_listing(mailbox: &str) -> Result<()> { .child(mail_content_resized); siv.add_fullscreen_layer(ResizedView::with_full_screen(main)); + let mut setup = LinearLayout::vertical(); + { + let config = CONFIG.get().unwrap().read(); + let show_email_addresses = Checkbox::new() + .with_checked(config.browse.show_email_addresses) + .on_change(|_siv, checked| { + CONFIG.get().unwrap().write().browse.show_email_addresses = checked; + }); + setup.add_child( + LinearLayout::horizontal() + .child(show_email_addresses) + .child(TextView::new(" Show email addresses")) + ); + } + // most horrible hack + let setup: Arc>>> = Arc::new(RwLock::new(Some(Box::new(ResizedView::new(SizeConstraint::Full, SizeConstraint::Full, setup))))); + let setup2 = Arc::clone(&setup); + let setup_view: ResizedView = *setup.write().take().unwrap().as_boxed_any().downcast().unwrap(); + let setup_view = OnEventView::new(setup_view) + .on_event(Event::Key(Key::F10), move |s| { + let setup = s.pop_layer().unwrap(); + *setup2.write() = Some(setup); + if let Err(e) = CONFIG.get().unwrap().read().save() { + error!("failed to save config {:?}", e); + } + }); + *setup.write() = Some(Box::new(setup_view)); + + let setup2 = Arc::clone(&setup); + siv.add_global_callback(Event::Key(Key::F2), move |s| { + let setup = setup2.write().take().unwrap(); + s.add_fullscreen_layer(setup); + }); + siv.add_global_callback('q', |s| s.quit()); siv.run(); diff --git a/src/lib.rs b/src/lib.rs index e313ba7..8730405 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,20 @@ -use std::{borrow::Cow, cmp, convert::{TryFrom, TryInto}, env, fmt::{Debug, Display}, fs, hash::Hash, io, net::TcpStream, ops::Deref}; +use std::{borrow::Cow, cmp, convert::{TryFrom, TryInto}, env, fmt::{Debug, Display}, fs, hash::Hash, io, net::TcpStream, ops::Deref, path::PathBuf}; use anyhow::Context; use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; use cursive::{theme::Style, utils::span::{IndexedCow, IndexedSpan, SpannedString}}; use cursive_tree_view::TreeEntry; +use directories_next::ProjectDirs; use imap::{Session, types::Flag}; +use log::info; use maildir::{MailEntry, Maildir}; use mailparse::{MailHeaderMap, ParsedMail, SingleInfo, addrparse, dateparse}; +use once_cell::sync::OnceCell; +use parking_lot::RwLock; use petgraph::{Graph, graph::NodeIndex}; use rusqlite::{Connection, params}; use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}}; +use serde_derive::{Deserialize, Serialize}; pub type Result = std::result::Result>; pub type ImapSession = Session>; @@ -107,14 +112,14 @@ pub struct EasyMail<'a> { impl EasyMail<'_> { pub fn new_pseudo(subject: String) -> Self { Self { - mail: None, - id: MaildirID::new(0, 0), + mail: None, + id: MaildirID::new(0, 0), flags: "S".to_owned(), from: SingleInfo { display_name: None, addr: String::new() }, - subject, + subject, date: Local.from_utc_datetime(&NaiveDateTime::from_timestamp(0, 0)), date_iso: "????-??-??".to_owned() } @@ -125,7 +130,13 @@ impl EasyMail<'_> { } pub fn from(&self) -> String { - self.from.display_name.as_deref().unwrap_or_default().to_owned() + let name = self.from.display_name.as_deref().unwrap_or_default(); + if let Some(config) = CONFIG.get() { + if config.read().browse.show_email_addresses { + return format!("{} <{}>", name, self.from.addr); + } + } + name.to_owned() } pub fn get_header(&self, header: &str) -> String { @@ -138,32 +149,32 @@ impl EasyMail<'_> { } impl Debug for EasyMail<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Mail[ID={},Subject={:?}]", self.id.uid, self.subject) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Mail[ID={},Subject={:?}]", self.id.uid, self.subject) + } } impl Display for EasyMail<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.subject) - } + write!(f, "{}", self.subject) + } } impl PartialEq for EasyMail<'_> { - fn eq(&self, other: &Self) -> bool { - self.id == other.id && self.from == other.from && self.subject == other.subject - } + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.from == other.from && self.subject == other.subject + } } impl Eq for EasyMail<'_> {} impl Hash for EasyMail<'_> { - fn hash(&self, state: &mut H) { - self.id.hash(state); + fn hash(&self, state: &mut H) { + self.id.hash(state); self.from.display_name.hash(state); self.from.addr.hash(state); self.subject.hash(state); - } + } } impl<'a> Deref for EasyMail<'a> { @@ -387,3 +398,80 @@ pub fn get_imap_session() -> Result { let port = 993; connect(&host, port, &user, &password) } + +pub fn load_config() { + CONFIG.get_or_init(|| { + let config = Config::load_from_fs(); + let cfg = match config { + Ok(config) => if let Some(config) = config { + config.into() + } else { + Config::default().into() + }, + Err(e) => panic!("failed to load configuration: {:?}", e) + }; + info!("config {:?}", cfg); + cfg + }); +} + +pub static CONFIG: OnceCell> = OnceCell::new(); + +#[derive(Deserialize, Serialize, Debug)] +pub struct Config { + #[serde(default)] + pub browse: Browse +} + +fn get_paths() -> Result { + Ok(directories_next::ProjectDirs::from("", "", "Inboxid").context("unable to determine configuration directory")?) +} + +fn get_config_path() -> Result { + let paths = get_paths()?; + Ok(paths.config_dir().join("config.toml")) +} + +impl Config { + fn load_from_fs() -> Result> { + let config = get_config_path()?; + if config.exists() { + let content = fs::read_to_string(&config)?; + Ok(Some(toml::from_str(&content)?)) + } else { + Ok(None) + } + } + + pub fn save(&self) -> Result<()> { + let config = get_config_path()?; + fs::create_dir_all(config.parent().unwrap())?; + fs::write(config, toml::to_string(&self)?)?; + Ok(()) + } +} + +impl Default for Config { + fn default() -> Self { + Self { + browse: Browse { + show_email_addresses: false + } + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct Browse { + #[serde(default)] + pub show_email_addresses: bool +} + +impl Default for Browse { + fn default() -> Self { + Self { + show_email_addresses: Default::default() + } + } +}