mirror of
https://git.meli.delivery/meli/meli
synced 2024-11-03 09:40:15 +00:00
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.
This commit is contained in:
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.
|
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:
|
||||||
|
@ -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);
|
||||||
|
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.set_dirty();
|
||||||
self.pager.draw(grid, body_area, context);
|
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;
|
||||||
}
|
}
|
||||||
|
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
|
||||||
let result = f.read_to_string();
|
let result = f.read_to_string();
|
||||||
let mut new_draft = Draft::from_str(result.as_str()).unwrap();
|
match Draft::from_str(result.as_str()) {
|
||||||
|
Ok(mut new_draft) => {
|
||||||
std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut());
|
std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut());
|
||||||
if self.draft != new_draft {
|
if self.draft != new_draft {
|
||||||
self.has_changes = true;
|
self.has_changes = true;
|
||||||
}
|
}
|
||||||
self.draft = new_draft;
|
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.initialized = false;
|
||||||
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
|
|
||||||
context.restore_input();
|
context.restore_input();
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
return true;
|
return true;
|
||||||
@ -866,6 +1081,9 @@ impl Component for Composer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self) -> bool {
|
fn is_dirty(&self) -> bool {
|
||||||
|
match self.mode {
|
||||||
|
ViewMode::Embed => true,
|
||||||
|
_ => {
|
||||||
self.dirty
|
self.dirty
|
||||||
|| self.pager.is_dirty()
|
|| self.pager.is_dirty()
|
||||||
|| self
|
|| self
|
||||||
@ -875,6 +1093,8 @@ impl Component for Composer {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|| self.form.is_dirty()
|
|| self.form.is_dirty()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn set_dirty(&mut self) {
|
fn set_dirty(&mut self) {
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
|
@ -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(),
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user