ui: option to embed editor in composing tab

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.
jmap
Manos Pitsidianakis 2019-11-05 08:35:07 +02:00
parent 99da9a35b6
commit 599bda9f28
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
4 changed files with 274 additions and 28 deletions

View File

@ -200,6 +200,10 @@ and the account options
command to pipe new mail to, exit code must be 0 for success. command to pipe new mail to, exit code must be 0 for success.
.It Cm editor_cmd Ar String .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. 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 .El
.Sh SHORTCUTS .Sh SHORTCUTS
Shortcuts can take the following values: Shortcuts can take the following values:

View File

@ -21,9 +21,12 @@
use super::*; use super::*;
use crate::terminal::embed::EmbedGrid;
use melib::Draft; use melib::Draft;
use mime_apps::query_mime_info; use mime_apps::query_mime_info;
use nix::sys::wait::WaitStatus;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex};
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum Cursor { enum Cursor {
@ -32,6 +35,31 @@ enum Cursor {
//Attachments, //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)] #[derive(Debug)]
pub struct Composer { pub struct Composer {
reply_context: Option<((usize, usize), Box<ThreadView>)>, // (folder_index, thread_node_index) reply_context: Option<((usize, usize), Box<ThreadView>)>, // (folder_index, thread_node_index)
@ -44,6 +72,9 @@ pub struct Composer {
form: FormWidget, form: FormWidget,
mode: ViewMode, mode: ViewMode,
embed_area: Area,
embed: Option<EmbedStatus>,
sign_mail: ToggleFlag, sign_mail: ToggleFlag,
dirty: bool, dirty: bool,
has_changes: bool, has_changes: bool,
@ -67,6 +98,8 @@ impl Default for Composer {
sign_mail: ToggleFlag::Unset, sign_mail: ToggleFlag::Unset,
dirty: true, dirty: true,
has_changes: false, has_changes: false,
embed_area: ((0, 0), (0, 0)),
embed: None,
initialized: false, initialized: false,
id: ComponentId::new_v4(), id: ComponentId::new_v4(),
} }
@ -77,6 +110,7 @@ impl Default for Composer {
enum ViewMode { enum ViewMode {
Discard(Uuid, Selector<char>), Discard(Uuid, Selector<char>),
Edit, Edit,
Embed,
SelectRecipients(Selector<Address>), SelectRecipients(Selector<Address>),
ThreadView, ThreadView,
} }
@ -97,6 +131,14 @@ impl ViewMode {
false false
} }
} }
fn is_embed(&self) -> bool {
if let ViewMode::Embed = self {
true
} else {
false
}
}
} }
impl fmt::Display for Composer { impl fmt::Display for Composer {
@ -211,10 +253,9 @@ impl Composer {
fn update_draft(&mut self) { fn update_draft(&mut self) {
let header_values = self.form.values_mut(); let header_values = self.form.values_mut();
let draft_header_map = self.draft.headers_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() { for (k, v) in draft_header_map.iter_mut() {
if let Some(vn) = header_values.remove(k) { if let Some(ref vn) = header_values.get(k) {
std::mem::swap(v, &mut vn.into_string()); *v = vn.as_str().to_string();
} }
} }
} }
@ -504,8 +545,44 @@ impl Component for Composer {
/* Regardless of view mode, do the following */ /* Regardless of view mode, do the following */
self.form.draw(grid, header_area, context); self.form.draw(grid, header_area, context);
self.pager.set_dirty(); if let Some(ref mut embed_pty) = self.embed {
self.pager.draw(grid, body_area, context); 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 { if self.cursor == Cursor::Body {
change_colors( change_colors(
grid, grid,
@ -529,7 +606,7 @@ impl Component for Composer {
} }
match self.mode { match self.mode {
ViewMode::ThreadView | ViewMode::Edit => {} ViewMode::ThreadView | ViewMode::Edit | ViewMode::Embed => {}
ViewMode::SelectRecipients(ref mut s) => { ViewMode::SelectRecipients(ref mut s) => {
s.draw(grid, center_area(area, s.content.size()), context); s.draw(grid, center_area(area, s.content.size()), context);
} }
@ -685,6 +762,15 @@ impl Component for Composer {
match *event { match *event {
UIEvent::Resize => { UIEvent::Resize => {
self.set_dirty(); 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. */ /* Switch e-mail From: field to the `left` configured account. */
@ -744,9 +830,108 @@ impl Component for Composer {
} }
return true; 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')) => { UIEvent::Input(Key::Char('e')) => {
/* Edit draft in $EDITOR */ /* Edit draft in $EDITOR */
use std::process::{Command, Stdio};
let settings = &context.settings; let settings = &context.settings;
let editor = if let Some(editor_cmd) = settings.composing.editor_cmd.as_ref() { let editor = if let Some(editor_cmd) = settings.composing.editor_cmd.as_ref() {
editor_cmd.to_string() editor_cmd.to_string()
@ -763,10 +948,6 @@ impl Component for Composer {
Ok(v) => v, 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 */ /* update Draft's headers based on form values */
self.update_draft(); self.update_draft();
let f = create_temp_file( let f = create_temp_file(
@ -776,6 +957,28 @@ impl Component for Composer {
true, 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 parts = split_command!(editor);
let (cmd, args) = (parts[0], &parts[1..]); let (cmd, args) = (parts[0], &parts[1..]);
if let Err(e) = Command::new(cmd) if let Err(e) = Command::new(cmd)
@ -794,15 +997,27 @@ impl Component for Composer {
context.restore_input(); context.restore_input();
return true; return true;
} }
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;
}
self.draft = new_draft;
self.initialized = false;
context.replies.push_back(UIEvent::Fork(ForkType::Finished)); context.replies.push_back(UIEvent::Fork(ForkType::Finished));
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;
context.restore_input(); context.restore_input();
self.dirty = true; self.dirty = true;
return true; return true;
@ -866,14 +1081,19 @@ impl Component for Composer {
} }
fn is_dirty(&self) -> bool { fn is_dirty(&self) -> bool {
self.dirty match self.mode {
|| self.pager.is_dirty() ViewMode::Embed => true,
|| self _ => {
.reply_context self.dirty
.as_ref() || self.pager.is_dirty()
.map(|(_, p)| p.is_dirty()) || self
.unwrap_or(false) .reply_context
|| self.form.is_dirty() .as_ref()
.map(|(_, p)| p.is_dirty())
.unwrap_or(false)
|| self.form.is_dirty()
}
}
} }
fn set_dirty(&mut self) { fn set_dirty(&mut self) {

View File

@ -57,6 +57,13 @@ impl Field {
self.as_str().is_empty() self.as_str().is_empty()
} }
pub fn to_string(&self) -> String {
match self {
Text(ref s, _) => s.as_str().to_string(),
Choice(ref v, ref cursor) => v[*cursor].clone(),
}
}
pub fn into_string(self) -> String { pub fn into_string(self) -> String {
match self { match self {
Text(s, _) => s.into_string(), Text(s, _) => s.into_string(),

View File

@ -18,13 +18,28 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>. * along with meli. If not, see <http://www.gnu.org/licenses/>.
*/ */
use super::default_vals::{false_val, none};
/// Settings for writing and sending new e-mail /// Settings for writing and sending new e-mail
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ComposingSettings { pub struct ComposingSettings {
/// A command to pipe new emails to /// A command to pipe new emails to
/// Required /// Required
pub mailer_cmd: String, pub mailer_cmd: 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. /// 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.
#[serde(default = "none")]
pub editor_cmd: Option<String>, pub editor_cmd: Option<String>,
/// Embed editor (for terminal interfaces) instead of forking and waiting.
#[serde(default = "false_val")]
pub embed: bool,
}
impl Default for ComposingSettings {
fn default() -> Self {
ComposingSettings {
mailer_cmd: String::new(),
editor_cmd: None,
embed: false,
}
}
} }