Browse Source

ui: add reply-context in compose

tracking-issue: #24
tags/pre-alpha-0.0
Manos Pitsidianakis 3 years ago
parent
commit
b94687cdb0
Signed by untrusted user: epilys GPG Key ID: 73627C2F690DF710
  1. 12
      melib/src/mailbox/email/compose/mod.rs
  2. 46
      melib/src/mailbox/email/mod.rs
  3. 31
      melib/src/mailbox/thread.rs
  4. 24
      src/bin.rs
  5. 256
      ui/src/components/mail/compose.rs
  6. 15
      ui/src/components/mail/listing/compact.rs
  7. 5
      ui/src/components/mail/listing/mod.rs
  8. 5
      ui/src/components/mail/view/html.rs
  9. 53
      ui/src/components/mail/view/mod.rs
  10. 38
      ui/src/components/mail/view/thread.rs
  11. 42
      ui/src/components/mod.rs
  12. 52
      ui/src/components/utilities.rs
  13. 14
      ui/src/execute/actions.rs
  14. 18
      ui/src/execute/mod.rs
  15. 41
      ui/src/state.rs
  16. 7
      ui/src/types/mod.rs

12
melib/src/mailbox/email/compose/mod.rs

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

46
melib/src/mailbox/email/mod.rs

@ -41,6 +41,7 @@ use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::hash::Hasher;
use std::option::Option;
use std::str;
use std::string::String;
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 {
fn eq(&self, other: &MessageID) -> bool {
self.raw() == other.raw()
@ -261,6 +272,8 @@ pub struct Envelope {
date: String,
from: Vec<Address>,
to: Vec<Address>,
cc: Vec<Address>,
bcc: Vec<Address>,
body: Option<Attachment>,
subject: Option<Vec<u8>>,
message_id: Option<MessageID>,
@ -281,6 +294,8 @@ impl Envelope {
date: String::new(),
from: Vec::new(),
to: Vec::new(),
cc: Vec::new(),
bcc: Vec::new(),
body: None,
subject: None,
message_id: None,
@ -342,6 +357,22 @@ impl Envelope {
Vec::new()
};
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") {
let parse_result = parser::rfc2822address_list(value);
let value = if parse_result.is_done() {
@ -424,7 +455,14 @@ impl Envelope {
pub fn from(&self) -> &Vec<Address> {
&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 {
let _strings: Vec<String> = self.from.iter().map(|a| format!("{}", a)).collect();
_strings.join(", ")
@ -530,6 +568,12 @@ impl Envelope {
fn set_date(&mut self, new_val: &[u8]) -> () {
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>) -> () {
self.from = new_val;
}

31
melib/src/mailbox/thread.rs

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

24
src/bin.rs

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

256
ui/src/components/mail/compose.rs

@ -25,49 +25,148 @@ use melib::Draft;
#[derive(Debug)]
pub struct Composer {
mode: ViewMode,
pager: Pager,
reply_context: Option<((usize, usize), Box<ThreadView>)>, // (folder_index, container_index)
account_cursor: usize,
pager: Pager,
draft: Draft,
account_cursor: usize,
mode: ViewMode,
dirty: bool,
initialized: bool,
}
impl Default for Composer {
fn default() -> Self {
Composer {
dirty: true,
mode: ViewMode::Overview,
reply_context: None,
account_cursor: 0,
pager: Pager::default(),
draft: Draft::default(),
account_cursor: 0,
mode: ViewMode::Overview,
dirty: true,
initialized: false,
}
}
}
#[derive(Debug)]
enum ViewMode {
//Compose,
Discard(Uuid),
Pager,
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 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// 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 {
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 bottom_right = bottom_right!(area);
let headers = self.draft.headers();
{
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 (x, y) = write_string_to_grid(
k,
@ -127,22 +226,37 @@ impl Composer {
impl Component for Composer {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.dirty {
self.draft.headers_mut().insert(
"From".into(),
get_display_name(context, self.account_cursor),
);
if !self.initialized {
clear_area(grid, area);
self.initialized = true;
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let upper_left = set_y(upper_left, get_y(upper_left) + 1);
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 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 {
for i in get_y(upper_left)..=get_y(bottom_right) {
@ -159,6 +273,12 @@ impl Component for Composer {
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 {
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);
@ -174,26 +294,62 @@ impl Component for Composer {
);
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;
}
match self.mode {
ViewMode::Overview => {
self.draw_header_table(grid, header_area, context);
self.pager.draw(grid, body_area, context);
/* Regardless of view mode, do the following */
clear_area(grid, header_area);
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 {
if self.pager.process_event(event, context) {
return true;
match (&mut self.mode, &mut self.reply_context) {
(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 {
UIEventType::Resize => {
self.dirty = true;
self.initialized = false;
}
/* Switch e-mail From: field to the `left` configured account. */
UIEventType::Input(Key::Left) => {
self.account_cursor = self.account_cursor.saturating_sub(1);
self.draft.headers_mut().insert(
@ -203,6 +359,7 @@ impl Component for Composer {
self.dirty = true;
return true;
}
/* Switch e-mail From: field to the `right` configured account. */
UIEventType::Input(Key::Right) => {
if self.account_cursor + 1 < context.accounts.len() {
self.account_cursor += 1;
@ -214,7 +371,38 @@ impl Component for Composer {
}
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};
/* 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();
self.draft = Draft::from_str(result.as_str()).unwrap();
self.pager.update_from_str(self.draft.body());
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::Fork(ForkType::Finished),
});
context.restore_input();
self.dirty = true;
return true;
}
// TODO: Replace EditDraft with compose tabs
UIEventType::Input(Key::Char('m')) => {
let mut f =
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 {
self.dirty || self.pager.is_dirty()
|| self
.reply_context
.as_ref()
.map(|(_, p)| p.is_dirty())
.unwrap_or(false)
}
fn set_dirty(&mut self) {
self.dirty = true;
self.initialized = false;
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);
}
}

15
ui/src/components/mail/listing/compact.rs

@ -303,6 +303,7 @@ impl Component for CompactListing {
if self.length == 0 && self.dirty {
clear_area(grid, area);
context.dirty_areas.push_back(area);
return;
}
/* Render the mail body in a pager */
@ -312,7 +313,7 @@ impl Component for CompactListing {
}
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.dirty = false;
}
@ -418,15 +419,6 @@ impl Component for CompactListing {
self.dirty = true;
}
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) => {
self.new_cursor_pos.1 = *idx;
self.dirty = true;
@ -446,7 +438,8 @@ impl Component for CompactListing {
self.dirty = true;
self.refresh_mailbox(context);
return true;
} // _ => {}
}
_ => {}
},
_ => {}
}

5
ui/src/components/mail/listing/mod.rs

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

5
ui/src/components/mail/view/html.rs

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

53
ui/src/components/mail/view/mod.rs

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

38
ui/src/components/mail/view/thread.rs

@ -46,7 +46,17 @@ pub struct 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 */
let mut stack: Vec<(usize, usize)> = Vec::with_capacity(32);
let mailbox = &context.accounts[coordinates.0][coordinates.1]
@ -78,6 +88,13 @@ impl ThreadView {
let entry = view.make_entry(context, (ind, idx, line));
view.entries.push(entry);
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];
if let Some(i) = container.next_sibling() {
stack.push((ind, i));
@ -87,8 +104,10 @@ impl ThreadView {
stack.push((ind + 1, i));
}
}
view.new_expanded_pos = view.entries.len().saturating_sub(1);
view.expanded_pos = view.new_expanded_pos + 1;
if expanded_idx.is_none() {
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 mut width = 0;
@ -545,6 +564,16 @@ impl Component for ThreadView {
return true;
}
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) => {
if self.cursor_pos > 0 {
self.new_cursor_pos = self.new_cursor_pos.saturating_sub(1);
@ -574,7 +603,7 @@ impl Component for ThreadView {
return true;
}
UIEventType::Resize => {
self.dirty = true;
self.set_dirty();
}
_ => {}
}
@ -584,6 +613,7 @@ impl Component for ThreadView {
self.dirty || self.mailview.is_dirty()
}
fn set_dirty(&mut self) {
self.initiated = false;
self.dirty = true;
self.mailview.set_dirty();
}

42
ui/src/components/mod.rs

@ -37,7 +37,9 @@ pub use self::utilities::*;
use std::fmt;
use std::fmt::{Debug, Display};
use std::ops::Deref;
use std::ops::{Deref, DerefMut};
use uuid::Uuid;
use super::{Key, StatusEvent, UIEvent, UIEventType};
/// The upper and lower boundary char.
@ -62,20 +64,46 @@ const LIGHT_DOWN_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
/// future (ie hold some information, id or state) it should be removed.
/// `Entity` is a container for Components.
#[derive(Debug)]
pub struct Entity {
//context: VecDeque,
id: Uuid,
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 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(&self.component, f)
}
}
impl DerefMut for Entity {
fn deref_mut(&mut self) -> &mut Box<Component> {
&mut self.component
}
}
impl Deref for Entity {
type Target = Box<Component>;
@ -85,6 +113,9 @@ impl Deref for Entity {
}
impl Entity {
pub fn uuid(&self) -> &Uuid {
&self.id
}
/// Pass events to child component.
pub fn rcv_event(&mut self, event: &UIEvent, context: &mut Context) -> bool {
self.component.process_event(&event, context)
@ -101,6 +132,7 @@ pub trait Component: Display + Debug {
true
}
fn set_dirty(&mut self);
fn kill(&mut self, _uuid: Uuid) {}
}
fn new_draft(_context: &mut Context) -> Vec<u8> {
@ -113,6 +145,7 @@ fn new_draft(_context: &mut Context) -> Vec<u8> {
v.into_bytes()
}
/*
pub(crate) fn is_box_char(ch: char) -> bool {
match ch {
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 {
* match ch {
* 'โ””' | 'โ”€' | 'โ”˜' | 'โ”ด' | 'โ”Œ' | 'โ”‚' | 'โ”œ' | 'โ”' | 'โ”ฌ' | 'โ”ค' | 'โ”ผ' | 'โ•ท' | 'โ•ต' | 'โ•ด' | 'โ•ถ' => true,

52
ui/src/components/utilities.rs

@ -712,14 +712,17 @@ impl Component for Progress {
#[derive(Debug)]
pub struct Tabbed {
children: Vec<Box<Component>>,
children: Vec<Entity>,
cursor_pos: usize,
}
impl Tabbed {
pub fn new(children: Vec<Box<Component>>) -> Self {
Tabbed {
children,
children: children
.into_iter()
.map(|x: Box<Component>| Entity::from(x))
.collect(),
cursor_pos: 0,
}
}
@ -748,13 +751,15 @@ impl Tabbed {
}
let (cols, _) = grid.size();
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_ch(' ');
}
context.dirty_areas.push_back(area);
}
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 {
if let UIEventType::Input(Key::Char('T')) = event.event_type {
self.cursor_pos = (self.cursor_pos + 1) % self.children.len();
self.children[self.cursor_pos].set_dirty();
return true;
match event.event_type {
UIEventType::Input(Key::Char('T')) => {
self.cursor_pos = (self.cursor_pos + 1) % self.children.len();
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)
}
fn is_dirty(&self) -> bool {
self.children[self.cursor_pos].is_dirty()
}
fn set_dirty(&mut self) {}
fn set_dirty(&mut self) {
self.children[self.cursor_pos].set_dirty();
}
}

14
ui/src/execute/actions.rs

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

18
ui/src/execute/mod.rs

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

41
ui/src/state.rs

@ -39,6 +39,8 @@ use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use termion::{clear, cursor, style};
type StateStdout = termion::screen::AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>;
struct InputHandler {
rx: Receiver<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
/// managing the terminal and interfacing with `melib`
pub struct State<W: Write> {
pub struct State {
cols: usize,
rows: usize,
grid: CellBuffer,
stdout: Option<termion::screen::AlternateScreen<termion::raw::RawTerminal<W>>>,
stdout: Option<StateStdout>,
child: Option<ForkType>,
pub mode: UIMode,
entities: Vec<Entity>,
@ -149,7 +151,7 @@ pub struct State<W: Write> {
threads: FnvHashMap<thread::ThreadId, (chan::Sender<bool>, thread::JoinHandle<()>)>,
}
impl<W: Write> Drop for State<W> {
impl Drop for State {
fn drop(&mut self) {
// When done, restore the defaults to avoid messing with the terminal.
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 {
Self::new()
}
}
impl State<std::io::Stdout> {
impl State {
pub fn new() -> Self {
/* 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();
self.flush();
}
}
impl<W: Write> State<W> {
pub fn receiver(&self) -> Receiver<ThreadEvent> {
self.context.receiver.clone()
}
@ -509,7 +510,15 @@ impl<W: Write> State<W> {
UIEventType::Fork(child) => {
self.mode = UIMode::Fork;
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;
}
UIEventType::EditDraft(mut file) => {
@ -538,8 +547,8 @@ impl<W: Write> State<W> {
self.entities[i].rcv_event(
&UIEvent {
id: 0,
event_type: UIEventType::Action(Action::PlainListing(
PlainListingAction::ToggleThreaded,
event_type: UIEventType::Action(Action::Listing(