Skip to content

Commit

Permalink
ui: option to embed editor in composing tab
Browse files Browse the repository at this point in the history
Add configuration option to embed editor in the composing tab instead of
executing and waiting for it.

Set embed = true in Composing section of your configuration to activate.
  • Loading branch information
epilys committed Nov 5, 2019
1 parent 99da9a3 commit 599bda9
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 26 deletions.
4 changes: 4 additions & 0 deletions meli.conf.5
Expand Up @@ -200,6 +200,10 @@ and the account options
command to pipe new mail to, exit code must be 0 for success.
.It Cm editor_cmd Ar String
command to launch editor. Can have arguments. Draft filename is given as the last argument. If it's missing, the environment variable $EDITOR is looked up.
.It Cm embed Ar boolean
(optional) embed editor within meli
.\" default value
.Pq Em false
.El
.Sh SHORTCUTS
Shortcuts can take the following values:
Expand Down
270 changes: 245 additions & 25 deletions ui/src/components/mail/compose.rs
Expand Up @@ -21,9 +21,12 @@

use super::*;

use crate::terminal::embed::EmbedGrid;
use melib::Draft;
use mime_apps::query_mime_info;
use nix::sys::wait::WaitStatus;
use std::str::FromStr;
use std::sync::{Arc, Mutex};

#[derive(Debug, PartialEq)]
enum Cursor {
Expand All @@ -32,6 +35,31 @@ enum Cursor {
//Attachments,
}

#[derive(Debug)]
enum EmbedStatus {
Stopped(Arc<Mutex<EmbedGrid>>, File),
Running(Arc<Mutex<EmbedGrid>>, File),
}

impl std::ops::Deref for EmbedStatus {
type Target = Arc<Mutex<EmbedGrid>>;
fn deref(&self) -> &Arc<Mutex<EmbedGrid>> {
use EmbedStatus::*;
match self {
Stopped(ref e, _) | Running(ref e, _) => e,
}
}
}

impl std::ops::DerefMut for EmbedStatus {
fn deref_mut(&mut self) -> &mut Arc<Mutex<EmbedGrid>> {
use EmbedStatus::*;
match self {
Stopped(ref mut e, _) | Running(ref mut e, _) => e,
}
}
}

#[derive(Debug)]
pub struct Composer {
reply_context: Option<((usize, usize), Box<ThreadView>)>, // (folder_index, thread_node_index)
Expand All @@ -44,6 +72,9 @@ pub struct Composer {
form: FormWidget,

mode: ViewMode,

embed_area: Area,
embed: Option<EmbedStatus>,
sign_mail: ToggleFlag,
dirty: bool,
has_changes: bool,
Expand All @@ -67,6 +98,8 @@ impl Default for Composer {
sign_mail: ToggleFlag::Unset,
dirty: true,
has_changes: false,
embed_area: ((0, 0), (0, 0)),
embed: None,
initialized: false,
id: ComponentId::new_v4(),
}
Expand All @@ -77,6 +110,7 @@ impl Default for Composer {
enum ViewMode {
Discard(Uuid, Selector<char>),
Edit,
Embed,
SelectRecipients(Selector<Address>),
ThreadView,
}
Expand All @@ -97,6 +131,14 @@ impl ViewMode {
false
}
}

fn is_embed(&self) -> bool {
if let ViewMode::Embed = self {
true
} else {
false
}
}
}

impl fmt::Display for Composer {
Expand Down Expand Up @@ -211,10 +253,9 @@ impl Composer {
fn update_draft(&mut self) {
let header_values = self.form.values_mut();
let draft_header_map = self.draft.headers_mut();
/* avoid extra allocations by updating values instead of inserting */
for (k, v) in draft_header_map.iter_mut() {
if let Some(vn) = header_values.remove(k) {
std::mem::swap(v, &mut vn.into_string());
if let Some(ref vn) = header_values.get(k) {
*v = vn.as_str().to_string();
}
}
}
Expand Down Expand Up @@ -504,8 +545,44 @@ impl Component for Composer {

/* Regardless of view mode, do the following */
self.form.draw(grid, header_area, context);
self.pager.set_dirty();
self.pager.draw(grid, body_area, context);
if let Some(ref mut embed_pty) = self.embed {
let body_area = (upper_left!(header_area), bottom_right!(body_area));
clear_area(grid, body_area);
match embed_pty {
EmbedStatus::Running(_, _) => {
let mut guard = embed_pty.lock().unwrap();
copy_area(
grid,
&guard.grid,
body_area,
((0, 0), pos_dec(guard.terminal_size, (1, 1))),
);
guard.set_terminal_size((width!(body_area), height!(body_area)));
context.dirty_areas.push_back(area);
self.dirty = false;
return;
}
EmbedStatus::Stopped(_, _) => {
write_string_to_grid(
"process has stopped, press 'e' to re-activate",
grid,
Color::Default,
Color::Default,
Attr::Default,
body_area,
false,
);
context.dirty_areas.push_back(body_area);
self.dirty = false;
return;
}
}
} else {
self.embed_area = (upper_left!(header_area), bottom_right!(body_area));
self.pager.set_dirty();
self.pager.draw(grid, body_area, context);
}

if self.cursor == Cursor::Body {
change_colors(
grid,
Expand All @@ -529,7 +606,7 @@ impl Component for Composer {
}

match self.mode {
ViewMode::ThreadView | ViewMode::Edit => {}
ViewMode::ThreadView | ViewMode::Edit | ViewMode::Embed => {}
ViewMode::SelectRecipients(ref mut s) => {
s.draw(grid, center_area(area, s.content.size()), context);
}
Expand Down Expand Up @@ -685,6 +762,15 @@ impl Component for Composer {
match *event {
UIEvent::Resize => {
self.set_dirty();
if let Some(ref mut embed_pty) = self.embed {
match embed_pty {
EmbedStatus::Running(_, _) => {
let mut guard = embed_pty.lock().unwrap();
guard.grid.clear(Cell::default());
}
_ => {}
}
}
}
/*
/* Switch e-mail From: field to the `left` configured account. */
Expand Down Expand Up @@ -744,9 +830,108 @@ impl Component for Composer {
}
return true;
}
UIEvent::EmbedInput((Key::Ctrl('z'), _)) => {
self.embed.as_ref().unwrap().lock().unwrap().stop();
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
self.dirty = true;
}
UIEvent::EmbedInput((ref k, ref b)) => {
use std::io::Write;
if let Some(ref mut embed) = self.embed {
let mut embed_guard = embed.lock().unwrap();
if embed_guard.stdin.write_all(b).is_err() {
match embed_guard.is_active() {
Ok(WaitStatus::Exited(_, exit_code)) => {
drop(embed_guard);
if exit_code != 0 {
context.replies.push_back(UIEvent::Notification(
None,
format!(
"Subprocess has exited with exit code {}",
exit_code
),
Some(NotificationType::ERROR),
));
} else if let EmbedStatus::Running(_, f) = embed {
let result = f.read_to_string();
match Draft::from_str(result.as_str()) {
Ok(mut new_draft) => {
std::mem::swap(
self.draft.attachments_mut(),
new_draft.attachments_mut(),
);
if self.draft != new_draft {
self.has_changes = true;
}
self.draft = new_draft;
}
Err(_) => {
context.replies.push_back(UIEvent::Notification(
None,
"Could not parse draft headers correctly. The invalid text has been set as the body of your draft".to_string(),
Some(NotificationType::ERROR),
));
self.draft.set_body(result);
self.has_changes = true;
}
}
self.initialized = false;
}
self.embed = None;
self.mode = ViewMode::Edit;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
}
Ok(WaitStatus::Stopped(_, _)) => {
drop(embed_guard);
match self.embed.take() {
Some(EmbedStatus::Running(e, f))
| Some(EmbedStatus::Stopped(e, f)) => {
self.embed = Some(EmbedStatus::Stopped(e, f));
}
_ => {}
}
self.dirty = true;
return true;
}
Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::StillAlive) => {
context
.replies
.push_back(UIEvent::EmbedInput((k.clone(), b.to_vec())));
return true;
}
e => {
context.replies.push_back(UIEvent::Notification(
None,
format!("Subprocess has exited with reason {:?}", e),
Some(NotificationType::ERROR),
));
drop(embed_guard);
self.embed = None;
self.mode = ViewMode::Edit;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
}
}
}
}
self.set_dirty();
return true;
}
UIEvent::Input(Key::Char('e')) if self.mode.is_embed() => {
self.embed.as_ref().unwrap().lock().unwrap().wake_up();
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
self.set_dirty();
return true;
}
UIEvent::Input(Key::Char('e')) => {
/* Edit draft in $EDITOR */
use std::process::{Command, Stdio};
let settings = &context.settings;
let editor = if let Some(editor_cmd) = settings.composing.editor_cmd.as_ref() {
editor_cmd.to_string()
Expand All @@ -763,10 +948,6 @@ impl Component for Composer {
Ok(v) => v,
}
};
/* Kill input thread so that spawned command can be sole receiver of stdin */
{
context.input_kill();
}
/* update Draft's headers based on form values */
self.update_draft();
let f = create_temp_file(
Expand All @@ -776,6 +957,28 @@ impl Component for Composer {
true,
);

if settings.composing.embed {
self.embed = Some(EmbedStatus::Running(
crate::terminal::embed::create_pty(
self.embed_area,
[editor, f.path().display().to_string()].join(" "),
)
.unwrap(),
f,
));
self.dirty = true;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
self.mode = ViewMode::Embed;
return true;
}
use std::process::{Command, Stdio};
/* Kill input thread so that spawned command can be sole receiver of stdin */
{
context.input_kill();
}

let parts = split_command!(editor);
let (cmd, args) = (parts[0], &parts[1..]);
if let Err(e) = Command::new(cmd)
Expand All @@ -794,15 +997,27 @@ impl Component for Composer {
context.restore_input();
return true;
}
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
let result = f.read_to_string();
let mut new_draft = Draft::from_str(result.as_str()).unwrap();
std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut());
if self.draft != new_draft {
self.has_changes = true;
match Draft::from_str(result.as_str()) {
Ok(mut new_draft) => {
std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut());
if self.draft != new_draft {
self.has_changes = true;
}
self.draft = new_draft;
}
Err(_) => {
context.replies.push_back(UIEvent::Notification(
None,
"Could not parse draft headers correctly. The invalid text has been set as the body of your draft".to_string(),
Some(NotificationType::ERROR),
));
self.draft.set_body(result);
self.has_changes = true;
}
}
self.draft = new_draft;
self.initialized = false;
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
context.restore_input();
self.dirty = true;
return true;
Expand Down Expand Up @@ -866,14 +1081,19 @@ 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)
|| self.form.is_dirty()
match self.mode {
ViewMode::Embed => true,
_ => {
self.dirty
|| self.pager.is_dirty()
|| self
.reply_context
.as_ref()
.map(|(_, p)| p.is_dirty())
.unwrap_or(false)
|| self.form.is_dirty()
}
}
}

fn set_dirty(&mut self) {
Expand Down

0 comments on commit 599bda9

Please sign in to comment.