From 599bda9f28bb35dcfe4b8cd35dbb381b4002568d Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 5 Nov 2019 08:35:07 +0200 Subject: [PATCH] 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. --- meli.conf.5 | 4 + ui/src/components/mail/compose.rs | 274 ++++++++++++++++++++++--- ui/src/components/utilities/widgets.rs | 7 + ui/src/conf/composing.rs | 17 +- 4 files changed, 274 insertions(+), 28 deletions(-) diff --git a/meli.conf.5 b/meli.conf.5 index 560675ec..64f50176 100644 --- a/meli.conf.5 +++ b/meli.conf.5 @@ -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: diff --git a/ui/src/components/mail/compose.rs b/ui/src/components/mail/compose.rs index 0002b9f3..db6df6ef 100644 --- a/ui/src/components/mail/compose.rs +++ b/ui/src/components/mail/compose.rs @@ -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 { @@ -32,6 +35,31 @@ enum Cursor { //Attachments, } +#[derive(Debug)] +enum EmbedStatus { + Stopped(Arc>, File), + Running(Arc>, File), +} + +impl std::ops::Deref for EmbedStatus { + type Target = Arc>; + fn deref(&self) -> &Arc> { + use EmbedStatus::*; + match self { + Stopped(ref e, _) | Running(ref e, _) => e, + } + } +} + +impl std::ops::DerefMut for EmbedStatus { + fn deref_mut(&mut self) -> &mut Arc> { + use EmbedStatus::*; + match self { + Stopped(ref mut e, _) | Running(ref mut e, _) => e, + } + } +} + #[derive(Debug)] pub struct Composer { reply_context: Option<((usize, usize), Box)>, // (folder_index, thread_node_index) @@ -44,6 +72,9 @@ pub struct Composer { form: FormWidget, mode: ViewMode, + + embed_area: Area, + embed: Option, sign_mail: ToggleFlag, dirty: bool, has_changes: bool, @@ -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(), } @@ -77,6 +110,7 @@ impl Default for Composer { enum ViewMode { Discard(Uuid, Selector), Edit, + Embed, SelectRecipients(Selector
), ThreadView, } @@ -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 { @@ -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(); } } } @@ -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, @@ -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); } @@ -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. */ @@ -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() @@ -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( @@ -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) @@ -794,15 +997,27 @@ impl Component for Composer { context.restore_input(); 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)); + 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(); self.dirty = true; return true; @@ -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) { diff --git a/ui/src/components/utilities/widgets.rs b/ui/src/components/utilities/widgets.rs index 6d8c41db..7e0e5404 100644 --- a/ui/src/components/utilities/widgets.rs +++ b/ui/src/components/utilities/widgets.rs @@ -57,6 +57,13 @@ impl Field { 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 { match self { Text(s, _) => s.into_string(), diff --git a/ui/src/conf/composing.rs b/ui/src/conf/composing.rs index deded5a4..736641ee 100644 --- a/ui/src/conf/composing.rs +++ b/ui/src/conf/composing.rs @@ -18,13 +18,28 @@ * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ +use super::default_vals::{false_val, none}; /// Settings for writing and sending new e-mail -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct ComposingSettings { /// A command to pipe new emails to /// Required 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. + #[serde(default = "none")] pub editor_cmd: Option, + /// 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, + } + } }