browse: basic config system

This commit is contained in:
FliegendeWurst 2021-04-07 16:03:34 +02:00 committed by Arne Keller
parent 7d2e6de827
commit 508934c50c
4 changed files with 217 additions and 23 deletions

66
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -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<RwLock<Option<Box<dyn View>>>> = Arc::new(RwLock::new(Some(Box::new(ResizedView::new(SizeConstraint::Full, SizeConstraint::Full, setup)))));
let setup2 = Arc::clone(&setup);
let setup_view: ResizedView<LinearLayout> = *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();

View File

@ -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<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub type ImapSession = Session<StreamOwned<ClientSession, TcpStream>>;
@ -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<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
fn hash<H: std::hash::Hasher>(&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<ImapSession> {
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<RwLock<Config>> = OnceCell::new();
#[derive(Deserialize, Serialize, Debug)]
pub struct Config {
#[serde(default)]
pub browse: Browse
}
fn get_paths() -> Result<ProjectDirs> {
Ok(directories_next::ProjectDirs::from("", "", "Inboxid").context("unable to determine configuration directory")?)
}
fn get_config_path() -> Result<PathBuf> {
let paths = get_paths()?;
Ok(paths.config_dir().join("config.toml"))
}
impl Config {
fn load_from_fs() -> Result<Option<Self>> {
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()
}
}
}