diff --git a/src/bin/browse.rs b/src/bin/browse.rs index ea13894..8b291fa 100644 --- a/src/bin/browse.rs +++ b/src/bin/browse.rs @@ -3,10 +3,11 @@ use std::{array::IntoIter, cell::RefCell, cmp, collections::{HashMap, HashSet}, env, fmt::Display, io, sync::{Arc, Mutex}}; use cursive::{Cursive, CursiveExt, Vec2}; +use cursive::align::HAlign; use cursive::event::{Event, Key}; use cursive::traits::Identifiable; use cursive::view::{Scrollable, SizeConstraint, View}; -use cursive::views::{Checkbox, LinearLayout, OnEventView, Panel, ResizedView, TextView}; +use cursive::views::{Checkbox, LinearLayout, OnEventView, Panel, ResizedView, SelectView, TextView}; use cursive_tree_view::{Placement, TreeEntry, TreeView}; use inboxid::*; use io::Write; @@ -261,9 +262,20 @@ fn show_listing(mailbox: &str) -> Result<()> { .child(show_email_addresses) .child(TextView::new(" Show email addresses")) ); + let mut style_select = SelectView::new().h_align(HAlign::Left); + let values = ["simple", "reverse", "bold", "italic", "strikethrough", "underline", "blink"]; + for &x in &values { + style_select.add_item(x, x); + } + let current = style_to_str(&config.browse.unread_style); + style_select.set_selection(values.iter().position(|&x| x == current).unwrap()); + style_select.set_on_select(|_s, style| { + CONFIG.get().unwrap().write().browse.unread_style = parse_effect(style).unwrap().into(); + }); + setup.add_child(ResizedView::new(SizeConstraint::AtLeast(28), SizeConstraint::Free, Panel::new(style_select).title("Unread message styling"))); } // most horrible hack - let setup: Arc>>> = Arc::new(RwLock::new(Some(Box::new(ResizedView::new(SizeConstraint::Full, SizeConstraint::Full, setup))))); + let setup: Arc>>> = Arc::new(RwLock::new(Some(Box::new(ResizedView::new(SizeConstraint::Free, 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) @@ -274,7 +286,7 @@ fn show_listing(mailbox: &str) -> Result<()> { error!("failed to save config {:?}", e); } }); - *setup.write() = Some(Box::new(setup_view)); + *setup.write() = Some(Box::new(Panel::new(setup_view).title("Settings"))); let setup2 = Arc::clone(&setup); siv.add_global_callback(Event::Key(Key::F2), move |s| { diff --git a/src/lib.rs b/src/lib.rs index 8730405..a3df0bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ -use std::{borrow::Cow, cmp, convert::{TryFrom, TryInto}, env, fmt::{Debug, Display}, fs, hash::Hash, io, net::TcpStream, ops::Deref, path::PathBuf}; +use std::{borrow::Cow, 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::{theme::{Effect, Style}, utils::span::{IndexedCow, IndexedSpan, SpannedString}}; use cursive_tree_view::TreeEntry; use directories_next::ProjectDirs; use imap::{Session, types::Flag}; @@ -14,6 +14,8 @@ use parking_lot::RwLock; use petgraph::{Graph, graph::NodeIndex}; use rusqlite::{Connection, params}; use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}}; +use serde::{Deserializer, Serializer}; +use serde::de::Visitor; use serde_derive::{Deserialize, Serialize}; pub type Result = std::result::Result>; @@ -209,13 +211,14 @@ impl TreeEntry for &EasyMail<'_> { line.push(' '); line += &self.date_iso; + let style = if self.flags.contains('S') { Style::default() } else { CONFIG.get().unwrap().read().browse.unread_style }; let spans = vec![ IndexedSpan { content: IndexedCow::Borrowed { start: 0, end: subj_len }, - attr: Style::default(), + attr: style, width: subj_len }, IndexedSpan { @@ -223,7 +226,7 @@ impl TreeEntry for &EasyMail<'_> { start: 0, end: 0 }, - attr: Style::default(), + attr: style, width: line.len() - subj_len - from.len() - self.date_iso.len() - 1 }, IndexedSpan { @@ -231,7 +234,7 @@ impl TreeEntry for &EasyMail<'_> { start: line.len() - self.date_iso.len() - 1 - from.len(), end: line.len() - self.date_iso.len() - 1 }, - attr: Style::default(), + attr: style, width: from.len() }, IndexedSpan { @@ -239,7 +242,7 @@ impl TreeEntry for &EasyMail<'_> { start: 0, end: 0 }, - attr: Style::default(), + attr: style, width: 1 }, IndexedSpan { @@ -247,7 +250,7 @@ impl TreeEntry for &EasyMail<'_> { start: line.len() - self.date_iso.len(), end: line.len() }, - attr: Style::default(), + attr: style, width: self.date_iso.len() }, ]; @@ -454,9 +457,7 @@ impl Config { impl Default for Config { fn default() -> Self { Self { - browse: Browse { - show_email_addresses: false - } + browse: Browse::default() } } } @@ -465,13 +466,71 @@ impl Default for Config { #[serde(rename_all = "kebab-case")] pub struct Browse { #[serde(default)] - pub show_email_addresses: bool + pub show_email_addresses: bool, + #[serde(default = "default_unread_style")] + #[serde(deserialize_with = "deserialize_style")] + #[serde(serialize_with = "serialize_style")] + pub unread_style: Style, } impl Default for Browse { fn default() -> Self { Self { - show_email_addresses: Default::default() + show_email_addresses: Default::default(), + unread_style: default_unread_style() } } } + +pub fn style_to_str(x: &Style) -> &'static str { + match x.effects.iter().next() { + Some(x) => match x { + Effect::Simple => "simple", + Effect::Reverse => "reverse", + Effect::Bold => "bold", + Effect::Italic => "italic", + Effect::Strikethrough => "strikethrough", + Effect::Underline => "underline", + Effect::Blink => "blink" + }, + None => "none" + } +} + +fn serialize_style(x: &Style, s: S) -> std::result::Result where S: Serializer { + s.serialize_str(style_to_str(x)) +} + +fn deserialize_style<'de, D>(de: D) -> std::result::Result where D: Deserializer<'de> { + struct StrVisitor; + impl<'de> Visitor<'de> for StrVisitor { + type Value = Style; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("style specification") + } + + fn visit_str(self, v: &str) -> std::result::Result { + parse_effect(v).map(Into::into).ok_or(serde::de::Error::invalid_value(serde::de::Unexpected::Str(v), &self)) + } + } + let vis = StrVisitor; + de.deserialize_str(vis) +} + +pub fn parse_effect(effect: &str) -> Option { + match effect { + "simple" => Some(Effect::Simple), + "reverse" => Some(Effect::Reverse), + "bold" => Some(Effect::Bold), + "italic" => Some(Effect::Italic), + "strikethrough" => Some(Effect::Strikethrough), + "underline" => Some(Effect::Underline), + "blink" => Some(Effect::Blink), + _ => None + } +} + +fn default_unread_style() -> Style { + Effect::Reverse.into() +}