Compare commits
12 Commits
50cd81772f
...
622ded8021
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | 622ded8021 | |
Manos Pitsidianakis | 6d63429ad3 | |
Manos Pitsidianakis | 5eb4342af8 | |
Manos Pitsidianakis | eca10a5660 | |
Manos Pitsidianakis | a697dfabbd | |
Manos Pitsidianakis | 23997bdec0 | |
Manos Pitsidianakis | 2e6a1e1ef8 | |
Manos Pitsidianakis | fe200a3218 | |
Manos Pitsidianakis | bf9143d8e4 | |
Manos Pitsidianakis | 441dcb62ca | |
Manos Pitsidianakis | 4cd3e28244 | |
Manos Pitsidianakis | 3dba6fdf60 |
File diff suppressed because it is too large
Load Diff
|
@ -37,7 +37,7 @@ serde = "1.0.71"
|
|||
serde_derive = "1.0.71"
|
||||
serde_json = "1.0"
|
||||
toml = { version = "0.5.6", features = ["preserve_order", ] }
|
||||
indexmap = { version = "^1.5", features = ["serde-1", ] }
|
||||
indexmap = { version = "^1.6", features = ["serde-1", ] }
|
||||
linkify = "0.4.0"
|
||||
notify = "4.0.1" # >:c
|
||||
termion = "1.5.1"
|
||||
|
|
|
@ -446,6 +446,31 @@ Store sent mail after successful submission.
|
|||
This setting is meant to be disabled for non-standard behaviour in gmail, which auto-saves sent mail on its own.
|
||||
.\" default value
|
||||
.Pq Em true
|
||||
.It Ic attribution_format_string Ar String
|
||||
.Pq Em optional
|
||||
The attribution line appears above the quoted reply text.
|
||||
The format specifiers for the replied address are:
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
.Li %+f
|
||||
— the sender's name and email address.
|
||||
.It
|
||||
.Li %+n
|
||||
— the sender's name (or email address, if no name is included).
|
||||
.It
|
||||
.Li %+a
|
||||
— the sender's email address.
|
||||
.El
|
||||
The format string is passed to
|
||||
.Xr strftime 3
|
||||
with the replied envelope's date.
|
||||
.\" default value
|
||||
.Pq Em "On %a, %0e %b %Y %H:%M, %+f wrote:%n"
|
||||
.It Ic attribution_use_posix_locale Ar boolean
|
||||
.Pq Em optional
|
||||
Whether the strftime call for the attribution string uses the POSIX locale instead of the user's active locale.
|
||||
.\" default value
|
||||
.Pq Em true
|
||||
.El
|
||||
.Sh SHORTCUTS
|
||||
Shortcuts can take the following values:
|
||||
|
|
|
@ -192,7 +192,7 @@ impl Card {
|
|||
self.key.as_str()
|
||||
}
|
||||
pub fn last_edited(&self) -> String {
|
||||
datetime::timestamp_to_string(self.last_edited, None)
|
||||
datetime::timestamp_to_string(self.last_edited, None, false)
|
||||
}
|
||||
|
||||
pub fn set_id(&mut self, new_val: CardId) {
|
||||
|
|
|
@ -201,7 +201,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
|||
T102200Z
|
||||
T102200-0800
|
||||
*/
|
||||
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d")
|
||||
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d\0")
|
||||
.unwrap_or_default();
|
||||
}
|
||||
if let Some(val) = self.0.remove("EMAIL") {
|
||||
|
|
|
@ -246,6 +246,14 @@ pub enum RefreshEventKind {
|
|||
NewFlags(EnvelopeHash, (Flag, Vec<String>)),
|
||||
Rescan,
|
||||
Failure(MeliError),
|
||||
MailboxCreate(Mailbox),
|
||||
MailboxDelete(MailboxHash),
|
||||
MailboxRename {
|
||||
old_mailbox_hash: MailboxHash,
|
||||
new_mailbox: Mailbox,
|
||||
},
|
||||
MailboxSubscribe(MailboxHash),
|
||||
MailboxUnsubscribe(MailboxHash),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -269,12 +269,6 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
env.push_references(env.in_reply_to().unwrap().clone());
|
||||
}
|
||||
if let Some(v) = t.headers.get("References") {
|
||||
let parse_result = crate::email::parser::address::msg_id_list(v.as_bytes());
|
||||
if let Ok((_, v)) = parse_result {
|
||||
for v in v {
|
||||
env.push_references(v);
|
||||
}
|
||||
}
|
||||
env.set_references(v.as_bytes());
|
||||
}
|
||||
if let Some(v) = t.headers.get("Date") {
|
||||
|
@ -282,6 +276,8 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
if let Ok(d) = crate::email::parser::dates::rfc5322_date(v.as_bytes()) {
|
||||
env.set_datetime(d);
|
||||
}
|
||||
} else if let Ok(d) = crate::email::parser::dates::rfc5322_date(t.received_at.as_bytes()) {
|
||||
env.set_datetime(d);
|
||||
}
|
||||
env.set_has_attachments(t.has_attachment);
|
||||
if let Some(ref mut subject) = t.subject {
|
||||
|
@ -581,6 +577,7 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
|||
fn from(val: crate::search::Query) -> Self {
|
||||
let mut ret = Filter::Condition(EmailFilterCondition::new().into());
|
||||
fn rec(q: &crate::search::Query, f: &mut Filter<EmailFilterCondition, EmailObject>) {
|
||||
use crate::datetime::{timestamp_to_string, RFC3339_FMT};
|
||||
use crate::search::Query::*;
|
||||
match q {
|
||||
Subject(t) => {
|
||||
|
@ -604,23 +601,48 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
|||
Body(t) => {
|
||||
*f = Filter::Condition(EmailFilterCondition::new().body(t.clone()).into());
|
||||
}
|
||||
Before(_) => {
|
||||
//TODO, convert UNIX timestamp into UtcDate
|
||||
Before(t) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.before(timestamp_to_string(*t, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
After(_) => {
|
||||
//TODO
|
||||
After(t) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.after(timestamp_to_string(*t, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Between(_, _) => {
|
||||
//TODO
|
||||
Between(a, b) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.after(timestamp_to_string(*a, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
*f &= Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.before(timestamp_to_string(*b, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
On(_) => {
|
||||
//TODO
|
||||
On(t) => {
|
||||
rec(&Between(*t, *t), f);
|
||||
}
|
||||
InReplyTo(_) => {
|
||||
//TODO, look inside Headers
|
||||
InReplyTo(ref s) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.header(vec!["In-Reply-To".to_string().into(), s.to_string().into()])
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
References(_) => {
|
||||
//TODO
|
||||
References(ref s) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.header(vec!["References".to_string().into(), s.to_string().into()])
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
AllAddresses(_) => {
|
||||
//TODO
|
||||
|
|
|
@ -38,37 +38,41 @@
|
|||
//! assert_eq!(s, "2020-01-08");
|
||||
//! ```
|
||||
use crate::error::{Result, ResultIntoMeliError};
|
||||
use std::borrow::Cow;
|
||||
use std::convert::TryInto;
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
pub type UnixTimestamp = u64;
|
||||
|
||||
use libc::{locale_t, timeval, timezone};
|
||||
pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0";
|
||||
pub const RFC3339_FMT: &str = "%Y-%m-%d\0";
|
||||
pub const RFC822_FMT_WITH_TIME: &str = "%a, %e %h %Y %H:%M:%S \0";
|
||||
pub const RFC822_FMT: &str = "%e %h %Y %H:%M:%S \0";
|
||||
pub const DEFAULT_FMT: &str = "%a, %d %b %Y %R\0";
|
||||
|
||||
extern "C" {
|
||||
fn strptime(
|
||||
s: *const ::std::os::raw::c_char,
|
||||
format: *const ::std::os::raw::c_char,
|
||||
tm: *mut ::libc::tm,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
s: *const std::os::raw::c_char,
|
||||
format: *const std::os::raw::c_char,
|
||||
tm: *mut libc::tm,
|
||||
) -> *const std::os::raw::c_char;
|
||||
|
||||
fn strftime(
|
||||
s: *mut ::std::os::raw::c_char,
|
||||
max: ::libc::size_t,
|
||||
format: *const ::std::os::raw::c_char,
|
||||
tm: *const ::libc::tm,
|
||||
) -> ::libc::size_t;
|
||||
s: *mut std::os::raw::c_char,
|
||||
max: libc::size_t,
|
||||
format: *const std::os::raw::c_char,
|
||||
tm: *const libc::tm,
|
||||
) -> libc::size_t;
|
||||
|
||||
fn mktime(tm: *const ::libc::tm) -> ::libc::time_t;
|
||||
fn mktime(tm: *const libc::tm) -> libc::time_t;
|
||||
|
||||
fn localtime_r(timep: *const ::libc::time_t, tm: *mut ::libc::tm) -> *mut ::libc::tm;
|
||||
fn localtime_r(timep: *const libc::time_t, tm: *mut libc::tm) -> *mut libc::tm;
|
||||
|
||||
fn gettimeofday(tv: *mut timeval, tz: *mut timezone) -> i32;
|
||||
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
|
||||
}
|
||||
|
||||
struct Locale {
|
||||
new_locale: locale_t,
|
||||
old_locale: locale_t,
|
||||
new_locale: libc::locale_t,
|
||||
old_locale: libc::locale_t,
|
||||
}
|
||||
|
||||
impl Drop for Locale {
|
||||
|
@ -83,9 +87,9 @@ impl Drop for Locale {
|
|||
// How to unit test this? Test machine is not guaranteed to have non-english locales.
|
||||
impl Locale {
|
||||
fn new(
|
||||
mask: ::std::os::raw::c_int,
|
||||
locale: *const ::std::os::raw::c_char,
|
||||
base: locale_t,
|
||||
mask: std::os::raw::c_int,
|
||||
locale: *const std::os::raw::c_char,
|
||||
base: libc::locale_t,
|
||||
) -> Result<Self> {
|
||||
let new_locale = unsafe { libc::newlocale(mask, locale, base) };
|
||||
if new_locale.is_null() {
|
||||
|
@ -103,35 +107,58 @@ impl Locale {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>) -> String {
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: bool) -> String {
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
let i: i64 = timestamp.try_into().unwrap_or(0);
|
||||
localtime_r(&i as *const i64, &mut new_tm as *mut ::libc::tm);
|
||||
localtime_r(&i as *const i64, &mut new_tm as *mut libc::tm);
|
||||
}
|
||||
let fmt = fmt
|
||||
let format: Cow<'_, CStr> = if let Some(cs) = fmt
|
||||
.map(str::as_bytes)
|
||||
.map(CStr::from_bytes_with_nul)
|
||||
.and_then(|res| res.ok())
|
||||
{
|
||||
Cow::from(cs)
|
||||
} else if let Some(cstring) = fmt
|
||||
.map(str::as_bytes)
|
||||
.map(CString::new)
|
||||
.map(|res| res.ok())
|
||||
.and_then(|opt| opt);
|
||||
let format: &CStr = if let Some(ref s) = fmt {
|
||||
&s
|
||||
.and_then(|res| res.ok())
|
||||
{
|
||||
Cow::from(cstring)
|
||||
} else {
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(b"%a, %d %b %Y %T %z\0") }
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(DEFAULT_FMT.as_bytes()).into() }
|
||||
};
|
||||
|
||||
let mut vec: [u8; 256] = [0; 256];
|
||||
let ret = unsafe {
|
||||
strftime(
|
||||
vec.as_mut_ptr() as *mut _,
|
||||
256,
|
||||
format.as_ptr(),
|
||||
&new_tm as *const _,
|
||||
)
|
||||
let ret = {
|
||||
let _with_locale: Option<Result<Locale>> = if posix {
|
||||
Some(
|
||||
Locale::new(
|
||||
libc::LC_TIME,
|
||||
b"C\0".as_ptr() as *const i8,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
unsafe {
|
||||
strftime(
|
||||
vec.as_mut_ptr() as *mut _,
|
||||
256,
|
||||
format.as_ptr(),
|
||||
&new_tm as *const _,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
String::from_utf8_lossy(&vec[0..ret]).into_owned()
|
||||
}
|
||||
|
||||
fn tm_to_secs(tm: ::libc::tm) -> std::result::Result<i64, ()> {
|
||||
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;
|
||||
|
@ -244,63 +271,58 @@ where
|
|||
T: Into<Vec<u8>>,
|
||||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[
|
||||
&b"%a, %e %h %Y %H:%M:%S \0"[..],
|
||||
&b"%e %h %Y %H:%M:%S \0"[..],
|
||||
] {
|
||||
unsafe {
|
||||
let fmt = CStr::from_bytes_with_nul_unchecked(fmt);
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[RFC822_FMT_WITH_TIME, RFC822_FMT] {
|
||||
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
libc::LC_TIME,
|
||||
b"C\0".as_ptr() as *const i8,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
|
||||
};
|
||||
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
::libc::LC_TIME,
|
||||
b"C\0".as_ptr() as *const i8,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _)
|
||||
};
|
||||
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
}
|
||||
let rest = CStr::from_ptr(ret);
|
||||
let tm_gmtoff = if rest.to_bytes().len() > 4
|
||||
&& rest.to_bytes().is_ascii()
|
||||
&& rest.to_bytes()[1..5].iter().all(u8::is_ascii_digit)
|
||||
{
|
||||
let offset = std::str::from_utf8_unchecked(&rest.to_bytes()[0..5]);
|
||||
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
|
||||
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
|
||||
{
|
||||
if rest.to_bytes()[0] == b'-' {
|
||||
hr_offset = -hr_offset;
|
||||
min_offset = -min_offset;
|
||||
}
|
||||
hr_offset * 60 * 60 + min_offset * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
|
||||
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
|
||||
} else {
|
||||
rest.to_bytes()
|
||||
};
|
||||
|
||||
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
|
||||
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
return Ok(tm_to_secs(new_tm)
|
||||
.map(|res| (res - tm_gmtoff) as u64)
|
||||
.unwrap_or(0));
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
}
|
||||
let rest = unsafe { CStr::from_ptr(ret) };
|
||||
let tm_gmtoff = if rest.to_bytes().len() > 4
|
||||
&& rest.to_bytes().is_ascii()
|
||||
&& rest.to_bytes()[1..5].iter().all(u8::is_ascii_digit)
|
||||
{
|
||||
// safe since rest.to_bytes().is_ascii()
|
||||
let offset = unsafe { std::str::from_utf8_unchecked(&rest.to_bytes()[0..5]) };
|
||||
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
|
||||
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
|
||||
{
|
||||
if rest.to_bytes()[0] == b'-' {
|
||||
hr_offset = -hr_offset;
|
||||
min_offset = -min_offset;
|
||||
}
|
||||
hr_offset * 60 * 60 + min_offset * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
|
||||
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
|
||||
} else {
|
||||
rest.to_bytes()
|
||||
};
|
||||
|
||||
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
|
||||
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
return Ok(tm_to_secs(new_tm)
|
||||
.map(|res| (res - tm_gmtoff) as u64)
|
||||
.unwrap_or(0));
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
@ -310,59 +332,58 @@ where
|
|||
T: Into<Vec<u8>>,
|
||||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[&b"%Y-%m-%dT%H:%M:%S\0"[..], &b"%Y-%m-%d\0"[..]] {
|
||||
unsafe {
|
||||
let fmt = CStr::from_bytes_with_nul_unchecked(fmt);
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
::libc::LC_TIME,
|
||||
b"C\0".as_ptr() as *const i8,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _)
|
||||
};
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
}
|
||||
let rest = CStr::from_ptr(ret);
|
||||
let tm_gmtoff = if rest.to_bytes().len() > 4
|
||||
&& rest.to_bytes().is_ascii()
|
||||
&& rest.to_bytes()[1..3].iter().all(u8::is_ascii_digit)
|
||||
&& rest.to_bytes()[4..6].iter().all(u8::is_ascii_digit)
|
||||
{
|
||||
let offset = std::str::from_utf8_unchecked(&rest.to_bytes()[0..6]);
|
||||
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
|
||||
(offset[1..3].parse::<i64>(), offset[4..6].parse::<i64>())
|
||||
{
|
||||
if rest.to_bytes()[0] == b'-' {
|
||||
hr_offset = -hr_offset;
|
||||
min_offset = -min_offset;
|
||||
}
|
||||
hr_offset * 60 * 60 + min_offset * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
|
||||
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
|
||||
} else {
|
||||
rest.to_bytes()
|
||||
};
|
||||
|
||||
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
|
||||
let (hr_offset, min_offset) = debug!(TIMEZONE_ABBR[idx]).1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
return Ok(tm_to_secs(new_tm)
|
||||
.map(|res| (res - tm_gmtoff) as u64)
|
||||
.unwrap_or(0));
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[RFC3339_FMT_WITH_TIME, RFC3339_FMT] {
|
||||
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
libc::LC_TIME,
|
||||
b"C\0".as_ptr() as *const i8,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
|
||||
};
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
}
|
||||
let rest = unsafe { CStr::from_ptr(ret) };
|
||||
let tm_gmtoff = if rest.to_bytes().len() > 4
|
||||
&& rest.to_bytes().is_ascii()
|
||||
&& rest.to_bytes()[1..3].iter().all(u8::is_ascii_digit)
|
||||
&& rest.to_bytes()[4..6].iter().all(u8::is_ascii_digit)
|
||||
{
|
||||
// safe since rest.to_bytes().is_ascii()
|
||||
let offset = unsafe { std::str::from_utf8_unchecked(&rest.to_bytes()[0..6]) };
|
||||
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
|
||||
(offset[1..3].parse::<i64>(), offset[4..6].parse::<i64>())
|
||||
{
|
||||
if rest.to_bytes()[0] == b'-' {
|
||||
hr_offset = -hr_offset;
|
||||
min_offset = -min_offset;
|
||||
}
|
||||
hr_offset * 60 * 60 + min_offset * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
|
||||
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
|
||||
} else {
|
||||
rest.to_bytes()
|
||||
};
|
||||
|
||||
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
|
||||
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
return Ok(tm_to_secs(new_tm)
|
||||
.map(|res| (res - tm_gmtoff) as u64)
|
||||
.unwrap_or(0));
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
@ -372,8 +393,12 @@ pub fn timestamp_from_string<T>(s: T, fmt: &str) -> Result<Option<UnixTimestamp>
|
|||
where
|
||||
T: Into<Vec<u8>>,
|
||||
{
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
let fmt = CString::new(fmt)?;
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
let fmt: Cow<'_, CStr> = if let Ok(cs) = CStr::from_bytes_with_nul(fmt.as_bytes()) {
|
||||
Cow::from(cs)
|
||||
} else {
|
||||
Cow::from(CString::new(fmt.as_bytes())?)
|
||||
};
|
||||
unsafe {
|
||||
let ret = strptime(
|
||||
CString::new(s)?.as_ptr(),
|
||||
|
@ -389,8 +414,8 @@ where
|
|||
|
||||
pub fn now() -> UnixTimestamp {
|
||||
use std::mem::MaybeUninit;
|
||||
let mut tv = MaybeUninit::<::libc::timeval>::uninit();
|
||||
let mut tz = MaybeUninit::<::libc::timezone>::uninit();
|
||||
let mut tv = MaybeUninit::<libc::timeval>::uninit();
|
||||
let mut tz = MaybeUninit::<libc::timezone>::uninit();
|
||||
unsafe {
|
||||
let ret = gettimeofday(tv.as_mut_ptr(), tz.as_mut_ptr());
|
||||
if ret == -1 {
|
||||
|
@ -401,12 +426,12 @@ pub fn now() -> UnixTimestamp {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp() {
|
||||
timestamp_to_string(0, None);
|
||||
fn test_datetime_timestamp() {
|
||||
timestamp_to_string(0, None, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rfcs() {
|
||||
fn test_datetime_rfcs() {
|
||||
if unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _) }.is_null() {
|
||||
println!("Unable to set locale.");
|
||||
}
|
||||
|
@ -420,7 +445,7 @@ fn test_rfcs() {
|
|||
/*
|
||||
macro_rules! mkt {
|
||||
($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
|
||||
::libc::tm {
|
||||
libc::tm {
|
||||
tm_sec: $second,
|
||||
tm_min: $minute,
|
||||
tm_hour: $hour,
|
||||
|
|
|
@ -53,7 +53,7 @@ impl Default for Draft {
|
|||
let mut headers = HeaderMap::default();
|
||||
headers.insert(
|
||||
HeaderName::new_unchecked("Date"),
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None),
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None, true),
|
||||
);
|
||||
headers.insert(HeaderName::new_unchecked("From"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("To"), "".into());
|
||||
|
|
|
@ -41,7 +41,11 @@ pub mod dbg {
|
|||
() => {
|
||||
eprint!(
|
||||
"[{}][{:?}] {}:{}_{}: ",
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), Some("%Y-%m-%d %T")),
|
||||
crate::datetime::timestamp_to_string(
|
||||
crate::datetime::now(),
|
||||
Some("%Y-%m-%d %T"),
|
||||
false
|
||||
),
|
||||
std::thread::current()
|
||||
.name()
|
||||
.map(std::string::ToString::to_string)
|
||||
|
|
|
@ -85,7 +85,8 @@ pub fn log<S: AsRef<str>>(val: S, level: LoggingLevel) {
|
|||
if level <= b.level {
|
||||
b.dest
|
||||
.write_all(
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None).as_bytes(),
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None, false)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
b.dest.write_all(b" [").unwrap();
|
||||
|
|
|
@ -66,6 +66,22 @@ pub enum PageMovement {
|
|||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ScrollContext {
|
||||
shown_lines: usize,
|
||||
total_lines: usize,
|
||||
has_more_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ScrollUpdate {
|
||||
End(ComponentId),
|
||||
Update {
|
||||
id: ComponentId,
|
||||
context: ScrollContext,
|
||||
},
|
||||
}
|
||||
|
||||
/// Types implementing this Trait can draw on the terminal and receive events.
|
||||
/// If a type wants to skip drawing if it has not changed anything, it can hold some flag in its
|
||||
/// fields (eg self.dirty = false) and act upon that in their `draw` implementation.
|
||||
|
|
|
@ -414,6 +414,27 @@ impl ContactList {
|
|||
|
||||
let top_idx = page_no * rows;
|
||||
|
||||
if self.length >= rows {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::Update {
|
||||
id: self.id,
|
||||
context: ScrollContext {
|
||||
shown_lines: top_idx + rows,
|
||||
total_lines: self.length,
|
||||
has_more_lines: false,
|
||||
},
|
||||
},
|
||||
)));
|
||||
} else {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
}
|
||||
|
||||
/* If cursor position has changed, remove the highlight from the previous position and
|
||||
* apply it in the new one. */
|
||||
if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no {
|
||||
|
@ -621,6 +642,11 @@ impl Component for ContactList {
|
|||
|
||||
self.mode = ViewMode::View(manager.id());
|
||||
self.view = Some(manager);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -639,6 +665,11 @@ impl Component for ContactList {
|
|||
|
||||
self.mode = ViewMode::View(manager.id());
|
||||
self.view = Some(manager);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -314,10 +314,21 @@ impl Composer {
|
|||
}
|
||||
}
|
||||
ret.draft.body = {
|
||||
let mut ret = format!(
|
||||
"On {} {} wrote:\n",
|
||||
envelope.date_as_str(),
|
||||
envelope.from()[0],
|
||||
let mut ret = attribution_string(
|
||||
account_settings!(
|
||||
context[ret.account_hash]
|
||||
.composing
|
||||
.attribution_format_string
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str()),
|
||||
envelope.from().get(0),
|
||||
envelope.date(),
|
||||
*account_settings!(
|
||||
context[ret.account_hash]
|
||||
.composing
|
||||
.attribution_use_posix_locale
|
||||
),
|
||||
);
|
||||
for l in reply_body.lines() {
|
||||
ret.push('>');
|
||||
|
@ -925,6 +936,9 @@ impl Component for Composer {
|
|||
}
|
||||
|
||||
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if let UIEvent::VisibilityChange(_) = event {
|
||||
self.pager.process_event(event, context);
|
||||
}
|
||||
let shortcuts = self.get_shortcuts(context);
|
||||
match (&mut self.mode, &mut event) {
|
||||
(ViewMode::Edit, _) => {
|
||||
|
@ -2207,3 +2221,36 @@ pub fn send_draft_async(
|
|||
ret
|
||||
}))
|
||||
}
|
||||
|
||||
/* Sender details
|
||||
* %+f — the sender's name and email address.
|
||||
* %+n — the sender's name (or email address, if no name is included).
|
||||
* %+a — the sender's email address.
|
||||
*/
|
||||
fn attribution_string(
|
||||
fmt: Option<&str>,
|
||||
sender: Option<&Address>,
|
||||
date: UnixTimestamp,
|
||||
posix: bool,
|
||||
) -> String {
|
||||
let fmt = fmt.unwrap_or("On %a, %0e %b %Y %H:%M, %+f wrote:%n");
|
||||
let fmt = fmt.replace(
|
||||
"%+f",
|
||||
&sender
|
||||
.map(|addr| addr.to_string())
|
||||
.unwrap_or_else(|| "\"\"".to_string()),
|
||||
);
|
||||
let fmt = fmt.replace(
|
||||
"%+n",
|
||||
&sender
|
||||
.map(|addr| addr.get_display_name().unwrap_or_else(|| addr.get_email()))
|
||||
.unwrap_or_else(|| "\"\"".to_string()),
|
||||
);
|
||||
let fmt = fmt.replace(
|
||||
"%+a",
|
||||
&sender
|
||||
.map(|addr| addr.get_email())
|
||||
.unwrap_or_else(|| "\"\"".to_string()),
|
||||
);
|
||||
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
|
||||
}
|
||||
|
|
|
@ -703,6 +703,8 @@ impl Component for Listing {
|
|||
fallback = *cur;
|
||||
}
|
||||
if self.component.coordinates() == (*account_hash, *mailbox_hash) {
|
||||
self.component
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
self.component.set_coordinates((
|
||||
self.accounts[self.cursor_pos.0].hash,
|
||||
self.accounts[self.cursor_pos.0].entries[fallback].3,
|
||||
|
@ -730,6 +732,8 @@ impl Component for Listing {
|
|||
let account_hash = self.accounts[self.cursor_pos.0].hash;
|
||||
self.cursor_pos.1 = MenuEntryCursor::Mailbox(*idx);
|
||||
self.status = None;
|
||||
self.component
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
self.component
|
||||
.set_coordinates((account_hash, *mailbox_hash));
|
||||
self.menu_content.empty();
|
||||
|
@ -1148,6 +1152,11 @@ impl Component for Listing {
|
|||
match *event {
|
||||
UIEvent::Input(Key::Right) => {
|
||||
self.focus = ListingFocus::Mailbox;
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
self.ratio = 90;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
|
@ -1161,6 +1170,11 @@ impl Component for Listing {
|
|||
self.set_dirty(true);
|
||||
self.focus = ListingFocus::Mailbox;
|
||||
self.ratio = 90;
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref k)
|
||||
|
@ -1171,6 +1185,11 @@ impl Component for Listing {
|
|||
self.focus = ListingFocus::Mailbox;
|
||||
self.ratio = 90;
|
||||
self.set_dirty(true);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
|
@ -1650,6 +1669,20 @@ impl Listing {
|
|||
),
|
||||
);
|
||||
if self.show_menu_scrollbar == ShowMenuScrollbar::True && total_height > rows {
|
||||
if self.focus == ListingFocus::Menu {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::Update {
|
||||
id: self.id,
|
||||
context: ScrollContext {
|
||||
shown_lines: skip_offset + rows,
|
||||
total_lines: total_height,
|
||||
has_more_lines: false,
|
||||
},
|
||||
},
|
||||
)));
|
||||
}
|
||||
ScrollBar::default().set_show_arrows(true).draw(
|
||||
grid,
|
||||
(
|
||||
|
@ -1664,6 +1697,12 @@ impl Listing {
|
|||
/* length */
|
||||
total_height,
|
||||
);
|
||||
} else if total_height < rows {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
}
|
||||
|
||||
context.dirty_areas.push_back(area);
|
||||
|
@ -1964,6 +2003,8 @@ impl Listing {
|
|||
if let Some((_, _, _, mailbox_hash)) =
|
||||
self.accounts[self.cursor_pos.0].entries.get(idx)
|
||||
{
|
||||
self.component
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
self.component
|
||||
.set_coordinates((account_hash, *mailbox_hash));
|
||||
/* Check if per-mailbox configuration overrides general configuration */
|
||||
|
|
|
@ -1627,6 +1627,8 @@ impl Component for CompactListing {
|
|||
) =>
|
||||
{
|
||||
self.unfocused = false;
|
||||
self.view
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
self.dirty = true;
|
||||
/* If self.row_updates is not empty and we exit a thread, the row_update events
|
||||
* will be performed but the list will not be drawn. So force a draw in any case.
|
||||
|
|
|
@ -1002,6 +1002,7 @@ impl ConversationsListing {
|
|||
.as_ref()
|
||||
.map(String::as_str)
|
||||
.or(Some("%Y-%m-%d %T")),
|
||||
false,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -1490,6 +1491,8 @@ impl Component for ConversationsListing {
|
|||
) =>
|
||||
{
|
||||
self.unfocused = false;
|
||||
self.view
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
self.dirty = true;
|
||||
/* If self.row_updates is not empty and we exit a thread, the row_update events
|
||||
* will be performed but the list will not be drawn. So force a draw in any case.
|
||||
|
|
|
@ -1010,7 +1010,7 @@ impl PlainListing {
|
|||
n if n < 4 * 24 * 60 * 60 => {
|
||||
format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9))
|
||||
}
|
||||
_ => melib::datetime::timestamp_to_string(envelope.datetime(), None),
|
||||
_ => melib::datetime::timestamp_to_string(envelope.datetime(), None, false),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1146,6 +1146,8 @@ impl Component for PlainListing {
|
|||
&& shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["exit_thread"]) =>
|
||||
{
|
||||
self.unfocused = false;
|
||||
self.view
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
self.dirty = true;
|
||||
/* If self.row_updates is not empty and we exit a thread, the row_update events
|
||||
* will be performed but the list will not be drawn. So force a draw in any case.
|
||||
|
|
|
@ -1224,6 +1224,9 @@ impl Component for ThreadListing {
|
|||
}
|
||||
UIEvent::Input(Key::Char('i')) if self.unfocused => {
|
||||
self.unfocused = false;
|
||||
if let Some(ref mut s) = self.view {
|
||||
s.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
}
|
||||
self.dirty = true;
|
||||
self.view = None;
|
||||
return true;
|
||||
|
|
|
@ -64,6 +64,7 @@ pub struct StatusBar {
|
|||
progress_spinner: ProgressSpinner,
|
||||
in_progress_jobs: HashSet<JobId>,
|
||||
done_jobs: HashSet<JobId>,
|
||||
scroll_contexts: IndexMap<ComponentId, ScrollContext>,
|
||||
|
||||
auto_complete: AutoComplete,
|
||||
cmd_history: Vec<String>,
|
||||
|
@ -104,6 +105,7 @@ impl StatusBar {
|
|||
progress_spinner,
|
||||
in_progress_jobs: HashSet::default(),
|
||||
done_jobs: HashSet::default(),
|
||||
scroll_contexts: IndexMap::default(),
|
||||
cmd_history: crate::command::history::old_cmd_history(),
|
||||
}
|
||||
}
|
||||
|
@ -140,6 +142,33 @@ impl StatusBar {
|
|||
grid[(x, y)].set_attrs(attribute.attrs | Attr::BOLD);
|
||||
}
|
||||
}
|
||||
if let Some((
|
||||
_,
|
||||
ScrollContext {
|
||||
shown_lines,
|
||||
total_lines,
|
||||
has_more_lines,
|
||||
},
|
||||
)) = self.scroll_contexts.last()
|
||||
{
|
||||
let s = format!(
|
||||
"| {shown_percentage}% {line_desc}{shown_lines}/{total_lines}{has_more_lines}",
|
||||
line_desc = if grid.ascii_drawing { "lines:" } else { "☰ " },
|
||||
shown_percentage = (*shown_lines as f32 / (*total_lines as f32) * 100.0) as usize,
|
||||
shown_lines = *shown_lines,
|
||||
total_lines = *total_lines,
|
||||
has_more_lines = if *has_more_lines { "(+)" } else { "" }
|
||||
);
|
||||
write_string_to_grid(
|
||||
&s,
|
||||
grid,
|
||||
attribute.fg,
|
||||
attribute.bg,
|
||||
attribute.attrs,
|
||||
((x + 1, y), bottom_right!(area)),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let (mut x, y) = bottom_right!(area);
|
||||
if self.progress_spinner.is_active() {
|
||||
|
@ -706,6 +735,21 @@ impl Component for StatusBar {
|
|||
self.progress_spinner.set_dirty(true);
|
||||
self.in_progress_jobs.insert(*job_id);
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::ScrollUpdate(ScrollUpdate::End(component_id))) => {
|
||||
if self.scroll_contexts.remove(component_id).is_some() {
|
||||
self.dirty = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::ScrollUpdate(ScrollUpdate::Update {
|
||||
id,
|
||||
context,
|
||||
})) => {
|
||||
if self.scroll_contexts.insert(*id, *context) != Some(*context) {
|
||||
self.dirty = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Timer(_) => {
|
||||
if self.progress_spinner.process_event(event, context) {
|
||||
return true;
|
||||
|
@ -975,6 +1019,21 @@ impl Component for Tabbed {
|
|||
),
|
||||
);
|
||||
if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::Update {
|
||||
id: self.id,
|
||||
context: ScrollContext {
|
||||
shown_lines: std::cmp::min(
|
||||
(height).saturating_sub(rows + 1),
|
||||
self.help_screen_cursor.1,
|
||||
) + rows,
|
||||
total_lines: height,
|
||||
has_more_lines: false,
|
||||
},
|
||||
},
|
||||
)));
|
||||
ScrollBar::default().set_show_arrows(true).draw(
|
||||
grid,
|
||||
(
|
||||
|
@ -989,6 +1048,12 @@ impl Component for Tabbed {
|
|||
/* length */
|
||||
height,
|
||||
);
|
||||
} else {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
}
|
||||
self.dirty = false;
|
||||
return;
|
||||
|
@ -1192,6 +1257,21 @@ impl Component for Tabbed {
|
|||
),
|
||||
);
|
||||
if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::Update {
|
||||
id: self.id,
|
||||
context: ScrollContext {
|
||||
shown_lines: std::cmp::min(
|
||||
(height).saturating_sub(rows),
|
||||
self.help_screen_cursor.1,
|
||||
) + rows,
|
||||
total_lines: height,
|
||||
has_more_lines: false,
|
||||
},
|
||||
},
|
||||
)));
|
||||
ScrollBar::default().set_show_arrows(true).draw(
|
||||
grid,
|
||||
(
|
||||
|
@ -1206,6 +1286,12 @@ impl Component for Tabbed {
|
|||
/* length */
|
||||
height,
|
||||
);
|
||||
} else {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
}
|
||||
}
|
||||
self.dirty = false;
|
||||
|
@ -1250,6 +1336,11 @@ impl Component for Tabbed {
|
|||
if self.show_shortcuts {
|
||||
/* children below the shortcut overlay must be redrawn */
|
||||
self.set_dirty(true);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
}
|
||||
self.show_shortcuts = !self.show_shortcuts;
|
||||
self.dirty = true;
|
||||
|
@ -1281,6 +1372,8 @@ impl Component for Tabbed {
|
|||
return true;
|
||||
}
|
||||
if let Some(c_idx) = self.children.iter().position(|x| x.id() == *id) {
|
||||
self.children[c_idx]
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
self.children.remove(c_idx);
|
||||
self.cursor_pos = 0;
|
||||
self.set_dirty(true);
|
||||
|
|
|
@ -533,28 +533,30 @@ impl Component for Pager {
|
|||
}
|
||||
if (rows < height) || self.search.is_some() {
|
||||
const RESULTS_STR: &str = "Results for ";
|
||||
let shown_percentage =
|
||||
((self.cursor.1 + rows) as f32 / (height as f32) * 100.0) as usize;
|
||||
let shown_lines = self.cursor.1 + rows;
|
||||
let total_lines = height;
|
||||
let scrolling = if rows < height {
|
||||
format!(
|
||||
"{shown_percentage}% {line_desc}{shown_lines}/{total_lines}{has_more_lines}",
|
||||
line_desc = if grid.ascii_drawing { "lines:" } else { "☰ " },
|
||||
shown_percentage = shown_percentage,
|
||||
shown_lines = shown_lines,
|
||||
total_lines = total_lines,
|
||||
has_more_lines = if self.line_breaker.is_finished() {
|
||||
""
|
||||
} else {
|
||||
"(+)"
|
||||
}
|
||||
)
|
||||
if rows < height {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::Update {
|
||||
id: self.id,
|
||||
context: ScrollContext {
|
||||
shown_lines,
|
||||
total_lines,
|
||||
has_more_lines: !self.line_breaker.is_finished(),
|
||||
},
|
||||
},
|
||||
)));
|
||||
} else {
|
||||
String::new()
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
};
|
||||
let search_results = if let Some(ref search) = self.search {
|
||||
format!(
|
||||
if let Some(ref search) = self.search {
|
||||
let status_message = format!(
|
||||
"{results_str}{search_pattern}: {current_pos}/{total_results}{has_more_lines}",
|
||||
results_str = RESULTS_STR,
|
||||
search_pattern = &search.pattern,
|
||||
|
@ -569,39 +571,29 @@ impl Component for Pager {
|
|||
} else {
|
||||
"(+)"
|
||||
}
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let status_message = format!(
|
||||
"{search_results}{divider}{scrolling}",
|
||||
search_results = search_results,
|
||||
divider = if self.search.is_some() { " " } else { "" },
|
||||
scrolling = scrolling,
|
||||
);
|
||||
let mut attribute = crate::conf::value(context, "status.bar");
|
||||
if !context.settings.terminal.use_color() {
|
||||
attribute.attrs |= Attr::REVERSE;
|
||||
}
|
||||
let (_, y) = write_string_to_grid(
|
||||
&status_message,
|
||||
grid,
|
||||
attribute.fg,
|
||||
attribute.bg,
|
||||
attribute.attrs,
|
||||
(
|
||||
set_y(upper_left!(area), get_y(bottom_right!(area))),
|
||||
bottom_right!(area),
|
||||
),
|
||||
None,
|
||||
);
|
||||
/* set search pattern to italics */
|
||||
if let Some(ref search) = self.search {
|
||||
);
|
||||
let mut attribute = crate::conf::value(context, "status.bar");
|
||||
if !context.settings.terminal.use_color() {
|
||||
attribute.attrs |= Attr::REVERSE;
|
||||
}
|
||||
let (_, y) = write_string_to_grid(
|
||||
&status_message,
|
||||
grid,
|
||||
attribute.fg,
|
||||
attribute.bg,
|
||||
attribute.attrs,
|
||||
(
|
||||
set_y(upper_left!(area), get_y(bottom_right!(area))),
|
||||
bottom_right!(area),
|
||||
),
|
||||
None,
|
||||
);
|
||||
/* set search pattern to italics */
|
||||
let start_x = get_x(upper_left!(area)) + RESULTS_STR.len();
|
||||
for c in grid.row_iter(start_x..(start_x + search.pattern.grapheme_width()), y) {
|
||||
grid[c].set_attrs(attribute.attrs | Attr::ITALICS);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
@ -746,6 +738,13 @@ impl Component for Pager {
|
|||
self.initialised = false;
|
||||
self.dirty = true;
|
||||
}
|
||||
UIEvent::VisibilityChange(false) => {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
|
|
|
@ -1011,6 +1011,14 @@ impl Account {
|
|||
Some(crate::types::NotificationType::Error(err.kind)),
|
||||
));
|
||||
}
|
||||
RefreshEventKind::MailboxCreate(_new_mailbox) => {}
|
||||
RefreshEventKind::MailboxDelete(_mailbox_hash) => {}
|
||||
RefreshEventKind::MailboxRename {
|
||||
old_mailbox_hash: _,
|
||||
new_mailbox: _,
|
||||
} => {}
|
||||
RefreshEventKind::MailboxSubscribe(_mailbox_hash) => {}
|
||||
RefreshEventKind::MailboxUnsubscribe(_mailbox_hash) => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
|
|
|
@ -58,6 +58,20 @@ pub struct ComposingSettings {
|
|||
/// Default: true
|
||||
#[serde(default = "true_val")]
|
||||
pub store_sent_mail: bool,
|
||||
/// The attribution line appears above the quoted reply text.
|
||||
/// The format specifiers for the replied address are:
|
||||
/// - `%+f` — the sender's name and email address.
|
||||
/// - `%+n` — the sender's name (or email address, if no name is included).
|
||||
/// - `%+a` — the sender's email address.
|
||||
/// The format string is passed to strftime(3) with the replied envelope's date.
|
||||
/// Default: "On %a, %0e %b %Y %H:%M, %+f wrote:%n"
|
||||
#[serde(default = "none")]
|
||||
pub attribution_format_string: Option<String>,
|
||||
/// Whether the strftime call for the attribution string uses the POSIX locale instead of
|
||||
/// the user's active locale
|
||||
/// Default: true
|
||||
#[serde(default = "true_val")]
|
||||
pub attribution_use_posix_locale: bool,
|
||||
}
|
||||
|
||||
impl Default for ComposingSettings {
|
||||
|
@ -70,6 +84,8 @@ impl Default for ComposingSettings {
|
|||
insert_user_agent: true,
|
||||
default_header_values: HashMap::default(),
|
||||
store_sent_mail: true,
|
||||
attribution_format_string: None,
|
||||
attribution_use_posix_locale: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -266,6 +266,20 @@ pub struct ComposingSettingsOverride {
|
|||
#[doc = " Default: true"]
|
||||
#[serde(default)]
|
||||
pub store_sent_mail: Option<bool>,
|
||||
#[doc = " The attribution line appears above the quoted reply text."]
|
||||
#[doc = " The format specifiers for the replied address are:"]
|
||||
#[doc = " - `%+f` — the sender's name and email address."]
|
||||
#[doc = " - `%+n` — the sender's name (or email address, if no name is included)."]
|
||||
#[doc = " - `%+a` — the sender's email address."]
|
||||
#[doc = " The format string is passed to strftime(3) with the replied envelope's date."]
|
||||
#[doc = " Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""]
|
||||
#[serde(default)]
|
||||
pub attribution_format_string: Option<Option<String>>,
|
||||
#[doc = " Whether the strftime call for the attribution string uses the POSIX locale instead of"]
|
||||
#[doc = " the user's active locale"]
|
||||
#[doc = " Default: true"]
|
||||
#[serde(default)]
|
||||
pub attribution_use_posix_locale: Option<bool>,
|
||||
}
|
||||
impl Default for ComposingSettingsOverride {
|
||||
fn default() -> Self {
|
||||
|
@ -277,6 +291,8 @@ impl Default for ComposingSettingsOverride {
|
|||
insert_user_agent: None,
|
||||
default_header_values: None,
|
||||
store_sent_mail: None,
|
||||
attribution_format_string: None,
|
||||
attribution_use_posix_locale: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -683,11 +683,9 @@ impl State {
|
|||
}
|
||||
}
|
||||
let ((x, mut y), box_displ_area_bottom_right) = box_displ_area;
|
||||
for line in msg_lines
|
||||
.into_iter()
|
||||
.chain(Some(String::new()))
|
||||
.chain(Some(melib::datetime::timestamp_to_string(*timestamp, None)))
|
||||
{
|
||||
for line in msg_lines.into_iter().chain(Some(String::new())).chain(Some(
|
||||
melib::datetime::timestamp_to_string(*timestamp, None, false),
|
||||
)) {
|
||||
write_string_to_grid(
|
||||
&line,
|
||||
&mut self.overlay_grid,
|
||||
|
|
|
@ -38,7 +38,7 @@ pub use self::helpers::*;
|
|||
use super::command::Action;
|
||||
use super::jobs::{JobExecutor, JobId};
|
||||
use super::terminal::*;
|
||||
use crate::components::{Component, ComponentId};
|
||||
use crate::components::{Component, ComponentId, ScrollUpdate};
|
||||
use std::sync::Arc;
|
||||
|
||||
use melib::backends::{AccountHash, BackendEvent, MailboxHash};
|
||||
|
@ -57,6 +57,7 @@ pub enum StatusEvent {
|
|||
JobFinished(JobId),
|
||||
JobCanceled(JobId),
|
||||
SetMouse(bool),
|
||||
ScrollUpdate(ScrollUpdate),
|
||||
}
|
||||
|
||||
/// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads
|
||||
|
@ -149,6 +150,7 @@ pub enum UIEvent {
|
|||
ConfigReload {
|
||||
old_settings: crate::conf::Settings,
|
||||
},
|
||||
VisibilityChange(bool),
|
||||
}
|
||||
|
||||
pub struct CallbackFn(pub Box<dyn FnOnce(&mut crate::Context) -> () + Send + 'static>);
|
||||
|
|
Loading…
Reference in New Issue