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
parent
99da9a35b6
commit
599bda9f28
|
@ -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:
|
||||
|
|
|
@ -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<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)
|
||||
|
@ -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,
|
||||
|
@ -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<char>),
|
||||
Edit,
|
||||
Embed,
|
||||
SelectRecipients(Selector<Address>),
|
||||
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) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -18,13 +18,28 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* 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
|
||||
#[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<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue