Add compact view listing, and compose tab pager

concerns #3
embed
Manos Pitsidianakis 2018-08-16 16:32:47 +03:00
parent d43d8d282c
commit b2c7430907
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
10 changed files with 480 additions and 495 deletions

View File

@ -107,6 +107,8 @@ pub struct Container {
struct ContainerTree {
id: usize,
children: Option<Vec<ContainerTree>>,
len: usize,
has_unseen: bool,
}
impl ContainerTree {
@ -114,6 +116,8 @@ impl ContainerTree {
ContainerTree {
id,
children: None,
len: 1,
has_unseen: false,
}
}
}
@ -173,8 +177,33 @@ impl<'a> IntoIterator for &'a Threads {
}
pub struct RootIterator<'a> {
pos: usize,
tree: Ref<'a ,Vec<ContainerTree>>,
}
impl<'a> Iterator for RootIterator<'a> {
type Item = (usize, usize, bool);
fn next(&mut self) -> Option<(usize, usize, bool)> {
if self.pos == self.tree.len() {
return None;
}
let node = &self.tree[self.pos];
self.pos += 1;
return Some((node.id, node.len, node.has_unseen));
}
}
impl Threads {
pub fn root_len(&self) -> usize {
self.tree.borrow().len()
}
pub fn root_set(&self) -> &Vec<usize> {
&self.root_set
}
pub fn root_set_iter(&self) -> RootIterator {
RootIterator { pos: 0, tree: self.tree.borrow() }
}
pub fn thread_to_mail(&self, i: usize) -> usize {
let thread = self.containers[self.threaded_collection[i]];
thread.message().unwrap()
@ -226,49 +255,49 @@ impl Threads {
}
}
});
}
}
}
}
fn inner_sort_by(&self, sort: (SortField, SortOrder), collection: &[Envelope]) {
let tree = &mut self.tree.borrow_mut();
let containers = &self.containers;
tree.sort_by(|a, b| { match sort {
(SortField::Date, SortOrder::Desc) => {
let a = containers[a.id];
let b = containers[b.id];
b.date.cmp(&a.date)
tree.sort_by(|a, b| { match sort {
(SortField::Date, SortOrder::Desc) => {
let a = containers[a.id];
let b = containers[b.id];
b.date.cmp(&a.date)
}
(SortField::Date, SortOrder::Asc) => {
let a = containers[a.id];
let b = containers[b.id];
a.date.cmp(&b.date)
}
(SortField::Subject, SortOrder::Desc) => {
let a = containers[a.id].message();
let b = containers[b.id].message();
if a.is_none() || b.is_none() {
return Ordering::Equal;
}
let ma = &collection[a.unwrap()];
let mb = &collection[b.unwrap()];
ma.subject().cmp(&mb.subject())
}
(SortField::Subject, SortOrder::Asc) => {
let a = containers[a.id].message();
let b = containers[b.id].message();
if a.is_none() || b.is_none() {
return Ordering::Equal;
}
let ma = &collection[a.unwrap()];
let mb = &collection[b.unwrap()];
mb.subject().cmp(&ma.subject())
}
}
});
(SortField::Date, SortOrder::Asc) => {
let a = containers[a.id];
let b = containers[b.id];
a.date.cmp(&b.date)
}
(SortField::Subject, SortOrder::Desc) => {
let a = containers[a.id].message();
let b = containers[b.id].message();
if a.is_none() || b.is_none() {
return Ordering::Equal;
}
let ma = &collection[a.unwrap()];
let mb = &collection[b.unwrap()];
ma.subject().cmp(&mb.subject())
}
(SortField::Subject, SortOrder::Asc) => {
let a = containers[a.id].message();
let b = containers[b.id].message();
if a.is_none() || b.is_none() {
return Ordering::Equal;
}
let ma = &collection[a.unwrap()];
let mb = &collection[b.unwrap()];
mb.subject().cmp(&ma.subject())
}
}
});
}
pub fn sort_by(&self, sort: (SortField, SortOrder), subsort: (SortField, SortOrder), collection: &[Envelope]) {
if *self.sort.borrow() != sort {
@ -292,7 +321,6 @@ impl Threads {
root_subject_idx: usize,
collection: &[Envelope],
) {
tree.id = i;
let thread = containers[i];
if let Some(msg_idx) = containers[root_subject_idx].message() {
let root_subject = collection[msg_idx].subject();
@ -301,6 +329,7 @@ impl Threads {
* list.) */
if indentation > 0 && thread.has_message() {
let subject = collection[thread.message().unwrap()].subject();
tree.has_unseen = !collection[thread.message().unwrap()].is_seen();
if subject == root_subject
|| subject.starts_with("Re: ")
&& subject.as_ref().ends_with(root_subject.as_ref())
@ -332,6 +361,7 @@ impl Threads {
loop {
let mut new_child_tree = ContainerTree::new(fc);
build_threaded(&mut new_child_tree, containers, indentation, threaded, fc, i, collection);
tree.has_unseen |= new_child_tree.has_unseen;
child_vec.push(new_child_tree);
let thread_ = containers[fc];
if !thread_.has_sibling() {
@ -339,6 +369,7 @@ impl Threads {
}
fc = thread_.next_sibling().unwrap();
}
tree.len = child_vec.iter().map(|c| c.len).sum();
tree.children = Some(child_vec);
}
}

View File

@ -35,7 +35,6 @@ extern crate ui;
pub use melib::*;
pub use ui::*;
use std::thread;
#[macro_use]
extern crate chan;
@ -45,28 +44,6 @@ use chan_signal::Signal;
extern crate nix;
fn make_input_thread(
sx: chan::Sender<ThreadEvent>,
rx: chan::Receiver<bool>,
) -> thread::JoinHandle<()> {
let stdin = std::io::stdin();
thread::Builder::new()
.name("input-thread".to_string())
.spawn(move || {
get_events(
stdin,
|k| {
sx.send(ThreadEvent::Input(k));
},
|| {
sx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode(UIMode::Fork)));
},
&rx,
)
})
.unwrap()
}
fn main() {
/* Lock all stdio outs */
//let _stdout = stdout();
@ -79,31 +56,22 @@ fn main() {
/* Catch SIGWINCH to handle terminal resizing */
let signal = chan_signal::notify(&[Signal::WINCH]);
/* Create a channel to communicate with other threads. The main process is the sole receiver.
* */
let (sender, receiver) = chan::sync(::std::mem::size_of::<ThreadEvent>());
/*
* Create async channel to block the input-thread if we need to fork and stop it from reading
* stdin, see get_events() for details
* */
let (tx, rx) = chan::async();
/* Get input thread handle to join it if we need to */
let mut _thread_handler = make_input_thread(sender.clone(), rx.clone());
/* Create the application State. This is the 'System' part of an ECS architecture */
let mut state = State::new(sender.clone(), tx);
let mut state = State::new();
let receiver = state.receiver();
/* Register some reasonably useful interfaces */
let menu = Entity {
component: Box::new(AccountMenu::new(&state.context.accounts)),
};
let listing = MailListing::new();
let listing = CompactListing::new();
let b = Entity {
component: 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 {}));
tabs.add_component(Box::new(Composer::default()));
let window = Entity { component: tabs };
let status_bar = Entity {
@ -137,7 +105,7 @@ fn main() {
let self_pid = nix::unistd::Pid::this();
nix::sys::signal::kill(self_pid, nix::sys::signal::Signal::SIGSTOP).unwrap();
state.switch_to_alternate_screen();
_thread_handler = make_input_thread(sender.clone(), rx.clone());
state.restore_input();
// BUG: thread sends input event after one received key
state.update_size();
state.render();
@ -237,7 +205,7 @@ fn main() {
'reap: loop {
match state.try_wait_on_child() {
Some(true) => {
make_input_thread(sender.clone(), rx.clone());
state.restore_input();
state.mode = UIMode::Normal;
state.render();
}

View File

@ -21,7 +21,26 @@
use super::*;
pub struct Composer {}
pub struct Composer {
dirty: bool,
mode: ViewMode,
pager: Pager,
}
impl Default for Composer {
fn default() -> Self {
Composer {
dirty: true,
mode: ViewMode::Overview,
pager: Pager::from_str("asdfs\nfdsfds\ndsfdsfs\n\n\n\naaaaaaaaaaaaaa\nfdgfd", None),
}
}
}
enum ViewMode {
//Compose,
Overview,
}
impl fmt::Display for Composer {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@ -32,14 +51,90 @@ impl fmt::Display for Composer {
impl Component for Composer {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
clear_area(grid, area);
context.dirty_areas.push_back(area);
if self.dirty {
clear_area(grid, area);
}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let header_height = 12;
let width = width!(area);
let mid = if width > 80 {
let width = width - 80;
let mid = width / 2;;
if self.dirty {
for i in get_y(upper_left)..=get_y(bottom_right) {
grid[(mid, i)].set_ch(VERT_BOUNDARY);
grid[(mid, i)].set_fg(Color::Default);
grid[(mid, i)].set_bg(Color::Default);
grid[(mid + 80, i)].set_ch(VERT_BOUNDARY);
grid[(mid + 80, i)].set_fg(Color::Default);
grid[(mid + 80, i)].set_bg(Color::Default);
}
}
mid
} else { 0 };
if self.dirty {
for i in get_x(upper_left)+ mid + 1..=get_x(upper_left) + mid + 79 {
grid[(i, header_height)].set_ch(HORZ_BOUNDARY);
grid[(i, header_height)].set_fg(Color::Default);
grid[(i, header_height)].set_bg(Color::Default);
}
}
let body_area = ((mid + 1, header_height+2), (mid + 78, get_y(bottom_right)));
if self.dirty {
context.dirty_areas.push_back(area);
self.dirty = false;
}
match self.mode {
ViewMode::Overview => {
self.pager.draw(grid, body_area, context);
},
}
}
fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) {}
fn process_event(&mut self, event: &UIEvent, context: &mut Context) {
match event.event_type {
UIEventType::Resize => {
self.dirty = true;
}
UIEventType::Input(Key::Char('\n')) => {
use std::process::{Command, Stdio};
/* Kill input thread so that spawned command can be sole receiver of stdin */
{
context.input_kill();
}
let mut f = create_temp_file(&new_draft(context), None);
//let mut f = Box::new(std::fs::File::create(&dir).unwrap());
// TODO: check exit status
Command::new("vim")
.arg("+/^$")
.arg(&f.path())
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.output()
.expect("failed to execute process");
self.pager.update_from_string(f.read_to_string());
context.restore_input();
self.dirty = true;
return;
},
_ => {},
}
self.pager.process_event(event, context);
}
fn is_dirty(&self) -> bool {
true
self.dirty || self.pager.is_dirty()
}
fn set_dirty(&mut self) {
self.dirty = true;
self.pager.set_dirty();
}
fn set_dirty(&mut self) {}
}

View File

@ -20,18 +20,20 @@
*/
use super::*;
use melib::mailbox::backends::BackendOp;
//use melib::mailbox::backends::BackendOp;
const MAX_COLS: usize = 500;
/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the thread's content in a
/// A list of all mail (`Envelope`s) in a `Mailbox`. On `\n` it opens the `Envelope` content in a
/// `ThreadView`.
pub struct CompactMailListing {
pub struct CompactListing {
/// (x, y, z): x is accounts, y is folders, z is index inside a folder.
cursor_pos: (usize, usize, usize),
new_cursor_pos: (usize, usize, usize),
length: usize,
sort: (SortField, SortOrder),
//subsort: (SortField, SortOrder),
subsort: (SortField, SortOrder),
/// Cache current view.
content: CellBuffer,
/// If we must redraw on next redraw event
@ -41,27 +43,48 @@ pub struct CompactMailListing {
view: Option<ThreadView>,
}
impl Default for CompactMailListing {
impl Default for CompactListing {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for CompactMailListing {
impl fmt::Display for CompactListing {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "mail")
}
}
impl CompactMailListing {
impl CompactListing {
/// Helper function to format entry strings for CompactListing */
/* TODO: Make this configurable */
fn make_entry_string(e: &Envelope, len: usize, idx: usize) -> String {
if len > 1 {
format!(
"{} {} {:.85} ({})",
idx,
&CompactListing::format_date(e),
e.subject(),
len
)
} else {
format!(
"{} {} {:.85}",
idx,
&CompactListing::format_date(e),
e.subject(),
)
}
}
pub fn new() -> Self {
let content = CellBuffer::new(0, 0, Cell::with_char(' '));
CompactMailListing {
CompactListing {
cursor_pos: (0, 1, 0),
new_cursor_pos: (0, 0, 0),
length: 0,
sort: (SortField::Date, SortOrder::Desc),
//subsort: (SortField::Date, SortOrder::Asc),
sort: (Default::default(), Default::default()),
subsort: (Default::default(), Default::default()),
content: content,
dirty: true,
unfocused: false,
@ -94,187 +117,86 @@ impl CompactMailListing {
.as_ref()
.unwrap();
self.length = mailbox.threads.containers().len();
let mut content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
self.length = mailbox.threads.root_len();
self.content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
if self.length == 0 {
write_string_to_grid(
&format!("Folder `{}` is empty.", mailbox.folder.name()),
&mut content,
&mut self.content,
Color::Default,
Color::Default,
((0, 0), (MAX_COLS - 1, 0)),
true,
);
self.content = content;
return;
}
// TODO: Fix the threaded hell and refactor stuff into seperate functions and/or modules.
let mut indentations: Vec<bool> = Vec::with_capacity(6);
let mut thread_idx = 0; // needed for alternate thread colors
/* Draw threaded view. */
let mut local_collection: Vec<usize> = mailbox.threads.threaded_collection().clone();
let threads: &Vec<Container> = &mailbox.threads.containers();
local_collection.sort_by(|a, b| match self.sort {
(SortField::Date, SortOrder::Desc) => {
mailbox.thread(*b).date().cmp(&mailbox.thread(*a).date())
}
(SortField::Date, SortOrder::Asc) => {
mailbox.thread(*a).date().cmp(&mailbox.thread(*b).date())
}
(SortField::Subject, SortOrder::Desc) => {
let a = mailbox.thread(*a);
let b = mailbox.thread(*b);
let ma = &mailbox.collection[*a.message().as_ref().unwrap()];
let mb = &mailbox.collection[*b.message().as_ref().unwrap()];
ma.subject().cmp(&mb.subject())
}
(SortField::Subject, SortOrder::Asc) => {
let a = mailbox.thread(*a);
let b = mailbox.thread(*b);
let ma = &mailbox.collection[*a.message().as_ref().unwrap()];
let mb = &mailbox.collection[*b.message().as_ref().unwrap()];
mb.subject().cmp(&ma.subject())
}
});
let mut iter = local_collection.iter().enumerate().peekable();
let len = mailbox
.threads
.threaded_collection()
.len()
.to_string()
.chars()
.count();
/* This is just a desugared for loop so that we can use .peek() */
while let Some((idx, i)) = iter.next() {
let container = &threads[*i];
let indentation = container.indentation();
if indentation == 0 {
thread_idx += 1;
}
assert!(container.has_message());
match iter.peek() {
Some(&(_, x)) if threads[*x].indentation() == indentation => {
indentations.pop();
indentations.push(true);
}
_ => {
indentations.pop();
indentations.push(false);
}
}
if container.has_sibling() {
indentations.pop();
indentations.push(true);
}
let envelope: &Envelope = &mailbox.collection[container.message().unwrap()];
let fg_color = if !envelope.is_seen() {
let threads = &mailbox.threads;
threads.sort_by(self.sort, self.subsort, &mailbox.collection);
for (idx, (t, len, has_unseen)) in threads.root_set_iter().enumerate() {
let container = &threads.containers()[t];
let i = if let Some(i) = container.message() {
i
} else {
threads.containers()[
container.first_child().unwrap()
].message().unwrap()
};
let root_envelope: &Envelope = &mailbox.collection[i];
let fg_color = if has_unseen {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if !envelope.is_seen() {
let bg_color = if has_unseen {
Color::Byte(251)
} else if thread_idx % 2 == 0 {
} else if idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
};
let (x, _) = write_string_to_grid(
&CompactMailListing::make_thread_entry(
envelope,
idx,
indentation,
container,
&indentations,
len,
context.accounts[self.cursor_pos.0].backend.operation(envelope.hash()),
),
&mut content,
&CompactListing::make_entry_string(root_envelope, len, idx),
&mut self.content,
fg_color,
bg_color,
((0, idx), (MAX_COLS - 1, idx)),
false,
);
);
for x in x..MAX_COLS {
content[(x, idx)].set_ch(' ');
content[(x, idx)].set_bg(bg_color);
self.content[(x, idx)].set_ch(' ');
self.content[(x, idx)].set_bg(bg_color);
}
match iter.peek() {
Some(&(_, x)) if threads[*x].indentation() > indentation => {
indentations.push(false);
}
Some(&(_, x)) if threads[*x].indentation() < indentation => {
for _ in 0..(indentation - threads[*x].indentation()) {
indentations.pop();
}
}
_ => {}
}
}
self.content = content;
}
fn highlight_line_self(&mut self, idx: usize, context: &Context) {
let threaded = context.accounts[self.cursor_pos.0]
.runtime_settings
.threaded;
let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1]
.as_ref()
.unwrap();
let envelope: &Envelope = if threaded {
let i = mailbox.threaded_mail(idx);
&mailbox.collection[i]
} else {
&mailbox.collection[idx]
};
let fg_color = if !envelope.is_seen() {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if !envelope.is_seen() {
Color::Byte(251)
} else if idx % 2 == 0 {
Color::Byte(236)
} else {
Color::Default
};
change_colors(
&mut self.content,
((0, idx), (MAX_COLS - 1, idx)),
fg_color,
bg_color,
);
}
fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
let threaded = context.accounts[self.cursor_pos.0]
.runtime_settings
.threaded;
let mailbox = &context.accounts[self.cursor_pos.0][self.cursor_pos.1]
.as_ref()
.unwrap();
let envelope: &Envelope = if threaded {
let i = mailbox.threaded_mail(idx);
&mailbox.collection[i]
let threads = &mailbox.threads;
let container = threads.root_set()[idx];
let container = &threads.containers()[container];
let i = if let Some(i) = container.message() {
i
} else {
&mailbox.collection[idx]
threads.containers()[
container.first_child().unwrap()
].message().unwrap()
};
let fg_color = if !envelope.is_seen() {
let root_envelope: &Envelope = &mailbox.collection[i];
let fg_color = if !root_envelope.is_seen() {
Color::Byte(0)
} else {
Color::Default
};
let bg_color = if self.cursor_pos.2 == idx {
Color::Byte(246)
} else if !envelope.is_seen() {
} else if !root_envelope.is_seen() {
Color::Byte(251)
} else if idx % 2 == 0 {
Color::Byte(236)
@ -343,56 +265,6 @@ impl CompactMailListing {
context.dirty_areas.push_back(area);
}
fn make_thread_entry(
envelope: &Envelope,
idx: usize,
indent: usize,
container: &Container,
indentations: &[bool],
idx_width: usize,
op: Box<BackendOp>,
) -> String {
let has_sibling = container.has_sibling();
let has_parent = container.has_parent();
let show_subject = container.show_subject();
let mut s = format!(
"{}{}{} ",
idx,
" ".repeat(idx_width + 2 - (idx.to_string().chars().count())),
CompactMailListing::format_date(&envelope)
);
for i in 0..indent {
if indentations.len() > i && indentations[i] {
s.push('│');
} else {
s.push(' ');
}
if i > 0 {
s.push(' ');
}
}
if indent > 0 {
if has_sibling && has_parent {
s.push('├');
} else if has_sibling {
s.push('┬');
} else {
s.push('└');
}
s.push('─');
s.push('>');
}
if show_subject {
s.push_str(&format!("{:.85}", envelope.subject()));
}
let attach_count = envelope.body(op).count_attachments();
if attach_count > 1 {
s.push_str(&format!(" {}", attach_count - 1));
}
s
}
fn format_date(envelope: &Envelope) -> String {
let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(envelope.date());
let now: std::time::Duration = std::time::SystemTime::now().duration_since(d).unwrap();
@ -407,7 +279,7 @@ impl CompactMailListing {
}
}
impl Component for CompactMailListing {
impl Component for CompactListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.unfocused {
if !self.is_dirty() {
@ -434,88 +306,14 @@ impl Component for CompactMailListing {
context.dirty_areas.push_back(area);
return;
}
/* Mark message as read */
let idx = self.cursor_pos.2;
let must_highlight = {
if self.length == 0 {
false
} else {
let threaded = context.accounts[self.cursor_pos.0]
.runtime_settings
.threaded;
let account = &mut context.accounts[self.cursor_pos.0];
let (hash, is_seen) = {
let mailbox = &mut account[self.cursor_pos.1]
.as_mut()
.unwrap();
let envelope: &mut Envelope = if threaded {
let i = mailbox.threaded_mail(idx);
&mut mailbox.collection[i]
} else {
&mut mailbox.collection[idx]
};
(envelope.hash(), envelope.is_seen())
};
if is_seen {
let op = {
let backend = &account.backend;
backend.operation(hash)
};
let mailbox = &mut account[self.cursor_pos.1]
.as_mut()
.unwrap();
let envelope: &mut Envelope = if threaded {
let i = mailbox.threaded_mail(idx);
&mut mailbox.collection[i]
} else {
&mut mailbox.collection[idx]
};
envelope.set_seen(op).unwrap();
true
} else {
false
}
}
};
if must_highlight {
self.highlight_line_self(idx, context);
}
let mid = get_y(upper_left) + total_rows - bottom_entity_rows;
self.draw_list(
grid,
(
upper_left,
(get_x(bottom_right), get_y(upper_left) + mid - 1),
),
context,
);
if self.length == 0 {
self.dirty = false;
return;
}
{
/* TODO: Move the box drawing business in separate functions */
if get_x(upper_left) > 0 && grid[(get_x(upper_left) - 1, mid)].ch() == VERT_BOUNDARY
{
grid[(get_x(upper_left) - 1, mid)].set_ch(LIGHT_VERTICAL_AND_RIGHT);
}
for i in get_x(upper_left)..=get_x(bottom_right) {
grid[(i, mid)].set_ch('─');
}
context
.dirty_areas
.push_back((set_y(upper_left, mid), set_y(bottom_right, mid)));
}
// TODO: Make headers view configurable
if !self.dirty {
if let Some(v) = self.view.as_mut() {
v.draw(grid, (set_y(upper_left, mid + 1), bottom_right), context);
}
return;
}
self.view = Some(ThreadView::new(Vec::new()));
self.view = Some(ThreadView::new(self.cursor_pos));
self.view.as_mut().unwrap().draw(
grid,
(set_y(upper_left, mid + 1), bottom_right),
@ -542,53 +340,6 @@ impl Component for CompactMailListing {
self.unfocused = true;
self.dirty = true;
}
UIEventType::Input(Key::Char('m')) if !self.unfocused => {
use std::process::{Command, Stdio};
/* Kill input thread so that spawned command can be sole receiver of stdin */
{
/* I tried thread::park() here but for some reason it never blocked and always
* returned. Spinlocks are also useless because you have to keep the mutex
* guard alive til the child process exits, which requires some effort.
*
* The only problem with this approach is tht the user has to send some input
* in order for the input-thread to wake up and realise it should kill itself.
*
* I tried writing to stdin/tty manually but for some reason rustty didn't
* acknowledge it.
*/
/*
* tx sends to input-thread and it kills itself.
*/
let tx = context.input_thread();
tx.send(true);
}
let mut f = create_temp_file(&new_draft(context), None);
//let mut f = Box::new(std::fs::File::create(&dir).unwrap());
// TODO: check exit status
let mut output = Command::new("vim")
.arg("+/^$")
.arg(&f.path())
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.spawn()
.expect("failed to execute process");
/*
* Main loop will wait on children and when they reap them the loop spawns a new
* input-thread
*/
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::Fork(ForkType::NewDraft(f, output)),
});
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::ChangeMode(UIMode::Fork),
});
return;
}
UIEventType::Input(Key::Char('i')) if self.unfocused => {
self.unfocused = false;
self.dirty = true;
@ -649,9 +400,10 @@ impl Component for CompactMailListing {
self.view = None;
}
UIEventType::MailboxUpdate((ref idxa, ref idxf)) => {
if *idxa == self.new_cursor_pos.1 && *idxf == self.new_cursor_pos.0 {
self.refresh_mailbox(context);
if *idxa == self.new_cursor_pos.0 && *idxf == self.new_cursor_pos.1 {
self.dirty = true;
self.refresh_mailbox(context);
return;
}
}
UIEventType::ChangeMode(UIMode::Normal) => {
@ -677,24 +429,28 @@ impl Component for CompactMailListing {
self.refresh_mailbox(context);
return;
}
Action::Sort(field, order) => {
self.sort = (field.clone(), order.clone());
Action::SubSort(field, order) => {
eprintln!("SubSort {:?} , {:?}", field, order);
self.subsort = (*field, *order);
self.dirty = true;
self.refresh_mailbox(context);
return;
}
_ => {}
Action::Sort(field, order) => {
eprintln!("Sort {:?} , {:?}", field, order);
self.sort = (*field, *order);
self.dirty = true;
self.refresh_mailbox(context);
return;
}
// _ => {}
},
_ => {}
}
if let Some(ref mut v) = self.view {
v.process_event(event, context);
}
}
fn is_dirty(&self) -> bool {
self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
true
}
fn set_dirty(&mut self) {
self.dirty = true;
}
}

View File

@ -119,17 +119,16 @@ impl MailListing {
} else {
mailbox.len()
};
let mut content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
self.content = CellBuffer::new(MAX_COLS, self.length + 1, Cell::with_char(' '));
if self.length == 0 {
write_string_to_grid(
&format!("Folder `{}` is empty.", mailbox.folder.name()),
&mut content,
&mut self.content,
Color::Default,
Color::Default,
((0, 0), (MAX_COLS - 1, 0)),
true,
);
self.content = content;
return;
}
@ -200,15 +199,15 @@ impl MailListing {
len,
// context.accounts[self.cursor_pos.0].backend.operation(envelope.hash())
),
&mut content,
&mut self.content,
fg_color,
bg_color,
((0, idx), (MAX_COLS - 1, idx)),
false,
);
for x in x..MAX_COLS {
content[(x, idx)].set_ch(' ');
content[(x, idx)].set_bg(bg_color);
self.content[(x, idx)].set_ch(' ');
self.content[(x, idx)].set_bg(bg_color);
}
match iter.peek() {
@ -230,7 +229,7 @@ impl MailListing {
for y in 0..=self.length {
if idx >= self.length {
/* No more entries left, so fill the rest of the area with empty space */
clear_area(&mut content, ((0, y), (MAX_COLS - 1, self.length)));
clear_area(&mut self.content, ((0, y), (MAX_COLS - 1, self.length)));
break;
}
/* Write an entire line for each envelope entry. */
@ -274,7 +273,7 @@ impl MailListing {
};
let (x, y) = write_string_to_grid(
&MailListing::make_entry_string(envelope, idx),
&mut content,
&mut self.content,
fg_color,
bg_color,
((0, y), (MAX_COLS - 1, y)),
@ -282,15 +281,13 @@ impl MailListing {
);
for x in x..MAX_COLS {
content[(x, y)].set_ch(' ');
content[(x, y)].set_bg(bg_color);
self.content[(x, y)].set_ch(' ');
self.content[(x, y)].set_bg(bg_color);
}
idx += 1;
}
}
self.content = content;
}
fn highlight_line_self(&mut self, idx: usize, context: &Context) {
@ -636,8 +633,7 @@ impl Component for MailListing {
/*
* tx sends to input-thread and it kills itself.
*/
let tx = context.input_thread();
tx.send(true);
context.input_kill();
}
let mut f = create_temp_file(&new_draft(context), None);
//let mut f = Box::new(std::fs::File::create(&dir).unwrap());

View File

@ -20,37 +20,20 @@
*/
use super::*;
use std::io::Write;
use std::process::{Command, Stdio};
pub struct ThreadView {
pager: Pager,
bytes: Vec<u8>,
dirty: bool,
coordinates: (usize, usize, usize),
}
impl ThreadView {
pub fn new(bytes: Vec<u8>) -> Self {
let mut html_filter = Command::new("w3m")
.args(&["-I", "utf-8", "-T", "text/html"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start html filter process");
html_filter
.stdin
.as_mut()
.unwrap()
.write_all(&bytes)
.expect("Failed to write to w3m stdin");
let mut display_text =
String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n");
display_text.push_str(&String::from_utf8_lossy(
&html_filter.wait_with_output().unwrap().stdout,
));
let buf = MailView::plain_text_to_buf(&display_text, true);
let pager = Pager::from_buf(&buf, None);
ThreadView { pager, bytes }
pub fn new(coordinates: (usize, usize, usize),
) -> Self {
ThreadView {
dirty: true,
coordinates,
}
}
}
@ -63,39 +46,95 @@ impl fmt::Display for ThreadView {
impl Component for ThreadView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
self.pager.draw(grid, area, context);
}
fn process_event(&mut self, event: &UIEvent, context: &mut Context) {
match event.event_type {
UIEventType::Input(Key::Char('v')) => {
// TODO: Optional filter that removes outgoing resource requests (images and
// scripts)
let binary = query_default_app("text/html");
if let Ok(binary) = binary {
let mut p = create_temp_file(&self.bytes, None);
Command::new(&binary)
.arg(p.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap_or_else(|_| panic!("Failed to start {}", binary.display()));
context.temp_files.push(p);
} else {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusNotification(format!(
"Couldn't find a default application for html files."
)),
});
}
return;
}
_ => {}
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let mailbox = &mut context.accounts[self.coordinates.0][self.coordinates.1].as_ref().unwrap();
let threads = &mailbox.threads;
let container = &threads.containers()[threads.root_set()[self.coordinates.2]];
let i = if let Some(i) = container.message() {
i
} else {
threads.containers()[
container.first_child().unwrap()
].message().unwrap()
};
let envelope: &Envelope = &mailbox.collection[i];
let (x, y) = write_string_to_grid(
&format!("Date: {}", envelope.date_as_str()),
grid,
Color::Byte(33),
Color::Default,
area,
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
self.pager.process_event(event, context);
let (x, y) = write_string_to_grid(
&format!("From: {}", envelope.from_to_string()),
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
let (x, y) = write_string_to_grid(
&format!("To: {}", envelope.to_to_string()),
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
let (x, y) = write_string_to_grid(
&format!("Subject: {}", envelope.subject()),
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
let (x, y) = write_string_to_grid(
&format!("Message-ID: <{}>", envelope.message_id_raw()),
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 2)));
context
.dirty_areas
.push_back((upper_left, set_y(bottom_right, y + 1)));
}
fn process_event(&mut self, _event: &UIEvent, _context: &mut Context) {
}
fn is_dirty(&self) -> bool {
self.pager.is_dirty()
self.dirty
}
fn set_dirty(&mut self) {
self.dirty = true;
}
fn set_dirty(&mut self) {}
}

View File

@ -193,6 +193,12 @@ pub struct Pager {
content: CellBuffer,
}
impl Default for Pager {
fn default() -> Self {
Pager::from_str("", None)
}
}
impl fmt::Display for Pager {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display info
@ -201,6 +207,19 @@ impl fmt::Display for Pager {
}
impl Pager {
pub fn update_from_string(&mut self, text: String) -> () {
let lines: Vec<&str> = text.trim().split('\n').collect();
let height = lines.len() + 1;
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0);
let mut content = CellBuffer::new(width, height, Cell::with_char(' '));
//interpret_format_flowed(&text);
Pager::print_string(&mut content, &text);
self.content = content;
self.height = height;
self.width = width;
self.dirty = true;
self.cursor_pos = 0;
}
pub fn from_string(mut text: String, context: &mut Context, cursor_pos: Option<usize>) -> Self {
let pager_filter: Option<&String> = context.settings.pager.filter.as_ref();
//let format_flowed: bool = context.settings.pager.format_flowed;

View File

@ -29,7 +29,7 @@
*/
use super::*;
use chan::Sender;
use chan::{Receiver, Sender};
use fnv::FnvHashMap;
use std::io::Write;
use std::thread;
@ -38,6 +38,36 @@ use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use termion::{clear, cursor, style};
struct InputHandler {
rx: Receiver<bool>,
tx: Sender<bool>,
}
impl InputHandler {
fn restore(&self, tx: Sender<ThreadEvent>) {
let stdin = std::io::stdin();
let rx = self.rx.clone();
thread::Builder::new()
.name("input-thread".to_string())
.spawn(move || {
get_events(
stdin,
|k| {
tx.send(ThreadEvent::Input(k));
},
|| {
tx.send(ThreadEvent::UIEvent(UIEventType::ChangeMode(UIMode::Fork)));
},
&rx,
)
})
.unwrap();
}
fn kill(&self) {
self.tx.send(false);
}
}
/// A context container for loaded settings, accounts, UI changes, etc.
pub struct Context {
pub accounts: Vec<Account>,
@ -50,8 +80,10 @@ pub struct Context {
/// Events queue that components send back to the state
pub replies: VecDeque<UIEvent>,
sender: Sender<ThreadEvent>,
receiver: Receiver<ThreadEvent>,
input: InputHandler,
input_thread: chan::Sender<bool>,
pub temp_files: Vec<File>,
}
@ -59,8 +91,11 @@ impl Context {
pub fn replies(&mut self) -> Vec<UIEvent> {
self.replies.drain(0..).collect()
}
pub fn input_thread(&mut self) -> &mut chan::Sender<bool> {
&mut self.input_thread
pub fn input_kill(&self) {
self.input.kill();
}
pub fn restore_input(&self) {
self.input.restore(self.sender.clone());
}
}
@ -74,7 +109,6 @@ pub struct State<W: Write> {
stdout: Option<termion::screen::AlternateScreen<termion::raw::RawTerminal<W>>>,
child: Option<ForkType>,
pub mode: UIMode,
sender: Sender<ThreadEvent>,
entities: Vec<Entity>,
pub context: Context,
@ -99,7 +133,16 @@ impl<W: Write> Drop for State<W> {
}
impl State<std::io::Stdout> {
pub fn new(sender: Sender<ThreadEvent>, input_thread: chan::Sender<bool>) -> Self {
pub fn new() -> Self {
/* Create a channel to communicate with other threads. The main process is the sole receiver.
* */
let (sender, receiver) = chan::sync(::std::mem::size_of::<ThreadEvent>());
/*
* Create async channel to block the input-thread if we need to fork and stop it from reading
* stdin, see get_events() for details
* */
let input_thread = chan::async();
let _stdout = std::io::stdout();
_stdout.lock();
let backends = Backends::new();
@ -149,7 +192,6 @@ impl State<std::io::Stdout> {
stdout: Some(stdout),
child: None,
mode: UIMode::Normal,
sender,
entities: Vec::with_capacity(1),
context: Context {
@ -162,7 +204,12 @@ impl State<std::io::Stdout> {
replies: VecDeque::with_capacity(5),
temp_files: Vec::new(),
input_thread,
sender,
receiver,
input: InputHandler {
rx: input_thread.1,
tx: input_thread.0,
},
},
startup_thread: Some(startup_tx.clone()),
threads: FnvHashMap::with_capacity_and_hasher(1, Default::default()),
@ -181,11 +228,12 @@ impl State<std::io::Stdout> {
for (y, folder) in account.backend.folders().iter().enumerate() {
s.context.mailbox_hashes.insert(folder.hash(), (x, y));
}
let sender = s.sender.clone();
let sender = s.context.sender.clone();
account.watch(RefreshEventConsumer::new(Box::new(move |r| {
sender.send(ThreadEvent::from(r));
})));
}
s.restore_input();
s
}
/*
@ -198,7 +246,7 @@ impl State<std::io::Stdout> {
self.context.accounts[idxa].reload(idxm);
let (startup_tx, startup_rx) = chan::async();
let startup_thread = {
let sender = self.sender.clone();
let sender = self.context.sender.clone();
let startup_rx = startup_rx.clone();
thread::Builder::new()
@ -257,7 +305,7 @@ impl State<std::io::Stdout> {
).unwrap();
self.flush();
self.stdout = None;
self.context.input_thread.send(false);
self.context.input.kill();
}
pub fn switch_to_alternate_screen(&mut self) {
let s = std::io::stdout();
@ -274,6 +322,13 @@ impl State<std::io::Stdout> {
}
}
impl<W: Write> State<W> {
pub fn receiver(&self) -> Receiver<ThreadEvent> {
self.context.receiver.clone()
}
pub fn restore_input(&mut self) {
self.context.restore_input();
}
/// On `SIGWNICH` the `State` redraws itself according to the new terminal size.
pub fn update_size(&mut self) {
let termsize = termion::terminal_size().ok();

View File

@ -20,8 +20,9 @@
*/
use std;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::io::{Write, Read};
use std::path::PathBuf;
use uuid::Uuid;
@ -50,6 +51,13 @@ impl File {
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn read_to_string(&self) -> String {
let mut buf = Vec::new();
let mut f = fs::File::open(&self.path).expect(&format!("Can't open {}", &self.path.display()));
f.read_to_end(&mut buf).expect(&format!("Can't read {}", &self.path.display()));
String::from_utf8(buf).unwrap()
}
}
/// Returned `File` will be deleted when dropped, so make sure to add it on `context.temp_files`

View File

@ -75,6 +75,24 @@ macro_rules! height {
};
}
/// Get an area's width
///
/// Example:
/// ```
/// # #[macro_use] extern crate ui; fn main() {
/// use ui::*;
///
/// let new_area = ((0, 0), (1, 1));
/// assert_eq!(width!(new_area), 1);
/// # }
/// ```
#[macro_export]
macro_rules! width{
($a:expr) => {
(get_x(bottom_right!($a))).saturating_sub(get_x(upper_left!($a)))
};
}
/// Get the upper left Position of an area
///
/// Example: