Browse Source

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.
tags/pre-alpha-0.4.0
Manos Pitsidianakis 2 years ago
parent
commit
599bda9f28
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 4
      meli.conf.5
  2. 270
      ui/src/components/mail/compose.rs
  3. 7
      ui/src/components/utilities/widgets.rs
  4. 17
      ui/src/conf/composing.rs

4
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:

270
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 {
@ -33,6 +36,31 @@ enum Cursor {
}
#[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)
account_cursor: usize,
@ -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;
}
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;
@ -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) {

7
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(),

17
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 <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…
Cancel
Save