Compare commits

...

30 Commits

Author SHA1 Message Date
Manos Pitsidianakis fead7a5da4
meli: add invalid flag combo check 2020-02-09 02:56:39 +02:00
Manos Pitsidianakis 962283f9fe
Add opt-level=z flag for release profile 2020-02-09 02:56:13 +02:00
Manos Pitsidianakis 63cdf1a38f
debian/: add mandoc build dependency 2020-02-09 02:46:27 +02:00
Manos Pitsidianakis 0aa2659072
meli: add cli-docs feature
Optionally build manpages to text with mandoc and print them from the
command line.
2020-02-09 02:26:21 +02:00
Manos Pitsidianakis c22a141b14
ui/themes: expand theme coverage to status panel and contacts 2020-02-09 00:30:50 +02:00
Manos Pitsidianakis 22fb0c0844
ui: handle ViewMailbox in listing.rs
handling viewmailbox inside a listing instead of their parent/manager
component is a leftover from before they even had a parent/manager.
2020-02-08 23:56:08 +02:00
Manos Pitsidianakis 647cb10b33
ui: Use FolderHash instead of usize for folder cursor
Use FolderHash directly as a cursor type for folders within an account
isntead of having a usize (being the order of the folder within the
account) and figuring out the folder_hash everytime it's needed.

Add OfflineListing for offline accounts and AccountStatusChange event.
2020-02-08 23:56:08 +02:00
Manos Pitsidianakis 42747ef590
ui/themes: make theme_default the default for other keys 2020-02-08 23:56:08 +02:00
Manos Pitsidianakis eef007600b
ui: improve theming coverage 2020-02-08 23:56:08 +02:00
Manos Pitsidianakis 9b7875c023
ui: change Component::get_status return type
There was no reason to return Option<String>, just return String::new()
instead of Option::None
2020-02-08 23:56:08 +02:00
Manos Pitsidianakis cadb1e1613
ui/conf: expand include() paths in config
Expand variables and `~` in included paths in user configuration.
2020-02-08 23:56:08 +02:00
Manos Pitsidianakis 0b4109dfdb
ui: fix wrong subscription status in folders
Subscription status was checked/modified in various places, whereas now
the universal truth is the `BackendFolder::is_subscribed()` method set
by the backend when a folder is created. The `Account` struct passes a
closure to the backend constructor that determines whether the folder is subscribed or not according to the user configuration.

- If subscribed_folders field is empty, then all folders are subscribed.
- OR check explicit folder configuration
- OR check if folder path matches to a glob in subscribed_folders.
2020-02-08 23:56:08 +02:00
Manos Pitsidianakis 9616fbb544
melib/maildir: fix wrong subscription status in folders
MaildirFolder::new() was checking for subscribed status though that is
supposed to be done in MaildirType::new()
2020-02-08 23:55:47 +02:00
Manos Pitsidianakis b107424258
melib: update GlobMatch algorithm
Taken from https://research.swtch.com/glob
2020-02-08 23:55:47 +02:00
Manos Pitsidianakis 50bfed7247
ui: fix subtraction overflow 2020-02-08 23:55:47 +02:00
Manos Pitsidianakis 6b7dea35dc
melib/parser: fix minor encoded word error 2020-02-08 23:55:47 +02:00
Manos Pitsidianakis 6afac835e0
melib/datetime: fix overflow panic on early date input 2020-02-08 23:55:47 +02:00
Manos Pitsidianakis eb501b6d50
ui: add ThemeAttribute argument to clear_area()
clear_area() sets the cleared cell attributes according to the new
argument.
2020-02-08 23:54:15 +02:00
Manos Pitsidianakis 3bca6d1d9c
ui: add floating notifications within terminal
`DisplayMessage` messages are for user input responses (eg errors for
user actions). They now appear as floating boxes in the bottom right
corner of the UI and can be browsed with Alt('<') and Alt('>')
2020-02-08 23:54:15 +02:00
Manos Pitsidianakis 4a4c8e265a
ui: add overlay grid
Add second layer grid for overlays (messages, notifications)
2020-02-08 23:54:15 +02:00
Manos Pitsidianakis 333db9ed37
ui: remove notifications from StatusBar
It's bad UX, they aren't very visible.
2020-02-08 23:54:15 +02:00
Manos Pitsidianakis d6e3c51b07
ui: move box drawing to src/terminal
No logical reason for it not to be in the terminal module anymore (the
set_and_join* functions predate the terminal module which is why they
weren't there to begin with).
2020-02-08 23:54:15 +02:00
Manos Pitsidianakis f131e01bfc
Fix drawing getting stuck in empty terminal
Fix drawing getting stuck in loops when terminal is too small by
checking for it.
2020-02-08 23:54:15 +02:00
Manos Pitsidianakis 4301fa3b04
ui: Change ascii branch drawings in attachment tree 2020-02-08 23:54:15 +02:00
Manos Pitsidianakis af38b1306a
ui: use quoted_argument parser in Ex command arguments 2020-02-08 23:54:15 +02:00
Manos Pitsidianakis 144eb62b76
ui: force refresh_mailbox etc on Mailbox{Delete,Create} 2020-02-08 23:54:15 +02:00
Manos Pitsidianakis f5e694cf5a
Make small cosmetic fixes 2020-02-08 23:54:15 +02:00
Manos Pitsidianakis f208948651
melib: add mailbox delete/create to IMAP 2020-02-08 23:54:14 +02:00
Manos Pitsidianakis d6f04c9ed3
Fix IntoIterator warning 2020-02-05 03:41:28 +02:00
Manos Pitsidianakis ad76d4d44d
Check for $TERM in Makefile
If $TERM is not set, for example in a build environment, tput prints out
warnings. Disable ANSI formatting completely when $TERM is unset or zero
2020-02-05 03:40:35 +02:00
48 changed files with 2907 additions and 2156 deletions

View File

@ -48,6 +48,7 @@ smallvec = { version = "1.1.0", features = ["serde", ] }
[profile.release]
lto = true
opt-level = "z"
debug = false
[workspace]
@ -58,6 +59,7 @@ default = ["sqlite3"]
notmuch = ["melib/notmuch_backend", ]
jmap = ["melib/jmap_backend",]
sqlite3 = ["rusqlite"]
cli-docs = []
# Print tracing logs as meli runs in stderr
# enable for debug tracing logs: build with --features=debug-tracing

View File

@ -28,18 +28,18 @@ CARGO_BIN ?= cargo
# Installation parameters
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5
FEATURES ?= --features="$(MELI_FEATURES)"
FEATURES ?= --features "${MELI_FEATURES}"
MANPATHS := `manpath 2> /dev/null`
VERSION ?= `sed -n "s/^version\s*=\s*\"\(.*\)\"/\1/p" Cargo.toml`
# Output parameters
BOLD ?= `tput bold`
UNDERLINE ?= `tput smul`
ANSI_RESET ?= `tput sgr0`
BOLD ?= `[ -z $${TERM} ] && echo "" || tput bold`
UNDERLINE ?= `[ -z $${TERM} ] && echo "" || tput smul`
ANSI_RESET ?= `[ -z $${TERM} ] && echo "" || tput sgr0`
CARGO_COLOR ?= `[ -z $${NO_COLOR+x} ] && echo "" || echo "--color=never "`
RED ?= `[ -z $${NO_COLOR+x} ] && tput setaf 1 || echo ""`
GREEN ?= `[ -z $${NO_COLOR+x} ] && tput setaf 2 || echo ""`
RED ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 1) || echo ""`
GREEN ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 2) || echo ""`
.POSIX:
.SUFFIXES:

66
build.rs 100644
View File

@ -0,0 +1,66 @@
/*
* meli - build.rs
*
* Copyright 2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
fn main() {
#[cfg(feature = "cli-docs")]
{
const MANDOC_OPTS: &[&'static str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use std::process::Command;
let out_dir = env::var("OUT_DIR").unwrap();
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
// Note that there are a number of downsides to this approach, the comments
// below detail how to improve the portability of these commands.
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("meli.1")
.output()
.unwrap();
out_dir_path.push("meli.txt");
let mut file = File::create(&out_dir_path).unwrap();
file.write_all(&output.stdout).unwrap();
out_dir_path.pop();
out_dir_path.push("meli.conf.txt");
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("meli.conf.5")
.output()
.unwrap();
let mut file = File::create(&out_dir_path).unwrap();
file.write_all(&output.stdout).unwrap();
out_dir_path.pop();
out_dir_path.push("meli-themes.txt");
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("meli-themes.5")
.output()
.unwrap();
let mut file = File::create(&out_dir_path).unwrap();
file.write_all(&output.stdout).unwrap();
}
}

5
debian/control vendored
View File

@ -2,13 +2,14 @@ Source: meli
Section: mail
Priority: optional
Maintainer: Manos Pitsidianakis <epilys@nessuent.xyz>
Build-Depends: debhelper (>=11~)
Build-Depends: debhelper (>=11~), mandoc (>=1.14.4-1)
Standards-Version: 4.1.4
Homepage: https://meli.delivery
Package: meli
Architecture: any
Multi-Arch: foreign
Depends: ${misc:Depends}, ${shlibs:Depends}
Depends: ${misc:Depends}, ${shlibs:Depends}, xdg-utils (>=1.1.3-1)
Recommends: libnotmuch5 (>=0.28.4-1)
Description: auto-generated package by debmake
terminal mail client

1
debian/rules vendored
View File

@ -4,6 +4,7 @@
#export DEB_BUILD_MAINT_OPTIONS = hardening=+all
#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic
#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
export MELI_FEATURES = cli-docs sqlite3
%:
dh $@ --with quilt

View File

@ -64,6 +64,17 @@ use std::sync::{Arc, RwLock};
use fnv::FnvHashMap;
use std;
#[macro_export]
macro_rules! get_path_hash {
($path:expr) => {{
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
$path.hash(&mut hasher);
hasher.finish()
}};
}
pub type BackendCreator = Box<
dyn Fn(
&AccountSettings,
@ -232,18 +243,6 @@ impl NotifyFn {
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum FolderOperation {
Create,
Delete,
Subscribe,
Unsubscribe,
Rename(NewFolderName),
SetPermissions(FolderPermissions),
}
type NewFolderName = String;
pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
fn is_online(&self) -> Result<()>;
fn connect(&mut self) {}
@ -264,9 +263,6 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
fn operation(&self, hash: EnvelopeHash) -> Box<dyn BackendOp>;
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()>;
fn create_folder(&mut self, _path: String) -> Result<Folder> {
unimplemented!()
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
None
}
@ -275,6 +271,30 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
fn as_any_mut(&mut self) -> &mut dyn Any {
unimplemented!()
}
fn create_folder(&mut self, _path: String) -> Result<Folder> {
Err(MeliError::new("Unimplemented."))
}
fn delete_folder(&mut self, _folder_hash: FolderHash) -> Result<()> {
Err(MeliError::new("Unimplemented."))
}
fn set_folder_subscription(&mut self, _folder_hash: FolderHash, _val: bool) -> Result<()> {
Err(MeliError::new("Unimplemented."))
}
fn rename_folder(&mut self, _folder_hash: FolderHash, _new_path: String) -> Result<Folder> {
Err(MeliError::new("Unimplemented."))
}
fn set_folder_permissions(
&mut self,
_folder_hash: FolderHash,
_val: FolderPermissions,
) -> Result<()> {
Err(MeliError::new("Unimplemented."))
}
}
/// A `BackendOp` manages common operations for the various mail backends. They only live for the
@ -319,8 +339,6 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send {
fn description(&self) -> String;
fn as_bytes(&mut self) -> Result<&[u8]>;
//fn delete(&self) -> ();
//fn copy(&self
fn fetch_flags(&self) -> Flag;
fn set_flag(&mut self, envelope: &mut Envelope, flag: Flag, value: bool) -> Result<()>;
fn set_tag(&mut self, envelope: &mut Envelope, tag: String, value: bool) -> Result<()>;
@ -536,3 +554,9 @@ impl Default for FolderPermissions {
}
}
}
impl std::fmt::Display for FolderPermissions {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "{:#?}", self)
}
}

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::get_path_hash;
use smallvec::SmallVec;
#[macro_use]
mod protocol_parser;
@ -158,12 +159,12 @@ impl MailBackend for ImapType {
let uid_store = self.uid_store.clone();
let tag_index = self.tag_index.clone();
let can_create_flags = self.can_create_flags.clone();
let folder_path = folder.path().to_string();
let folder_hash = folder.hash();
let (permissions, folder_exists, no_select, unseen) = {
let (permissions, folder_path, folder_exists, no_select, unseen) = {
let f = &self.folders.read().unwrap()[&folder_hash];
(
f.permissions.clone(),
f.imap_path().to_string(),
f.exists.clone(),
f.no_select,
f.unseen.clone(),
@ -374,7 +375,7 @@ impl MailBackend for ImapType {
Box::new(ImapOp::new(
uid,
self.folders.read().unwrap()[&folder_hash]
.path()
.imap_path()
.to_string(),
self.connection.clone(),
self.uid_store.clone(),
@ -400,7 +401,7 @@ impl MailBackend for ImapType {
}
f_result
.map(|v| v.path().to_string())
.map(|v| v.imap_path().to_string())
.ok_or(MeliError::new(format!(
"Folder with name {} not found.",
folder
@ -425,70 +426,6 @@ impl MailBackend for ImapType {
Ok(())
}
/*
fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> {
use FolderOperation::*;
match (
&op,
self.folders
.read()
.unwrap()
.values()
.any(|f| f.path == path),
) {
(Create, true) => {
return Err(MeliError::new(format!(
"Folder named `{}` in account `{}` already exists.",
path, self.account_name,
)));
}
(op, false) if *op != Create => {
return Err(MeliError::new(format!(
"No folder named `{}` in account `{}`",
path, self.account_name,
)));
}
_ => {}
}
let mut response = String::with_capacity(8 * 1024);
match op {
Create => {
let mut conn = self.connection.lock()?;
conn.send_command(format!("CREATE \"{}\"", path,).as_bytes())?;
conn.read_response(&mut response)?;
conn.send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes())?;
conn.read_response(&mut response)?;
}
Rename(dest) => {
let mut conn = self.connection.lock()?;
conn.send_command(format!("RENAME \"{}\" \"{}\"", path, dest).as_bytes())?;
conn.read_response(&mut response)?;
}
Delete => {
let mut conn = self.connection.lock()?;
conn.send_command(format!("DELETE \"{}\"", path,).as_bytes())?;
conn.read_response(&mut response)?;
}
Subscribe => {
let mut conn = self.connection.lock()?;
conn.send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes())?;
conn.read_response(&mut response)?;
}
Unsubscribe => {
let mut conn = self.connection.lock()?;
conn.send_command(format!("UNSUBSCRIBE \"{}\"", path,).as_bytes())?;
conn.read_response(&mut response)?;
}
SetPermissions(_new_val) => {
unimplemented!();
}
}
Ok(())
}
*/
fn as_any(&self) -> &dyn::std::any::Any {
self
}
@ -505,36 +442,191 @@ impl MailBackend for ImapType {
}
}
fn create_folder(&mut self, path: String) -> Result<Folder> {
let mut response = String::with_capacity(8 * 1024);
if self
.folders
.read()
.unwrap()
.values()
.any(|f| f.path == path)
{
fn create_folder(&mut self, mut path: String) -> Result<Folder> {
/* Must transform path to something the IMAP server will accept
*
* Each root mailbox has a hierarchy delimeter reported by the LIST entry. All paths
* must use this delimeter to indicate children of this mailbox.
*
* A new root mailbox should have the default delimeter, which can be found out by issuing
* an empty LIST command as described in RFC3501:
* C: A101 LIST "" ""
* S: * LIST (\Noselect) "/" ""
*
* The default delimiter for us is '/' just like UNIX paths. I apologise if this
* decision is unpleasant for you.
*/
let mut folders = self.folders.write().unwrap();
for root_folder in folders.values().filter(|f| f.parent.is_none()) {
if path.starts_with(&root_folder.name) {
debug!("path starts with {:?}", &root_folder);
path = path.replace(
'/',
(root_folder.separator as char).encode_utf8(&mut [0; 4]),
);
break;
}
}
if folders.values().any(|f| f.path == path) {
return Err(MeliError::new(format!(
"Folder named `{}` in account `{}` already exists.",
path, self.account_name,
)));
}
let mut conn_lck = self.connection.lock()?;
conn_lck.send_command(debug!(format!("CREATE \"{}\"", path,)).as_bytes())?;
conn_lck.read_response(&mut response)?;
conn_lck.send_command(debug!(format!("SUBSCRIBE \"{}\"", path,)).as_bytes())?;
conn_lck.read_response(&mut response)?;
drop(conn_lck);
self.folders.write().unwrap().clear();
self.folders().and_then(|f| {
debug!(f)
.into_iter()
.find(|(_, f)| f.path() == path)
.map(|f| f.1)
.ok_or(MeliError::new(
"Internal error: could not find folder after creating it?",
let mut response = String::with_capacity(8 * 1024);
{
let mut conn_lck = self.connection.lock()?;
conn_lck.send_command(format!("CREATE \"{}\"", path,).as_bytes())?;
conn_lck.read_response(&mut response)?;
conn_lck.send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes())?;
conn_lck.read_response(&mut response)?;
}
let ret: Result<()> = ImapResponse::from(&response).into();
ret?;
folders.clear();
drop(folders);
self.folders().map_err(|err| format!("Mailbox create was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err))?;
let new_hash = get_path_hash!(path.as_str());
Ok(BackendFolder::clone(
&self.folders.read().unwrap()[&new_hash],
))
}
fn delete_folder(&mut self, folder_hash: FolderHash) -> Result<()> {
let mut folders = self.folders.write().unwrap();
let permissions = folders[&folder_hash].permissions();
if !permissions.delete_mailbox {
return Err(MeliError::new(format!("You do not have permission to delete `{}`. Set permissions for this mailbox are {}", folders[&folder_hash].name(), permissions)));
}
let mut response = String::with_capacity(8 * 1024);
{
let mut conn_lck = self.connection.lock()?;
if folders[&folder_hash].is_subscribed() {
conn_lck.send_command(
format!("UNSUBSCRIBE \"{}\"", folders[&folder_hash].imap_path()).as_bytes(),
)?;
conn_lck.read_response(&mut response)?;
}
if !folders[&folder_hash].no_select {
/* make sure mailbox is not selected before it gets deleted, otherwise
* connection gets dropped by server */
if conn_lck
.capabilities
.iter()
.any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT"))
{
conn_lck.send_command(
format!("UNSELECT \"{}\"", folders[&folder_hash].imap_path()).as_bytes(),
)?;
conn_lck.read_response(&mut response)?;
} else {
conn_lck.send_command(
format!("SELECT \"{}\"", folders[&folder_hash].imap_path()).as_bytes(),
)?;
conn_lck.read_response(&mut response)?;
conn_lck.send_command(
format!("EXAMINE \"{}\"", folders[&folder_hash].imap_path()).as_bytes(),
)?;
conn_lck.read_response(&mut response)?;
}
}
conn_lck.send_command(
debug!(format!("DELETE \"{}\"", folders[&folder_hash].imap_path())).as_bytes(),
)?;
conn_lck.read_response(&mut response)?;
}
let ret: Result<()> = ImapResponse::from(&response).into();
if ret.is_ok() {
folders.clear();
drop(folders);
self.folders().map_err(|err| format!("Mailbox delete was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err))?;
}
ret
}
fn set_folder_subscription(&mut self, folder_hash: FolderHash, new_val: bool) -> Result<()> {
let mut folders = self.folders.write().unwrap();
if folders[&folder_hash].is_subscribed() == new_val {
return Ok(());
}
let mut response = String::with_capacity(8 * 1024);
{
let mut conn_lck = self.connection.lock()?;
if new_val {
conn_lck.send_command(
format!("SUBSCRIBE \"{}\"", folders[&folder_hash].imap_path()).as_bytes(),
)?;
} else {
conn_lck.send_command(
format!("UNSUBSCRIBE \"{}\"", folders[&folder_hash].imap_path()).as_bytes(),
)?;
}
conn_lck.read_response(&mut response)?;
}
let ret: Result<()> = ImapResponse::from(&response).into();
if ret.is_ok() {
folders.entry(folder_hash).and_modify(|entry| {
let _ = entry.set_is_subscribed(new_val);
});
}
ret
}
fn rename_folder(&mut self, folder_hash: FolderHash, mut new_path: String) -> Result<Folder> {
let mut folders = self.folders.write().unwrap();
let permissions = folders[&folder_hash].permissions();
if !permissions.delete_mailbox {
return Err(MeliError::new(format!("You do not have permission to rename folder `{}` (rename is equivalent to delete + create). Set permissions for this mailbox are {}", folders[&folder_hash].name(), permissions)));
}
let mut response = String::with_capacity(8 * 1024);
if folders[&folder_hash].separator != b'/' {
new_path = new_path.replace(
'/',
(folders[&folder_hash].separator as char).encode_utf8(&mut [0; 4]),
);
}
{
let mut conn_lck = self.connection.lock()?;
conn_lck.send_command(
debug!(format!(
"RENAME \"{}\" \"{}\"",
folders[&folder_hash].imap_path(),
new_path
))
})
.as_bytes(),
)?;
conn_lck.read_response(&mut response)?;
}
let new_hash = get_path_hash!(new_path.as_str());
let ret: Result<()> = ImapResponse::from(&response).into();
ret?;
folders.clear();
drop(folders);
self.folders().map_err(|err| format!("Mailbox rename was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", response, err))?;
Ok(BackendFolder::clone(
&self.folders.read().unwrap()[&new_hash],
))
}
fn set_folder_permissions(
&mut self,
folder_hash: FolderHash,
_val: crate::backends::FolderPermissions,
) -> Result<()> {
let folders = self.folders.write().unwrap();
let permissions = folders[&folder_hash].permissions();
if !permissions.change_permissions {
return Err(MeliError::new(format!("You do not have permission to change permissions for folder `{}`. Set permissions for this mailbox are {}", folders[&folder_hash].name(), permissions)));
}
Err(MeliError::new("Unimplemented."))
}
}
@ -547,13 +639,7 @@ impl ImapType {
let server_username = get_conf_val!(s["server_username"])?;
let server_password = get_conf_val!(s["server_password"])?;
let server_port = get_conf_val!(s["server_port"], 143)?;
let use_starttls = get_conf_val!(s["use_starttls"], {
if server_port == 993 {
false
} else {
true
}
})?;
let use_starttls = get_conf_val!(s["use_starttls"], !(server_port == 993))?;
let danger_accept_invalid_certs: bool =
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
let server_conf = ImapServerConf {
@ -707,7 +793,9 @@ impl ImapType {
let folders_lck = self.folders.read()?;
let mut response = String::with_capacity(8 * 1024);
let mut conn = self.connection.lock()?;
conn.send_command(format!("EXAMINE \"{}\"", folders_lck[&folder_hash].path()).as_bytes())?;
conn.send_command(
format!("EXAMINE \"{}\"", folders_lck[&folder_hash].imap_path()).as_bytes(),
)?;
conn.read_response(&mut response)?;
conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query).as_bytes())?;
conn.read_response(&mut response)?;

View File

@ -340,8 +340,14 @@ impl ImapConnection {
pub fn read_response(&mut self, ret: &mut String) -> Result<()> {
self.try_send(|s| s.read_response(ret))?;
let r: Result<()> = ImapResponse::from(&ret).into();
r
let r: ImapResponse = ImapResponse::from(&ret);
if let ImapResponse::Bye(ref response_code) = r {
self.stream = Err(MeliError::new(format!(
"Offline: received BYE: {:?}",
response_code
)));
}
r.into()
}
pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> {

View File

@ -25,10 +25,12 @@ use std::sync::{Arc, Mutex, RwLock};
#[derive(Debug, Default, Clone)]
pub struct ImapFolder {
pub(super) hash: FolderHash,
pub(super) imap_path: String,
pub(super) path: String,
pub(super) name: String,
pub(super) parent: Option<FolderHash>,
pub(super) children: Vec<FolderHash>,
pub separator: u8,
pub usage: Arc<RwLock<SpecialUsageMailbox>>,
pub no_select: bool,
pub is_subscribed: bool,
@ -38,6 +40,12 @@ pub struct ImapFolder {
pub unseen: Arc<Mutex<usize>>,
}
impl ImapFolder {
pub fn imap_path(&self) -> &str {
&self.imap_path
}
}
impl BackendFolder for ImapFolder {
fn hash(&self) -> FolderHash {
self.hash
@ -79,7 +87,6 @@ impl BackendFolder for ImapFolder {
}
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> {
self.is_subscribed = new_val;
// FIXME: imap subscribe
Ok(())
}

View File

@ -21,9 +21,8 @@
use super::*;
use crate::email::parser::BytesExt;
use crate::get_path_hash;
use nom::{digit, is_digit, rest, IResult};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
#[derive(Debug)]
@ -65,54 +64,55 @@ pub enum ResponseCode {
Uidvalidity(UID),
/// Followed by a decimal number, indicates the number of the first message without the \Seen flag set.
Unseen(usize),
None,
}
impl ResponseCode {
fn from(val: &str) -> Option<ResponseCode> {
fn from(val: &str) -> ResponseCode {
use ResponseCode::*;
if !val.starts_with("[") {
return None;
}
let val = &val[1..];
use ResponseCode::*;
if val.starts_with("BADCHARSET") {
Some(Badcharset)
Badcharset
} else if val.starts_with("READONLY") {
Some(ReadOnly)
ReadOnly
} else if val.starts_with("READWRITE") {
Some(ReadWrite)
ReadWrite
} else if val.starts_with("TRYCREATE") {
Some(Trycreate)
Trycreate
} else if val.starts_with("UIDNEXT") {
//FIXME
Some(Uidnext(0))
Uidnext(0)
} else if val.starts_with("UIDVALIDITY") {
//FIXME
Some(Uidvalidity(0))
Uidvalidity(0)
} else if val.starts_with("UNSEEN") {
//FIXME
Some(Unseen(0))
Unseen(0)
} else {
let msg = &val[val.as_bytes().find(b"] ").unwrap() + 1..].trim();
Some(Alert(msg.to_string()))
Alert(msg.to_string())
}
}
}
#[derive(Debug, PartialEq)]
pub enum ImapResponse {
Ok(Option<ResponseCode>),
No(Option<ResponseCode>),
Bad(Option<ResponseCode>),
Preauth(Option<ResponseCode>),
Bye(Option<ResponseCode>),
Ok(ResponseCode),
No(ResponseCode),
Bad(ResponseCode),
Preauth(ResponseCode),
Bye(ResponseCode),
}
impl<T: AsRef<str>> From<T> for ImapResponse {
fn from(val: T) -> ImapResponse {
let val: &str = val.as_ref().split_rn().last().unwrap_or(val.as_ref());
debug!(&val);
assert!(val.starts_with("M"));
let mut val = val[val.as_bytes().find(b" ").unwrap() + 1..].trim();
// M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n
if val.ends_with(" secs).") {
@ -139,8 +139,9 @@ impl Into<Result<()>> for ImapResponse {
fn into(self) -> Result<()> {
match self {
Self::Ok(_) | Self::Preauth(_) | Self::Bye(_) => Ok(()),
Self::No(Some(ResponseCode::Alert(msg)))
| Self::Bad(Some(ResponseCode::Alert(msg))) => Err(MeliError::new(msg)),
Self::No(ResponseCode::Alert(msg)) | Self::Bad(ResponseCode::Alert(msg)) => {
Err(MeliError::new(msg))
}
Self::No(_) => Err(MeliError::new("IMAP NO Response.")),
Self::Bad(_) => Err(MeliError::new("IMAP BAD Response.")),
}
@ -149,7 +150,7 @@ impl Into<Result<()>> for ImapResponse {
#[test]
fn test_imap_response() {
assert_eq!(ImapResponse::from("M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n"), ImapResponse::No(Some(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string()))));
assert_eq!(ImapResponse::from("M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n"), ImapResponse::No(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string())));
}
impl<'a> Iterator for ImapLineIterator<'a> {
@ -206,13 +207,7 @@ macro_rules! dbg_dmp (
dbg_dmp!($i, call!($f));
);
);
macro_rules! get_path_hash {
($path:expr) => {{
let mut hasher = DefaultHasher::new();
$path.hash(&mut hasher);
hasher.finish()
}};
}
/*
* LIST (\HasNoChildren) "." INBOX.Sent
* LIST (\HasChildren) "." INBOX
@ -243,14 +238,20 @@ named!(
let _ = f.set_special_usage(SpecialUsageMailbox::Drafts);
}
}
f.hash = get_path_hash!(path);
f.path = String::from_utf8_lossy(path).into();
f.name = if let Some(pos) = path.iter().rposition(|&c| c == separator) {
f.parent = Some(get_path_hash!(&path[..pos]));
String::from_utf8_lossy(&path[pos + 1..]).into()
f.imap_path = String::from_utf8_lossy(path).into();
f.hash = get_path_hash!(&f.imap_path);
f.path = if separator == b'/' {
f.imap_path.clone()
} else {
f.path.clone()
f.imap_path.replace(separator as char, "/")
};
f.name = if let Some(pos) = f.imap_path.as_bytes().iter().rposition(|&c| c == separator) {
f.parent = Some(get_path_hash!(&f.imap_path[..pos]));
f.imap_path[pos + 1..].to_string()
} else {
f.imap_path.clone()
};
f.separator = separator;
debug!(f)
})

View File

@ -149,7 +149,7 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> {
folder_hash,
work_context,
thread_id,
conn.send_command(format!("SELECT \"{}\"", folder.path()).as_bytes())
conn.send_command(format!("SELECT \"{}\"", folder.imap_path()).as_bytes())
conn.read_response(&mut response)
);
debug!("select response {}", &response);
@ -531,7 +531,7 @@ fn examine_updates(
folder_hash,
work_context,
thread_id,
conn.send_command(format!("EXAMINE \"{}\"", folder.path()).as_bytes())
conn.send_command(format!("EXAMINE \"{}\"", folder.imap_path()).as_bytes())
conn.read_response(&mut response)
);
match protocol_parser::select_response(&response) {

View File

@ -63,16 +63,6 @@ pub trait Method<OBJ: Object>: Serialize {
const NAME: &'static str;
}
macro_rules! get_path_hash {
($path:expr) => {{
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
$path.hash(&mut hasher);
hasher.finish()
}};
}
static USING: &'static [&'static str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
#[derive(Serialize)]

View File

@ -192,42 +192,20 @@ impl MaildirFolder {
children: Vec<FolderHash>,
settings: &AccountSettings,
) -> Result<Self> {
macro_rules! strip_slash {
($v:expr) => {
if $v.ends_with("/") {
&$v[..$v.len() - 1]
} else {
$v
}
};
}
let pathbuf = PathBuf::from(&path);
let mut h = DefaultHasher::new();
pathbuf.hash(&mut h);
/* Check if folder path (Eg `INBOX/Lists/luddites`) is included in the subscribed
* mailboxes in user configuration */
let fname = if let Ok(fname) = pathbuf.strip_prefix(
PathBuf::from(&settings.root_folder)
.expand()
.parent()
.unwrap_or_else(|| &Path::new("/")),
) {
if fname.components().count() != 0
&& !settings
.subscribed_folders
.iter()
.any(|x| x == strip_slash!(fname.to_str().unwrap()))
{
return Err(MeliError::new(format!(
"Folder with name `{}` is not included in configured subscribed mailboxes",
fname.display()
)));
}
Some(fname)
} else {
None
};
let fname = pathbuf
.strip_prefix(
PathBuf::from(&settings.root_folder)
.expand()
.parent()
.unwrap_or_else(|| &Path::new("/")),
)
.ok();
let read_only = if let Ok(metadata) = std::fs::metadata(&pathbuf) {
metadata.permissions().readonly()
@ -243,7 +221,7 @@ impl MaildirFolder {
parent,
children,
usage: Arc::new(RwLock::new(SpecialUsageMailbox::Normal)),
is_subscribed: true,
is_subscribed: false,
permissions: FolderPermissions {
create_messages: !read_only,
remove_messages: !read_only,

View File

@ -34,12 +34,7 @@ extern crate notify;
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::time::Duration;
use std::sync::mpsc::channel;
//use std::sync::mpsc::sync_channel;
//use std::sync::mpsc::SyncSender;
//use std::time::Duration;
use fnv::{FnvHashMap, FnvHashSet, FnvHasher};
use std::collections::hash_map::DefaultHasher;
use std::ffi::OsStr;
use std::fs;
use std::hash::{Hash, Hasher};
@ -48,6 +43,7 @@ use std::ops::{Deref, DerefMut};
use std::os::unix::fs::PermissionsExt;
use std::path::{Component, Path, PathBuf};
use std::result;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex};
use std::thread;
@ -132,7 +128,6 @@ macro_rules! path_is_new {
};
}
#[macro_export]
macro_rules! get_path_hash {
($path:expr) => {{
let mut path = $path.clone();
@ -145,9 +140,7 @@ macro_rules! get_path_hash {
path.pop();
};
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
hasher.finish()
crate::get_path_hash!(path)
}};
}
@ -642,6 +635,64 @@ impl MailBackend for MaildirType {
fn as_any(&self) -> &dyn::std::any::Any {
self
}
fn create_folder(&mut self, new_path: String) -> Result<Folder> {
let mut path = self.path.clone();
path.push(&new_path);
if !path.starts_with(&self.path) {
return Err(MeliError::new(format!("Path given (`{}`) is absolute. Please provide a path relative to the account's root folder.", &new_path)));
}
std::fs::create_dir(&path)?;
/* create_dir does not create intermediate directories (like `mkdir -p`), so the parent must be a valid
* folder at this point. */
let parent = path.parent().and_then(|p| {
self.folders
.iter()
.find(|(_, f)| f.fs_path == p)
.map(|item| *item.0)
});
let folder_hash = get_path_hash!(&path);
let new_folder = MaildirFolder {
hash: folder_hash,
path: PathBuf::from(&new_path),
name: new_path,
fs_path: path,
parent,
children: vec![],
usage: Default::default(),
is_subscribed: true,
permissions: Default::default(),
unseen: Default::default(),
total: Default::default(),
};
let ret = BackendFolder::clone(debug!(&new_folder));
self.folders.insert(folder_hash, new_folder);
Ok(ret)
}
fn delete_folder(&mut self, _folder_hash: FolderHash) -> Result<()> {
Err(MeliError::new("Unimplemented."))
}
fn set_folder_subscription(&mut self, _folder_hash: FolderHash, _val: bool) -> Result<()> {
Err(MeliError::new("Unimplemented."))
}
fn rename_folder(&mut self, _folder_hash: FolderHash, _new_path: String) -> Result<Folder> {
Err(MeliError::new("Unimplemented."))
}
fn set_folder_permissions(
&mut self,
_folder_hash: FolderHash,
_val: crate::backends::FolderPermissions,
) -> Result<()> {
Err(MeliError::new("Unimplemented."))
}
}
impl MaildirType {
@ -667,7 +718,7 @@ impl MaildirType {
)));
}
let mut children = Vec::new();
for mut f in fs::read_dir(p).unwrap() {
for mut f in fs::read_dir(&p).unwrap() {
'entries: for f in f.iter_mut() {
{
let path = f.path();
@ -736,10 +787,10 @@ impl MaildirType {
.count();
folders.get_mut(&root_hash).map(|f| f.children = children);
}
folders.retain(|_, f| is_subscribed(f.path()));
let keys = folders.keys().cloned().collect::<FnvHashSet<FolderHash>>();
for f in folders.values_mut() {
f.children.retain(|c| keys.contains(c));
if is_subscribed(f.path()) {
f.is_subscribed = true;
}
}
let mut hash_indexes =

View File

@ -34,6 +34,7 @@ use crate::conf::AccountSettings;
use crate::email::parser::BytesExt;
use crate::email::*;
use crate::error::{MeliError, Result};
use crate::get_path_hash;
use crate::shellexpand::ShellExpandTrait;
use fnv::FnvHashMap;
use libc;
@ -41,9 +42,7 @@ use memmap::{Mmap, Protection};
use nom::{IResult, Needed};
extern crate notify;
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
use std::collections::hash_map::DefaultHasher;
use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::BufReader;
use std::io::Read;
use std::os::unix::io::AsRawFd;
@ -74,14 +73,6 @@ fn get_rw_lock_blocking(f: &File) {
assert!(-1 != ret_val);
}
macro_rules! get_path_hash {
($path:expr) => {{
let mut hasher = DefaultHasher::new();
$path.hash(&mut hasher);
hasher.finish()
}};
}
#[derive(Debug)]
struct MboxFolder {
hash: FolderHash,

View File

@ -74,7 +74,7 @@ pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>) -> Strin
s.to_string_lossy().to_string()
}
fn tm_to_secs(tm: ::libc::tm) -> UnixTimestamp {
fn tm_to_secs(tm: ::libc::tm) -> std::result::Result<i64, ()> {
let mut is_leap = false;
let mut year = tm.tm_year;
let mut month = tm.tm_mon;
@ -87,19 +87,19 @@ fn tm_to_secs(tm: ::libc::tm) -> UnixTimestamp {
}
year += adj;
}
let mut t = year_to_secs(year as u64, &mut is_leap);
t += month_to_secs(month as usize, is_leap);
t += 86400 * (tm.tm_mday as u64 - 1);
t += 3600 * (tm.tm_hour as u64);
t += 60 * (tm.tm_min as u64);
t += tm.tm_sec as u64;
t
let mut t = year_to_secs(year.into(), &mut is_leap)?;
t += month_to_secs(month.try_into().unwrap_or(0), is_leap);
t += 86400 * (tm.tm_mday - 1) as i64;
t += 3600 * (tm.tm_hour) as i64;
t += 60 * (tm.tm_min) as i64;
t += tm.tm_sec as i64;
Ok(t)
}
fn year_to_secs(year: u64, is_leap: &mut bool) -> u64 {
if year < 100 {
fn year_to_secs(year: i64, is_leap: &mut bool) -> std::result::Result<i64, ()> {
if year < -100 {
/* Sorry time travelers. */
return 0;
return Err(());
}
if year - 2 <= 136 {
@ -111,7 +111,9 @@ fn year_to_secs(year: u64, is_leap: &mut bool) -> u64 {
} else {
*is_leap = false;
}
return 31536000 * (y - 70) + 86400 * leaps;
return Ok((31536000 * (y - 70) + 86400 * leaps)
.try_into()
.unwrap_or(0));
}
let cycles = (year - 100) / 400;
@ -154,11 +156,14 @@ fn year_to_secs(year: u64, is_leap: &mut bool) -> u64 {
leaps += 97 * cycles + 24 * centuries - if *is_leap { 1 } else { 0 };
return (year - 100) * 31536000 + leaps * 86400 + 946684800 + 86400;
return Ok(match (year - 100).overflowing_mul(31536000) {
(_, true) => return Err(()),
(res, false) => res + leaps * 86400 + 946684800 + 86400,
});
}
fn month_to_secs(month: usize, is_leap: bool) -> u64 {
const SECS_THROUGH_MONTH: [u64; 12] = [
fn month_to_secs(month: usize, is_leap: bool) -> i64 {
const SECS_THROUGH_MONTH: [i64; 12] = [
0,
31 * 86400,
59 * 86400,
@ -172,7 +177,7 @@ fn month_to_secs(month: usize, is_leap: bool) -> u64 {
304 * 86400,
334 * 86400,
];
let mut t = SECS_THROUGH_MONTH[month as usize];
let mut t = SECS_THROUGH_MONTH[month];
if is_leap && month >= 2 {
t += 86400;
}
@ -226,7 +231,9 @@ where
0
}
};
return (tm_to_secs(new_tm) as i64 - tm_gmtoff) as u64;
return tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0);
}
}
return 0;

View File

@ -929,7 +929,7 @@ pub fn phrase(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
/* We have the start of an encoded word but not the end, so parse it as ascii */
ascii_e = input[ascii_s..]
.find(b" ")
.unwrap_or_else(|| input[ascii_s..].len());
.unwrap_or_else(|| ascii_s + input[ascii_s..].len());
ptr = ascii_e;
}
@ -1052,7 +1052,7 @@ mod tests {
use crate::make_address;
#[test]
fn test_subject() {
fn test_phrase() {
let words = b"=?iso-8859-7?B?W215Y291cnNlcy5udHVhLmdyIC0gyvXs4fTp6t4g6uHpIMri4e306ere?=
=?iso-8859-7?B?INb18+nq3l0gzd3hIMHt4erv3+358+c6IMzF0c/TIMHQz9TFy8XTzMHU?=
=?iso-8859-7?B?2c0gwiDUzC4gysHNLiDFzsXUwdPH0yAyMDE3LTE4OiDTx8zFydnTxw==?=";
@ -1145,7 +1145,10 @@ mod tests {
debug!("{:?}", date(__s));
assert_eq!(date(s).unwrap(), date(_s).unwrap());
assert_eq!(date(_s).unwrap(), date(__s).unwrap());
let val = b"Fri, 23 Dec 0001 21:20:36 -0800 (PST)";
assert_eq!(date(val).unwrap(), 0);
}
#[test]
fn test_attachments() {
//FIXME: add file

View File

@ -301,8 +301,6 @@ pub mod shellexpand {
#[test]
fn test_shellexpandtrait() {
let path = &Path::new("~/.v");
//println!("ret = {:?}", path.expand().complete(false));
assert!(Path::new("~").expand().complete(false).is_empty());
assert!(!Path::new("~").expand().complete(true).is_empty());
}

View File

@ -71,10 +71,16 @@ pub trait TextProcessing: UnicodeSegmentation + CodePointsIter {
impl TextProcessing for str {
fn split_lines(&self, width: usize) -> Vec<String> {
if width == 0 {
return vec![];
}
super::line_break::linear(self, width)
}
fn split_lines_reflow(&self, reflow: Reflow, width: Option<usize>) -> Vec<String> {
if width == Some(0) {
return vec![];
}
super::line_break::split_lines_reflow(self, reflow, width)
}
}

View File

@ -1023,6 +1023,9 @@ pub fn split_lines_reflow(text: &str, reflow: Reflow, width: Option<usize>) -> V
ret.push(format!("{}", &line[prev_line_offset..end_offset]));
}
}
if prev_line_offset == end_offset && prev == new_off {
break;
}
prev_line_offset = end_offset;
prev = new_off;
}

View File

@ -56,36 +56,60 @@ pub trait GlobMatch {
}
impl GlobMatch for str {
fn matches_glob(&self, s: &str) -> bool {
let parts = s.split("*");
fn matches_glob(&self, _pattern: &str) -> bool {
macro_rules! strip_slash {
($v:expr) => {
if $v.ends_with("/") {
&$v[..$v.len() - 1]
} else {
$v
}
};
}
let pattern: Vec<&str> = strip_slash!(_pattern).split_graphemes();
let s: Vec<&str> = strip_slash!(self).split_graphemes();
let mut ptr = 0;
let mut part_no = 0;
// Taken from https://research.swtch.com/glob
for p in parts {
if p.is_empty() {
/* asterisk is at beginning and/or end of glob */
/* eg "*".split("*") gives ["", ""] */
part_no += 1;
continue;
}
if ptr >= self.len() {
return false;
}
if part_no > 0 {
while !&self[ptr..].starts_with(p) {
ptr += 1;
if ptr >= self.len() {
return false;
let mut px = 0;
let mut sx = 0;
let mut next_px = 0;
let mut next_sx = 0;
while px < pattern.len() || sx < s.len() {
if px < pattern.len() {
match pattern[px] {
"?" => {
if sx < s.len() {
px += 1;
sx += 1;
continue;
}
}
"*" => {
// Try to match at sx.
// If that doesn't work out,
// restart at sx+1 next.
next_px = px;
next_sx = sx + 1;
px += 1;
continue;
}
p => {
if sx < s.len() && s[sx] == p {
px += 1;
sx += 1;
continue;
}
}
}
}
if !&self[ptr..].starts_with(p) {
return false;
// Mismatch. Maybe restart.
if 0 < next_sx && next_sx <= s.len() {
px = next_px;
sx = next_sx;
continue;
}
ptr += p.len();
part_no += 1;
return false;
}
true
}
@ -98,8 +122,18 @@ impl GlobMatch for str {
#[test]
fn test_globmatch() {
assert!("INBOX".matches_glob("INBOX"));
assert!("INBOX/".matches_glob("INBOX"));
assert!("INBOX".matches_glob("INBO?"));
assert!("INBOX/Sent".matches_glob("INBOX/*"));
assert!(!"INBOX/Sent".matches_glob("INBOX"));
assert!(!"INBOX/Sent".matches_glob("*/Drafts"));
assert!("INBOX/Sent".matches_glob("*/Sent"));
assert!("INBOX/Archives/2047".matches_glob("*"));
assert!("INBOX/Archives/2047".matches_glob("INBOX/*/2047"));
assert!("INBOX/Archives/2047".matches_glob("INBOX/Archives/2*047"));
assert!("INBOX/Archives/2047".matches_glob("INBOX/Archives/204?"));
assert!(!"INBOX/Lists/".matches_glob("INBOX/Lists/*"));
}

View File

@ -134,8 +134,14 @@ macro_rules! error_and_exit {
}}
}
#[derive(Debug)]
enum ManPages {
Main = 0,
Conf = 1,
Themes = 2,
}
struct CommandLineArguments {
print_manpage: Option<ManPages>,
create_config: Option<String>,
test_config: Option<String>,
config: Option<String>,
@ -155,6 +161,7 @@ fn main() {
fn run_app() -> Result<()> {
enum CommandLineFlags {
PrintManPage,
CreateConfig,
TestConfig,
Config,
@ -162,6 +169,7 @@ fn run_app() -> Result<()> {
use CommandLineFlags::*;
let mut prev: Option<CommandLineFlags> = None;
let mut args = CommandLineArguments {
print_manpage: None,
create_config: None,
test_config: None,
config: None,
@ -176,12 +184,18 @@ fn run_app() -> Result<()> {
Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
Some(Config) => error_and_exit!("invalid value for flag `--config`"),
Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
Some(PrintManPage) => {
error_and_exit!("invalid value for flag `--print-documentation`")
}
},
"--create-config" => match prev {
None => prev = Some(CreateConfig),
Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
Some(Config) => error_and_exit!("invalid value for flag `--config`"),
Some(PrintManPage) => {
error_and_exit!("invalid value for flag `--print-documentation`")
}
},
"--config" | "-c" => match prev {
None => prev = Some(Config),
@ -192,6 +206,9 @@ fn run_app() -> Result<()> {
Some(CreateConfig) => error_and_exit!("invalid value for flag `--create-config`"),
Some(Config) => error_and_exit!("invalid value for flag `--config`"),
Some(TestConfig) => error_and_exit!("invalid value for flag `--test-config`"),
Some(PrintManPage) => {
error_and_exit!("invalid value for flag `--print-documentation`")
}
},
"--help" | "-h" => {
args.help = true;
@ -208,6 +225,13 @@ fn run_app() -> Result<()> {
print!("{}", conf::Theme::default().key_to_string("dark", false));
return Ok(());
}
"--print-documentation" => {
if args.print_manpage.is_some() {
error_and_exit!("Multiple invocations of --print-documentation");
}
prev = Some(PrintManPage);
args.print_manpage = Some(ManPages::Main);
}
e => match prev {
None => error_and_exit!("error: value without command {}", e),
Some(CreateConfig) if args.create_config.is_none() => {
@ -222,6 +246,18 @@ fn run_app() -> Result<()> {
args.test_config = Some(i);
prev = None;
}
Some(PrintManPage) => {
match e {
"meli" | "main" => { /* This is the default */ }
"meli.conf" | "conf" | "config" | "configuration" => {
args.print_manpage = Some(ManPages::Conf);
}
"meli-themes" | "themes" | "theming" | "theme" => {
args.print_manpage = Some(ManPages::Themes);
}
_ => error_and_exit!("Invalid documentation page: {}", e),
}
}
Some(TestConfig) => error_and_exit!("Duplicate value for flag `--test-config`"),
Some(CreateConfig) => error_and_exit!("Duplicate value for flag `--create-config`"),
Some(Config) => error_and_exit!("Duplicate value for flag `--config`"),
@ -230,10 +266,11 @@ fn run_app() -> Result<()> {
}
if args.help {
println!("usage:\tmeli [--create-config[ PATH]] [--config[ PATH]|-c[ PATH]]");
println!("usage:\tmeli [--config PATH|-c PATH]");
println!("\tmeli --help");
println!("\tmeli --version");
println!("");
println!("Other options:");
println!("\t--help, -h\t\tshow this message and exit");
println!("\t--version, -v\t\tprint version and exit");
println!("\t--create-config[ PATH]\tcreate a sample configuration file with available configuration options. If PATH is not specified, meli will try to create it in $XDG_CONFIG_HOME/meli/config.toml");
@ -243,6 +280,10 @@ fn run_app() -> Result<()> {
println!("\t--config PATH, -c PATH\tuse specified configuration file");
println!("\t--print-loaded-themes\tprint loaded themes in full to stdout and exit.");
println!("\t--print-default-theme\tprint default theme in full to stdout and exit.");
#[cfg(feature = "cli-docs")]
{
println!("\t--print-documentation [meli conf themes]\n\t\t\t\tprint documentation page and exit (Piping to a pager is recommended.).");
}
return Ok(());
}
@ -257,14 +298,25 @@ fn run_app() -> Result<()> {
Some(CreateConfig) => error_and_exit!("Duplicate value for flag `--create-config`"),
Some(Config) => error_and_exit!("error: flag without value: `--config`"),
Some(TestConfig) => error_and_exit!("error: flag without value: `--test-config`"),
Some(PrintManPage) => {}
};
if (args.print_manpage.is_some()
^ args.test_config.is_some()
^ args.create_config.is_some()
^ args.config.is_some())
&& !(args.print_manpage.is_some()
|| args.test_config.is_some()
|| args.create_config.is_some()
|| args.config.is_some())
{
error_and_exit!("error: illegal command-line flag combination");
}
if let Some(config_path) = args.test_config.as_ref() {
conf::FileSettings::validate(config_path)?;
return Ok(());
}
if let Some(config_path) = args.create_config.as_mut() {
} else if let Some(config_path) = args.create_config.as_mut() {
let config_path: PathBuf = if config_path.is_empty() {
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").unwrap();
xdg_dirs.place_config_file("config.toml").map_err(|e| {
@ -282,6 +334,21 @@ fn run_app() -> Result<()> {
}
conf::create_config_file(&config_path)?;
return Ok(());
} else if let Some(_page) = args.print_manpage {
#[cfg(feature = "cli-docs")]
{
const MANPAGES: [&'static str; 3] = [
include_str!(concat!(env!("OUT_DIR"), "/meli.txt")),
include_str!(concat!(env!("OUT_DIR"), "/meli.conf.txt")),
include_str!(concat!(env!("OUT_DIR"), "/meli-themes.txt")),
];
println!("{}", MANPAGES[_page as usize]);
return Ok(());
}
#[cfg(not(feature = "cli-docs"))]
{
error_and_exit!("error: this version of meli was not build with embedded documentation. You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery");
}
}
if let Some(config_location) = args.config.as_ref() {
@ -305,9 +372,12 @@ fn run_app() -> Result<()> {
let mut state = State::new(sender, receiver.clone())?;
let window = Box::new(Tabbed::new(vec![
Box::new(listing::Listing::new(&state.context.accounts)),
Box::new(listing::Listing::new(&mut state.context)),
Box::new(ContactList::new(&state.context)),
Box::new(StatusPanel::new()),
Box::new(StatusPanel::new(crate::conf::value(
&state.context,
"theme_default",
))),
]));
let status_bar = Box::new(StatusBar::new(window));

View File

@ -27,6 +27,7 @@
use super::*;
use crate::melib::text_processing::{TextProcessing, Truncate};
use crate::terminal::boundaries::*;
pub mod mail;
pub use crate::mail::*;
@ -46,32 +47,6 @@ use fnv::FnvHashMap;
use uuid::Uuid;
use super::{Key, StatusEvent, UIEvent};
/// The upper and lower boundary char.
const HORZ_BOUNDARY: char = '─';
/// The left and right boundary char.
const VERT_BOUNDARY: char = '│';
/// The top-left corner
const _TOP_LEFT_CORNER: char = '┌';
/// The top-right corner
const _TOP_RIGHT_CORNER: char = '┐';
/// The bottom-left corner
const _BOTTOM_LEFT_CORNER: char = '└';
/// The bottom-right corner
const _BOTTOM_RIGHT_CORNER: char = '┘';
const LIGHT_VERTICAL_AND_RIGHT: char = '├';
const _LIGHT_VERTICAL_AND_LEFT: char = '┤';
const LIGHT_DOWN_AND_HORIZONTAL: char = '┬';
const LIGHT_UP_AND_HORIZONTAL: char = '┴';
const _DOUBLE_DOWN_AND_RIGHT: char = '╔';
const _DOUBLE_DOWN_AND_LEFT: char = '╗';
const _DOUBLE_UP_AND_LEFT: char = '╝';
const _DOUBLE_UP_AND_RIGHT: char = '╚';
type ComponentId = Uuid;
@ -100,316 +75,7 @@ pub trait Component: Display + Debug + Send {
Default::default()
}
fn get_status(&self, _context: &Context) -> Option<String> {
None
}
}
fn bin_to_ch(b: u32) -> char {
match b {
0b0001 => '╶',
0b0010 => '╵',
0b0011 => '└',
0b0100 => '╴',
0b0101 => '─',
0b0110 => '┘',
0b0111 => '┴',
0b1000 => '╷',
0b1001 => '┌',
0b1010 => '│',
0b1011 => '├',
0b1100 => '┐',
0b1101 => '┬',
0b1110 => '┤',
0b1111 => '┼',
_ => unsafe { std::hint::unreachable_unchecked() },
}
}
fn ch_to_bin(ch: char) -> Option<u32> {
match ch {
'└' => Some(0b0011),
'─' => Some(0b0101),
'┘' => Some(0b0110),
'┴' => Some(0b0111),
'┌' => Some(0b1001),
'│' => Some(0b1010),
'├' => Some(0b1011),
'┐' => Some(0b1100),
'┬' => Some(0b1101),
'┤' => Some(0b1110),
'┼' => Some(0b1111),
'╷' => Some(0b1000),
'╵' => Some(0b0010),
'╴' => Some(0b0100),
'╶' => Some(0b0001),
_ => None,
}
}
#[allow(clippy::never_loop)]
fn set_and_join_vert(grid: &mut CellBuffer, idx: Pos) -> u32 {
let (x, y) = idx;
let mut bin_set = 0b1010;
/* Check left side
*
* 1
* -> 2 0
* 3
*/
loop {
if x > 0 {
if let Some(cell) = grid.get_mut(x - 1, y) {
if let Some(adj) = ch_to_bin(cell.ch()) {
if (adj & 0b0001) > 0 {
bin_set |= 0b0100;
break;
} else if adj == 0b0100 {
cell.set_ch(bin_to_ch(0b0101));
cell.set_fg(Color::Byte(240));
bin_set |= 0b0100;
break;
}
}
}
}
bin_set &= 0b1011;
break;
}
/* Check right side
*
* 1
* 2 0 <-
* 3
*/
loop {
if let Some(cell) = grid.get_mut(x + 1, y) {
if let Some(adj) = ch_to_bin(cell.ch()) {
if (adj & 0b0100) > 0 {
bin_set |= 0b0001;
break;
}
}
}
bin_set &= 0b1110;
break;
}
/* Set upper side
*
* 1 <-
* 2 0
* 3
*/
loop {
if y > 0 {
if let Some(cell) = grid.get_mut(x, y - 1) {
if let Some(adj) = ch_to_bin(cell.ch()) {
cell.set_ch(bin_to_ch(adj | 0b1000));
cell.set_fg(Color::Byte(240));
} else {
bin_set &= 0b1101;
}
}
}
break;
}
/* Set bottom side
*
* 1
* 2 0
* 3 <-
*/
loop {
if let Some(cell) = grid.get_mut(x, y + 1) {
if let Some(adj) = ch_to_bin(cell.ch()) {
cell.set_ch(bin_to_ch(adj | 0b0010));
cell.set_fg(Color::Byte(240));
} else {
bin_set &= 0b0111;
}
}
break;
}
if bin_set == 0 {
bin_set = 0b1010;
}
bin_set
}
#[allow(clippy::never_loop)]
fn set_and_join_horz(grid: &mut CellBuffer, idx: Pos) -> u32 {
let (x, y) = idx;
let mut bin_set = 0b0101;
/* Check upper side
*
* 1 <-
* 2 0
* 3
*/
loop {
if y > 0 {
if let Some(cell) = grid.get_mut(x, y - 1) {
if let Some(adj) = ch_to_bin(cell.ch()) {
if (adj & 0b1000) > 0 {
bin_set |= 0b0010;
break;
} else if adj == 0b0010 {
bin_set |= 0b0010;
cell.set_ch(bin_to_ch(0b1010));
cell.set_fg(Color::Byte(240));
break;
}
}
}
}
bin_set &= 0b1101;
break;
}
/* Check bottom side
*
* 1
* 2 0
* 3 <-
*/
loop {
if let Some(cell) = grid.get_mut(x, y + 1) {
if let Some(adj) = ch_to_bin(cell.ch()) {
if (adj & 0b0010) > 0 {
bin_set |= 0b1000;
break;
} else if adj == 0b1000 {
bin_set |= 0b1000;
cell.set_ch(bin_to_ch(0b1010));
cell.set_fg(Color::Byte(240));
break;
}
}
}
bin_set &= 0b0111;
break;
}
/* Set left side
*
* 1
* -> 2 0
* 3
*/
loop {
if x > 0 {
if let Some(cell) = grid.get_mut(x - 1, y) {
if let Some(adj) = ch_to_bin(cell.ch()) {
cell.set_ch(bin_to_ch(adj | 0b0001));
cell.set_fg(Color::Byte(240));
} else {
bin_set &= 0b1011;
}
}
}
break;
}
/* Set right side
*
* 1
* 2 0 <-
* 3
*/
loop {
if let Some(cell) = grid.get_mut(x + 1, y) {
if let Some(adj) = ch_to_bin(cell.ch()) {
cell.set_ch(bin_to_ch(adj | 0b0100));
cell.set_fg(Color::Byte(240));
} else {
bin_set &= 0b1110;
}
}
break;
}
if bin_set == 0 {
bin_set = 0b0101;
}
bin_set
}
pub enum BoxBoundary {
Horizontal,
Vertical,
}
pub(crate) fn set_and_join_box(grid: &mut CellBuffer, idx: Pos, ch: BoxBoundary) {
/* Connected sides:
*
* 1
* 2 c 0
* 3
*
* #3210
* 0b____
*/
if grid.ascii_drawing {
grid[idx].set_ch(match ch {
BoxBoundary::Vertical => '|',
BoxBoundary::Horizontal => '-',
});
grid[idx].set_fg(Color::Byte(240));
return;
}
let bin_set = match ch {
BoxBoundary::Vertical => set_and_join_vert(grid, idx),
BoxBoundary::Horizontal => set_and_join_horz(grid, idx),
};
grid[idx].set_ch(bin_to_ch(bin_set));
grid[idx].set_fg(Color::Byte(240));
}
pub fn create_box(grid: &mut CellBuffer, area: Area) {
if !is_valid_area!(area) {
return;
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if !grid.ascii_drawing {
for x in get_x(upper_left)..get_x(bottom_right) {
grid[(x, get_y(upper_left))].set_ch(HORZ_BOUNDARY);
grid[(x, get_y(bottom_right))].set_ch(HORZ_BOUNDARY);
grid[(x, get_y(bottom_right))].set_fg(Color::Byte(240));
}
for y in get_y(upper_left)..get_y(bottom_right) {
grid[(get_x(upper_left), y)].set_ch(VERT_BOUNDARY);
grid[(get_x(bottom_right), y)].set_ch(VERT_BOUNDARY);
grid[(get_x(bottom_right), y)].set_fg(Color::Byte(240));
}
set_and_join_box(grid, upper_left, BoxBoundary::Horizontal);
set_and_join_box(
grid,
set_x(upper_left, get_x(bottom_right)),
BoxBoundary::Horizontal,
);
set_and_join_box(
grid,
set_y(upper_left, get_y(bottom_right)),
BoxBoundary::Vertical,
);
set_and_join_box(grid, bottom_right, BoxBoundary::Vertical);
fn get_status(&self, _context: &Context) -> String {
String::new()
}
}

View File

@ -43,29 +43,13 @@ pub struct ContactManager {
form: FormWidget,
account_pos: usize,
content: CellBuffer,
theme_default: ThemeAttribute,
dirty: bool,
has_changes: bool,
initialized: bool,
}
impl Default for ContactManager {
fn default() -> Self {
ContactManager {
id: Uuid::nil(),
parent_id: Uuid::nil(),
card: Card::new(),
mode: ViewMode::Edit,
form: FormWidget::default(),
account_pos: 0,
content: CellBuffer::new(100, 1, Cell::with_char(' ')),
dirty: true,
has_changes: false,
initialized: false,
}
}
}
impl fmt::Display for ContactManager {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "contacts")
@ -73,6 +57,30 @@ impl fmt::Display for ContactManager {
}
impl ContactManager {
fn new(context: &Context) -> Self {
let theme_default: ThemeAttribute = crate::conf::value(context, "theme_default");
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(theme_default.fg)
.set_bg(theme_default.bg)
.set_attrs(theme_default.attrs);
ret
};
ContactManager {
id: Uuid::nil(),
parent_id: Uuid::nil(),
card: Card::new(),
mode: ViewMode::Edit,
form: FormWidget::default(),
account_pos: 0,
content: CellBuffer::new(100, 1, default_cell),
theme_default,
dirty: true,
has_changes: false,
initialized: false,
}
}
fn initialize(&mut self) {
let (width, _) = self.content.size();
@ -80,8 +88,8 @@ impl ContactManager {
"Last edited: ",
&mut self.content,
Color::Byte(250),
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
((0, 0), (width - 1, 0)),
None,
);
@ -89,8 +97,8 @@ impl ContactManager {
&self.card.last_edited(),
&mut self.content,
Color::Byte(250),
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
((x, 0), (width - 1, 0)),
None,
);
@ -103,8 +111,8 @@ impl ContactManager {
"This contact's origin is external and cannot be edited within meli.",
&mut self.content,
Color::Byte(250),
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
((x, y), (width - 1, y)),
None,
);
@ -151,6 +159,7 @@ impl Component for ContactManager {
clear_area(
grid,
(upper_left, set_y(bottom_right, get_y(upper_left) + 1)),
self.theme_default,
);
copy_area_with_break(grid, &self.content, area, ((0, 0), (width - 1, 0)));
self.dirty = false;

View File

@ -48,6 +48,7 @@ pub struct ContactList {
length: usize,
data_columns: DataColumns,
initialized: bool,
theme_default: ThemeAttribute,
id_positions: Vec<CardId>,
@ -89,6 +90,7 @@ impl ContactList {
id_positions: Vec::new(),
mode: ViewMode::List,
data_columns: DataColumns::default(),
theme_default: crate::conf::value(context, "theme_default"),
initialized: false,
dirty: true,
movement: None,
@ -129,22 +131,25 @@ impl ContactList {
min_width.2 = cmp::max(min_width.2, c.url().split_graphemes().len());
}
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg)
.set_attrs(self.theme_default.attrs);
ret
};
/* name column */
self.data_columns.columns[0] =
CellBuffer::new_with_context(min_width.0, self.length, Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.0, self.length, default_cell, context);
/* email column */
self.data_columns.columns[1] =
CellBuffer::new_with_context(min_width.1, self.length, Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.1, self.length, default_cell, context);
/* url column */
self.data_columns.columns[2] =
CellBuffer::new_with_context(min_width.2, self.length, Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.2, self.length, default_cell, context);
/* source column */
self.data_columns.columns[3] = CellBuffer::new_with_context(
"external".len(),
self.length,
Cell::with_char(' '),
context,
);
self.data_columns.columns[3] =
CellBuffer::new_with_context("external".len(), self.length, default_cell, context);
let account = &context.accounts[self.account_pos];
let book = &account.address_book;
@ -156,9 +161,9 @@ impl ContactList {
write_string_to_grid(
c.name(),
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((0, idx), (min_width.0, idx)),
None,
);
@ -166,9 +171,9 @@ impl ContactList {
write_string_to_grid(
c.email(),
&mut self.data_columns.columns[1],
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((0, idx), (min_width.1, idx)),
None,
);
@ -176,9 +181,9 @@ impl ContactList {
write_string_to_grid(
c.url(),
&mut self.data_columns.columns[2],
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((0, idx), (min_width.2, idx)),
None,
);
@ -190,28 +195,31 @@ impl ContactList {
"local"
},
&mut self.data_columns.columns[3],
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((0, idx), (min_width.3, idx)),
None,
);
}
if self.length == 0 {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg)
.set_attrs(self.theme_default.attrs);
ret
};
let message = "Address book is empty.".to_string();
self.data_columns.columns[0] = CellBuffer::new_with_context(
message.len(),
self.length,
Cell::with_char(' '),
context,
);
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), self.length, default_cell, context);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
@ -221,11 +229,11 @@ impl ContactList {
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize) {
/* Reset previously highlighted line */
let fg_color = Color::Default;
let fg_color = self.theme_default.fg;
let bg_color = if idx == self.new_cursor_pos {
Color::Byte(246)
} else {
Color::Default
self.theme_default.bg
};
change_colors(grid, area, fg_color, bg_color);
}
@ -234,7 +242,7 @@ impl ContactList {
if !self.is_dirty() {
return;
}
clear_area(grid, area);
clear_area(grid, area, self.theme_default);
/* visually divide menu and listing */
area = (area.0, pos_dec(area.1, (1, 0)));
let upper_left = upper_left!(area);
@ -271,7 +279,7 @@ impl ContactList {
(Color::Byte(15), Color::Byte(233))
}
} else {
(Color::Default, Color::Default)
(self.theme_default.fg, self.theme_default.bg)
};
let s = format!(" [{}]", context.accounts[a.index].address_book.len());
@ -348,7 +356,7 @@ impl ContactList {
let bottom_right = bottom_right!(area);
if self.length == 0 {
clear_area(grid, area);
clear_area(grid, area, self.theme_default);
copy_area(
grid,
&self.data_columns.columns[0],
@ -441,8 +449,10 @@ impl ContactList {
width.saturating_sub(self.data_columns.widths[0] + self.data_columns.widths[1] + 4);
self.data_columns.widths[2] = remainder / 6;
}
clear_area(grid, area);
clear_area(grid, area, self.theme_default);
/* Page_no has changed, so draw new page */
let header_attrs = crate::conf::value(context, "widgets.list.header");
let mut x = get_x(upper_left);
for i in 0..self.data_columns.columns.len() {
if self.data_columns.widths[i] == 0 {
@ -458,9 +468,9 @@ impl ContactList {
_ => "",
},
grid,
Color::Black,
Color::White,
Attr::Bold,
header_attrs.fg,
header_attrs.bg,
header_attrs.attrs,
(
set_x(upper_left!(area), x),
(
@ -500,8 +510,8 @@ impl ContactList {
upper_left!(area),
set_y(bottom_right, get_y(upper_left!(area))),
),
Color::Black,
Color::White,
header_attrs.fg,
header_attrs.bg,
);
if top_idx + rows > self.length {
@ -511,6 +521,7 @@ impl ContactList {
pos_inc(upper_left, (0, self.length - top_idx + 2)),
bottom_right,
),
self.theme_default,
);
}
self.highlight_line(
@ -552,14 +563,16 @@ impl Component for ContactList {
if self.dirty && mid != get_x(upper_left) {
if self.show_divider {
for i in get_y(upper_left)..=get_y(bottom_right) {
grid[(mid, i)].set_ch(VERT_BOUNDARY);
grid[(mid, i)].set_fg(Color::Default);
grid[(mid, i)].set_bg(Color::Default);
grid[(mid, i)]
.set_ch(VERT_BOUNDARY)
.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg);
}
} else {
for i in get_y(upper_left)..=get_y(bottom_right) {
grid[(mid, i)].set_fg(Color::Default);
grid[(mid, i)].set_bg(Color::Default);
grid[(mid, i)]
.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg);
}
}
context
@ -590,7 +603,7 @@ impl Component for ContactList {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Self::DESCRIPTION]["create_contact"]) =>
{
let mut manager = ContactManager::default();
let mut manager = ContactManager::new(context);
manager.set_parent_id(self.id);
manager.account_pos = self.account_pos;
@ -607,7 +620,7 @@ impl Component for ContactList {
let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book;
let card = book[&self.id_positions[self.cursor_pos]].clone();
let mut manager = ContactManager::default();
let mut manager = ContactManager::new(context);
manager.set_parent_id(self.id);
manager.card = card;
manager.account_pos = self.account_pos;
@ -665,7 +678,7 @@ impl Component for ContactList {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context).unwrap(),
self.get_status(context),
)));
}
@ -702,7 +715,7 @@ impl Component for ContactList {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context).unwrap(),
self.get_status(context),
)));
}
return true;
@ -889,10 +902,10 @@ impl Component for ContactList {
.unwrap_or(true)
}
fn get_status(&self, context: &Context) -> Option<String> {
Some(format!(
fn get_status(&self, context: &Context) -> String {
format!(
"{} entries",
context.accounts[self.account_pos].address_book.len()
))
)
}
}

View File

@ -141,12 +141,15 @@ impl fmt::Display for Composer {
impl Composer {
const DESCRIPTION: &'static str = "composing";
pub fn new(account_cursor: usize) -> Self {
Composer {
pub fn new(account_cursor: usize, context: &Context) -> Self {
let mut ret = Composer {
account_cursor,
id: ComponentId::new_v4(),
..Default::default()
}
};
ret.pager
.set_colors(crate::conf::value(context, "theme_default"));
ret
}
pub fn edit(account_pos: usize, h: EnvelopeHash, context: &Context) -> Result<Self> {
@ -167,6 +170,8 @@ impl Composer {
) -> Self {
let account = &context.accounts[coordinates.0];
let mut ret = Composer::default();
ret.pager
.set_colors(crate::conf::value(context, "theme_default"));
let parent_message = account.collection.get_env(msg);
/* If message is from a mailing list and we detect a List-Post header, ask user if they
* want to reply to the mailing list or the submitter of the message */
@ -258,7 +263,8 @@ impl Composer {
fn draw_attachments(&self, grid: &mut CellBuffer, area: Area, context: &Context) {
let attachments_no = self.draft.attachments().len();
clear_area(grid, area);
let theme_default = crate::conf::value(context, "theme_default");
clear_area(grid, area, theme_default);
if self.sign_mail.is_true() {
write_string_to_grid(
&format!(
@ -272,9 +278,9 @@ impl Composer {
.unwrap_or("default key")
),
grid,
Color::Default,
Color::Default,
Attr::Default,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
None,
);
@ -282,9 +288,9 @@ impl Composer {
write_string_to_grid(
"☐ don't sign",
grid,
Color::Default,
Color::Default,
Attr::Default,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 1)), bottom_right!(area)),
None,
);
@ -293,9 +299,9 @@ impl Composer {
write_string_to_grid(
"no attachments",
grid,
Color::Default,
Color::Default,
Attr::Default,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
None,
);
@ -303,9 +309,9 @@ impl Composer {
write_string_to_grid(
&format!("{} attachments ", attachments_no),
grid,
Color::Default,
Color::Default,
Attr::Default,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 2)), bottom_right!(area)),
None,
);
@ -320,9 +326,9 @@ impl Composer {
a.raw.len()
),
grid,
Color::Default,
Color::Default,
Attr::Default,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 3 + i)), bottom_right!(area)),
None,
);
@ -330,9 +336,9 @@ impl Composer {
write_string_to_grid(
&format!("[{}] {} {} bytes", i, a.content_type(), a.raw.len()),
grid,
Color::Default,
Color::Default,
Attr::Default,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(pos_inc(upper_left!(area), (0, 3 + i)), bottom_right!(area)),
None,
);
@ -371,6 +377,7 @@ impl Component for Composer {
self.initialized = true;
}
let header_height = self.form.len();
let theme_default = crate::conf::value(context, "theme_default");
let mid = if width > 80 {
let width = width - 80;
@ -379,11 +386,13 @@ impl Component for Composer {
if self.dirty {
for i in get_y(upper_left)..=get_y(bottom_right) {
//set_and_join_box(grid, (mid, i), VERT_BOUNDARY);
grid[(mid, i)].set_fg(Color::Default);
grid[(mid, i)].set_bg(Color::Default);
grid[(mid, i)]
.set_fg(theme_default.fg)
.set_bg(theme_default.bg);
//set_and_join_box(grid, (mid + 80, i), VERT_BOUNDARY);
grid[(mid + 80, i)].set_fg(Color::Default);
grid[(mid + 80, i)].set_bg(Color::Default);
grid[(mid + 80, i)]
.set_fg(theme_default.fg)
.set_bg(theme_default.bg);
}
}
mid
@ -425,7 +434,7 @@ impl Component for Composer {
),
None,
);
clear_area(grid, ((x, y), (set_y(bottom_right, y))));
clear_area(grid, ((x, y), (set_y(bottom_right, y))), theme_default);
change_colors(
grid,
(
@ -442,6 +451,7 @@ impl Component for Composer {
pos_dec(upper_left, (0, 1)),
set_x(bottom_right, get_x(upper_left) + mid),
),
theme_default,
);
clear_area(
grid,
@ -452,6 +462,7 @@ impl Component for Composer {
),
bottom_right,
),
theme_default,
);
}
@ -462,7 +473,7 @@ impl Component for Composer {
match embed_pty {
EmbedStatus::Running(_, _) => {
let mut guard = embed_pty.lock().unwrap();
clear_area(grid, embed_area);
clear_area(grid, embed_area, theme_default);
copy_area(
grid,
&guard.grid,
@ -475,13 +486,13 @@ impl Component for Composer {
return;
}
EmbedStatus::Stopped(_, _) => {
clear_area(grid, body_area);
clear_area(grid, body_area, theme_default);
write_string_to_grid(
"process has stopped, press 'e' to re-activate",
grid,
Color::Default,
Color::Default,
Attr::Default,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
body_area,
None,
);
@ -501,7 +512,7 @@ impl Component for Composer {
set_y(upper_left!(body_area), get_y(bottom_right!(body_area))),
bottom_right!(body_area),
),
Color::Default,
theme_default.fg,
Color::Byte(237),
);
} else {
@ -511,8 +522,8 @@ impl Component for Composer {
set_y(upper_left!(body_area), get_y(bottom_right!(body_area))),
bottom_right!(body_area),
),
Color::Default,
Color::Default,
theme_default.fg,
theme_default.bg,
);
}

View File

@ -36,6 +36,9 @@ pub use self::thread::*;
mod plain;
pub use self::plain::*;
mod offline;
pub use self::offline::*;
#[derive(Debug, Default, Clone)]
pub struct DataColumns {
pub columns: [CellBuffer; 12],
@ -122,6 +125,7 @@ struct AccountMenuEntry {
name: String,
// Index in the config account vector.
index: usize,
entries: SmallVec<[(usize, FolderHash); 16]>,
}
pub trait MailListingTrait: ListingTrait {
@ -200,11 +204,14 @@ pub trait MailListingTrait: ListingTrait {
fn redraw_list(&mut self, _context: &Context, _items: Box<dyn Iterator<Item = ThreadHash>>) {
unimplemented!()
}
/// Use `force` when there have been changes in the mailbox or account lists in `context`
fn refresh_mailbox(&mut self, context: &mut Context, force: bool);
}
pub trait ListingTrait: Component {
fn coordinates(&self) -> (usize, usize);
fn set_coordinates(&mut self, _: (usize, usize));
fn coordinates(&self) -> (usize, FolderHash);
fn set_coordinates(&mut self, _: (usize, FolderHash));
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context);
fn filter(&mut self, _filter_term: &str, _context: &Context) {}
@ -217,6 +224,7 @@ pub enum ListingComponent {
Threaded(ThreadListing),
Compact(CompactListing),
Conversations(ConversationsListing),
Offline(OfflineListing),
}
use crate::ListingComponent::*;
@ -229,6 +237,7 @@ impl core::ops::Deref for ListingComponent {
Plain(ref l) => l,
Threaded(ref l) => l,
Conversations(ref l) => l,
Offline(ref l) => l,
}
}
}
@ -240,6 +249,7 @@ impl core::ops::DerefMut for ListingComponent {
Plain(l) => l,
Threaded(l) => l,
Conversations(l) => l,
Offline(l) => l,
}
}
}
@ -251,37 +261,25 @@ impl ListingComponent {
if let Plain(_) = self {
return;
}
let mut new_l = PlainListing::default();
let coors = self.coordinates();
new_l.set_coordinates((coors.0, coors.1));
*self = Plain(new_l);
*self = Plain(PlainListing::new(self.coordinates()));
}
IndexStyle::Threaded => {
if let Threaded(_) = self {
return;
}
let mut new_l = ThreadListing::default();
let coors = self.coordinates();
new_l.set_coordinates((coors.0, coors.1));
*self = Threaded(new_l);
*self = Threaded(ThreadListing::new(self.coordinates()));
}
IndexStyle::Compact => {
if let Compact(_) = self {
return;
}
let mut new_l = CompactListing::default();
let coors = self.coordinates();
new_l.set_coordinates((coors.0, coors.1));
*self = Compact(new_l);
*self = Compact(CompactListing::new(self.coordinates()));
}
IndexStyle::Conversations => {
if let Conversations(_) = self {
return;
}
let mut new_l = ConversationsListing::default();
let coors = self.coordinates();
new_l.set_coordinates((coors.0, coors.1));
*self = Conversations(new_l);
*self = Conversations(ConversationsListing::new(self.coordinates()));
}
}
}
@ -312,6 +310,7 @@ impl fmt::Display for Listing {
Plain(ref l) => write!(f, "{}", l),
Threaded(ref l) => write!(f, "{}", l),
Conversations(ref l) => write!(f, "{}", l),
Offline(ref l) => write!(f, "{}", l),
}
}
}
@ -328,7 +327,6 @@ impl Component for Listing {
if !is_valid_area!(area) {
return;
}
self.theme_default = crate::conf::value(context, "theme_default");
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let total_cols = get_x(bottom_right) - get_x(upper_left);
@ -363,13 +361,13 @@ impl Component for Listing {
if right_component_width == total_cols {
if let Err(err) = context.is_online(self.cursor_pos.0) {
clear_area(grid, area);
clear_area(grid, area, self.theme_default);
let (x, _) = write_string_to_grid(
"offline: ",
grid,
Color::Byte(243),
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
(set_x(upper_left, mid + 1), bottom_right),
None,
);
@ -377,8 +375,8 @@ impl Component for Listing {
&err.to_string(),
grid,
Color::Red,
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
(set_x(upper_left, x + 1), bottom_right),
None,
);
@ -392,13 +390,17 @@ impl Component for Listing {
} else {
self.draw_menu(grid, (upper_left, (mid, get_y(bottom_right))), context);
if let Err(err) = context.is_online(self.cursor_pos.0) {
clear_area(grid, (set_x(upper_left, mid + 1), bottom_right));
clear_area(
grid,
(set_x(upper_left, mid + 1), bottom_right),
self.theme_default,
);
let (x, _) = write_string_to_grid(
"offline: ",
grid,
Color::Byte(243),
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
(set_x(upper_left, mid + 1), bottom_right),
None,
);
@ -406,8 +408,8 @@ impl Component for Listing {
&err.to_string(),
grid,
Color::Red,
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
(set_x(upper_left, x + 1), bottom_right),
None,
);
@ -420,15 +422,11 @@ impl Component for Listing {
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
match event {
UIEvent::StartupCheck(ref f) => {
if context.accounts[self.component.coordinates().0]
.folders_order
.get(self.component.coordinates().1)
.map(|&folder_hash| *f == folder_hash)
.unwrap_or(false)
{
if self.component.coordinates().1 == *f {
if !self.startup_checks_rate.tick() {
return false;
}
@ -437,15 +435,54 @@ impl Component for Listing {
UIEvent::Timer(n) if *n == self.startup_checks_rate.id() => {
if self.startup_checks_rate.active {
self.startup_checks_rate.reset();
if let Some(folder_hash) = context.accounts[self.component.coordinates().0]
.folders_order
.get(self.component.coordinates().1)
{
return self
.process_event(&mut UIEvent::StartupCheck(*folder_hash), context);
}
return self.process_event(
&mut UIEvent::StartupCheck(self.component.coordinates().1),
context,
);
}
}
UIEvent::AccountStatusChange(account_index) => {
if self.cursor_pos.0 == *account_index {
self.change_account(context);
} else {
self.accounts[*account_index].entries = context.accounts[*account_index]
.list_folders()
.into_iter()
.filter(|folder_node| {
context.accounts[*account_index].ref_folders()[&folder_node.hash]
.is_subscribed()
})
.map(|f| (f.depth, f.hash))
.collect::<_>();
self.set_dirty(true);
}
return true;
}
UIEvent::MailboxDelete((account_index, _folder_hash))
| UIEvent::MailboxCreate((account_index, _folder_hash)) => {
self.accounts[*account_index].entries = context.accounts[*account_index]
.list_folders()
.into_iter()
.filter(|folder_node| {
context.accounts[*account_index].ref_folders()[&folder_node.hash]
.is_subscribed()
})
.map(|f| (f.depth, f.hash))
.collect::<_>();
if self.cursor_pos.0 == *account_index {
self.cursor_pos.1 = std::cmp::min(
self.accounts[self.cursor_pos.0].entries.len() - 1,
self.cursor_pos.1,
);
self.component.set_coordinates((
self.cursor_pos.0,
self.accounts[self.cursor_pos.0].entries[self.cursor_pos.1].1,
));
self.component.refresh_mailbox(context, true);
}
self.set_dirty(true);
return true;
}
_ => {}
}
@ -474,15 +511,15 @@ impl Component for Listing {
.push_back(UIEvent::StatusEvent(StatusEvent::BufClear));
return true;
};
let folder_length = context.accounts[self.cursor_pos.0].len();
match k {
k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["next_folder"])
&& folder_length > 0 =>
{
if self.cursor_pos.1 + amount < folder_length {
k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["next_folder"]) => {
if let Some((_, folder_hash)) = self.accounts[self.cursor_pos.0]
.entries
.get(self.cursor_pos.1 + amount)
{
self.cursor_pos.1 += amount;
self.component
.set_coordinates((self.cursor_pos.0, self.cursor_pos.1));
.set_coordinates((self.cursor_pos.0, *folder_hash));
self.set_dirty(true);
} else {
return true;
@ -490,25 +527,32 @@ impl Component for Listing {
}
k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["prev_folder"]) => {
if self.cursor_pos.1 >= amount {
self.cursor_pos.1 -= amount;
self.component
.set_coordinates((self.cursor_pos.0, self.cursor_pos.1));
self.set_dirty(true);
if let Some((_, folder_hash)) = self.accounts[self.cursor_pos.0]
.entries
.get(self.cursor_pos.1 - amount)
{
self.cursor_pos.1 -= amount;
self.component
.set_coordinates((self.cursor_pos.0, *folder_hash));
self.set_dirty(true);
} else {
return true;
}
} else {
return true;
}
}
_ => return false,
_ => {}
}
/* Account might have no folders yet if it's offline */
if let Some(&folder_hash) = context.accounts[self.cursor_pos.0]
.folders_order
if let Some((_, folder_hash)) = self.accounts[self.cursor_pos.0]
.entries
.get(self.cursor_pos.1)
{
/* Account might have no folders yet if it's offline */
/* Check if per-folder configuration overrides general configuration */
if let Some(index_style) =
context.accounts.get(self.cursor_pos.0).and_then(|account| {
account.folder_confs(folder_hash).conf_override.index_style
account.folder_confs(*folder_hash).conf_override.index_style
})
{
self.component.set_style(index_style);
@ -523,7 +567,7 @@ impl Component for Listing {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context).unwrap(),
self.get_status(context),
)));
return true;
}
@ -550,8 +594,6 @@ impl Component for Listing {
k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["next_account"]) => {
if self.cursor_pos.0 + amount < self.accounts.len() {
self.cursor_pos = (self.cursor_pos.0 + amount, 0);
self.component.set_coordinates((self.cursor_pos.0, 0));
self.set_dirty(true);
} else {
return true;
}
@ -559,40 +601,14 @@ impl Component for Listing {
k if shortcut!(k == shortcuts[Listing::DESCRIPTION]["prev_account"]) => {
if self.cursor_pos.0 >= amount {
self.cursor_pos = (self.cursor_pos.0 - amount, 0);
self.component.set_coordinates((self.cursor_pos.0, 0));
self.set_dirty(true);
} else {
return true;
}
}
_ => return false,
}
self.change_account(context);
/* Account might have no folders yet if it's offline */
if let Some(&folder_hash) = context.accounts[self.cursor_pos.0]
.folders_order
.get(self.cursor_pos.1)
{
/* Check if per-folder configuration overrides general configuration */
if let Some(index_style) =
context.accounts.get(self.cursor_pos.0).and_then(|account| {
account.folder_confs(folder_hash).conf_override.index_style
})
{
self.component.set_style(index_style);
} else if let Some(index_style) = context
.accounts
.get(self.cursor_pos.0)
.and_then(|account| Some(account.settings.conf.index_style()))
{
self.component.set_style(index_style);
}
}
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context).unwrap(),
)));
return true;
}
UIEvent::Action(ref action) => match action {
@ -623,6 +639,19 @@ impl Component for Listing {
self.component.set_dirty(true);
return true;
}
Action::ViewMailbox(idx) => {
if let Some((_, folder_hash)) =
self.accounts[self.cursor_pos.0].entries.get(*idx)
{
self.cursor_pos.1 = *idx;
self.component
.set_coordinates((self.cursor_pos.0, *folder_hash));
self.set_dirty(true);
} else {
return true;
}
return true;
}
_ => {}
},
UIEvent::ChangeMode(UIMode::Normal) => {
@ -776,7 +805,7 @@ impl Component for Listing {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context).unwrap(),
self.get_status(context),
)));
}
UIEvent::MailboxUpdate(_) => {
@ -784,7 +813,7 @@ impl Component for Listing {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context).unwrap(),
self.get_status(context),
)));
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt('')) => {
@ -830,86 +859,79 @@ impl Component for Listing {
self.component.set_id(id);
}
fn get_status(&self, context: &Context) -> Option<String> {
Some({
let folder_hash = if let Some(h) = context.accounts[self.cursor_pos.0]
.folders_order
.get(self.cursor_pos.1)
{
*h
} else {
return Some(String::new());
};
if !context.accounts[self.cursor_pos.0].folders[&folder_hash].is_available() {
return Some(String::new());
}
let account = &context.accounts[self.cursor_pos.0];
let m = if account[self.cursor_pos.1].is_available() {
account[self.cursor_pos.1].unwrap()
} else {
return Some(String::new());
};
fn get_status(&self, context: &Context) -> String {
let folder_hash = if let Some((_, folder_hash)) = self.accounts[self.cursor_pos.0]
.entries
.get(self.cursor_pos.1)
{
*folder_hash
} else {
return String::new();
};
let account = &context.accounts[self.cursor_pos.0];
if let Ok(m) = account[folder_hash].as_result() {
format!(
"Mailbox: {}, Messages: {}, New: {}",
m.folder.name(),
m.envelopes.len(),
m.folder.count().ok().map(|(v, _)| v).unwrap_or(0),
)
})
} else {
account[folder_hash].to_string()
}
}
}
impl From<IndexStyle> for ListingComponent {
fn from(index_style: IndexStyle) -> Self {
impl From<(IndexStyle, (usize, FolderHash))> for ListingComponent {
fn from((index_style, coordinates): (IndexStyle, (usize, FolderHash))) -> Self {
match index_style {
IndexStyle::Plain => Plain(Default::default()),
IndexStyle::Threaded => Threaded(Default::default()),
IndexStyle::Compact => Compact(Default::default()),
IndexStyle::Conversations => Conversations(Default::default()),
IndexStyle::Plain => Plain(PlainListing::new(coordinates)),
IndexStyle::Threaded => Threaded(ThreadListing::new(coordinates)),
IndexStyle::Compact => Compact(CompactListing::new(coordinates)),
IndexStyle::Conversations => Conversations(ConversationsListing::new(coordinates)),
}
}
}
impl Listing {
const DESCRIPTION: &'static str = "listing";
pub fn new(accounts: &[Account]) -> Self {
let account_entries = accounts
pub fn new(context: &mut Context) -> Self {
let account_entries: Vec<AccountMenuEntry> = context
.accounts
.iter()
.enumerate()
.map(|(i, a)| AccountMenuEntry {
name: a.name().to_string(),
index: i,
.map(|(i, a)| {
let entries: SmallVec<[(usize, FolderHash); 16]> = a
.list_folders()
.into_iter()
.filter(|folder_node| a.ref_folders()[&folder_node.hash].is_subscribed())
.map(|f| (f.depth, f.hash))
.collect::<_>();
AccountMenuEntry {
name: a.name().to_string(),
index: i,
entries,
}
})
.collect();
/* Check if per-folder configuration overrides general configuration */
let component = if let Some(index_style) = accounts.get(0).and_then(|account| {
account.folders_order.get(0).and_then(|folder_hash| {
account.folder_confs(*folder_hash).conf_override.index_style
})
}) {
ListingComponent::from(index_style)
} else if let Some(index_style) = accounts
.get(0)
.and_then(|account| Some(account.settings.conf.index_style()))
{
ListingComponent::from(index_style)
} else {
Conversations(Default::default())
};
Listing {
component,
let mut ret = Listing {
component: Offline(OfflineListing::new((0, 0))),
accounts: account_entries,
visible: true,
dirty: true,
cursor_pos: (0, 0),
startup_checks_rate: RateLimit::new(2, 1000),
theme_default: ThemeAttribute::default(),
theme_default: conf::value(context, "theme_default"),
id: ComponentId::new_v4(),
show_divider: false,
menu_visibility: true,
ratio: 90,
cmd_buf: String::with_capacity(4),
}
};
ret.change_account(context);
ret
}
fn draw_menu(&mut self, grid: &mut CellBuffer, mut area: Area, context: &mut Context) {
@ -952,86 +974,31 @@ impl Listing {
debug!("BUG: invalid area in print_account");
}
// Each entry and its index in the account
let entries: FnvHashMap<FolderHash, Folder> = context.accounts[a.index]
.list_folders()
.into_iter()
.map(|f| (f.hash(), f))
.collect();
let folders_order: FnvHashMap<FolderHash, usize> = context.accounts[a.index]
.folders_order()
.iter()
.enumerate()
.map(|(i, &fh)| (fh, i))
.collect();
let folders: FnvHashMap<FolderHash, Folder> =
context.accounts[a.index].ref_folders().clone();
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let must_highlight_account: bool = self.cursor_pos.0 == a.index;
let mut inc = 0;
let mut depth = 0;
let mut lines: Vec<(usize, usize, FolderHash, Option<usize>)> = Vec::new();
/* Gather the folder tree structure in `lines` recursively */
fn print(
folder_idx: FolderHash,
depth: &mut usize,
inc: &mut usize,
entries: &FnvHashMap<FolderHash, Folder>,
folders_order: &FnvHashMap<FolderHash, usize>,
lines: &mut Vec<(usize, usize, FolderHash, Option<usize>)>,
index: usize, //account index
context: &mut Context,
) {
match context.accounts[index].status(entries[&folder_idx].hash()) {
Ok(_) => {
lines.push((
*depth,
*inc,
folder_idx,
entries[&folder_idx].count().ok().map(|(v, _)| v),
));
for (i, &(depth, folder_hash)) in a.entries.iter().enumerate() {
if folders[&folder_hash].is_subscribed() {
match context.accounts[a.index].status(folder_hash) {
Ok(_) => {
lines.push((
depth,
i,
folder_hash,
folders[&folder_hash].count().ok().map(|(v, _)| v),
));
}
Err(_) => {
lines.push((depth, i, folder_hash, None));
}
}
Err(_) => {
lines.push((*depth, *inc, folder_idx, None));
}
}
*inc += 1;
let mut children: Vec<FolderHash> = entries[&folder_idx].children().to_vec();
children
.sort_unstable_by(|a, b| folders_order[a].partial_cmp(&folders_order[b]).unwrap());
*depth += 1;
for child in children {
print(
child,
depth,
inc,
entries,
folders_order,
lines,
index,
context,
);
}
*depth -= 1;
}
let mut keys = entries.keys().cloned().collect::<Vec<FolderHash>>();
keys.sort_unstable_by(|a, b| folders_order[a].partial_cmp(&folders_order[b]).unwrap());
/* Start with roots */
for f in keys {
if entries[&f].parent().is_none() {
print(
f,
&mut depth,
&mut inc,
&entries,
&folders_order,
&mut lines,
a.index,
context,
);
}
}
@ -1051,8 +1018,8 @@ impl Listing {
"offline",
grid,
Color::Byte(243),
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
(pos_inc(upper_left, (0, 1)), bottom_right),
None,
);
@ -1138,7 +1105,7 @@ impl Listing {
None,
);
let (x, _) = write_string_to_grid(
entries[&folder_idx].name(),
folders[&folder_idx].name(),
grid,
att.fg,
att.bg,
@ -1190,4 +1157,46 @@ impl Listing {
idx - 1
}
}
fn change_account(&mut self, context: &mut Context) {
self.accounts[self.cursor_pos.0].entries = context.accounts[self.cursor_pos.0]
.list_folders()
.into_iter()
.filter(|folder_node| {
context.accounts[self.cursor_pos.0].ref_folders()[&folder_node.hash].is_subscribed()
})
.map(|f| (f.depth, f.hash))
.collect::<_>();
/* Account might have no folders yet if it's offline */
if let Some((_, folder_hash)) = self.accounts[self.cursor_pos.0]
.entries
.get(self.cursor_pos.1)
{
self.component
.set_coordinates((self.cursor_pos.0, *folder_hash));
/* Check if per-folder configuration overrides general configuration */
if let Some(index_style) = context
.accounts
.get(self.cursor_pos.0)
.and_then(|account| account.folder_confs(*folder_hash).conf_override.index_style)
{
self.component.set_style(index_style);
} else if let Some(index_style) = context
.accounts
.get(self.cursor_pos.0)
.and_then(|account| Some(account.settings.conf.index_style()))
{
self.component.set_style(index_style);
}
} else {
/* Set to dummy */
self.component = Offline(OfflineListing::new((self.cursor_pos.0, 0)));
}
self.set_dirty(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(debug!(
self.get_status(context)
))));
}
}

View File

@ -49,9 +49,8 @@ macro_rules! address_list {
#[derive(Debug)]
pub struct CompactListing {
/// (x, y, z): x is accounts, y is folders, z is index inside a folder.
cursor_pos: (usize, usize, usize),
new_cursor_pos: (usize, usize, usize),
folder_hash: FolderHash,
cursor_pos: (usize, FolderHash, usize),
new_cursor_pos: (usize, FolderHash, usize),
length: usize,
sort: (SortField, SortOrder),
subsort: (SortField, SortOrder),
@ -100,16 +99,102 @@ impl MailListingTrait for CompactListing {
.cloned();
SmallVec::from_iter(iter.into_iter())
}
/// Fill the `self.data_columns` `CellBuffers` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context, force: bool) {
self.dirty = true;
let old_cursor_pos = self.cursor_pos;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
{
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
}
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
self.color_cache = ColorCache {
unseen: crate::conf::value(context, "mail.listing.compact.unseen"),
highlighted: crate::conf::value(context, "mail.listing.compact.highlighted"),
even: crate::conf::value(context, "mail.listing.compact.even"),
odd: crate::conf::value(context, "mail.listing.compact.odd"),
selected: crate::conf::value(context, "mail.listing.compact.selected"),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"),
theme_default: crate::conf::value(context, "theme_default"),
..self.color_cache
};
if std::env::var("NO_COLOR").is_ok()
&& (context.settings.terminal.use_color.is_false()
|| context.settings.terminal.use_color.is_internal())
{
self.color_cache.highlighted.attrs |= Attr::Reverse;
}
// Get mailbox as a reference.
//
match context.accounts[self.cursor_pos.0].status(self.cursor_pos.1) {
Ok(()) => {}
Err(_) => {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
let message: String =
context.accounts[self.cursor_pos.0][self.cursor_pos.1].to_string();
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
self.length = 0;
write_string_to_grid(
message.as_str(),
&mut self.data_columns.columns[0],
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
return;
}
}
let threads = &context.accounts[self.cursor_pos.0].collection.threads[&self.cursor_pos.1];
self.all_threads.clear();
let mut roots = threads.roots();
threads.group_inner_sort_by(
&mut roots,
self.sort,
&context.accounts[self.cursor_pos.0].collection.envelopes,
);
self.redraw_list(
context,
Box::new(roots.into_iter()) as Box<dyn Iterator<Item = ThreadHash>>,
);
if !force && old_cursor_pos == self.new_cursor_pos {
self.view.update(context);
} else if self.unfocused {
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.new_cursor_pos, thread, None, context);
}
}
}
impl ListingTrait for CompactListing {
fn coordinates(&self) -> (usize, usize) {
fn coordinates(&self) -> (usize, FolderHash) {
(self.new_cursor_pos.0, self.new_cursor_pos.1)
}
fn set_coordinates(&mut self, coordinates: (usize, usize)) {
fn set_coordinates(&mut self, coordinates: (usize, FolderHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.unfocused = false;
self.view = ThreadView::default();
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term.clear();
@ -123,7 +208,7 @@ impl ListingTrait for CompactListing {
let thread_hash = self.get_thread_under_cursor(idx);
let account = &context.accounts[self.cursor_pos.0];
let threads = &account.collection.threads[&self.folder_hash];
let threads = &account.collection.threads[&self.cursor_pos.1];
let thread = threads.thread_ref(thread_hash);
let fg_color = if thread.unseen() > 0 {
@ -190,12 +275,12 @@ impl ListingTrait for CompactListing {
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.cursor_pos.1 != self.new_cursor_pos.1 || self.cursor_pos.0 != self.new_cursor_pos.0
{
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.length == 0 {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
copy_area(
grid,
&self.data_columns.columns[0],
@ -206,6 +291,9 @@ impl ListingTrait for CompactListing {
return;
}
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
if rows == 0 {
return;
}
if let Some(mvm) = self.movement.take() {
match mvm {
@ -326,7 +414,7 @@ impl ListingTrait for CompactListing {
);
}
}
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
/* Page_no has changed, so draw new page */
let mut x = get_x(upper_left);
let mut flag_x = 0;
@ -419,6 +507,7 @@ impl ListingTrait for CompactListing {
pos_inc(upper_left, (0, self.length - top_idx)),
bottom_right,
),
self.color_cache.theme_default,
);
}
context.dirty_areas.push_back(area);
@ -441,9 +530,9 @@ impl ListingTrait for CompactListing {
}
let account = &context.accounts[self.cursor_pos.0];
match account.search(&self.filter_term, self.sort, self.folder_hash) {
match account.search(&self.filter_term, self.sort, self.cursor_pos.1) {
Ok(results) => {
let threads = &account.collection.threads[&self.folder_hash];
let threads = &account.collection.threads[&self.cursor_pos.1];
for env_hash in results {
if !account.collection.contains_key(&env_hash) {
continue;
@ -472,8 +561,15 @@ impl ListingTrait for CompactListing {
self.new_cursor_pos.2 =
std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2);
} else {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.data_columns.columns[0] =
CellBuffer::new_with_context(0, 0, Cell::with_char(' '), context);
CellBuffer::new_with_context(0, 0, default_cell, context);
}
self.redraw_list(
context,
@ -492,14 +588,21 @@ impl ListingTrait for CompactListing {
format!("Failed to search for term {}: {}", &self.filter_term, e),
ERROR,
);
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context);
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (message.len() - 1, 0)),
None,
);
@ -519,19 +622,12 @@ impl fmt::Display for CompactListing {
}
}
impl Default for CompactListing {
fn default() -> Self {
CompactListing::new()
}
}
impl CompactListing {
const DESCRIPTION: &'static str = "compact listing";
fn new() -> Self {
pub fn new(coordinates: (usize, FolderHash)) -> Self {
CompactListing {
cursor_pos: (0, 1, 0),
new_cursor_pos: (0, 0, 0),
folder_hash: 0,
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
sort: (Default::default(), Default::default()),
subsort: (SortField::Date, SortOrder::Desc),
@ -560,7 +656,7 @@ impl CompactListing {
hash: ThreadHash,
) -> EntryStrings {
let thread = threads.thread_ref(hash);
let folder = &context.accounts[self.cursor_pos.0].folder_confs[&self.folder_hash];
let folder = &context.accounts[self.cursor_pos.0].folder_confs[&self.cursor_pos.1];
let mut tags = String::new();
let mut colors: SmallVec<[_; 8]> = SmallVec::new();
let backend_lck = context.accounts[self.cursor_pos.0].backend.read().unwrap();
@ -624,93 +720,6 @@ impl CompactListing {
}
}
/// Fill the `self.data_columns` `CellBuffers` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context) {
self.dirty = true;
let old_cursor_pos = self.cursor_pos;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
{
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
}
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
self.folder_hash = if let Some(h) = context.accounts[self.cursor_pos.0]
.folders_order
.get(self.cursor_pos.1)
{
*h
} else {
self.cursor_pos.1 = old_cursor_pos.1;
self.dirty = false;
return;
};
self.color_cache = ColorCache {
unseen: crate::conf::value(context, "mail.listing.compact.unseen"),
highlighted: crate::conf::value(context, "mail.listing.compact.highlighted"),
even: crate::conf::value(context, "mail.listing.compact.even"),
odd: crate::conf::value(context, "mail.listing.compact.odd"),
selected: crate::conf::value(context, "mail.listing.compact.selected"),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"),
..self.color_cache
};
if std::env::var("NO_COLOR").is_ok()
&& (context.settings.terminal.use_color.is_false()
|| context.settings.terminal.use_color.is_internal())
{
self.color_cache.highlighted.attrs |= Attr::Reverse;
}
// Get mailbox as a reference.
//
match context.accounts[self.cursor_pos.0].status(self.folder_hash) {
Ok(()) => {}
Err(_) => {
let message: String =
context.accounts[self.cursor_pos.0][self.folder_hash].to_string();
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context);
self.length = 0;
write_string_to_grid(
message.as_str(),
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
return;
}
}
let threads = &context.accounts[self.cursor_pos.0].collection.threads[&self.folder_hash];
self.all_threads.clear();
let mut roots = threads.roots();
threads.group_inner_sort_by(
&mut roots,
self.sort,
&context.accounts[self.cursor_pos.0].collection.envelopes,
);
self.redraw_list(
context,
Box::new(roots.into_iter()) as Box<dyn Iterator<Item = ThreadHash>>,
);
if old_cursor_pos == self.new_cursor_pos {
self.view.update(context);
} else if self.unfocused {
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.new_cursor_pos, thread, None, context);
}
}
fn redraw_list(&mut self, context: &Context, items: Box<dyn Iterator<Item = ThreadHash>>) {
let account = &context.accounts[self.cursor_pos.0];
let mailbox = &account[self.cursor_pos.1].unwrap();
@ -736,7 +745,6 @@ impl CompactListing {
);
for (idx, thread) in items.enumerate() {
debug!(thread);
self.length += 1;
let thread_node = &threads.thread_nodes()[&threads.thread_ref(thread).root()];
let root_env_hash = thread_node.message().unwrap_or_else(|| {
@ -804,23 +812,30 @@ impl CompactListing {
min_width.0 = self.length.saturating_sub(1).to_string().len();
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
/* index column */
self.data_columns.columns[0] =
CellBuffer::new_with_context(min_width.0, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.0, rows.len(), default_cell, context);
/* date column */
self.data_columns.columns[1] =
CellBuffer::new_with_context(min_width.1, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.1, rows.len(), default_cell, context);
/* from column */
self.data_columns.columns[2] =
CellBuffer::new_with_context(min_width.2, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.2, rows.len(), default_cell, context);
self.data_columns.segment_tree[2] = row_widths.2.into();
/* flags column */
self.data_columns.columns[3] =
CellBuffer::new_with_context(min_width.3, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.3, rows.len(), default_cell, context);
/* subject column */
self.data_columns.columns[4] =
CellBuffer::new_with_context(min_width.4, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.4, rows.len(), default_cell, context);
self.data_columns.segment_tree[4] = row_widths.4.into();
for ((idx, (thread, root_env_hash)), strings) in rows {
@ -836,67 +851,75 @@ impl CompactListing {
panic!();
}
let thread = threads.thread_ref(thread);
let (fg_color, bg_color) = if thread.unseen() > 0 {
(self.color_cache.unseen.fg, self.color_cache.unseen.bg)
let row_attr = if thread.unseen() > 0 {
self.color_cache.unseen
} else if idx % 2 == 0 {
(self.color_cache.even.fg, self.color_cache.even.bg)
self.color_cache.even
} else {
(self.color_cache.odd.fg, self.color_cache.odd.bg)
self.color_cache.odd
};
let (x, _) = write_string_to_grid(
&idx.to_string(),
&mut self.data_columns.columns[0],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.0, idx)),
None,
);
for x in x..min_width.0 {
self.data_columns.columns[0][(x, idx)].set_bg(bg_color);
self.data_columns.columns[0][(x, idx)]
.set_bg(row_attr.bg)
.set_attrs(row_attr.attrs);
}
let (x, _) = write_string_to_grid(
&strings.date,
&mut self.data_columns.columns[1],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.1, idx)),
None,
);
for x in x..min_width.1 {
self.data_columns.columns[1][(x, idx)].set_bg(bg_color);
self.data_columns.columns[1][(x, idx)]
.set_bg(row_attr.bg)
.set_attrs(row_attr.attrs);
}
let (x, _) = write_string_to_grid(
&strings.from,
&mut self.data_columns.columns[2],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.2, idx)),
None,
);
for x in x..min_width.2 {
self.data_columns.columns[2][(x, idx)].set_bg(bg_color);
self.data_columns.columns[2][(x, idx)]
.set_bg(row_attr.bg)
.set_attrs(row_attr.attrs);
}
let (x, _) = write_string_to_grid(
&strings.flag,
&mut self.data_columns.columns[3],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.3, idx)),
None,
);
for x in x..min_width.3 {
self.data_columns.columns[3][(x, idx)].set_bg(bg_color);
self.data_columns.columns[3][(x, idx)]
.set_bg(row_attr.bg)
.set_attrs(row_attr.attrs);
}
let (x, _) = write_string_to_grid(
&strings.subject,
&mut self.data_columns.columns[4],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.4, idx)),
None,
);
@ -927,8 +950,10 @@ impl CompactListing {
x
};
for x in x..min_width.4 {
self.data_columns.columns[4][(x, idx)].set_ch(' ');
self.data_columns.columns[4][(x, idx)].set_bg(bg_color);
self.data_columns.columns[4][(x, idx)]
.set_ch(' ')
.set_bg(row_attr.bg)
.set_attrs(row_attr.attrs);
}
match (thread.snoozed(), thread.has_attachments()) {
(true, true) => {
@ -951,18 +976,14 @@ impl CompactListing {
if self.length == 0 && self.filter_term.is_empty() {
let mailbox = &account[self.cursor_pos.1];
let message = mailbox.to_string();
self.data_columns.columns[0] = CellBuffer::new_with_context(
message.len(),
self.length + 1,
Cell::with_char(' '),
context,
);
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), self.length + 1, default_cell, context);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
@ -987,7 +1008,7 @@ impl CompactListing {
fn update_line(&mut self, context: &Context, thread_hash: ThreadHash) {
let account = &context.accounts[self.cursor_pos.0];
let threads = &account.collection.threads[&self.folder_hash];
let threads = &account.collection.threads[&self.cursor_pos.1];
let thread = threads.thread_ref(thread_hash);
let thread_node_hash = threads.thread_group_iter(thread_hash).next().unwrap().1;
if let Some(env_hash) = threads.thread_nodes()[&thread_node_hash].message() {
@ -1139,13 +1160,24 @@ impl Component for CompactListing {
self.filter_term
),
grid,
Color::Default,
Color::Default,
Attr::Default,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
area,
Some(get_x(upper_left)),
);
clear_area(grid, ((x, y), set_y(bottom_right, y)));
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
for row in grid.bounds_iter(((x, y), set_y(bottom_right, y))) {
for c in row {
grid[c] = default_cell;
}
}
context
.dirty_areas
.push_back((upper_left, set_y(bottom_right, y + 1)));
@ -1183,7 +1215,7 @@ impl Component for CompactListing {
}
} else {
if self.length == 0 && self.dirty {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
context.dirty_areas.push_back(area);
return;
}
@ -1244,7 +1276,7 @@ impl Component for CompactListing {
// FIXME: perform sort
self.dirty = true;
} else {
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
}
return true;
}
@ -1260,13 +1292,13 @@ impl Component for CompactListing {
account
.collection
.threads
.entry(self.folder_hash)
.entry(self.cursor_pos.1)
.and_modify(|threads| {
let is_snoozed = threads.thread_ref(thread).snoozed();
threads.thread_ref_mut(thread).set_snoozed(!is_snoozed);
});
self.row_updates.push(thread);
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
return true;
}
@ -1278,18 +1310,18 @@ impl Component for CompactListing {
}
match *event {
UIEvent::MailboxUpdate((ref idxa, ref idxf))
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.folder_hash) =>
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.cursor_pos.1) =>
{
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
UIEvent::StartupCheck(ref f) if *f == self.folder_hash => {
self.refresh_mailbox(context);
UIEvent::StartupCheck(ref f) if *f == self.cursor_pos.1 => {
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => {
let account = &context.accounts[self.cursor_pos.0];
let threads = &account.collection.threads[&self.folder_hash];
let threads = &account.collection.threads[&self.cursor_pos.1];
if !account.collection.contains_key(&new_hash) {
return false;
}
@ -1316,24 +1348,11 @@ impl Component for CompactListing {
}
UIEvent::Input(Key::Esc) if !self.unfocused && !self.filter_term.is_empty() => {
self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1));
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
self.set_dirty(true);
return true;
}
UIEvent::Action(ref action) => match action {
Action::ViewMailbox(idx) => {
if context.accounts[self.cursor_pos.0]
.folders_order
.get(*idx)
.is_none()
{
return true;
}
self.filtered_selection.clear();
self.new_cursor_pos.1 = *idx;
self.refresh_mailbox(context);
return true;
}
Action::Listing(Filter(ref filter_term)) if !self.unfocused => {
self.filter(filter_term, context);
self.dirty = true;

View File

@ -28,9 +28,8 @@ use std::iter::FromIterator;
#[derive(Debug)]
pub struct ConversationsListing {
/// (x, y, z): x is accounts, y is folders, z is index inside a folder.
cursor_pos: (usize, usize, usize),
new_cursor_pos: (usize, usize, usize),
folder_hash: FolderHash,
cursor_pos: (usize, FolderHash, usize),
new_cursor_pos: (usize, FolderHash, usize),
length: usize,
sort: (SortField, SortOrder),
subsort: (SortField, SortOrder),
@ -79,16 +78,109 @@ impl MailListingTrait for ConversationsListing {
.cloned();
SmallVec::from_iter(iter.into_iter())
}
/// Fill the `self.data_columns` `CellBuffers` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context, force: bool) {
self.dirty = true;
let old_folder_hash = self.cursor_pos.1;
let old_cursor_pos = self.cursor_pos;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
{
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
}
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
self.color_cache = ColorCache {
theme_default: crate::conf::value(context, "mail.listing.conversations"),
subject: crate::conf::value(context, "mail.listing.conversations.subject"),
from: crate::conf::value(context, "mail.listing.conversations.from"),
date: crate::conf::value(context, "mail.listing.conversations.date"),
padding: crate::conf::value(context, "mail.listing.conversations.padding"),
unseen: crate::conf::value(context, "mail.listing.conversations.unseen"),
unseen_padding: crate::conf::value(
context,
"mail.listing.conversations.unseen_padding",
),
highlighted: crate::conf::value(context, "mail.listing.conversations.highlighted"),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"),
..self.color_cache
};
if std::env::var("NO_COLOR").is_ok()
&& (context.settings.terminal.use_color.is_false()
|| context.settings.terminal.use_color.is_internal())
{
self.color_cache.highlighted.attrs |= Attr::Reverse;
}
// Get mailbox as a reference.
//
match context.accounts[self.cursor_pos.0].status(self.cursor_pos.1) {
Ok(()) => {}
Err(_) => {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
let message: String =
context.accounts[self.cursor_pos.0][self.cursor_pos.1].to_string();
self.content =
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
self.length = 0;
write_string_to_grid(
message.as_str(),
&mut self.content,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (message.len() - 1, 0)),
None,
);
return;
}
}
let threads = &context.accounts[self.cursor_pos.0].collection.threads[&self.cursor_pos.1];
self.all_threads.clear();
let mut roots = threads.roots();
threads.group_inner_sort_by(
&mut roots,
self.sort,
&context.accounts[self.cursor_pos.0].collection.envelopes,
);
self.redraw_list(
context,
Box::new(roots.into_iter()) as Box<dyn Iterator<Item = ThreadHash>>,
);
if !force && old_cursor_pos == self.new_cursor_pos && old_folder_hash == self.cursor_pos.1 {
self.view.update(context);
} else if self.unfocused {
let thread_group = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context);
}
}
}
impl ListingTrait for ConversationsListing {
fn coordinates(&self) -> (usize, usize) {
fn coordinates(&self) -> (usize, FolderHash) {
(self.new_cursor_pos.0, self.new_cursor_pos.1)
}
fn set_coordinates(&mut self, coordinates: (usize, usize)) {
fn set_coordinates(&mut self, coordinates: (usize, FolderHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.unfocused = false;
self.view = ThreadView::default();
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term.clear();
@ -102,7 +194,7 @@ impl ListingTrait for ConversationsListing {
let thread_hash = self.get_thread_under_cursor(idx);
let account = &context.accounts[self.cursor_pos.0];
let threads = &account.collection.threads[&self.folder_hash];
let threads = &account.collection.threads[&self.cursor_pos.1];
let thread = threads.thread_ref(thread_hash);
let fg_color = if thread.unseen() > 0 {
@ -183,12 +275,12 @@ impl ListingTrait for ConversationsListing {
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.cursor_pos.1 != self.new_cursor_pos.1 || self.cursor_pos.0 != self.new_cursor_pos.0
{
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.length == 0 {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
copy_area(
grid,
&self.content,
@ -199,6 +291,9 @@ impl ListingTrait for ConversationsListing {
return;
}
let rows = (get_y(bottom_right) - get_y(upper_left) + 1) / 3;
if rows == 0 {
return;
}
let pad = (get_y(bottom_right) - get_y(upper_left) + 1) % 3;
if let Some(mvm) = self.movement.take() {
@ -265,7 +360,7 @@ impl ListingTrait for ConversationsListing {
self.cursor_pos.2 = self.new_cursor_pos.2;
}
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
/* Page_no has changed, so draw new page */
copy_area(
grid,
@ -305,6 +400,7 @@ impl ListingTrait for ConversationsListing {
pos_inc(upper_left, (0, 3 * (self.length - top_idx))),
bottom_right,
),
self.color_cache.theme_default,
);
(0, self.length - top_idx)
} else {
@ -321,12 +417,14 @@ impl ListingTrait for ConversationsListing {
let bg_color = grid[(get_x(upper_left) + width - 1, y_offset + 3 * y)].bg();
for x in (get_x(upper_left) + width)..=get_x(bottom_right) {
grid[(x, y_offset + 3 * y)].set_bg(bg_color);
grid[(x, y_offset + 3 * y + 1)].set_ch('▁');
grid[(x, y_offset + 3 * y + 2)].set_fg(Color::Default);
grid[(x, y_offset + 3 * y + 1)].set_bg(bg_color);
grid[(x, y_offset + 3 * y + 2)].set_ch('▓');
grid[(x, y_offset + 3 * y + 2)].set_fg(padding_fg);
grid[(x, y_offset + 3 * y + 2)].set_bg(bg_color);
grid[(x, y_offset + 3 * y + 1)]
.set_ch('▁')
.set_fg(self.color_cache.theme_default.fg)
.set_bg(bg_color);
grid[(x, y_offset + 3 * y + 2)]
.set_ch('▓')
.set_fg(padding_fg)
.set_bg(bg_color);
}
}
if pad > 0 {
@ -337,10 +435,10 @@ impl ListingTrait for ConversationsListing {
grid[(x, y_offset + y + 1)].set_ch('▁');
grid[(x, y_offset + y + 1)].set_bg(bg_color);
if pad == 2 {
grid[(x, y_offset + y + 2)].set_fg(Color::Default);
grid[(x, y_offset + y + 2)].set_ch('▓');
grid[(x, y_offset + y + 2)].set_fg(padding_fg);
grid[(x, y_offset + y + 2)].set_bg(bg_color);
grid[(x, y_offset + y + 2)]
.set_ch('▓')
.set_fg(padding_fg)
.set_bg(bg_color);
}
}
}
@ -366,9 +464,9 @@ impl ListingTrait for ConversationsListing {
}
let account = &context.accounts[self.cursor_pos.0];
match account.search(&self.filter_term, self.sort, self.folder_hash) {
match account.search(&self.filter_term, self.sort, self.cursor_pos.1) {
Ok(results) => {
let threads = &account.collection.threads[&self.folder_hash];
let threads = &account.collection.threads[&self.cursor_pos.1];
for env_hash in results {
if !account.collection.contains_key(&env_hash) {
continue;
@ -397,8 +495,14 @@ impl ListingTrait for ConversationsListing {
self.new_cursor_pos.2 =
std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2);
} else {
self.content =
CellBuffer::new_with_context(0, 0, Cell::with_char(' '), context);
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.content = CellBuffer::new_with_context(0, 0, default_cell, context);
}
self.redraw_list(
context,
@ -417,14 +521,21 @@ impl ListingTrait for ConversationsListing {
format!("Failed to search for term {}: {}", self.filter_term, e),
ERROR,
);
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.content =
CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context);
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
write_string_to_grid(
&message,
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (message.len() - 1, 0)),
None,
);
@ -444,19 +555,12 @@ impl fmt::Display for ConversationsListing {
}
}
impl Default for ConversationsListing {
fn default() -> Self {
ConversationsListing::new()
}
}
impl ConversationsListing {
const DESCRIPTION: &'static str = "compact listing";
fn new() -> Self {
pub fn new(coordinates: (usize, FolderHash)) -> Self {
ConversationsListing {
cursor_pos: (0, 1, 0),
new_cursor_pos: (0, 0, 0),
folder_hash: 0,
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
sort: (Default::default(), Default::default()),
subsort: (SortField::Date, SortOrder::Desc),
@ -486,7 +590,7 @@ impl ConversationsListing {
hash: ThreadHash,
) -> EntryStrings {
let thread = threads.thread_ref(hash);
let folder = &context.accounts[self.cursor_pos.0].folder_confs[&self.folder_hash];
let folder = &context.accounts[self.cursor_pos.0].folder_confs[&self.cursor_pos.1];
let mut tags = String::new();
let mut colors = SmallVec::new();
let backend_lck = context.accounts[self.cursor_pos.0].backend.read().unwrap();
@ -550,99 +654,6 @@ impl ConversationsListing {
}
}
/// Fill the `self.data_columns` `CellBuffers` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context) {
self.dirty = true;
let old_cursor_pos = self.cursor_pos;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
{
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
}
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
self.folder_hash = if let Some(h) = context.accounts[self.cursor_pos.0]
.folders_order
.get(self.cursor_pos.1)
{
*h
} else {
self.cursor_pos.1 = old_cursor_pos.1;
self.dirty = false;
return;
};
self.color_cache = ColorCache {
theme_default: crate::conf::value(context, "mail.listing.conversations"),
subject: crate::conf::value(context, "mail.listing.conversations.subject"),
from: crate::conf::value(context, "mail.listing.conversations.from"),
date: crate::conf::value(context, "mail.listing.conversations.date"),
padding: crate::conf::value(context, "mail.listing.conversations.padding"),
unseen: crate::conf::value(context, "mail.listing.conversations.unseen"),
unseen_padding: crate::conf::value(
context,
"mail.listing.conversations.unseen_padding",
),
highlighted: crate::conf::value(context, "mail.listing.conversations.highlighted"),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"),
..self.color_cache
};
if std::env::var("NO_COLOR").is_ok()
&& (context.settings.terminal.use_color.is_false()
|| context.settings.terminal.use_color.is_internal())
{
self.color_cache.highlighted.attrs |= Attr::Reverse;
}
// Get mailbox as a reference.
//
match context.accounts[self.cursor_pos.0].status(self.folder_hash) {
Ok(()) => {}
Err(_) => {
let message: String =
context.accounts[self.cursor_pos.0][self.folder_hash].to_string();
self.content =
CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context);
self.length = 0;
write_string_to_grid(
message.as_str(),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
((0, 0), (message.len() - 1, 0)),
None,
);
return;
}
}
let threads = &context.accounts[self.cursor_pos.0].collection.threads[&self.folder_hash];
self.all_threads.clear();
let mut roots = threads.roots();
threads.group_inner_sort_by(
&mut roots,
self.sort,
&context.accounts[self.cursor_pos.0].collection.envelopes,
);
self.redraw_list(
context,
Box::new(roots.into_iter()) as Box<dyn Iterator<Item = ThreadHash>>,
);
if old_cursor_pos == self.new_cursor_pos {
self.view.update(context);
} else if self.unfocused {
let thread_group = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context);
}
}
fn redraw_list(&mut self, context: &Context, items: Box<dyn Iterator<Item = ThreadHash>>) {
let account = &context.accounts[self.cursor_pos.0];
let mailbox = &account[self.cursor_pos.1].unwrap();
@ -832,16 +843,22 @@ impl ConversationsListing {
}
}
if self.length == 0 && self.filter_term.is_empty() {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
let mailbox = &account[self.cursor_pos.1];
let message = mailbox.to_string();
self.content =
CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context);
self.content = CellBuffer::new_with_context(message.len(), 1, default_cell, context);
write_string_to_grid(
&message,
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (message.len() - 1, 0)),
None,
);
@ -900,7 +917,7 @@ impl ConversationsListing {
fn update_line(&mut self, context: &Context, thread_hash: ThreadHash) {
let account = &context.accounts[self.cursor_pos.0];
let threads = &account.collection.threads[&self.folder_hash];
let threads = &account.collection.threads[&self.cursor_pos.1];
let thread = threads.thread_ref(thread_hash);
let thread_node_hash = threads.thread_group_iter(thread_hash).next().unwrap().1;
let idx: usize = self.order[&thread_hash];
@ -1048,6 +1065,7 @@ impl Component for ConversationsListing {
pos_inc(upper_left, (width!(area) / 3, 0)),
set_x(bottom_right, get_x(upper_left) + width!(area) / 3 + 1),
),
self.color_cache.theme_default,
);
context.dirty_areas.push_back((
pos_inc(upper_left, (width!(area) / 3, 0)),
@ -1069,16 +1087,20 @@ impl Component for ConversationsListing {
self.filter_term
),
grid,
Color::Default,
Color::Default,
Attr::Default,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
area,
Some(get_x(upper_left)),
);
for c in grid.row_iter(x..(get_x(bottom_right) + 1), y) {
grid[c] = Cell::default();
}
clear_area(grid, ((x, y), set_y(bottom_right, y)));
clear_area(
grid,
((x, y), set_y(bottom_right, y)),
self.color_cache.theme_default,
);
context
.dirty_areas
.push_back((upper_left, set_y(bottom_right, y + 1)));
@ -1119,7 +1141,7 @@ impl Component for ConversationsListing {
}
if self.unfocused {
if self.length == 0 && self.dirty {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
context.dirty_areas.push_back(area);
return;
}
@ -1178,7 +1200,7 @@ impl Component for ConversationsListing {
}
UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => {
let account = &context.accounts[self.cursor_pos.0];
let threads = &account.collection.threads[&self.folder_hash];
let threads = &account.collection.threads[&self.cursor_pos.1];
if !account.collection.contains_key(&new_hash) {
return false;
}
@ -1203,10 +1225,10 @@ impl Component for ConversationsListing {
self.subsort = (*field, *order);
// FIXME subsort
//if !self.filtered_selection.is_empty() {
// let threads = &account.collection.threads[&self.folder_hash];
// let threads = &account.collection.threads[&self.cursor_pos.1];
// threads.vec_inner_sort_by(&mut self.filtered_selection, self.sort, &account.collection);
//} else {
// self.refresh_mailbox(context);
// self.refresh_mailbox(context, false);
//}
return true;
}
@ -1217,7 +1239,7 @@ impl Component for ConversationsListing {
self.sort = (*field, *order);
if !self.filtered_selection.is_empty() {
let threads = &context.accounts[self.cursor_pos.0].collection.threads
[&self.folder_hash];
[&self.cursor_pos.1];
threads.vec_inner_sort_by(
&mut self.filtered_selection,
self.sort,
@ -1225,7 +1247,7 @@ impl Component for ConversationsListing {
);
self.dirty = true;
} else {
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
}
*/
return true;
@ -1236,13 +1258,13 @@ impl Component for ConversationsListing {
account
.collection
.threads
.entry(self.folder_hash)
.entry(self.cursor_pos.1)
.and_modify(|threads| {
let is_snoozed = threads.thread_ref(thread).snoozed();
threads.thread_ref_mut(thread).set_snoozed(!is_snoozed);
});
self.row_updates.push(thread);
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
return true;
}
_ => {}
@ -1252,13 +1274,13 @@ impl Component for ConversationsListing {
}
match *event {
UIEvent::MailboxUpdate((ref idxa, ref idxf))
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.folder_hash) =>
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.cursor_pos.1) =>
{
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
UIEvent::StartupCheck(ref f) if *f == self.folder_hash => {
self.refresh_mailbox(context);
UIEvent::StartupCheck(ref f) if *f == self.cursor_pos.1 => {
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
UIEvent::ChangeMode(UIMode::Normal) => {
@ -1268,19 +1290,6 @@ impl Component for ConversationsListing {
self.dirty = true;
}
UIEvent::Action(ref action) => match action {
Action::ViewMailbox(idx) => {
if context.accounts[self.cursor_pos.0]
.folders_order
.get(*idx)
.is_none()
{
return true;
}
self.set_coordinates((self.new_cursor_pos.0, *idx));
self.refresh_mailbox(context);
return true;
}
Action::Listing(Filter(ref filter_term)) if !self.unfocused => {
self.filter(filter_term, context);
self.dirty = true;
@ -1292,7 +1301,7 @@ impl Component for ConversationsListing {
if !self.unfocused && !&self.filter_term.is_empty() =>
{
self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1));
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
self.force_draw = false;
self.set_dirty(true);
return true;

View File

@ -0,0 +1,103 @@
/*
* meli
*
* Copyright 2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use crate::components::utilities::PageMovement;
#[derive(Debug)]
pub struct OfflineListing {
cursor_pos: (usize, FolderHash),
_row_updates: SmallVec<[ThreadHash; 8]>,
id: ComponentId,
}
impl MailListingTrait for OfflineListing {
fn row_updates(&mut self) -> &mut SmallVec<[ThreadHash; 8]> {
&mut self._row_updates
}
fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]> {
return SmallVec::new();
}
/// chosen.
fn refresh_mailbox(&mut self, _context: &mut Context, _force: bool) {}
}
impl ListingTrait for OfflineListing {
fn coordinates(&self) -> (usize, FolderHash) {
self.cursor_pos
}
fn set_coordinates(&mut self, coordinates: (usize, FolderHash)) {
self.cursor_pos = coordinates;
}
fn highlight_line(
&mut self,
_grid: &mut CellBuffer,
_area: Area,
_idx: usize,
_context: &Context,
) {
}
fn draw_list(&mut self, _: &mut CellBuffer, _: Area, _: &mut Context) {}
fn filter(&mut self, _: &str, _: &Context) {}
fn set_movement(&mut self, _: PageMovement) {}
}
impl fmt::Display for OfflineListing {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "mail")
}
}
impl OfflineListing {
pub fn new(cursor_pos: (usize, FolderHash)) -> Self {
OfflineListing {
cursor_pos,
_row_updates: SmallVec::new(),
id: ComponentId::new_v4(),
}
}
}
impl Component for OfflineListing {
fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {}
fn process_event(&mut self, _event: &mut UIEvent, _context: &mut Context) -> bool {
false
}
fn is_dirty(&self) -> bool {
false
}
fn set_dirty(&mut self, _value: bool) {}
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
}

View File

@ -48,9 +48,8 @@ macro_rules! address_list {
#[derive(Debug)]
pub struct PlainListing {
/// (x, y, z): x is accounts, y is folders, z is index inside a folder.
cursor_pos: (usize, usize, usize),
new_cursor_pos: (usize, usize, usize),
folder_hash: FolderHash,
cursor_pos: (usize, FolderHash, usize),
new_cursor_pos: (usize, FolderHash, usize),
length: usize,
sort: (SortField, SortOrder),
subsort: (SortField, SortOrder),
@ -101,16 +100,108 @@ impl MailListingTrait for PlainListing {
}
*/
}
/// Fill the `self.data_columns` `CellBuffers` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context, force: bool) {
self.dirty = true;
let old_cursor_pos = self.cursor_pos;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
{
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
}
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
self.color_cache = ColorCache {
unseen: crate::conf::value(context, "mail.listing.plain.unseen"),
highlighted: crate::conf::value(context, "mail.listing.plain.highlighted"),
even: crate::conf::value(context, "mail.listing.plain.even"),
odd: crate::conf::value(context, "mail.listing.plain.odd"),
selected: crate::conf::value(context, "mail.listing.plain.selected"),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"),
..self.color_cache
};
if std::env::var("NO_COLOR").is_ok()
&& (context.settings.terminal.use_color.is_false()
|| context.settings.terminal.use_color.is_internal())
{
self.color_cache.highlighted.attrs |= Attr::Reverse;
}
// Get mailbox as a reference.
//
match context.accounts[self.cursor_pos.0].status(self.cursor_pos.1) {
Ok(()) => {}
Err(_) => {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
let message: String =
context.accounts[self.cursor_pos.0][self.cursor_pos.1].to_string();
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
self.length = 0;
write_string_to_grid(
message.as_str(),
&mut self.data_columns.columns[0],
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
return;
}
}
self.local_collection = context.accounts[self.cursor_pos.0][self.cursor_pos.1]
.unwrap()
.envelopes
.iter()
.cloned()
.collect();
let env_lck = context.accounts[self.cursor_pos.0]
.collection
.envelopes
.read()
.unwrap();
self.thread_node_hashes = context.accounts[self.cursor_pos.0][self.cursor_pos.1]
.unwrap()
.envelopes
.iter()
.map(|h| (*h, env_lck[h].thread()))
.collect();
drop(env_lck);
self.redraw_list(context);
if self.length > 0 {
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
let temp = (self.new_cursor_pos.0, self.new_cursor_pos.1, env_hash);
if !force && old_cursor_pos == self.new_cursor_pos {
self.view.update(temp);
} else if self.unfocused {
self.view = MailView::new(temp, None, None, context);
}
}
}
}
impl ListingTrait for PlainListing {
fn coordinates(&self) -> (usize, usize) {
fn coordinates(&self) -> (usize, FolderHash) {
(self.new_cursor_pos.0, self.new_cursor_pos.1)
}
fn set_coordinates(&mut self, coordinates: (usize, usize)) {
fn set_coordinates(&mut self, coordinates: (usize, FolderHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.unfocused = false;
self.view = MailView::default();
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term.clear();
@ -189,12 +280,12 @@ impl ListingTrait for PlainListing {
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.cursor_pos.1 != self.new_cursor_pos.1 || self.cursor_pos.0 != self.new_cursor_pos.0
{
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.length == 0 {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
copy_area(
grid,
&self.data_columns.columns[0],
@ -205,6 +296,9 @@ impl ListingTrait for PlainListing {
return;
}
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
if rows == 0 {
return;
}
if let Some(mvm) = self.movement.take() {
match mvm {
@ -307,7 +401,7 @@ impl ListingTrait for PlainListing {
self.data_columns.widths[2] = min_col_width;
}
}
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
/* Page_no has changed, so draw new page */
let mut x = get_x(upper_left);
let mut flag_x = 0;
@ -395,6 +489,7 @@ impl ListingTrait for PlainListing {
pos_inc(upper_left, (0, self.length - top_idx)),
bottom_right,
),
self.color_cache.theme_default,
);
}
context.dirty_areas.push_back(area);
@ -417,7 +512,7 @@ impl ListingTrait for PlainListing {
}
let account = &context.accounts[self.cursor_pos.0];
match account.search(&self.filter_term, self.sort, self.folder_hash) {
match account.search(&self.filter_term, self.sort, self.cursor_pos.1) {
Ok(results) => {
for env_hash in results {
if !account.collection.contains_key(&env_hash) {
@ -436,8 +531,15 @@ impl ListingTrait for PlainListing {
self.new_cursor_pos.2 =
std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2);
} else {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.data_columns.columns[0] =
CellBuffer::new_with_context(0, 0, Cell::with_char(' '), context);
CellBuffer::new_with_context(0, 0, default_cell, context);
}
self.redraw_list(context);
}
@ -452,14 +554,21 @@ impl ListingTrait for PlainListing {
format!("Failed to search for term {}: {}", &self.filter_term, e),
ERROR,
);
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context);
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (message.len() - 1, 0)),
None,
);
@ -479,19 +588,12 @@ impl fmt::Display for PlainListing {
}
}
impl Default for PlainListing {
fn default() -> Self {
PlainListing::new()
}
}
impl PlainListing {
const DESCRIPTION: &'static str = "plain listing";
fn new() -> Self {
pub fn new(coordinates: (usize, FolderHash)) -> Self {
PlainListing {
cursor_pos: (0, 1, 0),
new_cursor_pos: (0, 0, 0),
folder_hash: 0,
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
sort: (Default::default(), Default::default()),
subsort: (SortField::Date, SortOrder::Desc),
@ -517,7 +619,7 @@ impl PlainListing {
}
}
fn make_entry_string(&self, e: EnvelopeRef, context: &Context) -> EntryStrings {
let folder = &context.accounts[self.cursor_pos.0].folder_confs[&self.folder_hash];
let folder = &context.accounts[self.cursor_pos.0].folder_confs[&self.cursor_pos.1];
let mut tags = String::new();
let mut colors = SmallVec::new();
let backend_lck = context.accounts[self.cursor_pos.0].backend.read().unwrap();
@ -563,100 +665,6 @@ impl PlainListing {
}
}
/// Fill the `self.data_columns` `CellBuffers` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context) {
self.dirty = true;
let old_cursor_pos = self.cursor_pos;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
{
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
}
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
self.folder_hash = if let Some(h) = context.accounts[self.cursor_pos.0]
.folders_order
.get(self.cursor_pos.1)
{
*h
} else {
self.cursor_pos.1 = old_cursor_pos.1;
self.dirty = false;
return;
};
self.color_cache = ColorCache {
unseen: crate::conf::value(context, "mail.listing.plain.unseen"),
highlighted: crate::conf::value(context, "mail.listing.plain.highlighted"),
even: crate::conf::value(context, "mail.listing.plain.even"),
odd: crate::conf::value(context, "mail.listing.plain.odd"),
selected: crate::conf::value(context, "mail.listing.plain.selected"),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"),
..self.color_cache
};
if std::env::var("NO_COLOR").is_ok()
&& (context.settings.terminal.use_color.is_false()
|| context.settings.terminal.use_color.is_internal())
{
self.color_cache.highlighted.attrs |= Attr::Reverse;
}
// Get mailbox as a reference.
//
match context.accounts[self.cursor_pos.0].status(self.folder_hash) {
Ok(()) => {}
Err(_) => {
let message: String =
context.accounts[self.cursor_pos.0][self.folder_hash].to_string();
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context);
self.length = 0;
write_string_to_grid(
message.as_str(),
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
return;
}
}
self.local_collection = context.accounts[self.cursor_pos.0][self.folder_hash]
.unwrap()
.envelopes
.iter()
.cloned()
.collect();
let env_lck = context.accounts[self.cursor_pos.0]
.collection
.envelopes
.read()
.unwrap();
self.thread_node_hashes = context.accounts[self.cursor_pos.0][self.folder_hash]
.unwrap()
.envelopes
.iter()
.map(|h| (*h, env_lck[h].thread()))
.collect();
drop(env_lck);
self.redraw_list(context);
if self.length > 0 {
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
let temp = (self.new_cursor_pos.0, self.new_cursor_pos.1, env_hash);
if old_cursor_pos == self.new_cursor_pos {
self.view.update(temp);
} else if self.unfocused {
self.view = MailView::new(temp, None, None);
}
}
}
fn redraw_list(&mut self, context: &Context) {
let account = &context.accounts[self.cursor_pos.0];
let mailbox = &account[self.cursor_pos.1].unwrap();
@ -735,21 +743,28 @@ impl PlainListing {
min_width.0 = self.length.saturating_sub(1).to_string().len();
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
/* index column */
self.data_columns.columns[0] =
CellBuffer::new_with_context(min_width.0, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.0, rows.len(), default_cell, context);
/* date column */
self.data_columns.columns[1] =
CellBuffer::new_with_context(min_width.1, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.1, rows.len(), default_cell, context);
/* from column */
self.data_columns.columns[2] =
CellBuffer::new_with_context(min_width.2, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.2, rows.len(), default_cell, context);
/* flags column */
self.data_columns.columns[3] =
CellBuffer::new_with_context(min_width.3, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.3, rows.len(), default_cell, context);
/* subject column */
self.data_columns.columns[4] =
CellBuffer::new_with_context(min_width.4, rows.len(), Cell::with_char(' '), context);
CellBuffer::new_with_context(min_width.4, rows.len(), default_cell, context);
let iter = if self.filter_term.is_empty() {
Box::new(self.local_collection.iter().cloned())
@ -774,82 +789,67 @@ impl PlainListing {
}
let envelope: EnvelopeRef = context.accounts[self.cursor_pos.0].collection.get_env(i);
let fg_color = if !envelope.is_seen() {
Color::Byte(0)
let row_attr = if !envelope.is_seen() {
self.color_cache.unseen
} else if idx % 2 == 0 {
self.color_cache.even
} else {
Color::Default
};
let bg_color = if context.settings.terminal.theme == "light" {
if !envelope.is_seen() {
Color::Byte(251)
} else if idx % 2 == 0 {
Color::Byte(252)
} else {
Color::Default
}
} else {
if !envelope.is_seen() {
Color::Byte(253)
} else if idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
}
self.color_cache.odd
};
let (x, _) = write_string_to_grid(
&idx.to_string(),
&mut columns[0],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.0, idx)),
None,
);
for c in columns[0].row_iter(x..min_width.0, idx) {
columns[0][c].set_bg(bg_color);
columns[0][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
}
let (x, _) = write_string_to_grid(
&strings.date,
&mut columns[1],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.1, idx)),
None,
);
for c in columns[1].row_iter(x..min_width.1, idx) {
columns[1][c].set_bg(bg_color);
columns[1][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
}
let (x, _) = write_string_to_grid(
&strings.from,
&mut columns[2],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.2, idx)),
None,
);
for c in columns[2].row_iter(x..min_width.2, idx) {
columns[2][c].set_bg(bg_color);
columns[2][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
}
let (x, _) = write_string_to_grid(
&strings.flag,
&mut columns[3],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.3, idx)),
None,
);
for c in columns[3].row_iter(x..min_width.3, idx) {
columns[3][c].set_bg(bg_color);
columns[3][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
}
let (x, _) = write_string_to_grid(
&strings.subject,
&mut columns[4],
fg_color,
bg_color,
Attr::Default,
row_attr.fg,
row_attr.bg,
row_attr.attrs,
((0, idx), (min_width.4, idx)),
None,
);
@ -869,12 +869,10 @@ impl PlainListing {
columns[4][c].set_bg(color);
}
for c in columns[4].row_iter(_x..(_x + 1), idx) {
columns[4][c].set_bg(color);
columns[4][c].set_keep_bg(true);
columns[4][c].set_bg(color).set_keep_bg(true);
}
for c in columns[4].row_iter((x + 1)..(_x + 1), idx) {
columns[4][c].set_keep_fg(true);
columns[4][c].set_keep_bg(true);
columns[4][c].set_keep_fg(true).set_keep_bg(true);
}
for c in columns[4].row_iter(x..(x + 1), idx) {
columns[4][c].set_keep_bg(true);
@ -884,7 +882,7 @@ impl PlainListing {
x
};
for c in columns[4].row_iter(x..min_width.4, idx) {
columns[4][c].set_bg(bg_color);
columns[4][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
}
if context.accounts[self.cursor_pos.0]
.collection
@ -897,18 +895,14 @@ impl PlainListing {
if self.length == 0 && self.filter_term.is_empty() {
let mailbox = &account[self.cursor_pos.1];
let message = mailbox.to_string();
self.data_columns.columns[0] = CellBuffer::new_with_context(
message.len(),
self.length + 1,
Cell::with_char(' '),
context,
);
self.data_columns.columns[0] =
CellBuffer::new_with_context(message.len(), self.length + 1, default_cell, context);
write_string_to_grid(
&message,
&mut self.data_columns.columns[0],
Color::Default,
Color::Default,
Attr::Default,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
@ -991,7 +985,11 @@ impl Component for PlainListing {
area,
Some(get_x(upper_left)),
);
clear_area(grid, ((x, y), set_y(bottom_right, y)));
clear_area(
grid,
((x, y), set_y(bottom_right, y)),
self.color_cache.theme_default,
);
context
.dirty_areas
.push_back((upper_left, set_y(bottom_right, y + 1)));
@ -1027,7 +1025,7 @@ impl Component for PlainListing {
}
} else {
if self.length == 0 && self.dirty {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
context.dirty_areas.push_back(area);
return;
}
@ -1050,7 +1048,7 @@ impl Component for PlainListing {
{
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash);
self.view = MailView::new(temp, None, None);
self.view = MailView::new(temp, None, None, context);
self.unfocused = true;
self.dirty = true;
return true;
@ -1081,10 +1079,10 @@ impl Component for PlainListing {
debug!("SubSort {:?} , {:?}", field, order);
self.subsort = (*field, *order);
//if !self.filtered_selection.is_empty() {
// let threads = &account.collection.threads[&self.folder_hash];
// let threads = &account.collection.threads[&self.cursor_pos.1];
// threads.vec_inner_sort_by(&mut self.filtered_selection, self.sort, &account.collection);
//} else {
// self.refresh_mailbox(context);
// self.refresh_mailbox(contex, falset);
//}
return true;
}
@ -1132,19 +1130,19 @@ impl Component for PlainListing {
}
match *event {
UIEvent::MailboxUpdate((ref idxa, ref idxf))
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.folder_hash) =>
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.cursor_pos.1) =>
{
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
UIEvent::StartupCheck(ref f) if *f == self.folder_hash => {
self.refresh_mailbox(context);
UIEvent::StartupCheck(ref f) if *f == self.cursor_pos.1 => {
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => {
let account = &context.accounts[self.cursor_pos.0];
if !account.collection.contains_key(new_hash)
|| !account[self.folder_hash]
|| !account[self.cursor_pos.1]
.unwrap()
.envelopes
.contains(new_hash)
@ -1179,29 +1177,13 @@ impl Component for PlainListing {
UIEvent::Input(Key::Esc) if !self.unfocused && !self.filter_term.is_empty() => {
self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1));
self.set_dirty(true);
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
return true;
}
UIEvent::Action(ref action) => match action {
Action::ViewMailbox(idx) => {
if context.accounts[self.cursor_pos.0]
.folders_order
.get(*idx)
.is_none()
{
return true;
}
self.filtered_selection.clear();
self.new_cursor_pos.1 = *idx;
self.refresh_mailbox(context);
return true;
}
Action::Listing(Filter(ref filter_term)) if !self.unfocused => {
self.filter(filter_term, context);
self.dirty = true;
}
_ => {}
},
UIEvent::Action(Action::Listing(Filter(ref filter_term))) if !self.unfocused => {
self.filter(filter_term, context);
self.dirty = true;
}
_ => {}
}
false

View File

@ -29,14 +29,14 @@ const MAX_COLS: usize = 500;
#[derive(Debug)]
pub struct ThreadListing {
/// (x, y, z): x is accounts, y is folders, z is index inside a folder.
cursor_pos: (usize, usize, usize),
new_cursor_pos: (usize, usize, usize),
folder_hash: FolderHash,
cursor_pos: (usize, FolderHash, usize),
new_cursor_pos: (usize, FolderHash, usize),
length: usize,
sort: (SortField, SortOrder),
subsort: (SortField, SortOrder),
/// Cache current view.
content: CellBuffer,
color_cache: ColorCache,
row_updates: SmallVec<[ThreadHash; 8]>,
locations: Vec<EnvelopeHash>,
@ -58,15 +58,180 @@ impl MailListingTrait for ThreadListing {
fn get_focused_items(&self, _context: &Context) -> SmallVec<[ThreadHash; 8]> {
SmallVec::new()
}
/// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context, _force: bool) {
self.dirty = true;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
{
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
}
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
self.color_cache = ColorCache {
unseen: crate::conf::value(context, "mail.listing.plain.unseen"),
highlighted: crate::conf::value(context, "mail.listing.plain.highlighted"),
even: crate::conf::value(context, "mail.listing.plain.even"),
odd: crate::conf::value(context, "mail.listing.plain.odd"),
selected: crate::conf::value(context, "mail.listing.plain.selected"),
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"),
..self.color_cache
};
if std::env::var("NO_COLOR").is_ok()
&& (context.settings.terminal.use_color.is_false()
|| context.settings.terminal.use_color.is_internal())
{
self.color_cache.highlighted.attrs |= Attr::Reverse;
}
// Get mailbox as a reference.
//
match context.accounts[self.cursor_pos.0].status(self.cursor_pos.1) {
Ok(_) => {}
Err(_) => {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
let message: String =
context.accounts[self.cursor_pos.0][self.cursor_pos.1].to_string();
self.content =
CellBuffer::new_with_context(message.len(), 1, default_cell, context);
self.length = 0;
write_string_to_grid(
message.as_str(),
&mut self.content,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
return;
}
}
let account = &context.accounts[self.cursor_pos.0];
let mailbox = account[self.cursor_pos.1].unwrap();
let threads = &account.collection.threads[&mailbox.folder.hash()];
self.length = threads.len();
self.locations.clear();
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.color_cache.theme_default.fg)
.set_bg(self.color_cache.theme_default.bg)
.set_attrs(self.color_cache.theme_default.attrs);
ret
};
if self.length == 0 {
let message = format!("Folder `{}` is empty.", mailbox.folder.name());
self.content = CellBuffer::new_with_context(message.len(), 1, default_cell, context);
write_string_to_grid(
&message,
&mut self.content,
self.color_cache.theme_default.fg,
self.color_cache.theme_default.bg,
self.color_cache.theme_default.attrs,
((0, 0), (message.len() - 1, 0)),
None,
);
return;
}
self.content =
CellBuffer::new_with_context(MAX_COLS, self.length + 1, default_cell, context);
let mut indentations: Vec<bool> = Vec::with_capacity(6);
let mut thread_idx = 0; // needed for alternate thread colors
/* Draw threaded view. */
threads.sort_by(self.sort, self.subsort, &account.collection.envelopes);
let thread_nodes: &FnvHashMap<ThreadNodeHash, ThreadNode> = &threads.thread_nodes();
let mut iter = threads.threads_iter().peekable();
/* This is just a desugared for loop so that we can use .peek() */
let mut idx = 0;
while let Some((indentation, thread_node_hash, has_sibling)) = iter.next() {
let thread_node = &thread_nodes[&thread_node_hash];
if indentation == 0 {
thread_idx += 1;
}
if thread_node.has_message() {
let envelope: EnvelopeRef =
account.collection.get_env(thread_node.message().unwrap());
self.locations.push(envelope.hash());
let fg_color = if !envelope.is_seen() {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if !envelope.is_seen() {
Color::Byte(251)
} else if thread_idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
};
let (x, _) = write_string_to_grid(
&ThreadListing::make_thread_entry(
&envelope,
idx,
indentation,
thread_node_hash,
threads,
&indentations,
has_sibling,
),
&mut self.content,
fg_color,
bg_color,
Attr::Default,
((0, idx), (MAX_COLS - 1, idx)),
None,
);
for x in x..MAX_COLS {
self.content[(x, idx)].set_ch(' ');
self.content[(x, idx)].set_bg(bg_color);
}
idx += 1;
} else {
continue;
}
match iter.peek() {
Some((x, _, _)) if *x > indentation => {
if has_sibling {
indentations.push(true);
} else {
indentations.push(false);
}
}
Some((x, _, _)) if *x < indentation => {
for _ in 0..(indentation - *x) {
indentations.pop();
}
}
_ => {}
}
}
}
}
impl ListingTrait for ThreadListing {
fn coordinates(&self) -> (usize, usize) {
fn coordinates(&self) -> (usize, FolderHash) {
(self.new_cursor_pos.0, self.new_cursor_pos.1)
}
fn set_coordinates(&mut self, coordinates: (usize, usize)) {
fn set_coordinates(&mut self, coordinates: (usize, FolderHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.unfocused = false;
self.view = None;
self.locations.clear();
self.row_updates.clear();
self.initialised = false;
@ -74,17 +239,20 @@ impl ListingTrait for ThreadListing {
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.cursor_pos.1 != self.new_cursor_pos.1 || self.cursor_pos.0 != self.new_cursor_pos.0
{
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.length == 0 {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
copy_area(grid, &self.content, area, ((0, 0), (MAX_COLS - 1, 0)));
context.dirty_areas.push_back(area);
return;
}
let rows = get_y(bottom_right) - get_y(upper_left) + 1;
if rows == 0 {
return;
}
if let Some(mvm) = self.movement.take() {
match mvm {
PageMovement::Up(amount) => {
@ -221,12 +389,6 @@ impl ListingTrait for ThreadListing {
}
}
impl Default for ThreadListing {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ThreadListing {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "mail")
@ -234,16 +396,15 @@ impl fmt::Display for ThreadListing {
}
impl ThreadListing {
pub fn new() -> Self {
let content = CellBuffer::new(0, 0, Cell::with_char(' '));
pub fn new(coordinates: (usize, FolderHash)) -> Self {
ThreadListing {
cursor_pos: (0, 1, 0),
new_cursor_pos: (0, 0, 0),
folder_hash: 0,
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
sort: (Default::default(), Default::default()),
subsort: (Default::default(), Default::default()),
content,
content: CellBuffer::new(0, 0, Cell::with_char(' ')),
color_cache: ColorCache::default(),
row_updates: SmallVec::new(),
locations: Vec::new(),
dirty: true,
@ -254,144 +415,6 @@ impl ThreadListing {
id: ComponentId::new_v4(),
}
}
/// Fill the `self.content` `CellBuffer` with the contents of the account folder the user has
/// chosen.
fn refresh_mailbox(&mut self, context: &mut Context) {
self.dirty = true;
if !(self.cursor_pos.0 == self.new_cursor_pos.0
&& self.cursor_pos.1 == self.new_cursor_pos.1)
{
self.cursor_pos.2 = 0;
self.new_cursor_pos.2 = 0;
}
self.cursor_pos.1 = self.new_cursor_pos.1;
self.cursor_pos.0 = self.new_cursor_pos.0;
self.folder_hash = if let Some(h) = context.accounts[self.cursor_pos.0]
.folders_order
.get(self.cursor_pos.1)
{
*h
} else {
return;
};
// Get mailbox as a reference.
//
match context.accounts[self.cursor_pos.0].status(self.folder_hash) {
Ok(_) => {}
Err(_) => {
let message: String =
context.accounts[self.cursor_pos.0][self.folder_hash].to_string();
self.content = CellBuffer::new(message.len(), 1, Cell::with_char(' '));
self.length = 0;
write_string_to_grid(
message.as_str(),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
((0, 0), (MAX_COLS - 1, 0)),
None,
);
return;
}
}
let account = &context.accounts[self.cursor_pos.0];
let mailbox = account[self.cursor_pos.1].unwrap();
let threads = &account.collection.threads[&mailbox.folder.hash()];
self.length = threads.len();
self.locations.clear();
if self.length == 0 {
let message = format!("Folder `{}` is empty.", mailbox.folder.name());
self.content = CellBuffer::new(message.len(), 1, Cell::with_char(' '));
write_string_to_grid(
&message,
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
((0, 0), (message.len() - 1, 0)),
None,
);
return;
}
self.content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
let mut indentations: Vec<bool> = Vec::with_capacity(6);
let mut thread_idx = 0; // needed for alternate thread colors
/* Draw threaded view. */
threads.sort_by(self.sort, self.subsort, &account.collection.envelopes);
let thread_nodes: &FnvHashMap<ThreadNodeHash, ThreadNode> = &threads.thread_nodes();
let mut iter = threads.threads_iter().peekable();
/* This is just a desugared for loop so that we can use .peek() */
let mut idx = 0;
while let Some((indentation, thread_node_hash, has_sibling)) = iter.next() {
let thread_node = &thread_nodes[&thread_node_hash];
if indentation == 0 {
thread_idx += 1;
}
if thread_node.has_message() {
let envelope: EnvelopeRef =
account.collection.get_env(thread_node.message().unwrap());
self.locations.push(envelope.hash());
let fg_color = if !envelope.is_seen() {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if !envelope.is_seen() {
Color::Byte(251)
} else if thread_idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
};
let (x, _) = write_string_to_grid(
&ThreadListing::make_thread_entry(
&envelope,
idx,
indentation,
thread_node_hash,
threads,
&indentations,
has_sibling,
),
&mut self.content,
fg_color,
bg_color,
Attr::Default,
((0, idx), (MAX_COLS - 1, idx)),
None,
);
for x in x..MAX_COLS {
self.content[(x, idx)].set_ch(' ');
self.content[(x, idx)].set_bg(bg_color);
}
idx += 1;
} else {
continue;
}
match iter.peek() {
Some((x, _, _)) if *x > indentation => {
if has_sibling {
indentations.push(true);
} else {
indentations.push(false);
}
}
Some((x, _, _)) if *x < indentation => {
for _ in 0..(indentation - *x) {
indentations.pop();
}
}
_ => {}
}
}
}
fn highlight_line_self(&mut self, idx: usize, context: &Context) {
let mailbox = if context.accounts[self.cursor_pos.0][self.cursor_pos.1].is_available() {
@ -505,7 +528,7 @@ impl Component for ThreadListing {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if self.length == 0 && self.dirty {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
context.dirty_areas.push_back(area);
}
@ -515,7 +538,7 @@ impl Component for ThreadListing {
let bottom_entity_rows = (pager_ratio * total_rows) / 100;
if bottom_entity_rows > total_rows {
clear_area(grid, area);
clear_area(grid, area, self.color_cache.theme_default);
context.dirty_areas.push_back(area);
return;
}
@ -591,7 +614,7 @@ impl Component for ThreadListing {
if let Some(ref mut v) = self.view {
v.update(coordinates);
} else {
self.view = Some(MailView::new(coordinates, None, None));
self.view = Some(MailView::new(coordinates, None, None, context));
}
self.view.as_mut().unwrap().draw(
@ -622,13 +645,13 @@ impl Component for ThreadListing {
return true;
}
UIEvent::MailboxUpdate((ref idxa, ref idxf))
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.folder_hash) =>
if (*idxa, *idxf) == (self.new_cursor_pos.0, self.cursor_pos.1) =>
{
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
UIEvent::StartupCheck(ref f) if *f == self.folder_hash => {
self.refresh_mailbox(context);
UIEvent::StartupCheck(ref f) if *f == self.cursor_pos.1 => {
self.refresh_mailbox(context, false);
self.set_dirty(true);
}
UIEvent::ChangeMode(UIMode::Normal) => {
@ -638,24 +661,18 @@ impl Component for ThreadListing {
self.dirty = true;
}
UIEvent::Action(ref action) => match action {
Action::ViewMailbox(idx_m) => {
self.new_cursor_pos.1 = *idx_m;
self.dirty = true;
self.refresh_mailbox(context);
return true;
}
Action::SubSort(field, order) => {
debug!("SubSort {:?} , {:?}", field, order);
self.subsort = (*field, *order);
self.dirty = true;
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
return true;
}
Action::Sort(field, order) => {
debug!("Sort {:?} , {:?}", field, order);
self.sort = (*field, *order);
self.dirty = true;
self.refresh_mailbox(context);
self.refresh_mailbox(context, false);
return true;
}
_ => {}

View File

@ -29,6 +29,7 @@ pub struct StatusPanel {
status: Option<AccountStatus>,
content: CellBuffer,
dirty: bool,
theme_default: ThemeAttribute,
id: ComponentId,
}
@ -50,8 +51,8 @@ impl Component for StatusPanel {
let (_, y) = write_string_to_grid(
"Worker threads",
&mut self.content,
Color::Default,
Color::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((1, 1), (width - 1, height - 1)),
Some(1),
@ -73,9 +74,9 @@ impl Component for StatusPanel {
max_name = max_name
),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((1, y), (width - 1, height - 1)),
Some(1),
);
@ -88,8 +89,8 @@ impl Component for StatusPanel {
write_string_to_grid(
"Static threads",
&mut self.content,
Color::Default,
Color::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((1, y + 1), (width - 1, height - 1)),
Some(1),
@ -113,9 +114,9 @@ impl Component for StatusPanel {
max_name = max_name
),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((1, y), (width - 1, height - 1)),
Some(1),
);
@ -131,7 +132,7 @@ impl Component for StatusPanel {
std::cmp::min(width.saturating_sub(cols), self.cursor.0),
std::cmp::min(height.saturating_sub(rows), self.cursor.1),
);
clear_area(grid, area);
clear_area(grid, area, self.theme_default);
copy_area(
grid,
&self.content,
@ -170,7 +171,7 @@ impl Component for StatusPanel {
return true;
}
UIEvent::Input(Key::Char('\n')) if self.status.is_none() => {
self.status = Some(AccountStatus::new(self.account_cursor));
self.status = Some(AccountStatus::new(self.account_cursor, self.theme_default));
return true;
}
UIEvent::Input(Key::Esc) if self.status.is_some() => {
@ -224,8 +225,15 @@ impl Component for StatusPanel {
}
impl StatusPanel {
pub fn new() -> StatusPanel {
let mut content = CellBuffer::new(120, 40, Cell::default());
pub fn new(theme_default: ThemeAttribute) -> StatusPanel {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(theme_default.fg)
.set_bg(theme_default.bg)
.set_attrs(theme_default.attrs);
ret
};
let mut content = CellBuffer::new(120, 40, default_cell);
content.set_growable(true);
StatusPanel {
@ -234,18 +242,26 @@ impl StatusPanel {
content,
status: None,
dirty: true,
theme_default,
id: ComponentId::new_v4(),
}
}
fn draw_accounts(&mut self, context: &Context) {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(self.theme_default.fg)
.set_bg(self.theme_default.bg)
.set_attrs(self.theme_default.attrs);
ret
};
self.content
.resize(120, 40 + context.accounts.len() * 20, Cell::default());
.resize(120, 40 + context.accounts.len() * 20, default_cell);
write_string_to_grid(
"Accounts",
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((2, 10), (120 - 1, 10)),
Some(2),
);
@ -258,8 +274,8 @@ impl StatusPanel {
let (x, y) = write_string_to_grid(
a.name(),
&mut self.content,
Color::Default,
Color::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((3, 12 + i * 10), (120 - 2, 12 + i * 10)),
Some(3),
@ -268,17 +284,17 @@ impl StatusPanel {
" ▒██▒ ",
&mut self.content,
Color::Byte(32),
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
((x, y), (120 - 2, 12 + i * 10)),
None,
);
write_string_to_grid(
&a.runtime_settings.account().identity,
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((4, y + 2), (120 - 2, y + 2)),
None,
);
@ -298,9 +314,9 @@ impl StatusPanel {
let (mut column_width, _) = write_string_to_grid(
&format!("Messages total {}, unseen {}", count.1, count.0),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((5, y + 3), (120 - 2, y + 3)),
None,
);
@ -309,9 +325,9 @@ impl StatusPanel {
write_string_to_grid(
&format!("Contacts total {}", a.address_book.len()),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((5, y + 4), (120 - 2, y + 4)),
None,
)
@ -322,9 +338,9 @@ impl StatusPanel {
write_string_to_grid(
&format!("Backend {}", a.settings.account().format()),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((5, y + 5), (120 - 2, y + 5)),
None,
)
@ -334,9 +350,9 @@ impl StatusPanel {
write_string_to_grid(
"Special Mailboxes:",
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((5 + column_width, y + 2), (120 - 2, y + 2)),
None,
);
@ -349,9 +365,9 @@ impl StatusPanel {
write_string_to_grid(
&format!("{}: {}", f.path(), f.special_usage()),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((5 + column_width, y + 3 + i), (120 - 2, y + 2)),
None,
);
@ -372,9 +388,9 @@ impl Component for AccountStatus {
let (_x, _y) = write_string_to_grid(
"(Press Esc to return)",
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((1, 0), (width - 1, height - 1)),
None,
);
@ -383,8 +399,8 @@ impl Component for AccountStatus {
let (_x, _y) = write_string_to_grid(
"Tag support: ",
&mut self.content,
Color::Default,
Color::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((1, line), (width - 1, height - 1)),
None,
@ -396,9 +412,9 @@ impl Component for AccountStatus {
"no"
},
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((_x, _y), (width - 1, height - 1)),
None,
);
@ -406,8 +422,8 @@ impl Component for AccountStatus {
let (_x, _y) = write_string_to_grid(
"Cache backend: ",
&mut self.content,
Color::Default,
Color::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((1, line), (width - 1, height - 1)),
None,
@ -440,9 +456,9 @@ impl Component for AccountStatus {
}
},
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((_x, _y), (width - 1, height - 1)),
None,
);
@ -451,8 +467,8 @@ impl Component for AccountStatus {
write_string_to_grid(
"Special Mailboxes:",
&mut self.content,
Color::Default,
Color::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((1, line), (width - 1, height - 1)),
None,
@ -466,9 +482,9 @@ impl Component for AccountStatus {
write_string_to_grid(
&format!("{}: {}", f.path(), f.special_usage()),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((1, line), (width - 1, height - 1)),
None,
);
@ -477,21 +493,22 @@ impl Component for AccountStatus {
write_string_to_grid(
"Subscribed folders:",
&mut self.content,
Color::Default,
Color::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((1, line), (width - 1, height - 1)),
None,
);
line += 2;
for f in a.list_folders() {
for folder_node in a.list_folders() {
let f: &Folder = &a.ref_folders()[&folder_node.hash];
if f.is_subscribed() {
write_string_to_grid(
f.path(),
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((1, line), (width - 1, height - 1)),
None,
);
@ -506,8 +523,8 @@ impl Component for AccountStatus {
write_string_to_grid(
"Server Capabilities:",
&mut self.content,
Color::Default,
Color::Default,
self.theme_default.fg,
self.theme_default.bg,
Attr::Bold,
((1, line), (width - 1, height - 1)),
None,
@ -520,9 +537,9 @@ impl Component for AccountStatus {
write_string_to_grid(
"meli support:",
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((max_name_width + 6, line), (width - 1, height - 1)),
None,
);
@ -533,9 +550,9 @@ impl Component for AccountStatus {
write_string_to_grid(
&cap,
&mut self.content,
Color::Default,
Color::Default,
Attr::Default,
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
((1, line + i), (width - 1, height - 1)),
None,
);
@ -549,8 +566,8 @@ impl Component for AccountStatus {
"supported",
&mut self.content,
Color::Green,
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
((max_name_width + 6, line + i), (width - 1, height - 1)),
None,
);
@ -559,8 +576,8 @@ impl Component for AccountStatus {
"not supported",
&mut self.content,
Color::Red,
Color::Default,
Attr::Default,
self.theme_default.bg,
self.theme_default.attrs,
((max_name_width + 6, line + i), (width - 1, height - 1)),
None,
);
@ -577,7 +594,7 @@ impl Component for AccountStatus {
std::cmp::min(width.saturating_sub(cols), self.cursor.0),
std::cmp::min(height.saturating_sub(rows), self.cursor.1),
);
clear_area(grid, area);
clear_area(grid, area, self.theme_default);
copy_area(
grid,
&self.content,
@ -640,8 +657,15 @@ impl Component for AccountStatus {
}
impl AccountStatus {
pub fn new(account_pos: usize) -> AccountStatus {
let mut content = CellBuffer::new(120, 5, Cell::default());
pub fn new(account_pos: usize, theme_default: ThemeAttribute) -> AccountStatus {
let default_cell = {
let mut ret = Cell::with_char(' ');
ret.set_fg(theme_default.fg)
.set_bg(theme_default.bg)
.set_attrs(theme_default.attrs);
ret
};
let mut content = CellBuffer::new(120, 5, default_cell);
content.set_growable(true);
AccountStatus {
@ -649,6 +673,7 @@ impl AccountStatus {
account_pos,
content,
dirty: true,
theme_default,
id: ComponentId::new_v4(),
}
}
@ -660,6 +685,7 @@ struct AccountStatus {
account_pos: usize,
content: CellBuffer,
dirty: bool,
theme_default: ThemeAttribute,
id: ComponentId,
}

View File

@ -80,7 +80,7 @@ impl ViewMode {
/// menus
#[derive(Debug, Default)]
pub struct MailView {
coordinates: (usize, usize, EnvelopeHash),
coordinates: (usize, FolderHash, EnvelopeHash),
pager: Pager,
subview: Option<Box<dyn Component>>,
dirty: bool,
@ -89,6 +89,7 @@ pub struct MailView {
headers_no: usize,
headers_cursor: usize,
force_draw_headers: bool,
theme_default: ThemeAttribute,
cmd_buf: String,
id: ComponentId,
@ -116,9 +117,10 @@ impl fmt::Display for MailView {
impl MailView {
const DESCRIPTION: &'static str = "view mail";
pub fn new(
coordinates: (usize, usize, EnvelopeHash),
coordinates: (usize, FolderHash, EnvelopeHash),
pager: Option<Pager>,
subview: Option<Box<dyn Component>>,
context: &Context,
) -> Self {
MailView {
coordinates,
@ -132,6 +134,8 @@ impl MailView {
headers_cursor: 0,
force_draw_headers: false,
theme_default: crate::conf::value(context, "mail.view.body"),
cmd_buf: String::with_capacity(4),
id: ComponentId::new_v4(),
}
@ -249,8 +253,7 @@ impl MailView {
} else {
s.push(' ');
}
s.push('\\');
s.push(' ');
s.push_str("\\_ ");
} else {
if has_sibling {
s.push('|');
@ -328,7 +331,7 @@ impl MailView {
}
}
pub fn update(&mut self, new_coordinates: (usize, usize, EnvelopeHash)) {
pub fn update(&mut self, new_coordinates: (usize, FolderHash, EnvelopeHash)) {
self.coordinates = new_coordinates;
self.mode = ViewMode::Normal;
self.set_dirty(true);
@ -374,7 +377,7 @@ impl Component for MailView {
let headers = crate::conf::value(context, "mail.view.headers");
if self.mode == ViewMode::Raw {
clear_area(grid, area);
clear_area(grid, area, self.theme_default);
context.dirty_areas.push_back(area);
get_y(upper_left)
} else {
@ -399,7 +402,7 @@ impl Component for MailView {
(set_y(upper_left, y), bottom_right),
Some(get_x(upper_left)),
);
clear_area(grid, ((_x, _y), (get_x(bottom_right), _y)));
clear_area(grid, ((_x, _y), (get_x(bottom_right), _y)), headers);
y = _y + 1;
} else {
skip_header_ctr -= 1;
@ -441,7 +444,11 @@ impl Component for MailView {
let mut x = get_x(upper_left);
if let Some(id) = id {
if sticky || skip_header_ctr == 0 {
clear_area(grid, (set_y(upper_left, y), set_y(bottom_right, y)));
clear_area(
grid,
(set_y(upper_left, y), set_y(bottom_right, y)),
headers,
);
let (_x, _) = write_string_to_grid(
"List-ID: ",
grid,
@ -542,14 +549,18 @@ impl Component for MailView {
}
self.force_draw_headers = false;
clear_area(grid, (set_y(upper_left, y), set_y(bottom_right, y)));
clear_area(
grid,
(set_y(upper_left, y), set_y(bottom_right, y)),
headers,
);
context
.dirty_areas
.push_back((upper_left, set_y(bottom_right, y + 3)));
if !context.settings.pager.headers_sticky {
let height_p = self.pager.size().1;
let height = height!(area) - y - 1;
let height = height!(area).saturating_sub(y).saturating_sub(1);
if self.pager.cursor_pos() >= self.headers_no {
get_y(upper_left)
} else if height_p > height && self.headers_cursor < self.headers_no + 1 {
@ -576,7 +587,11 @@ impl Component for MailView {
Ok(body) => body,
Err(e) => {
self.dirty = false;
clear_area(grid, (set_y(upper_left, y), bottom_right));
clear_area(
grid,
(set_y(upper_left, y), bottom_right),
self.theme_default,
);
context
.dirty_areas
.push_back((set_y(upper_left, y), bottom_right));
@ -776,10 +791,8 @@ impl Component for MailView {
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["reply"]) =>
{
let account = &context.accounts[self.coordinates.0];
let folder_hash = account[self.coordinates.1].unwrap().folder.hash();
context.replies.push_back(UIEvent::Action(Tab(Reply(
(self.coordinates.0, folder_hash),
(self.coordinates.0, self.coordinates.1),
self.coordinates.2,
))));
return true;

View File

@ -232,7 +232,7 @@ impl Component for EnvelopeView {
let envelope: &Envelope = &self.wrapper;
if self.mode == ViewMode::Raw {
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
context.dirty_areas.push_back(area);
get_y(upper_left) - 1
} else {
@ -306,7 +306,11 @@ impl Component for EnvelopeView {
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 2)));
clear_area(
grid,
(set_y(upper_left, y + 1), set_y(bottom_right, y + 2)),
crate::conf::value(context, "theme_default"),
);
context
.dirty_areas
.push_back((upper_left, set_y(bottom_right, y + 1)));

View File

@ -51,7 +51,7 @@ pub struct ThreadView {
expanded_pos: usize,
new_expanded_pos: usize,
reversed: bool,
coordinates: (usize, usize, usize),
coordinates: (usize, FolderHash, usize),
thread_group: ThreadHash,
mailview: MailView,
show_mailview: bool,
@ -75,7 +75,7 @@ impl ThreadView {
* context: current context
*/
pub fn new(
coordinates: (usize, usize, usize),
coordinates: (usize, FolderHash, usize),
thread_group: ThreadHash,
expanded_hash: Option<ThreadNodeHash>,
context: &Context,
@ -160,12 +160,16 @@ impl ThreadView {
}
self.set_dirty(true);
}
fn initiate(&mut self, expanded_hash: Option<ThreadNodeHash>, context: &Context) {
/* stack to push thread messages in order in order to pop and print them later */
let account = &context.accounts[self.coordinates.0];
let mailbox = &account[self.coordinates.1].unwrap();
let threads = &account.collection.threads[&mailbox.folder.hash()];
if !threads.groups.contains_key(&self.thread_group) {
return;
}
let thread_iter = threads.thread_group_iter(self.thread_group);
self.entries.clear();
for (line, (ind, thread_node_hash)) in thread_iter.enumerate() {
@ -496,7 +500,7 @@ impl ThreadView {
if self.dirty || (page_no != prev_page_no) {
if page_no != prev_page_no {
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
}
let visibles: Vec<&usize> = self
.visible_entries
@ -563,6 +567,7 @@ impl ThreadView {
upper_left!(area),
set_x(bottom_right, get_x(upper_left!(area)) + 1),
),
context,
self.cursor_pos,
rows,
visibles.len(),
@ -616,6 +621,7 @@ impl ThreadView {
upper_left!(area),
set_x(bottom_right, get_x(upper_left!(area)) + 1),
),
context,
self.cursor_pos,
rows,
visibles.len(),
@ -680,7 +686,11 @@ impl ThreadView {
context
.dirty_areas
.push_back(((mid, y + 1), set_x(bottom_right, mid)));
clear_area(grid, ((mid, y + 1), set_x(bottom_right, mid)));
clear_area(
grid,
((mid, y + 1), set_x(bottom_right, mid)),
crate::conf::value(context, "theme_default"),
);
y + 2
} else {
get_y(upper_left) + 2
@ -707,7 +717,11 @@ impl ThreadView {
.draw(grid, (upper_left, bottom_right), context);
}
(false, true) => {
clear_area(grid, ((mid + 1, get_y(upper_left) + y - 1), bottom_right));
clear_area(
grid,
((mid + 1, get_y(upper_left) + y - 1), bottom_right),
crate::conf::value(context, "theme_default"),
);
self.draw_list(grid, (set_y(upper_left, y), bottom_right), context);
}
(_, false) => {
@ -724,7 +738,7 @@ impl ThreadView {
let bottom_entity_rows = (pager_ratio * total_rows) / 100;
if bottom_entity_rows > total_rows {
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
context.dirty_areas.push_back(area);
return;
}
@ -789,7 +803,11 @@ impl ThreadView {
/* if this is the first ever draw, there is nothing on the grid to update so populate it
* first */
if !self.initiated {
clear_area(grid, (set_y(upper_left, y), bottom_right));
clear_area(
grid,
(set_y(upper_left, y), bottom_right),
crate::conf::value(context, "theme_default"),
);
let (width, height) = self.content.size();
match (self.show_mailview, self.show_thread) {

View File

@ -179,7 +179,7 @@ impl Component for VSplit {
(false, true) => total_cols,
(true, false) => 0,
(false, false) => {
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
return;
}
};
@ -305,8 +305,14 @@ impl fmt::Display for Pager {
impl Pager {
pub const DESCRIPTION: &'static str = "pager";
pub fn set_reflow(&mut self, new_val: Reflow) {
pub fn set_colors(&mut self, new_val: ThemeAttribute) -> &mut Self {
self.colors = new_val;
self
}
pub fn set_reflow(&mut self, new_val: Reflow) -> &mut Self {
self.reflow = new_val;
self
}
pub fn reflow(&self) -> Reflow {
@ -575,7 +581,7 @@ impl Component for Pager {
return;
}
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
let (width, height) = self.content.size();
let (cols, rows) = (width!(area), height!(area));
self.cursor = (
@ -699,6 +705,7 @@ impl Component for Pager {
fn is_dirty(&self) -> bool {
self.dirty
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
}
@ -713,6 +720,7 @@ impl Component for Pager {
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}
@ -723,8 +731,6 @@ impl Component for Pager {
pub struct StatusBar {
container: Box<dyn Component>,
status: String,
notifications: VecDeque<String>,
cur_notification: Option<(std::time::Instant, String)>,
ex_buffer: Field,
ex_buffer_cmd_history_pos: Option<usize>,
display_buffer: String,
@ -749,8 +755,6 @@ impl StatusBar {
StatusBar {
container,
status: String::with_capacity(256),
notifications: VecDeque::new(),
cur_notification: None,
ex_buffer: Field::Text(UText::new(String::with_capacity(256)), None),
ex_buffer_cmd_history_pos: None,
display_buffer: String::with_capacity(8),
@ -798,43 +802,6 @@ impl StatusBar {
grid[(x, y)].set_attrs(attribute.attrs | Attr::Bold);
}
}
let noto_colors = crate::conf::value(context, "status.notification");
if self.cur_notification.is_some() {
let (t, n) = self.cur_notification.as_ref().unwrap();
if std::time::Instant::now().duration_since(*t) < std::time::Duration::new(5, 0) {
write_string_to_grid(
n,
grid,
noto_colors.fg,
noto_colors.bg,
noto_colors.attrs,
(
(std::cmp::max(x, width!(area).saturating_sub(n.len())), y),
bottom_right!(area),
),
None,
);
} else {
self.cur_notification = None;
}
}
if self.cur_notification.is_none() {
if let Some(n) = self.notifications.pop_front() {
write_string_to_grid(
&n,
grid,
noto_colors.fg,
noto_colors.bg,
noto_colors.attrs,
(
(std::cmp::max(x, width!(area).saturating_sub(n.len())), y),
bottom_right!(area),
),
None,
);
self.cur_notification = Some((std::time::Instant::now(), n));
}
}
let (x, y) = bottom_right!(area);
for (idx, c) in self.display_buffer.chars().rev().enumerate() {
@ -849,7 +816,7 @@ impl StatusBar {
}
fn draw_execute_bar(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
let (x, y) = write_string_to_grid(
self.ex_buffer.as_str(),
grid,
@ -979,6 +946,7 @@ impl Component for StatusBar {
),
set_y(bottom_right, get_y(bottom_right) - height),
),
context,
self.auto_complete.cursor(),
hist_height,
self.auto_complete.suggestions().len(),
@ -1025,7 +993,11 @@ impl Component for StatusBar {
} else {
self.auto_complete.cursor()
};
clear_area(grid, hist_area);
clear_area(
grid,
hist_area,
crate::conf::value(context, "theme_default"),
);
if hist_height > 0 {
change_colors(
grid,
@ -1229,10 +1201,6 @@ impl Component for StatusBar {
UIEvent::Resize => {
self.dirty = true;
}
UIEvent::StatusEvent(StatusEvent::DisplayMessage(s)) => {
self.notifications.push_back(s.clone());
self.dirty = true;
}
UIEvent::StatusEvent(StatusEvent::BufClear) => {
self.display_buffer.clear();
self.dirty = true;
@ -1370,12 +1338,13 @@ impl Tabbed {
}
fn draw_tabs(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let tab_bar_attribute = crate::conf::value(context, "tab.bar");
if self.children.is_empty() {
clear_area(grid, area);
clear_area(grid, area, tab_bar_attribute);
return;
}
let tab_bar_attribute = crate::conf::value(context, "tab.bar");
let tab_unfocused_attribute = crate::conf::value(context, "tab.unfocused");
let mut tab_focused_attribute = crate::conf::value(context, "tab.focused");
if std::env::var("NO_COLOR").is_ok()
@ -1409,6 +1378,10 @@ impl Tabbed {
if y != _y_ {
break;
}
if x > get_x(bottom_right) {
x = get_x(bottom_right);
break;
}
grid[(x_, _y_)]
.set_fg(tab_bar_attribute.fg)
.set_bg(tab_bar_attribute.bg)
@ -1463,6 +1436,7 @@ impl Component for Tabbed {
upper_left!(area),
set_x(upper_left!(area), get_x(bottom_right!(area))),
),
crate::conf::value(context, "tab.bar"),
);
context.dirty_areas.push_back((
upper_left!(area),
@ -1504,7 +1478,7 @@ impl Component for Tabbed {
),
);
context.dirty_areas.push_back(area);
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
create_box(grid, area);
let area = (
pos_inc(upper_left!(area), (3, 2)),
@ -1653,9 +1627,7 @@ impl Component for Tabbed {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.children[self.cursor_pos]
.get_status(context)
.unwrap_or_default(),
self.children[self.cursor_pos].get_status(context),
)));
self.set_dirty(true);
}
@ -1666,9 +1638,7 @@ impl Component for Tabbed {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.children[self.cursor_pos]
.get_status(context)
.unwrap_or_default(),
self.children[self.cursor_pos].get_status(context),
)));
self.set_dirty(true);
return true;
@ -1683,7 +1653,7 @@ impl Component for Tabbed {
return true;
}
UIEvent::Action(Tab(NewDraft(account_idx, ref draft))) => {
let mut composer = Composer::new(account_idx);
let mut composer = Composer::new(account_idx, context);
if let Some(draft) = draft {
composer.set_draft(draft.clone());
}
@ -2367,14 +2337,13 @@ impl fmt::Display for RawBuffer {
impl Component for RawBuffer {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.dirty {
clear_area(grid, area);
let (width, height) = self.buf.size();
let (cols, rows) = (width!(area), height!(area));
self.cursor = (
std::cmp::min(width.saturating_sub(cols), self.cursor.0),
std::cmp::min(height.saturating_sub(rows), self.cursor.1),
);
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
copy_area(
grid,
&self.buf,

View File

@ -133,13 +133,14 @@ impl Field {
}
impl Component for Field {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, _context: &mut Context) {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let theme_attr = crate::conf::value(context, "widgets.form.field");
write_string_to_grid(
self.as_str(),
grid,
Color::Default,
Color::Default,
Attr::Default,
theme_attr.fg,
theme_attr.bg,
theme_attr.attrs,
area,
None,
);
@ -388,15 +389,17 @@ impl Component for FormWidget {
let bottom_right = bottom_right!(area);
if self.dirty {
let theme_default = crate::conf::value(context, "theme_default");
clear_area(
grid,
(
upper_left,
set_y(bottom_right, get_y(upper_left) + self.layout.len()),
),
theme_default,
);
let label_attrs = crate::conf::value(context, "widgets.form.label");
for (i, k) in self.layout.iter().enumerate() {
let v = self.fields.get_mut(k).unwrap();
/* Write field label */
@ -472,6 +475,7 @@ impl Component for FormWidget {
pos_inc(upper_left, (0, length)),
set_y(bottom_right, length + 2 + get_y(upper_left)),
),
theme_default,
);
if !self.hide_buttons {
self.buttons.draw(
@ -489,6 +493,7 @@ impl Component for FormWidget {
set_y(upper_left, length + 4 + get_y(upper_left)),
bottom_right,
),
theme_default,
);
self.dirty = false;
}
@ -642,9 +647,10 @@ impl<T> Component for ButtonWidget<T>
where
T: std::fmt::Debug + Default + Send,
{
fn draw(&mut self, grid: &mut CellBuffer, area: Area, _context: &mut Context) {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.dirty {
clear_area(grid, area);
let theme_default = crate::conf::value(context, "theme_default");
clear_area(grid, area, theme_default);
let upper_left = upper_left!(area);
let mut len = 0;
@ -653,11 +659,11 @@ where
write_string_to_grid(
k.as_str(),
grid,
Color::Default,
theme_default.fg,
if i == self.cursor && self.focus {
Color::Byte(246)
} else {
Color::Default
theme_default.bg
},
Attr::Bold,
(
@ -927,6 +933,7 @@ impl ScrollBar {
self,
grid: &mut CellBuffer,
area: Area,
context: &Context,
pos: usize,
visible_rows: usize,
length: usize,
@ -941,7 +948,7 @@ impl ScrollBar {
if self.show_arrows {
height -= height;
}
clear_area(grid, area);
clear_area(grid, area, crate::conf::value(context, "theme_default"));
let visible_ratio: f32 = (std::cmp::min(visible_rows, length) as f32) / (length as f32);
let scrollbar_height = std::cmp::max((visible_ratio * (height as f32)) as usize, 1);

View File

@ -51,7 +51,6 @@ use self::notifications::NotificationsSettings;
use self::terminal::TerminalSettings;
use crate::pager::PagerSettings;
use crate::plugins::Plugin;
use melib::backends::SpecialUsageMailbox;
use melib::conf::{AccountSettings, FolderConf, ToggleFlag};
use melib::error::*;
@ -115,6 +114,7 @@ pub struct FileAccount {
#[serde(default = "false_val")]
read_only: bool,
#[serde(default)]
subscribed_folders: Vec<String>,
#[serde(default)]
folders: HashMap<String, FileFolderConf>,
@ -141,7 +141,7 @@ impl From<FileAccount> for AccountConf {
.map(|(k, v)| (k.clone(), v.folder_conf.clone()))
.collect();
let mut acc = AccountSettings {
let acc = AccountSettings {
name: String::new(),
root_folder,
format,
@ -154,60 +154,7 @@ impl From<FileAccount> for AccountConf {
extra: x.extra.clone(),
};
let root_path = PathBuf::from(acc.root_folder.as_str());
let root_tmp = root_path
.components()
.last()
.and_then(|c| c.as_os_str().to_str())
.unwrap_or("")
.to_string();
if !acc.subscribed_folders.contains(&root_tmp) {
acc.subscribed_folders.push(root_tmp);
}
let mut folder_confs = x.folders.clone();
for s in &x.subscribed_folders {
if !folder_confs.contains_key(s) {
use melib::text_processing::GlobMatch;
if s.is_glob() {
continue;
}
folder_confs.insert(
s.to_string(),
FileFolderConf {
folder_conf: FolderConf {
subscribe: ToggleFlag::True,
..FolderConf::default()
},
..FileFolderConf::default()
},
);
} else {
if !folder_confs[s].folder_conf().subscribe.is_unset() {
continue;
}
folder_confs.get_mut(s).unwrap().folder_conf.subscribe = ToggleFlag::True;
}
if folder_confs[s].folder_conf().usage.is_none() {
let name = s
.split(if s.contains('/') { '/' } else { '.' })
.last()
.unwrap_or("");
folder_confs.get_mut(s).unwrap().folder_conf.usage =
SpecialUsageMailbox::detect_usage(name);
}
if folder_confs[s].folder_conf().ignore.is_unset() {
use SpecialUsageMailbox::*;
if [Junk, Sent, Trash]
.contains(&folder_confs[s].folder_conf().usage.as_ref().unwrap())
{
folder_confs.get_mut(s).unwrap().folder_conf.ignore =
ToggleFlag::InternalVal(true);
}
}
}
let folder_confs = x.folders.clone();
AccountConf {
account: acc,
conf: x,
@ -655,6 +602,7 @@ mod pp {
use melib::{
error::{MeliError, Result},
parsec::*,
ShellExpandTrait,
};
use std::io::Read;
use std::path::{Path, PathBuf};
@ -735,19 +683,16 @@ mod pp {
l
))
})? {
let p = &Path::new(sub_path);
debug!(p);
let p_buf = if p.is_relative() {
let mut p = Path::new(sub_path).expand();
if p.is_relative() {
/* We checked that path is ok above so we can do unwrap here */
debug!(path);
let prefix = path.parent().unwrap();
debug!(prefix);
prefix.join(p)
} else {
p.to_path_buf()
};
p = prefix.join(p)
}
ret.extend(pp_helper(&p_buf, level + 1)?.chars());
ret.extend(pp_helper(&p, level + 1)?.chars());
} else {
ret.push_str(l);
ret.push('\n');
@ -761,9 +706,9 @@ mod pp {
/// in the filesystem.
pub fn pp<P: AsRef<Path>>(path: P) -> Result<String> {
let p_buf: PathBuf = if path.as_ref().is_relative() {
path.as_ref().canonicalize()?
path.as_ref().expand().canonicalize()?
} else {
path.as_ref().to_path_buf()
path.as_ref().expand()
};
let mut ret = pp_helper(&p_buf, 0)?;

View File

@ -27,8 +27,8 @@ use super::{AccountConf, FileFolderConf};
use fnv::FnvHashMap;
use melib::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
use melib::backends::{
BackendOp, Backends, Folder, FolderHash, FolderOperation, MailBackend, NotifyFn, ReadOnlyOp,
RefreshEvent, RefreshEventConsumer, RefreshEventKind, SpecialUsageMailbox,
BackendOp, Backends, Folder, FolderHash, MailBackend, NotifyFn, ReadOnlyOp, RefreshEvent,
RefreshEventConsumer, RefreshEventKind, SpecialUsageMailbox,
};
use melib::error::{MeliError, Result};
use melib::mailbox::*;
@ -62,6 +62,7 @@ pub enum MailboxEntry {
Failed(MeliError),
/// first argument is done work, and second is total work
Parsing(Mailbox, usize, usize),
None,
}
impl Default for MailboxEntry {
@ -78,6 +79,7 @@ impl std::fmt::Display for MailboxEntry {
match self {
MailboxEntry::Available(ref m) => m.name().to_string(),
MailboxEntry::Failed(ref e) => e.to_string(),
MailboxEntry::None => "Not subscribed, is this a bug?".to_string(),
MailboxEntry::Parsing(_, done, total) => {
format!("Parsing messages. [{}/{}]", done, total)
}
@ -109,6 +111,7 @@ impl MailboxEntry {
"Mailbox is not available: {}",
e.to_string()
))),
MailboxEntry::None => Err(MeliError::new("Mailbox is not subscribed.")),
}
}
@ -120,6 +123,7 @@ impl MailboxEntry {
"Mailbox is not available: {}",
e.to_string()
))),
MailboxEntry::None => Err(MeliError::new("Mailbox is not subscribed.")),
}
}
@ -234,10 +238,11 @@ impl<'a> Iterator for MailboxIterator<'a> {
}
}
#[derive(Serialize, Debug, Default)]
struct FolderNode {
hash: FolderHash,
kids: Vec<FolderNode>,
#[derive(Serialize, Debug, Clone, Default)]
pub struct FolderNode {
pub hash: FolderHash,
pub depth: usize,
pub children: Vec<FolderNode>,
}
impl Account {
@ -254,8 +259,9 @@ impl Account {
let backend = map.get(settings.account().format())(
settings.account(),
Box::new(move |path: &str| {
(s.folder_confs.contains_key(path)
&& s.folder_confs[path].folder_conf().subscribe.is_true())
s.account.subscribed_folders.is_empty()
|| (s.folder_confs.contains_key(path)
&& s.folder_confs[path].folder_conf().subscribe.is_true())
|| s.account
.subscribed_folders
.iter()
@ -328,22 +334,6 @@ impl Account {
let mut sent_folder = None;
for f in ref_folders.values_mut() {
if !((self.settings.folder_confs.contains_key(f.path())
&& self.settings.folder_confs[f.path()]
.folder_conf()
.subscribe
.is_true())
|| self
.settings
.account
.subscribed_folders
.iter()
.any(|m| f.path().matches_glob(m)))
{
/* Skip unsubscribed folder */
continue;
}
if let Some(conf) = self.settings.folder_confs.get_mut(f.path()) {
conf.folder_conf.usage = if f.special_usage() != SpecialUsageMailbox::Normal {
Some(f.special_usage())
@ -368,7 +358,6 @@ impl Account {
folder_confs.insert(f.hash(), conf.clone());
} else {
let mut new = FileFolderConf::default();
new.folder_conf.subscribe = super::ToggleFlag::InternalVal(true);
new.folder_conf.usage = if f.special_usage() != SpecialUsageMailbox::Normal {
Some(f.special_usage())
} else {
@ -387,8 +376,10 @@ impl Account {
let mut tree: Vec<FolderNode> = Vec::new();
let mut collection: Collection = Collection::new(Default::default());
for (h, f) in ref_folders.iter() {
if !folder_confs.contains_key(&h) {
if !f.is_subscribed() {
/* Skip unsubscribed folder */
folders.insert(*h, MailboxEntry::None);
workers.insert(*h, None);
continue;
}
folders.insert(
@ -407,7 +398,7 @@ impl Account {
collection.threads.insert(*h, Threads::default());
}
build_folders_order(&folder_confs, &mut tree, &ref_folders, &mut folders_order);
build_folders_order(&mut tree, &ref_folders, &mut folders_order);
self.folders = folders;
self.ref_folders = ref_folders;
self.folder_confs = folder_confs;
@ -731,50 +722,25 @@ impl Account {
pub fn is_empty(&self) -> bool {
self.folders.is_empty()
}
pub fn list_folders(&self) -> Vec<Folder> {
let mut folders = self.ref_folders.clone();
let folder_confs = &self.folder_confs;
//debug!("folder renames: {:?}", folder_renames);
for f in folders.values_mut() {
if let Some(r) = folder_confs.get(&f.hash()) {
if let Some(rename) = r.folder_conf().alias() {
f.change_name(rename);
}
pub fn ref_folders(&self) -> &FnvHashMap<FolderHash, Folder> {
&self.ref_folders
}
pub fn list_folders(&self) -> Vec<FolderNode> {
let mut ret = Vec::with_capacity(self.folders.len());
fn rec(node: &FolderNode, ret: &mut Vec<FolderNode>) {
ret.push(node.clone());
for c in node.children.iter() {
rec(c, ret);
}
}
/*
if let Some(pos) = folders
.iter()
.position(|f| f.name().eq_ignore_ascii_case("INBOX"))
{
folders.swap(pos, 0);
for node in &self.tree {
rec(node, &mut ret);
}
*/
let order: FnvHashMap<FolderHash, usize> = self
.folders_order
.iter()
.enumerate()
.map(|(i, &fh)| (fh, i))
.collect();
let mut folders: Vec<Folder> = folders
.drain()
.map(|(_, f)| f)
.filter(|f| {
self.folders.contains_key(&f.hash())
|| self
.settings
.account
.subscribed_folders
.iter()
.any(|m| f.path().matches_glob(m))
})
.collect();
if order.is_empty() {
return Vec::new();
}
folders.sort_unstable_by(|a, b| order[&a.hash()].partial_cmp(&order[&b.hash()]).unwrap());
folders
ret
}
pub fn folders_order(&self) -> &Vec<FolderHash> {
&self.folders_order
}
@ -800,7 +766,7 @@ impl Account {
.map(|e| (e.hash(), e))
.collect::<FnvHashMap<EnvelopeHash, Envelope>>();
match self.folders.entry(folder_hash).or_default() {
MailboxEntry::Failed(_) => {}
MailboxEntry::Failed(_) | MailboxEntry::None => {}
MailboxEntry::Parsing(ref mut m, _, _) | MailboxEntry::Available(ref mut m) => {
m.merge(&envelopes);
if let Some(updated_folders) =
@ -821,6 +787,9 @@ impl Account {
}
pub fn status(&mut self, folder_hash: FolderHash) -> result::Result<(), usize> {
if folder_hash == 0 {
return Err(0);
}
loop {
match self.workers.get_mut(&folder_hash).unwrap() {
None => {
@ -964,67 +933,107 @@ impl Account {
&self.collection.threads[&f].thread_nodes()[&h]
}
pub fn folder_operation(&mut self, path: &str, op: FolderOperation) -> Result<()> {
pub fn folder_operation(
&mut self,
op: crate::execute::actions::FolderOperation,
) -> Result<String> {
use crate::execute::actions::FolderOperation;
if self.settings.account.read_only() {
return Err(MeliError::new("Account is read-only."));
}
match op {
FolderOperation::Create => {
if self.settings.account.read_only() {
Err(MeliError::new("Account is read-only."))
FolderOperation::Create(path) => {
let mut folder = self
.backend
.write()
.unwrap()
.create_folder(path.to_string())?;
self.sender
.send(ThreadEvent::UIEvent(UIEvent::MailboxCreate((
self.index,
folder.hash(),
))))
.unwrap();
let mut new = FileFolderConf::default();
new.folder_conf.subscribe = super::ToggleFlag::InternalVal(true);
new.folder_conf.usage = if folder.special_usage() != SpecialUsageMailbox::Normal {
Some(folder.special_usage())
} else {
let mut folder = self
.backend
.write()
.unwrap()
.create_folder(path.to_string())?;
let mut new = FileFolderConf::default();
new.folder_conf.subscribe = super::ToggleFlag::InternalVal(true);
new.folder_conf.usage = if folder.special_usage() != SpecialUsageMailbox::Normal
{
Some(folder.special_usage())
} else {
let tmp = SpecialUsageMailbox::detect_usage(folder.name());
if tmp != Some(SpecialUsageMailbox::Normal) && tmp != None {
let _ = folder.set_special_usage(tmp.unwrap());
}
tmp
};
let tmp = SpecialUsageMailbox::detect_usage(folder.name());
if tmp != Some(SpecialUsageMailbox::Normal) && tmp != None {
let _ = folder.set_special_usage(tmp.unwrap());
}
tmp
};
self.folder_confs.insert(folder.hash(), new);
self.folder_names
.insert(folder.hash(), folder.path().to_string());
self.folders.insert(
folder.hash(),
MailboxEntry::Parsing(
Mailbox::new(folder.clone(), &FnvHashMap::default()),
0,
0,
),
);
self.workers.insert(
folder.hash(),
Account::new_worker(
folder.clone(),
&mut self.backend,
&self.work_context,
self.notify_fn.clone(),
),
);
self.collection
.threads
.insert(folder.hash(), Threads::default());
self.ref_folders.insert(folder.hash(), folder);
build_folders_order(
&self.folder_confs,
&mut self.tree,
&self.ref_folders,
&mut self.folders_order,
);
Ok(())
}
self.folder_confs.insert(folder.hash(), new);
self.folder_names
.insert(folder.hash(), folder.path().to_string());
self.folders.insert(
folder.hash(),
MailboxEntry::Parsing(
Mailbox::new(folder.clone(), &FnvHashMap::default()),
0,
0,
),
);
self.workers.insert(
folder.hash(),
Account::new_worker(
folder.clone(),
&mut self.backend,
&self.work_context,
self.notify_fn.clone(),
),
);
self.collection
.threads
.insert(folder.hash(), Threads::default());
self.ref_folders = self.backend.read().unwrap().folders()?;
build_folders_order(&mut self.tree, &self.ref_folders, &mut self.folders_order);
Ok(format!("`{}` successfully created.", &path))
}
FolderOperation::Delete => Err(MeliError::new("Not implemented.")),
FolderOperation::Subscribe => Err(MeliError::new("Not implemented.")),
FolderOperation::Unsubscribe => Err(MeliError::new("Not implemented.")),
FolderOperation::Rename(_) => Err(MeliError::new("Not implemented.")),
FolderOperation::Delete(path) => {
if self.ref_folders.len() == 1 {
return Err(MeliError::new("Cannot delete only mailbox."));
}
let folder_hash = if let Some((folder_hash, _)) =
self.ref_folders.iter().find(|(_, f)| f.path() == path)
{
*folder_hash
} else {
return Err(MeliError::new("Mailbox with that path not found."));
};
self.backend.write().unwrap().delete_folder(folder_hash)?;
self.sender
.send(ThreadEvent::UIEvent(UIEvent::MailboxDelete((
self.index,
folder_hash,
))))
.unwrap();
self.folders.remove(&folder_hash);
self.ref_folders = self.backend.read().unwrap().folders()?;
self.folder_confs.remove(&folder_hash);
if let Some(pos) = self.folders_order.iter().position(|&h| h == folder_hash) {
self.folders_order.remove(pos);
}
self.folder_names.remove(&folder_hash);
if let Some(pos) = self.tree.iter().position(|n| n.hash == folder_hash) {
self.tree.remove(pos);
}
if self.sent_folder == Some(folder_hash) {
self.sent_folder = None;
}
self.collection.threads.remove(&folder_hash);
self.workers.remove(&folder_hash); // FIXME Kill worker as well
// FIXME remove from settings as well
Ok(format!("'`{}` has been deleted.", &path))
}
FolderOperation::Subscribe(_) => Err(MeliError::new("Not implemented.")),
FolderOperation::Unsubscribe(_) => Err(MeliError::new("Not implemented.")),
FolderOperation::Rename(_, _) => Err(MeliError::new("Not implemented.")),
FolderOperation::SetPermissions(_) => Err(MeliError::new("Not implemented.")),
}
}
@ -1159,7 +1168,6 @@ impl IndexMut<usize> for Account {
}
fn build_folders_order(
folder_confs: &FnvHashMap<FolderHash, FileFolderConf>,
tree: &mut Vec<FolderNode>,
ref_folders: &FnvHashMap<FolderHash, Folder>,
folders_order: &mut Vec<FolderHash>,
@ -1168,23 +1176,24 @@ fn build_folders_order(
tree.clear();
folders_order.clear();
for (h, f) in ref_folders.iter() {
if !folder_confs.contains_key(&h) {
continue;
}
if f.parent().is_none() {
fn rec(h: FolderHash, ref_folders: &FnvHashMap<FolderHash, Folder>) -> FolderNode {
fn rec(
h: FolderHash,
ref_folders: &FnvHashMap<FolderHash, Folder>,
depth: usize,
) -> FolderNode {
let mut node = FolderNode {
hash: h,
kids: Vec::new(),
children: Vec::new(),
depth,
};
for &c in ref_folders[&h].children() {
node.kids.push(rec(c, ref_folders));
node.children.push(rec(c, ref_folders, depth + 1));
}
node
};
tree.push(rec(*h, &ref_folders));
tree.push(rec(*h, &ref_folders, 0));
for &c in f.children() {
stack.push(c);
}
@ -1208,10 +1217,10 @@ fn build_folders_order(
}
});
let mut stack: SmallVec<[Option<&FolderNode>; 8]> = SmallVec::new();
let mut stack: SmallVec<[Option<&FolderNode>; 16]> = SmallVec::new();
for n in tree.iter_mut() {
folders_order.push(n.hash);
n.kids.sort_unstable_by(|a, b| {
n.children.sort_unstable_by(|a, b| {
if ref_folders[&b.hash].path().eq_ignore_ascii_case("INBOX") {
std::cmp::Ordering::Greater
} else if ref_folders[&a.hash].path().eq_ignore_ascii_case("INBOX") {
@ -1222,10 +1231,10 @@ fn build_folders_order(
.cmp(&ref_folders[&b.hash].path())
}
});
stack.extend(n.kids.iter().rev().map(Some));
stack.extend(n.children.iter().rev().map(Some));
while let Some(Some(next)) = stack.pop() {
folders_order.push(next.hash);
stack.extend(next.kids.iter().rev().map(Some));
stack.extend(next.children.iter().rev().map(Some));
}
}
}

View File

@ -158,6 +158,7 @@ const DEFAULT_KEYS: &'static [&'static str] = &[
"tab.focused",
"tab.unfocused",
"tab.bar",
"widgets.list.header",
"widgets.form.label",
"widgets.form.field",
"widgets.form.highlighted",
@ -205,7 +206,7 @@ pub struct ThemeAttribute {
}
/// Holds {fore,back}ground color and terminal attribute values.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThemeAttributeInner {
#[serde(default)]
fg: ThemeValue<Color>,
@ -215,6 +216,16 @@ pub struct ThemeAttributeInner {
attrs: ThemeValue<Attr>,
}
impl Default for ThemeAttributeInner {
fn default() -> Self {
Self {
fg: "theme_default".into(),
bg: "theme_default".into(),
attrs: "theme_default".into(),
}
}
}
#[derive(Debug, Clone)]
/// Holds either an actual value or refers to the key name of the attribute that holds the value.
pub enum ThemeValue<T> {
@ -459,13 +470,18 @@ impl Default for Theme {
dark.insert($key.into(), ThemeAttributeInner::default());
};
}
add!("theme_default");
add!("theme_default", dark = { fg: Color::Default, bg: Color::Default, attrs: Attr::Default }, light = { fg: Color::Default, bg: Color::Default, attrs: Attr::Default });
add!("status.bar", dark = { fg: Color::Byte(123), bg: Color::Byte(26) }, light = { fg: Color::Byte(123), bg: Color::Byte(26) });
add!("status.notification", dark = { fg: Color::Byte(219), bg: Color::Byte(88) }, light = { fg: Color::Byte(219), bg: Color::Byte(88) });
add!("tab.focused");
add!("tab.unfocused", dark = { fg: Color::Byte(15), bg: Color::Byte(8), }, light = { fg: Color::Byte(15), bg: Color::Byte(8), });
add!("tab.bar");
add!(
"widgets.list.header",
dark = { fg: Color::Black, bg: Color::White, attrs: Attr::Bold },
light = {fg: Color::White, bg: Color::Black, attrs: Attr::Bold }
);
add!(
"widgets.form.label",
dark = { attrs: Attr::Bold },
@ -837,7 +853,7 @@ fn is_cyclic(
.map(|k| (k, false))
.collect::<HashMap<&Cow<'static, str>, bool>>();
for k in theme.keys() {
for course in [Course::Fg, Course::Bg, Course::Attrs].into_iter() {
for course in [Course::Fg, Course::Bg, Course::Attrs].iter() {
path.push(k);
if is_cyclic_util(course, k, &mut visited, &mut stack, &mut path, &theme) {
let path = path

View File

@ -21,11 +21,11 @@
/*! A parser module for user commands passed through the Execute mode.
*/
use melib::backends::FolderOperation;
pub use melib::thread::{SortField, SortOrder};
use nom::{digit, not_line_ending, IResult};
use std;
pub mod actions;
use actions::FolderOperation;
pub mod history;
pub use crate::actions::AccountAction::{self, *};
pub use crate::actions::Action::{self, *};
@ -78,7 +78,7 @@ define_commands!([
map!(ws!(tag!("seen")), |_| Listing(SetSeen))
| map!(ws!(tag!("unseen")), |_| Listing(SetUnseen))
)
) | map!(ws!(tag!("delete")), |_| Listing(Delete))
) | map!(preceded!(tag!("delete"), eof!()), |_| Listing(Delete))
)
); )
},
@ -197,14 +197,14 @@ define_commands!([
alt_complete!(
do_parse!(
ws!(tag!("pipe"))
>> bin: map_res!(is_not!(" "), std::str::from_utf8)
>> bin: quoted_argument
>> is_a!(" ")
>> args: separated_list!(is_a!(" "), quoted_argument)
>> ({
View(Pipe(bin.to_string(), args.into_iter().map(String::from).collect::<Vec<String>>()))
})) | do_parse!(
ws!(tag!("pipe"))
>> bin: ws!(map_res!(is_not!(" "), std::str::from_utf8))
>> bin: ws!(quoted_argument)
>> ({
View(Pipe(bin.to_string(), Vec::new()))
})
@ -218,7 +218,7 @@ define_commands!([
named!( add_attachment<Action>,
do_parse!(
ws!(tag!("add-attachment"))
>> path: map_res!(call!(not_line_ending), std::str::from_utf8)
>> path: quoted_argument
>> (Compose(AddAttachment(path.to_string())))
)
);
@ -230,7 +230,7 @@ define_commands!([
named!( remove_attachment<Action>,
do_parse!(
ws!(tag!("remove-attachment"))
>> idx: map_res!(map_res!(call!(not_line_ending), std::str::from_utf8), usize::from_str)
>> idx: map_res!(quoted_argument, usize::from_str)
>> (Compose(RemoveAttachment(idx)))
)
);
@ -255,8 +255,8 @@ define_commands!([
ws!(tag!("create-folder"))
>> account: quoted_argument
>> is_a!(" ")
>> path: map_res!(call!(not_line_ending), std::str::from_utf8)
>> (Folder(account.to_string(), path.to_string(), FolderOperation::Create))
>> path: quoted_argument
>> (Folder(account.to_string(), FolderOperation::Create(path.to_string())))
)
);
)
@ -269,8 +269,8 @@ define_commands!([
ws!(tag!("subscribe-folder"))
>> account: quoted_argument
>> is_a!(" ")
>> path: map_res!(call!(not_line_ending), std::str::from_utf8)
>> (Folder(account.to_string(), path.to_string(), FolderOperation::Subscribe))
>> path: quoted_argument
>> (Folder(account.to_string(), FolderOperation::Subscribe(path.to_string())))
)
);
)
@ -283,8 +283,8 @@ define_commands!([
ws!(tag!("unsubscribe-folder"))
>> account: quoted_argument
>> is_a!(" ")
>> path: map_res!(call!(not_line_ending), std::str::from_utf8)
>> (Folder(account.to_string(), path.to_string(), FolderOperation::Unsubscribe))
>> path: quoted_argument
>> (Folder(account.to_string(), FolderOperation::Unsubscribe(path.to_string())))
)
);
)
@ -299,8 +299,8 @@ define_commands!([
>> is_a!(" ")
>> src: quoted_argument
>> is_a!(" ")
>> dest: map_res!(call!(not_line_ending), std::str::from_utf8)
>> (Folder(account.to_string(), src.to_string(), FolderOperation::Rename(dest.to_string())))
>> dest: quoted_argument
>> (Folder(account.to_string(), FolderOperation::Rename(src.to_string(), dest.to_string())))
)
);
)
@ -314,7 +314,7 @@ define_commands!([
>> account: quoted_argument
>> is_a!(" ")
>> path: quoted_argument
>> (Folder(account.to_string(), path.to_string(), FolderOperation::Delete))
>> (Folder(account.to_string(), FolderOperation::Delete(path.to_string())))
)
);
)
@ -348,7 +348,7 @@ define_commands!([
named!( save_attachment<Action>,
do_parse!(
ws!(tag!("save-attachment"))
>> idx: map_res!(map_res!(is_not!(" "), std::str::from_utf8), usize::from_str)
>> idx: map_res!(quoted_argument, usize::from_str)
>> path: ws!(quoted_argument)
>> (View(SaveAttachment(idx, path.to_string())))
)
@ -365,11 +365,11 @@ define_commands!([
alt_complete!(
do_parse!(
ws!(tag!("add"))
>> tag: ws!(map_res!(call!(not_line_ending), std::str::from_utf8))
>> tag: ws!(quoted_argument)
>> (Listing(Tag(Add(tag.to_string())))))
| do_parse!(
ws!(tag!("remove"))
>> tag: ws!(map_res!(call!(not_line_ending), std::str::from_utf8))
>> tag: ws!(quoted_argument)
>> (Listing(Tag(Remove(tag.to_string())))))
)

View File

@ -24,7 +24,7 @@
*/
use crate::components::Component;
use melib::backends::{FolderHash, FolderOperation};
use melib::backends::FolderHash;
pub use melib::thread::{SortField, SortOrder};
use melib::{Draft, EnvelopeHash};
@ -86,6 +86,17 @@ pub enum AccountAction {
ReIndex,
}
#[derive(Debug)]
pub enum FolderOperation {
Create(NewFolderPath),
Delete(FolderPath),
Subscribe(FolderPath),
Unsubscribe(FolderPath),
Rename(FolderPath, NewFolderPath),
// Placeholder
SetPermissions(FolderPath),
}
#[derive(Debug)]
pub enum Action {
Listing(ListingAction),
@ -99,9 +110,10 @@ pub enum Action {
SetEnv(String, String),
PrintEnv(String),
Compose(ComposeAction),
Folder(AccountName, FolderPath, FolderOperation),
Folder(AccountName, FolderOperation),
AccountAction(AccountName, AccountAction),
}
type AccountName = String;
type FolderPath = String;
type NewFolderPath = String;

View File

@ -34,6 +34,7 @@ use melib::backends::{FolderHash, NotifyFn};
use crossbeam::channel::{unbounded, Receiver, Sender};
use fnv::FnvHashMap;
use smallvec::SmallVec;
use std::env;
use std::io::Write;
use std::result;
@ -141,15 +142,20 @@ impl Context {
let Context {
ref mut accounts,
ref mut mailbox_hashes,
ref mut replies,
..
} = self;
let was_online = accounts[account_pos].is_online;
let ret = accounts[account_pos].is_online();
if ret.is_ok() {
if !was_online {
for folder in accounts[account_pos].list_folders() {
debug!("hash & folder: {:?} {}", folder.hash(), folder.name());
mailbox_hashes.insert(folder.hash(), account_pos);
for folder_node in accounts[account_pos].list_folders() {
debug!(
"hash & folder: {:?} {}",
folder_node.hash,
accounts[account_pos].ref_folders()[&folder_node.hash].name()
);
mailbox_hashes.insert(folder_node.hash, account_pos);
}
/* Account::watch() needs
* - work_controller to pass `work_context` to the watcher threads and then add them
@ -159,6 +165,8 @@ impl Context {
* - replies to report any failures to the user
*/
accounts[account_pos].watch();
replies.push_back(UIEvent::AccountStatusChange(account_pos));
}
}
ret
@ -176,14 +184,26 @@ pub struct State {
rows: usize,
grid: CellBuffer,
overlay_grid: CellBuffer,
draw_rate_limit: RateLimit,
stdout: Option<StateStdout>,
child: Option<ForkType>,
draw_horizontal_segment_fn: fn(&mut State, usize, usize, usize) -> (),
draw_horizontal_segment_fn: fn(&mut CellBuffer, &mut StateStdout, usize, usize, usize) -> (),
pub mode: UIMode,
components: Vec<Box<dyn Component>>,
pub context: Context,
timer: thread::JoinHandle<()>,
display_messages: SmallVec<[DisplayMessage; 8]>,
display_messages_expiration_start: Option<UnixTimestamp>,
display_messages_active: bool,
display_messages_pos: usize,
}
#[derive(Debug)]
struct DisplayMessage {
timestamp: UnixTimestamp,
msg: String,
}
impl Drop for State {
@ -269,6 +289,7 @@ impl State {
cols,
rows,
grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
overlay_grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
stdout: None,
child: None,
mode: UIMode::Normal,
@ -283,6 +304,10 @@ impl State {
} else {
State::draw_horizontal_segment
},
display_messages: SmallVec::new(),
display_messages_expiration_start: None,
display_messages_pos: 0,
display_messages_active: false,
context: Context {
accounts,
@ -309,6 +334,7 @@ impl State {
.set_value(std::time::Duration::from_millis(3));
if s.context.settings.terminal.ascii_drawing {
s.grid.set_ascii_drawing(true);
s.overlay_grid.set_ascii_drawing(true);
}
s.switch_to_alternate_screen();
@ -434,6 +460,8 @@ impl State {
self.cols = termcols.unwrap_or(72) as usize;
self.rows = termrows.unwrap_or(120) as usize;
self.grid.resize(self.cols, self.rows, Cell::with_char(' '));
self.overlay_grid
.resize(self.cols, self.rows, Cell::with_char(' '));
self.rcv_event(UIEvent::Resize);
@ -452,9 +480,22 @@ impl State {
}
let mut areas: smallvec::SmallVec<[Area; 8]> =
self.context.dirty_areas.drain(0..).collect();
if areas.is_empty() {
return;
if self.display_messages_active {
let now = melib::datetime::now();
if self
.display_messages_expiration_start
.map(|t| t + 5 < now)
.unwrap_or(false)
{
self.display_messages_active = false;
self.display_messages_expiration_start = None;
areas.push((
(0, 0),
(self.cols.saturating_sub(1), self.rows.saturating_sub(1)),
));
}
}
/* Sort by x_start, ie upper_left corner's x coordinate */
areas.sort_by(|a, b| (a.0).0.partial_cmp(&(b.0).0).unwrap());
/* draw each dirty area */
@ -466,18 +507,36 @@ impl State {
continue;
}
if let Some((x_start, x_end)) = segment.take() {
(self.draw_horizontal_segment_fn)(self, x_start, x_end, y);
(self.draw_horizontal_segment_fn)(
&mut self.grid,
self.stdout.as_mut().unwrap(),
x_start,
x_end,
y,
);
}
match segment {
ref mut s @ None => {
*s = Some((*x_start, *x_end));
}
ref mut s @ Some(_) if s.unwrap().1 < *x_start => {
(self.draw_horizontal_segment_fn)(self, s.unwrap().0, s.unwrap().1, y);
(self.draw_horizontal_segment_fn)(
&mut self.grid,
self.stdout.as_mut().unwrap(),
s.unwrap().0,
s.unwrap().1,
y,
);
*s = Some((*x_start, *x_end));
}
ref mut s @ Some(_) if s.unwrap().1 < *x_end => {
(self.draw_horizontal_segment_fn)(self, s.unwrap().0, s.unwrap().1, y);
(self.draw_horizontal_segment_fn)(
&mut self.grid,
self.stdout.as_mut().unwrap(),
s.unwrap().0,
s.unwrap().1,
y,
);
*s = Some((s.unwrap().1, *x_end));
}
Some((_, ref mut x)) => {
@ -486,16 +545,111 @@ impl State {
}
}
if let Some((x_start, x_end)) = segment {
(self.draw_horizontal_segment_fn)(self, x_start, x_end, y);
(self.draw_horizontal_segment_fn)(
&mut self.grid,
self.stdout.as_mut().unwrap(),
x_start,
x_end,
y,
);
}
}
if self.display_messages_active {
if let Some(DisplayMessage {
ref timestamp,
ref msg,
..
}) = self.display_messages.get(self.display_messages_pos)
{
let noto_colors = crate::conf::value(&self.context, "status.notification");
use crate::melib::text_processing::{Reflow, TextProcessing};
let msg_lines = msg.split_lines_reflow(Reflow::All, Some(self.cols / 3));
let width = msg_lines
.iter()
.map(|line| line.grapheme_len() + 4)
.max()
.unwrap_or(0);
let displ_area = place_in_area(
(
(0, 0),
(self.cols.saturating_sub(1), self.rows.saturating_sub(1)),
),
(width, std::cmp::min(self.rows, msg_lines.len() + 4)),
false,
false,
);
for row in self.overlay_grid.bounds_iter(displ_area) {
for c in row {
self.overlay_grid[c]
.set_ch(' ')
.set_fg(noto_colors.fg)
.set_bg(noto_colors.bg)
.set_attrs(noto_colors.attrs);
}
}
let ((x, mut y), box_displ_area_bottom_right) =
create_box(&mut self.overlay_grid, displ_area);
for line in msg_lines
.into_iter()
.chain(Some(String::new()))
.chain(Some(melib::datetime::timestamp_to_string(*timestamp, None)))
{
write_string_to_grid(
&line,
&mut self.overlay_grid,
noto_colors.fg,
noto_colors.bg,
noto_colors.attrs,
((x, y), box_displ_area_bottom_right),
Some(x),
);
y += 1;
}
if self.display_messages.len() > 1 {
write_string_to_grid(
if self.display_messages_pos == 0 {
"Next: >"
} else if self.display_messages_pos + 1 == self.display_messages.len() {
"Prev: <"
} else {
"Prev: <, Next: >"
},
&mut self.overlay_grid,
noto_colors.fg,
noto_colors.bg,
noto_colors.attrs,
((x, y), box_displ_area_bottom_right),
Some(x),
);
}
for y in get_y(upper_left!(displ_area))..=get_y(bottom_right!(displ_area)) {
(self.draw_horizontal_segment_fn)(
&mut self.overlay_grid,
self.stdout.as_mut().unwrap(),
get_x(upper_left!(displ_area)),
get_x(bottom_right!(displ_area)),
y,
);
}
}
}
self.flush();
}
/// Draw only a specific `area` on the screen.
fn draw_horizontal_segment(&mut self, x_start: usize, x_end: usize, y: usize) {
fn draw_horizontal_segment(
grid: &mut CellBuffer,
stdout: &mut StateStdout,
x_start: usize,
x_end: usize,
y: usize,
) {
write!(
self.stdout(),
stdout,
"{}",
cursor::Goto(x_start as u16 + 1, (y + 1) as u16)
)
@ -503,12 +657,6 @@ impl State {
let mut current_fg = Color::Default;
let mut current_bg = Color::Default;
let mut current_attrs = Attr::Default;
let Self {
ref grid,
ref mut stdout,
..
} = self;
let stdout = stdout.as_mut().unwrap();
write!(stdout, "\x1B[m").unwrap();
for x in x_start..=x_end {
let c = &grid[(x, y)];
@ -530,20 +678,20 @@ impl State {
}
}
fn draw_horizontal_segment_no_color(&mut self, x_start: usize, x_end: usize, y: usize) {
fn draw_horizontal_segment_no_color(
grid: &mut CellBuffer,
stdout: &mut StateStdout,
x_start: usize,
x_end: usize,
y: usize,
) {
write!(
self.stdout(),
stdout,
"{}",
cursor::Goto(x_start as u16 + 1, (y + 1) as u16)
)
.unwrap();
let mut current_attrs = Attr::Default;
let Self {
ref grid,
ref mut stdout,
..
} = self;
let stdout = stdout.as_mut().unwrap();
write!(stdout, "\x1B[m").unwrap();
for x in x_start..=x_end {
let c = &grid[(x, y)];
@ -612,24 +760,27 @@ impl State {
),
));
}
Folder(account_name, path, op) => {
Folder(account_name, op) => {
if let Some(account) = self
.context
.accounts
.iter_mut()
.find(|a| a.name() == account_name)
{
if let Err(e) = account.folder_operation(&path, op) {
self.context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(e.to_string()),
));
} else {
self.context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"{} succesfully created in `{}`",
path, account_name
)),
));
match account.folder_operation(op) {
Err(err) => {
self.context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
}
Ok(msg) => {
self.context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"`{}`: {}",
account_name, msg
)),
));
}
}
} else {
self.context.replies.push_back(UIEvent::StatusEvent(
@ -683,6 +834,12 @@ impl State {
/// The application's main loop sends `UIEvents` to state via this method.
pub fn rcv_event(&mut self, mut event: UIEvent) {
if let UIEvent::Input(_) = event {
if self.display_messages_expiration_start.is_none() {
self.display_messages_expiration_start = Some(melib::datetime::now());
}
}
match event {
// Command type is handled only by State.
UIEvent::Command(cmd) => {
@ -730,6 +887,29 @@ impl State {
self.redraw();
return;
}
UIEvent::Input(Key::Alt('<')) => {
self.display_messages_expiration_start = Some(melib::datetime::now());
self.display_messages_active = true;
self.display_messages_pos = self.display_messages_pos.saturating_sub(1);
return;
}
UIEvent::Input(Key::Alt('>')) => {
self.display_messages_expiration_start = Some(melib::datetime::now());
self.display_messages_active = true;
self.display_messages_pos = std::cmp::min(
self.display_messages.len().saturating_sub(1),
self.display_messages_pos + 1,
);
return;
}
UIEvent::StatusEvent(StatusEvent::DisplayMessage(ref msg)) => {
self.display_messages.push(DisplayMessage {
timestamp: melib::datetime::now(),
msg: msg.clone(),
});
self.display_messages_active = true;
self.display_messages_pos = self.display_messages.len() - 1;
}
_ => {}
}
/* inform each component */

View File

@ -1595,10 +1595,13 @@ pub fn copy_area(grid_dest: &mut CellBuffer, grid_src: &CellBuffer, dest: Area,
src_x = get_x(upper_left!(src));
src_y += 1;
if src_y > get_y(bottom_right!(src)) {
clear_area(
grid_dest,
((get_x(upper_left!(dest)), y + 1), bottom_right!(dest)),
);
for row in
grid_dest.bounds_iter(((get_x(upper_left!(dest)), y + 1), bottom_right!(dest)))
{
for c in row {
grid_dest[c].set_ch(' ');
}
}
ret.1 = y;
break;
}
@ -1747,35 +1750,21 @@ pub fn write_string_to_grid(
}
/// Completely clear an `Area` with an empty char and the terminal's default colors.
pub fn clear_area(grid: &mut CellBuffer, area: Area) {
pub fn clear_area(grid: &mut CellBuffer, area: Area, attributes: crate::conf::ThemeAttribute) {
if !is_valid_area!(area) {
return;
}
for row in grid.bounds_iter(area) {
for c in row {
grid[c] = Cell::default();
grid[c]
.set_fg(attributes.fg)
.set_bg(attributes.bg)
.set_attrs(attributes.attrs);
}
}
}
pub fn center_area(area: Area, (width, height): (usize, usize)) -> Area {
let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
let mid_y = { std::cmp::max(height!(area) / 2, height / 2) - height / 2 };
let (upper_x, upper_y) = upper_left!(area);
let (max_x, max_y) = bottom_right!(area);
(
(
std::cmp::min(max_x, upper_x + mid_x),
std::cmp::min(max_y, upper_y + mid_y),
),
(
std::cmp::min(max_x, upper_x + mid_x + width),
std::cmp::min(max_y, upper_y + mid_y + height),
),
)
}
pub mod ansi {
//! Create a `CellBuffer` from a string slice containing ANSI escape codes.
use super::{Cell, CellBuffer, Color};
@ -2065,3 +2054,366 @@ impl RowIterator {
}
}
}
pub use boundaries::create_box;
pub mod boundaries {
use super::*;
/// The upper and lower boundary char.
pub(crate) const HORZ_BOUNDARY: char = '─';
/// The left and right boundary char.
pub(crate) const VERT_BOUNDARY: char = '│';
/// The top-left corner
pub(crate) const _TOP_LEFT_CORNER: char = '┌';
/// The top-right corner
pub(crate) const _TOP_RIGHT_CORNER: char = '┐';
/// The bottom-left corner
pub(crate) const _BOTTOM_LEFT_CORNER: char = '└';
/// The bottom-right corner
pub(crate) const _BOTTOM_RIGHT_CORNER: char = '┘';
pub(crate) const LIGHT_VERTICAL_AND_RIGHT: char = '├';
pub(crate) const _LIGHT_VERTICAL_AND_LEFT: char = '┤';
pub(crate) const LIGHT_DOWN_AND_HORIZONTAL: char = '┬';
pub(crate) const LIGHT_UP_AND_HORIZONTAL: char = '┴';
pub(crate) const _DOUBLE_DOWN_AND_RIGHT: char = '╔';
pub(crate) const _DOUBLE_DOWN_AND_LEFT: char = '╗';
pub(crate) const _DOUBLE_UP_AND_LEFT: char = '╝';
pub(crate) const _DOUBLE_UP_AND_RIGHT: char = '╚';
fn bin_to_ch(b: u32) -> char {
match b {
0b0001 => '╶',
0b0010 => '╵',
0b0011 => '└',
0b0100 => '╴',
0b0101 => '─',
0b0110 => '┘',
0b0111 => '┴',
0b1000 => '╷',
0b1001 => '┌',
0b1010 => '│',
0b1011 => '├',
0b1100 => '┐',
0b1101 => '┬',
0b1110 => '┤',
0b1111 => '┼',
_ => unsafe { std::hint::unreachable_unchecked() },
}
}
fn ch_to_bin(ch: char) -> Option<u32> {
match ch {
'└' => Some(0b0011),
'─' => Some(0b0101),
'┘' => Some(0b0110),
'┴' => Some(0b0111),
'┌' => Some(0b1001),
'│' => Some(0b1010),
'├' => Some(0b1011),
'┐' => Some(0b1100),
'┬' => Some(0b1101),
'┤' => Some(0b1110),
'┼' => Some(0b1111),
'╷' => Some(0b1000),
'╵' => Some(0b0010),
'╴' => Some(0b0100),
'╶' => Some(0b0001),
_ => None,
}
}
#[allow(clippy::never_loop)]
fn set_and_join_vert(grid: &mut CellBuffer, idx: Pos) -> u32 {
let (x, y) = idx;
let mut bin_set = 0b1010;
/* Check left side
*
* 1
* -> 2 0
* 3
*/
loop {
if x > 0 {
if let Some(cell) = grid.get_mut(x - 1, y) {
if let Some(adj) = ch_to_bin(cell.ch()) {
if (adj & 0b0001) > 0 {
bin_set |= 0b0100;
break;
} else if adj == 0b0100 {
cell.set_ch(bin_to_ch(0b0101));
cell.set_fg(Color::Byte(240));
bin_set |= 0b0100;
break;
}
}
}
}
bin_set &= 0b1011;
break;
}
/* Check right side
*
* 1
* 2 0 <-
* 3
*/
loop {
if let Some(cell) = grid.get_mut(x + 1, y) {
if let Some(adj) = ch_to_bin(cell.ch()) {
if (adj & 0b0100) > 0 {
bin_set |= 0b0001;
break;
}
}
}
bin_set &= 0b1110;
break;
}
/* Set upper side
*
* 1 <-
* 2 0
* 3
*/
loop {
if y > 0 {
if let Some(cell) = grid.get_mut(x, y - 1) {
if let Some(adj) = ch_to_bin(cell.ch()) {
cell.set_ch(bin_to_ch(adj | 0b1000));
cell.set_fg(Color::Byte(240));
} else {
bin_set &= 0b1101;
}
}
}
break;
}
/* Set bottom side
*
* 1
* 2 0
* 3 <-
*/
loop {
if let Some(cell) = grid.get_mut(x, y + 1) {
if let Some(adj) = ch_to_bin(cell.ch()) {
cell.set_ch(bin_to_ch(adj | 0b0010));
cell.set_fg(Color::Byte(240));
} else {
bin_set &= 0b0111;
}
}
break;
}
if bin_set == 0 {
bin_set = 0b1010;
}
bin_set
}
#[allow(clippy::never_loop)]
fn set_and_join_horz(grid: &mut CellBuffer, idx: Pos) -> u32 {
let (x, y) = idx;
let mut bin_set = 0b0101;
/* Check upper side
*
* 1 <-
* 2 0
* 3
*/
loop {
if y > 0 {
if let Some(cell) = grid.get_mut(x, y - 1) {
if let Some(adj) = ch_to_bin(cell.ch()) {
if (adj & 0b1000) > 0 {
bin_set |= 0b0010;
break;
} else if adj == 0b0010 {
bin_set |= 0b0010;
cell.set_ch(bin_to_ch(0b1010));
cell.set_fg(Color::Byte(240));
break;
}
}
}
}
bin_set &= 0b1101;
break;
}
/* Check bottom side
*
* 1
* 2 0
* 3 <-
*/
loop {
if let Some(cell) = grid.get_mut(x, y + 1) {
if let Some(adj) = ch_to_bin(cell.ch()) {
if (adj & 0b0010) > 0 {
bin_set |= 0b1000;
break;
} else if adj == 0b1000 {
bin_set |= 0b1000;
cell.set_ch(bin_to_ch(0b1010));
cell.set_fg(Color::Byte(240));
break;
}
}
}
bin_set &= 0b0111;
break;
}
/* Set left side
*
* 1
* -> 2 0
* 3
*/
loop {
if x > 0 {
if let Some(cell) = grid.get_mut(x - 1, y) {
if let Some(adj) = ch_to_bin(cell.ch()) {
cell.set_ch(bin_to_ch(adj | 0b0001));
cell.set_fg(Color::Byte(240));
} else {
bin_set &= 0b1011;
}
}
}
break;
}
/* Set right side
*
* 1
* 2 0 <-
* 3
*/
loop {
if let Some(cell) = grid.get_mut(x + 1, y) {
if let Some(adj) = ch_to_bin(cell.ch()) {
cell.set_ch(bin_to_ch(adj | 0b0100));
cell.set_fg(Color::Byte(240));
} else {
bin_set &= 0b1110;
}
}
break;
}
if bin_set == 0 {
bin_set = 0b0101;
}
bin_set
}
pub(crate) enum BoxBoundary {
Horizontal,
Vertical,
}
pub(crate) fn set_and_join_box(grid: &mut CellBuffer, idx: Pos, ch: BoxBoundary) {
/* Connected sides:
*
* 1
* 2 c 0
* 3
*
* #3210
* 0b____
*/
if grid.ascii_drawing {
grid[idx].set_ch(match ch {
BoxBoundary::Vertical => '|',
BoxBoundary::Horizontal => '-',
});
grid[idx].set_fg(Color::Byte(240));
return;
}
let bin_set = match ch {
BoxBoundary::Vertical => set_and_join_vert(grid, idx),
BoxBoundary::Horizontal => set_and_join_horz(grid, idx),
};
grid[idx].set_ch(bin_to_ch(bin_set));
grid[idx].set_fg(Color::Byte(240));
}
/// Puts boundaries in `area`.
/// Returns the inner area of the created box.
pub fn create_box(grid: &mut CellBuffer, area: Area) -> Area {
if !is_valid_area!(area) {
return ((0, 0), (0, 0));
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
if !grid.ascii_drawing {
for x in get_x(upper_left)..get_x(bottom_right) {
grid[(x, get_y(upper_left))].set_ch(HORZ_BOUNDARY);
grid[(x, get_y(bottom_right))].set_ch(HORZ_BOUNDARY);
}
for y in get_y(upper_left)..get_y(bottom_right) {
grid[(get_x(upper_left), y)].set_ch(VERT_BOUNDARY);
grid[(get_x(bottom_right), y)].set_ch(VERT_BOUNDARY);
}
set_and_join_box(grid, upper_left, BoxBoundary::Horizontal);
set_and_join_box(
grid,
set_x(upper_left, get_x(bottom_right)),
BoxBoundary::Horizontal,
);
set_and_join_box(
grid,
set_y(upper_left, get_y(bottom_right)),
BoxBoundary::Vertical,
);
set_and_join_box(grid, bottom_right, BoxBoundary::Vertical);
}
(
(
std::cmp::min(
get_x(upper_left) + 2,
std::cmp::min(get_x(upper_left) + 1, get_x(bottom_right)),
),
std::cmp::min(
get_y(upper_left) + 2,
std::cmp::min(get_y(upper_left) + 1, get_y(bottom_right)),
),
),
(
std::cmp::max(
get_x(bottom_right).saturating_sub(2),
std::cmp::max(get_x(bottom_right).saturating_sub(1), get_x(upper_left)),
),
std::cmp::max(
get_y(bottom_right).saturating_sub(2),
std::cmp::max(get_y(bottom_right).saturating_sub(1), get_y(upper_left)),
),
),
)
}
}

View File

@ -483,6 +483,7 @@ impl EmbedGrid {
terminal_size.1.saturating_sub(1),
),
),
Default::default(),
);
debug!("{}", EscCode::from((&(*state), byte)));
*state = State::Normal;
@ -565,7 +566,11 @@ impl EmbedGrid {
}
}
debug!("{}", EscCode::from((&(*state), byte)));
clear_area(grid, ((0, 0), pos_dec(*terminal_size, (1, 1))));
clear_area(
grid,
((0, 0), pos_dec(*terminal_size, (1, 1))),
Default::default(),
);
*state = State::Normal;
}
(b'J', State::Csi1(ref buf)) if buf == b"0" => {
@ -586,6 +591,7 @@ impl EmbedGrid {
terminal_size.1.saturating_sub(1),
),
),
Default::default(),
);
debug!("{}", EscCode::from((&(*state), byte)));
*state = State::Normal;
@ -602,6 +608,7 @@ impl EmbedGrid {
cursor.1.saturating_sub(1) + scroll_region.top,
),
),
Default::default(),
);
debug!("{}", EscCode::from((&(*state), byte)));
*state = State::Normal;
@ -609,7 +616,11 @@ impl EmbedGrid {
(b'J', State::Csi1(ref buf)) if buf == b"2" => {
/* Erase in Display (ED), VT100.*/
/* Erase All */
clear_area(grid, ((0, 0), pos_dec(*terminal_size, (1, 1))));
clear_area(
grid,
((0, 0), pos_dec(*terminal_size, (1, 1))),
Default::default(),
);
debug!("{}", EscCode::from((&(*state), byte)));
*state = State::Normal;
}

View File

@ -57,12 +57,8 @@ pub fn pos_dec(p: Pos, dec: (usize, usize)) -> Pos {
/// An `Area` consists of two points: the upper left and bottom right corners.
///
/// Example:
/// ```
/// # #[macro_use] extern crate ui; fn main() {
/// use ui::*;
///
/// ```no_run
/// let new_area = ((0, 0), (1, 1));
/// # }
/// ```
pub type Area = (Pos, Pos);
@ -70,12 +66,8 @@ pub type Area = (Pos, Pos);
///
/// Example:
/// ```
/// # #[macro_use] extern crate ui; fn main() {
/// use ui::*;
///
/// let new_area = ((0, 0), (1, 1));
/// assert_eq!(height!(new_area), 1);
/// # }
/// ```
#[macro_export]
macro_rules! height {
@ -88,12 +80,8 @@ macro_rules! height {
///
/// Example:
/// ```
/// # #[macro_use] extern crate ui; fn main() {
/// use ui::*;
///
/// let new_area = ((0, 0), (1, 1));
/// assert_eq!(width!(new_area), 1);
/// # }
/// ```
#[macro_export]
macro_rules! width {
@ -106,12 +94,8 @@ macro_rules! width {
///
/// Example:
/// ```
/// # #[macro_use] extern crate ui; fn main() {
/// use ui::*;
///
/// let new_area = ((0, 0), (1, 1));
/// assert_eq!(upper_left!(new_area), (0, 0));
/// # }
/// ```
#[macro_export]
macro_rules! upper_left {
@ -124,12 +108,8 @@ macro_rules! upper_left {
///
/// Example:
/// ```
/// # #[macro_use] extern crate ui; fn main() {
/// use ui::*;
///
/// let new_area = ((0, 0), (1, 1));
/// assert_eq!(bottom_right!(new_area), (1, 1));
/// # }
/// ```
#[macro_export]
macro_rules! bottom_right {
@ -142,15 +122,12 @@ macro_rules! bottom_right {
///
/// Example:
/// ```
/// # #[macro_use] extern crate ui; fn main() {
/// use ui::*;
///
/// let valid_area = ((0, 0), (1, 1));
/// assert!(is_valid_area!(valid_area));
///
/// let invalid_area = ((2, 2), (1, 1));
/// assert!(!is_valid_area!(invalid_area));
/// # }
/// ```
///
#[macro_export]
macro_rules! is_valid_area {
@ -160,3 +137,47 @@ macro_rules! is_valid_area {
!(get_y(upper_left) > get_y(bottom_right) || get_x(upper_left) > get_x(bottom_right))
}};
}
/// Place box given by `(width, height)` in center of `area`
pub fn center_area(area: Area, (width, height): (usize, usize)) -> Area {
let mid_x = { std::cmp::max(width!(area) / 2, width / 2) - width / 2 };
let mid_y = { std::cmp::max(height!(area) / 2, height / 2) - height / 2 };
let (upper_x, upper_y) = upper_left!(area);
let (max_x, max_y) = bottom_right!(area);
(
(
std::cmp::min(max_x, upper_x + mid_x),
std::cmp::min(max_y, upper_y + mid_y),
),
(
std::cmp::min(max_x, upper_x + mid_x + width),
std::cmp::min(max_y, upper_y + mid_y + height),
),
)
}
/// Place box given by `(width, height)` in corner of `area`
pub fn place_in_area(area: Area, (width, height): (usize, usize), upper: bool, left: bool) -> Area {
let (upper_x, upper_y) = upper_left!(area);
let (max_x, max_y) = bottom_right!(area);
let x = if upper {
upper_x + 2
} else {
max_x.saturating_sub(2).saturating_sub(width)
};
let y = if left {
upper_y + 2
} else {
max_y.saturating_sub(2).saturating_sub(height)
};
(
(std::cmp::min(x, max_x), std::cmp::min(y, max_y)),
(
std::cmp::min(x + width, max_x),
std::cmp::min(y + height, max_y),
),
)
}

View File

@ -111,6 +111,9 @@ pub enum UIEvent {
Action(Action),
StatusEvent(StatusEvent),
MailboxUpdate((usize, FolderHash)), // (account_idx, mailbox_idx)
MailboxDelete((usize, FolderHash)),
MailboxCreate((usize, FolderHash)),
AccountStatusChange(usize),
ComponentKill(Uuid),
WorkerProgress(FolderHash),
StartupCheck(FolderHash),