ui: add reply-context in compose

tracking-issue: #24
embed
Manos Pitsidianakis 2018-09-04 01:49:29 +03:00
parent f6caf993ae
commit b94687cdb0
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
16 changed files with 535 additions and 126 deletions

View File

@ -22,12 +22,14 @@ pub struct Draft {
impl Default for Draft { impl Default for Draft {
fn default() -> Self { fn default() -> Self {
let mut headers = FnvHashMap::with_capacity_and_hasher(8, Default::default()); let mut headers = FnvHashMap::with_capacity_and_hasher(8, Default::default());
headers.insert("From".into(), "x".into()); headers.insert("From".into(), "".into());
headers.insert("To".into(), "x".into()); headers.insert("To".into(), "".into());
headers.insert("Cc".into(), "".into());
headers.insert("Bcc".into(), "".into());
let now: DateTime<Local> = Local::now(); let now: DateTime<Local> = Local::now();
headers.insert("Date".into(), now.to_rfc2822()); headers.insert("Date".into(), now.to_rfc2822());
headers.insert("Subject".into(), "x".into()); headers.insert("Subject".into(), "".into());
headers.insert("Message-ID".into(), random::gen_message_id()); headers.insert("Message-ID".into(), random::gen_message_id());
headers.insert("User-Agent".into(), "meli".into()); headers.insert("User-Agent".into(), "meli".into());
Draft { Draft {
@ -58,7 +60,7 @@ impl Draft {
pub fn to_string(&self) -> Result<String> { pub fn to_string(&self) -> Result<String> {
let mut ret = String::new(); let mut ret = String::new();
let headers = &["Date", "From", "To", "Subject", "Message-ID"]; let headers = &["Date", "From", "To", "Cc", "Bcc", "Subject", "Message-ID"];
for k in headers { for k in headers {
ret.extend(format!("{}: {}\n", k, &self.headers[*k]).chars()); ret.extend(format!("{}: {}\n", k, &self.headers[*k]).chars());
} }
@ -131,6 +133,8 @@ fn ignore_header(header: &[u8]) -> bool {
b"Reply-to" => false, b"Reply-to" => false,
b"Cc" => false, b"Cc" => false,
b"Bcc" => false, b"Bcc" => false,
b"In-Reply-To" => false,
b"References" => false,
h if h.starts_with(b"X-") => false, h if h.starts_with(b"X-") => false,
_ => true, _ => true,
} }

View File

@ -41,6 +41,7 @@ use std::collections::hash_map::DefaultHasher;
use std::fmt; use std::fmt;
use std::hash::Hasher; use std::hash::Hasher;
use std::option::Option; use std::option::Option;
use std::str;
use std::string::String; use std::string::String;
use chrono; use chrono;
@ -181,6 +182,16 @@ fn test_strbuilder() {
); );
} }
impl fmt::Display for MessageID {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.val().is_ascii() {
write!(f, "{}", unsafe { str::from_utf8_unchecked(self.val()) })
} else {
write!(f, "{}", String::from_utf8_lossy(self.val()))
}
}
}
impl PartialEq for MessageID { impl PartialEq for MessageID {
fn eq(&self, other: &MessageID) -> bool { fn eq(&self, other: &MessageID) -> bool {
self.raw() == other.raw() self.raw() == other.raw()
@ -261,6 +272,8 @@ pub struct Envelope {
date: String, date: String,
from: Vec<Address>, from: Vec<Address>,
to: Vec<Address>, to: Vec<Address>,
cc: Vec<Address>,
bcc: Vec<Address>,
body: Option<Attachment>, body: Option<Attachment>,
subject: Option<Vec<u8>>, subject: Option<Vec<u8>>,
message_id: Option<MessageID>, message_id: Option<MessageID>,
@ -281,6 +294,8 @@ impl Envelope {
date: String::new(), date: String::new(),
from: Vec::new(), from: Vec::new(),
to: Vec::new(), to: Vec::new(),
cc: Vec::new(),
bcc: Vec::new(),
body: None, body: None,
subject: None, subject: None,
message_id: None, message_id: None,
@ -342,6 +357,22 @@ impl Envelope {
Vec::new() Vec::new()
}; };
self.set_to(value); self.set_to(value);
} else if name.eq_ignore_ascii_case(b"cc") {
let parse_result = parser::rfc2822address_list(value);
let value = if parse_result.is_done() {
parse_result.to_full_result().unwrap()
} else {
Vec::new()
};
self.set_cc(value);
} else if name.eq_ignore_ascii_case(b"bcc") {
let parse_result = parser::rfc2822address_list(value);
let value = if parse_result.is_done() {
parse_result.to_full_result().unwrap()
} else {
Vec::new()
};
self.set_bcc(value);
} else if name.eq_ignore_ascii_case(b"from") { } else if name.eq_ignore_ascii_case(b"from") {
let parse_result = parser::rfc2822address_list(value); let parse_result = parser::rfc2822address_list(value);
let value = if parse_result.is_done() { let value = if parse_result.is_done() {
@ -424,7 +455,14 @@ impl Envelope {
pub fn from(&self) -> &Vec<Address> { pub fn from(&self) -> &Vec<Address> {
&self.from &self.from
} }
pub fn field_bcc_to_string(&self) -> String {
let _strings: Vec<String> = self.bcc.iter().map(|a| format!("{}", a)).collect();
_strings.join(", ")
}
pub fn field_cc_to_string(&self) -> String {
let _strings: Vec<String> = self.cc.iter().map(|a| format!("{}", a)).collect();
_strings.join(", ")
}
pub fn field_from_to_string(&self) -> String { pub fn field_from_to_string(&self) -> String {
let _strings: Vec<String> = self.from.iter().map(|a| format!("{}", a)).collect(); let _strings: Vec<String> = self.from.iter().map(|a| format!("{}", a)).collect();
_strings.join(", ") _strings.join(", ")
@ -530,6 +568,12 @@ impl Envelope {
fn set_date(&mut self, new_val: &[u8]) -> () { fn set_date(&mut self, new_val: &[u8]) -> () {
self.date = String::from_utf8_lossy(new_val).into_owned(); self.date = String::from_utf8_lossy(new_val).into_owned();
} }
fn set_bcc(&mut self, new_val: Vec<Address>) -> () {
self.from = new_val;
}
fn set_cc(&mut self, new_val: Vec<Address>) -> () {
self.from = new_val;
}
fn set_from(&mut self, new_val: Vec<Address>) -> () { fn set_from(&mut self, new_val: Vec<Address>) -> () {
self.from = new_val; self.from = new_val;
} }

View File

@ -103,6 +103,21 @@ pub struct Container {
show_subject: bool, show_subject: bool,
} }
impl Default for Container {
fn default() -> Container {
Container {
id: 0,
message: None,
parent: None,
first_child: None,
next_sibling: None,
date: UnixTimestamp::default(),
indentation: 0,
show_subject: true,
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct ContainerTree { struct ContainerTree {
id: usize, id: usize,
@ -513,12 +528,8 @@ fn build_collection(
threads.push(Container { threads.push(Container {
message: Some(i), message: Some(i),
id: x_index, id: x_index,
parent: None,
first_child: None,
next_sibling: None,
date: x.date(), date: x.date(),
indentation: 0, ..Default::default()
show_subject: true,
}); });
x.set_thread(x_index); x.set_thread(x_index);
id_table.insert(m_id, x_index); id_table.insert(m_id, x_index);
@ -565,12 +576,9 @@ fn build_collection(
threads.push(Container { threads.push(Container {
message: None, message: None,
id: idx, id: idx,
parent: None,
first_child: Some(curr_ref), first_child: Some(curr_ref),
next_sibling: None,
date: x.date(), date: x.date(),
indentation: 0, ..Default::default()
show_subject: true,
}); });
if threads[curr_ref].parent.is_none() { if threads[curr_ref].parent.is_none() {
threads[curr_ref].parent = Some(idx); threads[curr_ref].parent = Some(idx);
@ -661,11 +669,8 @@ pub fn build_threads(
message: Some(idx), message: Some(idx),
id: tidx, id: tidx,
parent: Some(p), parent: Some(p),
first_child: None,
next_sibling: None,
date: x.date(), date: x.date(),
indentation: 0, ..Default::default()
show_subject: true,
}); });
id_table.insert(Cow::from(m_id.into_owned()), tidx); id_table.insert(Cow::from(m_id.into_owned()), tidx);
x.set_thread(tidx); x.set_thread(tidx);

View File

@ -61,25 +61,18 @@ fn main() {
let receiver = state.receiver(); let receiver = state.receiver();
/* Register some reasonably useful interfaces */ /* Register some reasonably useful interfaces */
let menu = Entity { let menu = Entity::from(Box::new(AccountMenu::new(&state.context.accounts)));
component: Box::new(AccountMenu::new(&state.context.accounts)),
};
let listing = CompactListing::new(); let listing = CompactListing::new();
let b = Entity { let b = Entity::from(Box::new(listing));
component: Box::new(listing),
};
let mut tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true))])); let mut tabs = Box::new(Tabbed::new(vec![Box::new(VSplit::new(menu, b, 90, true))]));
tabs.add_component(Box::new(Composer::default())); tabs.add_component(Box::new(Composer::default()));
let window = Entity { component: tabs }; let window = Entity::from(tabs);
let status_bar = Entity { let status_bar = Entity::from(Box::new(StatusBar::new(window)));
component: Box::new(StatusBar::new(window)),
};
state.register_entity(status_bar); state.register_entity(status_bar);
let xdg_notifications = Entity { let xdg_notifications =
component: Box::new(ui::components::notifications::XDGNotifications {}), Entity::from(Box::new(ui::components::notifications::XDGNotifications {}));
};
state.register_entity(xdg_notifications); state.register_entity(xdg_notifications);
/* Keep track of the input mode. See ui::UIMode for details */ /* Keep track of the input mode. See ui::UIMode for details */
@ -203,8 +196,7 @@ fn main() {
match state.try_wait_on_child() { match state.try_wait_on_child() {
Some(true) => { Some(true) => {
state.restore_input(); state.restore_input();
state.mode = UIMode::Normal; state.switch_to_alternate_screen();
state.render();
} }
Some(false) => { Some(false) => {
use std::{thread, time}; use std::{thread, time};
@ -214,6 +206,8 @@ fn main() {
continue 'reap; continue 'reap;
} }
None => { None => {
state.mode = UIMode::Normal;
state.render();
break 'reap; break 'reap;
} }
} }

View File

@ -25,49 +25,148 @@ use melib::Draft;
#[derive(Debug)] #[derive(Debug)]
pub struct Composer { pub struct Composer {
mode: ViewMode, reply_context: Option<((usize, usize), Box<ThreadView>)>, // (folder_index, container_index)
pager: Pager,
draft: Draft,
account_cursor: usize, account_cursor: usize,
pager: Pager,
draft: Draft,
mode: ViewMode,
dirty: bool, dirty: bool,
initialized: bool,
} }
impl Default for Composer { impl Default for Composer {
fn default() -> Self { fn default() -> Self {
Composer { Composer {
dirty: true, reply_context: None,
mode: ViewMode::Overview, account_cursor: 0,
pager: Pager::default(), pager: Pager::default(),
draft: Draft::default(), draft: Draft::default(),
account_cursor: 0,
mode: ViewMode::Overview,
dirty: true,
initialized: false,
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
enum ViewMode { enum ViewMode {
//Compose, Discard(Uuid),
Pager,
Overview, Overview,
} }
impl ViewMode {
fn is_discard(&self) -> bool {
if let ViewMode::Discard(_) = self {
true
} else {
false
}
}
fn is_overview(&self) -> bool {
if let ViewMode::Overview = self {
true
} else {
false
}
}
fn is_pager(&self) -> bool {
if let ViewMode::Pager = self {
true
} else {
false
}
}
}
impl fmt::Display for Composer { impl fmt::Display for Composer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info // TODO display subject/info
write!(f, "compose") if self.reply_context.is_some() {
write!(f, "reply: {:8}", self.draft.headers()["Subject"])
} else {
write!(f, "compose")
}
} }
} }
impl Composer { impl Composer {
fn draw_header_table(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { /*
* coordinates: (account index, mailbox index, root set container index)
* msg: index of message we reply to in containers
* context: current context
*/
pub fn with_context(coordinates: (usize, usize, usize), msg: usize, context: &Context) -> Self {
let mailbox = &context.accounts[coordinates.0][coordinates.1]
.as_ref()
.unwrap();
let threads = &mailbox.threads;
let containers = &threads.containers();
let mut ret = Composer::default();
let p = containers[msg];
ret.draft.headers_mut().insert(
"Subject".into(),
if p.show_subject() {
format!(
"Re: {}",
mailbox.collection[p.message().unwrap()].subject().clone()
)
} else {
mailbox.collection[p.message().unwrap()].subject().into()
},
);
let parent_message = &mailbox.collection[p.message().unwrap()];
ret.draft.headers_mut().insert(
"References".into(),
format!(
"{} {}",
parent_message
.references()
.iter()
.fold(String::new(), |mut acc, x| {
if !acc.is_empty() {
acc.push(' ');
}
acc.push_str(&x.to_string());
acc
}),
parent_message.message_id()
),
);
ret.draft
.headers_mut()
.insert("In-Reply-To".into(), parent_message.message_id().into());
ret.draft
.headers_mut()
.insert("To".into(), parent_message.field_from_to_string());
ret.draft
.headers_mut()
.insert("Cc".into(), parent_message.field_cc_to_string());
ret.account_cursor = coordinates.0;
ret.reply_context = Some((
(coordinates.1, coordinates.2),
Box::new(ThreadView::new(coordinates, Some(msg), context)),
));
ret
}
fn draw_header_table(&mut self, grid: &mut CellBuffer, area: Area) {
let upper_left = upper_left!(area); let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area); let bottom_right = bottom_right!(area);
let headers = self.draft.headers(); let headers = self.draft.headers();
{ {
let (mut x, mut y) = upper_left; let (mut x, mut y) = upper_left;
for k in &["Date", "From", "To", "Subject"] { for k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] {
let update = { let update = {
let (x, y) = write_string_to_grid( let (x, y) = write_string_to_grid(
k, k,
@ -127,22 +226,37 @@ impl Composer {
impl Component for Composer { impl Component for Composer {
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 { if !self.initialized {
self.draft.headers_mut().insert(
"From".into(),
get_display_name(context, self.account_cursor),
);
clear_area(grid, area); clear_area(grid, area);
self.initialized = true;
} }
let upper_left = upper_left!(area); let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area); let bottom_right = bottom_right!(area);
let upper_left = set_y(upper_left, get_y(upper_left) + 1); let upper_left = set_y(upper_left, get_y(upper_left) + 1);
let header_height = 5; let header_height = 5;
let width = width!(area); let width = if width!(area) > 80 && self.reply_context.is_some() {
width!(area) / 2
} else {
width!(area)
};
let mid = if width > 80 { let mid = if width > 80 {
let width = width - 80; let width = width - 80;
let mid = width / 2; let mid = if self.reply_context.is_some() {
width!(area) / 2 + width / 2
} else {
width / 2
};
if self.reply_context.is_some() {
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);
}
}
if self.dirty { if self.dirty {
for i in get_y(upper_left)..=get_y(bottom_right) { for i in get_y(upper_left)..=get_y(bottom_right) {
@ -159,6 +273,12 @@ impl Component for Composer {
0 0
}; };
if width > 80 && self.reply_context.is_some() {
let area = (upper_left, set_x(bottom_right, mid - 1));
let view = &mut self.reply_context.as_mut().unwrap().1;
view.draw(grid, area, context);
}
if self.dirty { if self.dirty {
for i in get_x(upper_left) + mid + 1..=get_x(upper_left) + mid + 79 { for i in get_x(upper_left) + mid + 1..=get_x(upper_left) + mid + 79 {
//set_and_join_box(grid, (i, header_height), HORZ_BOUNDARY); //set_and_join_box(grid, (i, header_height), HORZ_BOUNDARY);
@ -174,26 +294,62 @@ impl Component for Composer {
); );
if self.dirty { if self.dirty {
context.dirty_areas.push_back(area); self.draft.headers_mut().insert(
"From".into(),
get_display_name(context, self.account_cursor),
);
self.dirty = false; self.dirty = false;
} }
match self.mode {
ViewMode::Overview => { /* Regardless of view mode, do the following */
self.draw_header_table(grid, header_area, context); clear_area(grid, header_area);
self.pager.draw(grid, body_area, context); clear_area(grid, body_area);
self.draw_header_table(grid, header_area);
self.pager.draw(grid, body_area, context);
if let ViewMode::Discard(_) = self.mode {
let mid_x = width!(area) / 2;
let mid_y = height!(area) / 2;
for i in mid_x - 40..=mid_x + 40 {
set_and_join_box(grid, (i, mid_y - 11), HORZ_BOUNDARY);
set_and_join_box(grid, (i, mid_y + 11), HORZ_BOUNDARY);
}
for i in mid_y - 11..=mid_y + 11 {
set_and_join_box(grid, (mid_x - 40, i), VERT_BOUNDARY);
set_and_join_box(grid, (mid_x + 40, i), VERT_BOUNDARY);
} }
} }
context.dirty_areas.push_back(area);
} }
fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool {
if self.pager.process_event(event, context) { match (&mut self.mode, &mut self.reply_context) {
return true; (ViewMode::Pager, _) => {
/* Cannot mutably borrow in pattern guard, pah! */
if self.pager.process_event(event, context) {
return true;
}
}
(ViewMode::Overview, Some((_, ref mut view))) => {
if view.process_event(event, context) {
self.dirty = true;
return true;
}
}
_ => {}
} }
match event.event_type { match event.event_type {
UIEventType::Resize => { UIEventType::Resize => {
self.dirty = true; self.dirty = true;
self.initialized = false;
} }
/* Switch e-mail From: field to the `left` configured account. */
UIEventType::Input(Key::Left) => { UIEventType::Input(Key::Left) => {
self.account_cursor = self.account_cursor.saturating_sub(1); self.account_cursor = self.account_cursor.saturating_sub(1);
self.draft.headers_mut().insert( self.draft.headers_mut().insert(
@ -203,6 +359,7 @@ impl Component for Composer {
self.dirty = true; self.dirty = true;
return true; return true;
} }
/* Switch e-mail From: field to the `right` configured account. */
UIEventType::Input(Key::Right) => { UIEventType::Input(Key::Right) => {
if self.account_cursor + 1 < context.accounts.len() { if self.account_cursor + 1 < context.accounts.len() {
self.account_cursor += 1; self.account_cursor += 1;
@ -214,7 +371,38 @@ impl Component for Composer {
} }
return true; return true;
} }
UIEventType::Input(Key::Char('\n')) => { UIEventType::Input(Key::Char(k)) if self.mode.is_discard() => {
match (k, &self.mode) {
('y', ViewMode::Discard(u)) => {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::Action(Tab(Kill(u.clone()))),
});
return true;
}
('n', _) => {}
_ => {
return false;
}
}
self.mode = ViewMode::Overview;
self.set_dirty();
return true;
}
/* Switch to Overview mode if we're on Pager mode */
UIEventType::Input(Key::Char('o')) if self.mode.is_pager() => {
self.mode = ViewMode::Overview;
self.set_dirty();
return true;
}
/* Switch to Pager mode if we're on Overview mode */
UIEventType::Input(Key::Char('p')) if self.mode.is_overview() => {
self.mode = ViewMode::Pager;
self.set_dirty();
return true;
}
/* Edit draft in $EDITOR */
UIEventType::Input(Key::Char('e')) => {
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
/* Kill input thread so that spawned command can be sole receiver of stdin */ /* Kill input thread so that spawned command can be sole receiver of stdin */
{ {
@ -235,10 +423,15 @@ impl Component for Composer {
let result = f.read_to_string(); let result = f.read_to_string();
self.draft = Draft::from_str(result.as_str()).unwrap(); self.draft = Draft::from_str(result.as_str()).unwrap();
self.pager.update_from_str(self.draft.body()); self.pager.update_from_str(self.draft.body());
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::Fork(ForkType::Finished),
});
context.restore_input(); context.restore_input();
self.dirty = true; self.dirty = true;
return true; return true;
} }
// TODO: Replace EditDraft with compose tabs
UIEventType::Input(Key::Char('m')) => { UIEventType::Input(Key::Char('m')) => {
let mut f = let mut f =
create_temp_file(self.draft.to_string().unwrap().as_str().as_bytes(), None); create_temp_file(self.draft.to_string().unwrap().as_str().as_bytes(), None);
@ -256,11 +449,24 @@ impl Component for Composer {
fn is_dirty(&self) -> bool { fn is_dirty(&self) -> bool {
self.dirty || self.pager.is_dirty() self.dirty || self.pager.is_dirty()
|| self
.reply_context
.as_ref()
.map(|(_, p)| p.is_dirty())
.unwrap_or(false)
} }
fn set_dirty(&mut self) { fn set_dirty(&mut self) {
self.dirty = true; self.dirty = true;
self.initialized = false;
self.pager.set_dirty(); self.pager.set_dirty();
if let Some((_, ref mut view)) = self.reply_context {
view.set_dirty();
}
}
fn kill(&mut self, uuid: Uuid) {
self.mode = ViewMode::Discard(uuid);
} }
} }

View File

@ -303,6 +303,7 @@ impl Component for CompactListing {
if self.length == 0 && self.dirty { if self.length == 0 && self.dirty {
clear_area(grid, area); clear_area(grid, area);
context.dirty_areas.push_back(area); context.dirty_areas.push_back(area);
return;
} }
/* Render the mail body in a pager */ /* Render the mail body in a pager */
@ -312,7 +313,7 @@ impl Component for CompactListing {
} }
return; return;
} }
self.view = Some(ThreadView::new(self.cursor_pos, context)); self.view = Some(ThreadView::new(self.cursor_pos, None, context));
self.view.as_mut().unwrap().draw(grid, area, context); self.view.as_mut().unwrap().draw(grid, area, context);
self.dirty = false; self.dirty = false;
} }
@ -418,15 +419,6 @@ impl Component for CompactListing {
self.dirty = true; self.dirty = true;
} }
UIEventType::Action(ref action) => match action { UIEventType::Action(ref action) => match action {
Action::PlainListing(PlainListingAction::ToggleThreaded) => {
context.accounts[self.cursor_pos.0]
.runtime_settings
.conf_mut()
.toggle_threaded();
self.refresh_mailbox(context);
self.dirty = true;
return true;
}
Action::ViewMailbox(idx) => { Action::ViewMailbox(idx) => {
self.new_cursor_pos.1 = *idx; self.new_cursor_pos.1 = *idx;
self.dirty = true; self.dirty = true;
@ -446,7 +438,8 @@ impl Component for CompactListing {
self.dirty = true; self.dirty = true;
self.refresh_mailbox(context); self.refresh_mailbox(context);
return true; return true;
} // _ => {} }
_ => {}
}, },
_ => {} _ => {}
} }

View File

@ -760,7 +760,7 @@ impl Component for PlainListing {
self.dirty = true; self.dirty = true;
} }
UIEventType::Action(ref action) => match action { UIEventType::Action(ref action) => match action {
Action::PlainListing(PlainListingAction::ToggleThreaded) => { Action::Listing(ListingAction::ToggleThreaded) => {
context.accounts[self.cursor_pos.0] context.accounts[self.cursor_pos.0]
.runtime_settings .runtime_settings
.conf_mut() .conf_mut()
@ -788,7 +788,8 @@ impl Component for PlainListing {
self.dirty = true; self.dirty = true;
self.refresh_mailbox(context); self.refresh_mailbox(context);
return true; return true;
} // _ => {} }
_ => {}
}, },
_ => {} _ => {}
} }

View File

@ -70,6 +70,7 @@ impl Component for HtmlView {
if self.pager.process_event(event, context) { if self.pager.process_event(event, context) {
return true; return true;
} }
if let UIEventType::Input(Key::Char('v')) = event.event_type { if let UIEventType::Input(Key::Char('v')) = event.event_type {
// TODO: Optional filter that removes outgoing resource requests (images and // TODO: Optional filter that removes outgoing resource requests (images and
// scripts) // scripts)
@ -98,5 +99,7 @@ impl Component for HtmlView {
fn is_dirty(&self) -> bool { fn is_dirty(&self) -> bool {
self.pager.is_dirty() self.pager.is_dirty()
} }
fn set_dirty(&mut self) {} fn set_dirty(&mut self) {
self.pager.set_dirty();
}
} }

View File

@ -309,15 +309,19 @@ impl Component for MailView {
let body = envelope.body(op); let body = envelope.body(op);
match self.mode { match self.mode {
ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => { ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
self.pager = None;
self.subview = Some(Box::new(HtmlView::new(decode( self.subview = Some(Box::new(HtmlView::new(decode(
&body.attachments()[aidx], &body.attachments()[aidx],
None, None,
)))); ))));
self.mode = ViewMode::Subview;
} }
ViewMode::Normal if body.is_html() => { ViewMode::Normal if body.is_html() => {
self.subview = Some(Box::new(HtmlView::new(decode(&body, None)))); self.subview = Some(Box::new(HtmlView::new(decode(&body, None))));
self.pager = None;
self.mode = ViewMode::Subview; self.mode = ViewMode::Subview;
} }
ViewMode::Subview => {}
_ => { _ => {
let buf = { let buf = {
let text = self.attachment_to_text(&body); let text = self.attachment_to_text(&body);
@ -330,27 +334,43 @@ impl Component for MailView {
self.pager.as_mut().map(|p| p.cursor_pos()) self.pager.as_mut().map(|p| p.cursor_pos())
}; };
self.pager = Some(Pager::from_buf(buf.split_newlines(), cursor_pos)); self.pager = Some(Pager::from_buf(buf.split_newlines(), cursor_pos));
self.subview = None;
} }
}; };
self.dirty = false; self.dirty = false;
} }
if let Some(s) = self.subview.as_mut() { match self.mode {
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); ViewMode::Subview => {
} else if let Some(p) = self.pager.as_mut() { if let Some(s) = self.subview.as_mut() {
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context); s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
}
_ => {
if let Some(p) = self.pager.as_mut() {
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
}
} }
} }
fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool {
if let Some(ref mut sub) = self.subview { match self.mode {
if sub.process_event(event, context) { ViewMode::Subview => {
return true; if let Some(s) = self.subview.as_mut() {
if s.process_event(event, context) {
return true;
}
}
} }
} else if let Some(ref mut p) = self.pager { _ => {
if p.process_event(event, context) { if let Some(p) = self.pager.as_mut() {
return true; if p.process_event(event, context) {
return true;
}
}
} }
} }
match event.event_type { match event.event_type {
UIEventType::Input(Key::Esc) | UIEventType::Input(Key::Alt('')) => { UIEventType::Input(Key::Esc) | UIEventType::Input(Key::Alt('')) => {
self.cmd_buf.clear(); self.cmd_buf.clear();
@ -542,5 +562,18 @@ impl Component for MailView {
} }
fn set_dirty(&mut self) { fn set_dirty(&mut self) {
self.dirty = true; self.dirty = true;
match self.mode {
ViewMode::Normal => {
if let Some(p) = self.pager.as_mut() {
p.set_dirty();
}
}
ViewMode::Subview => {
if let Some(s) = self.subview.as_mut() {
s.set_dirty();
}
}
_ => {}
}
} }
} }

View File

@ -46,7 +46,17 @@ pub struct ThreadView {
} }
impl ThreadView { impl ThreadView {
pub fn new(coordinates: (usize, usize, usize), context: &Context) -> Self { /*
* coordinates: (account index, mailbox index, root set container index)
* expanded_idx: optional position of expanded entry when we render the threadview. Default
* expanded message is the last one.
* context: current context
*/
pub fn new(
coordinates: (usize, usize, usize),
expanded_idx: Option<usize>,
context: &Context,
) -> Self {
/* stack to push thread messages in order in order to pop and print them later */ /* stack to push thread messages in order in order to pop and print them later */
let mut stack: Vec<(usize, usize)> = Vec::with_capacity(32); let mut stack: Vec<(usize, usize)> = Vec::with_capacity(32);
let mailbox = &context.accounts[coordinates.0][coordinates.1] let mailbox = &context.accounts[coordinates.0][coordinates.1]
@ -78,6 +88,13 @@ impl ThreadView {
let entry = view.make_entry(context, (ind, idx, line)); let entry = view.make_entry(context, (ind, idx, line));
view.entries.push(entry); view.entries.push(entry);
line += 1; line += 1;
match expanded_idx {
Some(expanded_idx) if expanded_idx == idx => {
view.new_expanded_pos = view.entries.len().saturating_sub(1);
view.expanded_pos = view.new_expanded_pos + 1;
}
_ => {}
}
let container = &threads.containers()[idx]; let container = &threads.containers()[idx];
if let Some(i) = container.next_sibling() { if let Some(i) = container.next_sibling() {
stack.push((ind, i)); stack.push((ind, i));
@ -87,8 +104,10 @@ impl ThreadView {
stack.push((ind + 1, i)); stack.push((ind + 1, i));
} }
} }
view.new_expanded_pos = view.entries.len().saturating_sub(1); if expanded_idx.is_none() {
view.expanded_pos = view.new_expanded_pos + 1; view.new_expanded_pos = view.entries.len().saturating_sub(1);
view.expanded_pos = view.new_expanded_pos + 1;
}
let height = 2 * view.entries.len() + 1; let height = 2 * view.entries.len() + 1;
let mut width = 0; let mut width = 0;
@ -545,6 +564,16 @@ impl Component for ThreadView {
return true; return true;
} }
match event.event_type { match event.event_type {
UIEventType::Input(Key::Char('R')) => {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::Reply(
self.coordinates,
self.entries[self.expanded_pos].index.1,
),
});
return true;
}
UIEventType::Input(Key::Up) => { UIEventType::Input(Key::Up) => {
if self.cursor_pos > 0 { if self.cursor_pos > 0 {
self.new_cursor_pos = self.new_cursor_pos.saturating_sub(1); self.new_cursor_pos = self.new_cursor_pos.saturating_sub(1);
@ -574,7 +603,7 @@ impl Component for ThreadView {
return true; return true;
} }
UIEventType::Resize => { UIEventType::Resize => {
self.dirty = true; self.set_dirty();
} }
_ => {} _ => {}
} }
@ -584,6 +613,7 @@ impl Component for ThreadView {
self.dirty || self.mailview.is_dirty() self.dirty || self.mailview.is_dirty()
} }
fn set_dirty(&mut self) { fn set_dirty(&mut self) {
self.initiated = false;
self.dirty = true; self.dirty = true;
self.mailview.set_dirty(); self.mailview.set_dirty();
} }

View File

@ -37,7 +37,9 @@ pub use self::utilities::*;
use std::fmt; use std::fmt;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use std::ops::Deref; use std::ops::{Deref, DerefMut};
use uuid::Uuid;
use super::{Key, StatusEvent, UIEvent, UIEventType}; use super::{Key, StatusEvent, UIEvent, UIEventType};
/// The upper and lower boundary char. /// The upper and lower boundary char.
@ -62,20 +64,46 @@ const LIGHT_DOWN_AND_HORIZONTAL: char = '┬';
const LIGHT_UP_AND_HORIZONTAL: char = '┴'; const LIGHT_UP_AND_HORIZONTAL: char = '┴';
/// `Entity` is a container for Components. Totally useless now so if it is not useful in the /// `Entity` is a container for Components.
/// future (ie hold some information, id or state) it should be removed.
#[derive(Debug)] #[derive(Debug)]
pub struct Entity { pub struct Entity {
//context: VecDeque, id: Uuid,
pub component: Box<Component>, // more than one? pub component: Box<Component>, // more than one?
} }
impl From<Box<Component>> for Entity {
fn from(kind: Box<Component>) -> Entity {
Entity {
id: Uuid::new_v4(),
component: kind,
}
}
}
impl<C: 'static> From<Box<C>> for Entity
where
C: Component,
{
fn from(kind: Box<C>) -> Entity {
Entity {
id: Uuid::new_v4(),
component: kind,
}
}
}
impl Display for Entity { impl Display for Entity {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(&self.component, f) Display::fmt(&self.component, f)
} }
} }
impl DerefMut for Entity {
fn deref_mut(&mut self) -> &mut Box<Component> {
&mut self.component
}
}
impl Deref for Entity { impl Deref for Entity {
type Target = Box<Component>; type Target = Box<Component>;
@ -85,6 +113,9 @@ impl Deref for Entity {
} }
impl Entity { impl Entity {
pub fn uuid(&self) -> &Uuid {
&self.id
}
/// Pass events to child component. /// Pass events to child component.
pub fn rcv_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { pub fn rcv_event(&mut self, event: &UIEvent, context: &mut Context) -> bool {
self.component.process_event(&event, context) self.component.process_event(&event, context)
@ -101,6 +132,7 @@ pub trait Component: Display + Debug {
true true
} }
fn set_dirty(&mut self); fn set_dirty(&mut self);
fn kill(&mut self, _uuid: Uuid) {}
} }
fn new_draft(_context: &mut Context) -> Vec<u8> { fn new_draft(_context: &mut Context) -> Vec<u8> {
@ -113,6 +145,7 @@ fn new_draft(_context: &mut Context) -> Vec<u8> {
v.into_bytes() v.into_bytes()
} }
/*
pub(crate) fn is_box_char(ch: char) -> bool { pub(crate) fn is_box_char(ch: char) -> bool {
match ch { match ch {
HORZ_BOUNDARY | VERT_BOUNDARY => true, HORZ_BOUNDARY | VERT_BOUNDARY => true,
@ -120,7 +153,6 @@ pub(crate) fn is_box_char(ch: char) -> bool {
} }
} }
/*
* pub(crate) fn is_box_char(ch: char) -> bool { * pub(crate) fn is_box_char(ch: char) -> bool {
* match ch { * match ch {
* '└' | '─' | '┘' | '┴' | '┌' | '│' | '├' | '┐' | '┬' | '┤' | '┼' | '╷' | '╵' | '╴' | '╶' => true, * '└' | '─' | '┘' | '┴' | '┌' | '│' | '├' | '┐' | '┬' | '┤' | '┼' | '╷' | '╵' | '╴' | '╶' => true,

View File

@ -712,14 +712,17 @@ impl Component for Progress {
#[derive(Debug)] #[derive(Debug)]
pub struct Tabbed { pub struct Tabbed {
children: Vec<Box<Component>>, children: Vec<Entity>,
cursor_pos: usize, cursor_pos: usize,
} }
impl Tabbed { impl Tabbed {
pub fn new(children: Vec<Box<Component>>) -> Self { pub fn new(children: Vec<Box<Component>>) -> Self {
Tabbed { Tabbed {
children, children: children
.into_iter()
.map(|x: Box<Component>| Entity::from(x))
.collect(),
cursor_pos: 0, cursor_pos: 0,
} }
} }
@ -748,13 +751,15 @@ impl Tabbed {
} }
let (cols, _) = grid.size(); let (cols, _) = grid.size();
let cslice: &mut [Cell] = grid; let cslice: &mut [Cell] = grid;
for c in cslice[(y * cols) + x..(y * cols) + cols].iter_mut() { for c in cslice[(y * cols) + x - 1..(y * cols) + cols].iter_mut() {
c.set_bg(Color::Byte(7)); c.set_bg(Color::Byte(7));
c.set_ch(' ');
} }
context.dirty_areas.push_back(area); context.dirty_areas.push_back(area);
} }
pub fn add_component(&mut self, new: Box<Component>) { pub fn add_component(&mut self, new: Box<Component>) {
self.children.push(new); self.children.push(Entity::from(new));
} }
} }
@ -787,15 +792,44 @@ impl Component for Tabbed {
} }
} }
fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool { fn process_event(&mut self, event: &UIEvent, context: &mut Context) -> bool {
if let UIEventType::Input(Key::Char('T')) = event.event_type { match event.event_type {
self.cursor_pos = (self.cursor_pos + 1) % self.children.len(); UIEventType::Input(Key::Char('T')) => {
self.children[self.cursor_pos].set_dirty(); self.cursor_pos = (self.cursor_pos + 1) % self.children.len();
return true; self.set_dirty();
return true;
}
UIEventType::Reply(coordinates, msg) => {
self.add_component(Box::new(Composer::with_context(coordinates, msg, context)));
self.cursor_pos = self.children.len() - 1;
self.children[self.cursor_pos].set_dirty();
return true;
}
UIEventType::Action(Tab(Close)) => {
let uuid = self.children[self.cursor_pos].uuid().clone();
self.children[self.cursor_pos].kill(uuid);
return true;
}
UIEventType::Action(Tab(Kill(ref uuid))) => {
if let Some(c_idx) = self.children.iter().position(|x| x.uuid() == uuid) {
self.children.remove(c_idx);
self.cursor_pos = self.cursor_pos.saturating_sub(1);
self.set_dirty();
return true;
} else {
eprintln!(
"DEBUG: Child entity with uuid {:?} not found.\nList: {:?}",
uuid, self.children
);
}
}
_ => {}
} }
self.children[self.cursor_pos].process_event(event, context) self.children[self.cursor_pos].process_event(event, context)
} }
fn is_dirty(&self) -> bool { fn is_dirty(&self) -> bool {
self.children[self.cursor_pos].is_dirty() self.children[self.cursor_pos].is_dirty()
} }
fn set_dirty(&mut self) {} fn set_dirty(&mut self) {
self.children[self.cursor_pos].set_dirty();
}
} }

View File

@ -25,15 +25,25 @@
pub use melib::mailbox::{SortField, SortOrder}; pub use melib::mailbox::{SortField, SortOrder};
extern crate uuid;
use uuid::Uuid;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum PlainListingAction { pub enum ListingAction {
ToggleThreaded, ToggleThreaded,
} }
#[derive(Debug, Clone)]
pub enum TabAction {
Close,
Kill(Uuid),
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Action { pub enum Action {
PlainListing(PlainListingAction), Listing(ListingAction),
ViewMailbox(usize), ViewMailbox(usize),
Sort(SortField, SortOrder), Sort(SortField, SortOrder),
SubSort(SortField, SortOrder), SubSort(SortField, SortOrder),
Tab(TabAction),
} }

View File

@ -21,10 +21,13 @@
/*! A parser module for user commands passed through the Ex mode. /*! A parser module for user commands passed through the Ex mode.
*/ */
pub use melib::mailbox::{SortField, SortOrder};
use nom::{digit, not_line_ending}; use nom::{digit, not_line_ending};
use std; use std;
pub mod actions; pub mod actions;
pub use actions::*; pub use actions::Action::{self, *};
pub use actions::ListingAction::{self, *};
pub use actions::TabAction::{self, *};
named!( named!(
usize_c<usize>, usize_c<usize>,
@ -50,6 +53,7 @@ named!(
) )
); );
named!(close<Action>, map!(ws!(tag!("close")), |_| Tab(Close)));
named!( named!(
goto<Action>, goto<Action>,
preceded!(tag!("b "), map!(call!(usize_c), Action::ViewMailbox)) preceded!(tag!("b "), map!(call!(usize_c), Action::ViewMailbox))
@ -57,22 +61,18 @@ named!(
named!( named!(
subsort<Action>, subsort<Action>,
do_parse!(tag!("subsort ") >> p: pair!(sortfield, sortorder) >> (Action::SubSort(p.0, p.1))) do_parse!(tag!("subsort ") >> p: pair!(sortfield, sortorder) >> (SubSort(p.0, p.1)))
); );
named!( named!(
sort<Action>, sort<Action>,
do_parse!( do_parse!(
tag!("sort ") tag!("sort ") >> p: separated_pair!(sortfield, tag!(" "), sortorder) >> (Sort(p.0, p.1))
>> p: separated_pair!(sortfield, tag!(" "), sortorder)
>> (Action::Sort(p.0, p.1))
) )
); );
named!( named!(
threaded<Action>, threaded<Action>,
map!(ws!(tag!("threaded")), |_| { map!(ws!(tag!("threaded")), |_| Listing(ToggleThreaded))
Action::PlainListing(PlainListingAction::ToggleThreaded)
})
); );
named!( named!(
toggle<Action>, toggle<Action>,
@ -80,5 +80,5 @@ named!(
); );
named!(pub parse_command<Action>, named!(pub parse_command<Action>,
alt_complete!( goto | toggle | sort | subsort) alt_complete!( goto | toggle | sort | subsort | close)
); );

View File

@ -39,6 +39,8 @@ use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen; use termion::screen::AlternateScreen;
use termion::{clear, cursor, style}; use termion::{clear, cursor, style};
type StateStdout = termion::screen::AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>;
struct InputHandler { struct InputHandler {
rx: Receiver<bool>, rx: Receiver<bool>,
tx: Sender<bool>, tx: Sender<bool>,
@ -133,12 +135,12 @@ impl Context {
/// A State object to manage and own components and entities of the UI. `State` is responsible for /// A State object to manage and own components and entities of the UI. `State` is responsible for
/// managing the terminal and interfacing with `melib` /// managing the terminal and interfacing with `melib`
pub struct State<W: Write> { pub struct State {
cols: usize, cols: usize,
rows: usize, rows: usize,
grid: CellBuffer, grid: CellBuffer,
stdout: Option<termion::screen::AlternateScreen<termion::raw::RawTerminal<W>>>, stdout: Option<StateStdout>,
child: Option<ForkType>, child: Option<ForkType>,
pub mode: UIMode, pub mode: UIMode,
entities: Vec<Entity>, entities: Vec<Entity>,
@ -149,7 +151,7 @@ pub struct State<W: Write> {
threads: FnvHashMap<thread::ThreadId, (chan::Sender<bool>, thread::JoinHandle<()>)>, threads: FnvHashMap<thread::ThreadId, (chan::Sender<bool>, thread::JoinHandle<()>)>,
} }
impl<W: Write> Drop for State<W> { impl Drop for State {
fn drop(&mut self) { fn drop(&mut self) {
// When done, restore the defaults to avoid messing with the terminal. // When done, restore the defaults to avoid messing with the terminal.
write!( write!(
@ -164,13 +166,13 @@ impl<W: Write> Drop for State<W> {
} }
} }
impl Default for State<std::io::Stdout> { impl Default for State {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
impl State<std::io::Stdout> { impl State {
pub fn new() -> Self { pub fn new() -> Self {
/* Create a channel to communicate with other threads. The main process is the sole receiver. /* Create a channel to communicate with other threads. The main process is the sole receiver.
* */ * */
@ -370,8 +372,7 @@ impl State<std::io::Stdout> {
).unwrap(); ).unwrap();
self.flush(); self.flush();
} }
}
impl<W: Write> State<W> {
pub fn receiver(&self) -> Receiver<ThreadEvent> { pub fn receiver(&self) -> Receiver<ThreadEvent> {
self.context.receiver.clone() self.context.receiver.clone()
} }
@ -509,7 +510,15 @@ impl<W: Write> State<W> {
UIEventType::Fork(child) => { UIEventType::Fork(child) => {
self.mode = UIMode::Fork; self.mode = UIMode::Fork;
self.child = Some(child); self.child = Some(child);
self.flush(); if let Some(ForkType::Finished) = self.child {
/*
* Fork has finished in the past.
* We're back in the AlternateScreen, but the cursor is reset to Shown, so fix
* it.
*/
write!(self.stdout(), "{}", cursor::Hide,).unwrap();
self.flush();
}
return; return;
} }
UIEventType::EditDraft(mut file) => { UIEventType::EditDraft(mut file) => {
@ -538,8 +547,8 @@ impl<W: Write> State<W> {
self.entities[i].rcv_event( self.entities[i].rcv_event(
&UIEvent { &UIEvent {
id: 0, id: 0,
event_type: UIEventType::Action(Action::PlainListing( event_type: UIEventType::Action(Action::Listing(
PlainListingAction::ToggleThreaded, ListingAction::ToggleThreaded,
)), )),
}, },
&mut self.context, &mut self.context,
@ -578,7 +587,7 @@ impl<W: Write> State<W> {
} }
pub fn try_wait_on_child(&mut self) -> Option<bool> { pub fn try_wait_on_child(&mut self) -> Option<bool> {
if match self.child { let should_return_flag = match self.child {
Some(ForkType::NewDraft(_, ref mut c)) => { Some(ForkType::NewDraft(_, ref mut c)) => {
let mut w = c.try_wait(); let mut w = c.try_wait();
match w { match w {
@ -599,10 +608,16 @@ impl<W: Write> State<W> {
} }
} }
} }
Some(ForkType::Finished) => {
/* Fork has already finished */
std::mem::replace(&mut self.child, None);
return None;
}
_ => { _ => {
return None; return None;
} }
} { };
if should_return_flag {
if let Some(ForkType::NewDraft(f, _)) = std::mem::replace(&mut self.child, None) { if let Some(ForkType::NewDraft(f, _)) = std::mem::replace(&mut self.child, None) {
self.rcv_event(UIEvent { self.rcv_event(UIEvent {
id: 0, id: 0,
@ -618,7 +633,7 @@ impl<W: Write> State<W> {
s.flush().unwrap(); s.flush().unwrap();
} }
} }
fn stdout(&mut self) -> &mut termion::screen::AlternateScreen<termion::raw::RawTerminal<W>> { fn stdout(&mut self) -> &mut StateStdout {
self.stdout.as_mut().unwrap() self.stdout.as_mut().unwrap()
} }
} }

View File

@ -40,6 +40,7 @@ use melib::RefreshEvent;
use std; use std;
use std::fmt; use std::fmt;
use std::thread; use std::thread;
use uuid::Uuid;
#[derive(Debug)] #[derive(Debug)]
pub enum StatusEvent { pub enum StatusEvent {
@ -71,6 +72,7 @@ impl From<RefreshEvent> for ThreadEvent {
#[derive(Debug)] #[derive(Debug)]
pub enum ForkType { pub enum ForkType {
Finished, // Already finished fork, we only want to restore input/output
Generic(std::process::Child), Generic(std::process::Child),
NewDraft(File, std::process::Child), NewDraft(File, std::process::Child),
} }
@ -91,7 +93,10 @@ pub enum UIEventType {
EditDraft(File), EditDraft(File),
Action(Action), Action(Action),
StatusEvent(StatusEvent), StatusEvent(StatusEvent),
MailboxUpdate((usize, usize)), MailboxUpdate((usize, usize)), // (account_idx, mailbox_idx)
Reply((usize, usize, usize), usize), // thread coordinates (account, mailbox, root_set idx) and message idx
EntityKill(Uuid),
StartupCheck, StartupCheck,
} }