terminal/embedded: overhaul refactor

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
pull/312/head
Manos Pitsidianakis 5 months ago
parent 54d21f25fd
commit 0a74c7d0e5
No known key found for this signature in database
GPG Key ID: 7729C7707F7E09D0

@ -46,9 +46,10 @@ pub struct ComposingSettings {
alias = "editor_cmd"
)]
pub editor_command: Option<String>,
/// Embed editor (for terminal interfaces) instead of forking and waiting.
#[serde(default = "false_val")]
pub embed: bool,
/// Embedded editor (for terminal interfaces) instead of forking and
/// waiting.
#[serde(default = "false_val", alias = "embed")]
pub embedded_pty: bool,
/// Set "format=flowed" in plain text attachments.
/// Default: true
#[serde(default = "true_val", alias = "format-flowed")]
@ -116,7 +117,7 @@ impl Default for ComposingSettings {
ComposingSettings {
send_mail: SendMail::ShellCommand("false".into()),
editor_command: None,
embed: false,
embedded_pty: false,
format_flowed: true,
insert_user_agent: true,
default_header_values: HashMap::default(),

@ -35,7 +35,7 @@ use melib::HeaderName;
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { ShortcutsOverride { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embed editor (for terminal interfaces) instead of forking and waiting."] # [serde (default)] pub embed : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Set User-Agent"] # [doc = " Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < HeaderName , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embed : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embedded editor (for terminal interfaces) instead of forking and"] # [doc = " waiting."] # [serde (alias = "embed")] # [serde (default)] pub embedded_pty : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Set User-Agent"] # [doc = " Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < HeaderName , String > > , # [doc = " Wrap header preample when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preample")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ToggleFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { ComposingSettingsOverride { send_mail : None , editor_command : None , embedded_pty : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } }
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { TagsSettingsOverride { colors : None , ignore_tags : None } } }

@ -37,7 +37,7 @@ use melib::{
use nix::sys::wait::WaitStatus;
use super::*;
use crate::{accounts::JobRequest, jobs::JoinHandle, terminal::embed::EmbedTerminal};
use crate::{accounts::JobRequest, jobs::JoinHandle, terminal::embedded::Terminal};
#[cfg(feature = "gpgme")]
pub mod gpg;
@ -57,41 +57,41 @@ enum Cursor {
}
#[derive(Debug)]
enum EmbedStatus {
Stopped(Arc<Mutex<EmbedTerminal>>, File),
Running(Arc<Mutex<EmbedTerminal>>, File),
struct EmbeddedPty {
running: bool,
terminal: Arc<Mutex<Terminal>>,
file: File,
}
impl EmbedStatus {
#[inline(always)]
impl EmbeddedPty {
#[inline]
fn is_stopped(&self) -> bool {
matches!(self, Self::Stopped(_, _))
!self.running
}
#[inline]
fn is_dirty(&self) -> bool {
std::ops::Deref::deref(self)
.try_lock()
.ok()
.map_or(true, |e| e.grid.is_dirty())
}
}
impl std::ops::Deref for EmbedStatus {
type Target = Arc<Mutex<EmbedTerminal>>;
impl std::ops::Deref for EmbeddedPty {
type Target = Arc<Mutex<Terminal>>;
fn deref(&self) -> &Self::Target {
match self {
Self::Stopped(ref e, _) | Self::Running(ref e, _) => e,
}
&self.terminal
}
}
impl std::ops::DerefMut for EmbedStatus {
impl std::ops::DerefMut for EmbeddedPty {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Stopped(ref mut e, _) | Self::Running(ref mut e, _) => e,
}
&mut self.terminal
}
}
#[derive(Debug)]
struct Embedded {
status: EmbedStatus,
}
#[derive(Debug)]
pub struct Composer {
reply_context: Option<(MailboxHash, EnvelopeHash)>,
@ -105,8 +105,8 @@ pub struct Composer {
mode: ViewMode,
embedded: Option<Embedded>,
embed_dimensions: (usize, usize),
embedded_pty: Option<EmbeddedPty>,
embedded_dimensions: (usize, usize),
#[cfg(feature = "gpgme")]
gpg_state: gpg::GpgComposeState,
dirty: bool,
@ -123,7 +123,7 @@ enum ViewMode {
// widget: EditAttachments,
//},
Edit,
Embed,
EmbeddedPty,
SelectRecipients(UIDialog<Address>),
#[cfg(feature = "gpgme")]
SelectEncryptKey(bool, gpg::KeySelection),
@ -185,8 +185,8 @@ impl Composer {
gpg_state: gpg::GpgComposeState::default(),
dirty: true,
has_changes: false,
embedded: None,
embed_dimensions: (80, 20),
embedded_pty: None,
embedded_dimensions: (80, 20),
initialized: false,
id: ComponentId::default(),
}
@ -955,72 +955,71 @@ impl Component for Composer {
self.form.draw(grid, header_area, context);
if let Some(ref mut embedded) = self.embedded {
let embed_pty = &mut embedded.status;
let embed_area = area;
match embed_pty {
EmbedStatus::Running(_, _) => {
if self.dirty {
let mut guard = embed_pty.lock().unwrap();
grid.clear_area(embed_area, theme_default);
grid.copy_area(guard.grid.buffer(), embed_area, guard.grid.area());
guard.set_terminal_size((embed_area.width(), embed_area.height()));
context.dirty_areas.push_back(embed_area);
self.dirty = false;
}
return;
}
EmbedStatus::Stopped(_, _) => {
if self.dirty {
let guard = embed_pty.lock().unwrap();
grid.copy_area(guard.grid.buffer(), embed_area, guard.grid.buffer().area());
grid.change_colors(embed_area, Color::Byte(8), theme_default.bg);
let our_map: ShortcutMap =
account_settings!(context[self.account_hash].shortcuts.composing)
.key_values();
let mut shortcuts: ShortcutMaps = Default::default();
shortcuts.insert(Shortcuts::COMPOSING, our_map);
let stopped_message: String =
format!("Process with PID {} has stopped.", guard.child_pid);
let stopped_message_2: String = format!(
"-press '{}' (edit shortcut) to re-activate.",
shortcuts[Shortcuts::COMPOSING]["edit"]
);
const STOPPED_MESSAGE_3: &str =
"-press Ctrl-C to forcefully kill it and return to editor.";
let max_len = std::cmp::max(
stopped_message.len(),
std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()),
if let Some(ref mut embedded_pty) = self.embedded_pty {
let embedded_area = area;
if embedded_pty.is_dirty() {
if embedded_pty.running {
let mut guard = embedded_pty.lock().unwrap();
grid.clear_area(embedded_area, theme_default);
grid.copy_area(guard.grid.buffer(), embedded_area, guard.grid.area());
guard.set_terminal_size((embedded_area.width(), embedded_area.height()));
guard.grid.set_dirty(false);
context.dirty_areas.push_back(embedded_area);
self.dirty = false;
} else {
let mut guard = embedded_pty.lock().unwrap();
grid.copy_area(
guard.grid.buffer(),
embedded_area,
guard.grid.buffer().area(),
);
grid.change_colors(embedded_area, Color::Byte(8), theme_default.bg);
let our_map: ShortcutMap =
account_settings!(context[self.account_hash].shortcuts.composing)
.key_values();
let mut shortcuts: ShortcutMaps = Default::default();
shortcuts.insert(Shortcuts::COMPOSING, our_map);
let stopped_message: String =
format!("Process with PID {} has stopped.", guard.child_pid);
let stopped_message_2: String = format!(
"-press '{}' (edit shortcut) to re-activate.",
shortcuts[Shortcuts::COMPOSING]["edit"]
);
const STOPPED_MESSAGE_3: &str =
"-press Ctrl-C to forcefully kill it and return to editor.";
let max_len = std::cmp::max(
stopped_message.len(),
std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()),
);
let inner_area = create_box(grid, area.center_inside((max_len + 5, 5)));
grid.clear_area(inner_area, theme_default);
for (i, l) in [
stopped_message.as_str(),
stopped_message_2.as_str(),
STOPPED_MESSAGE_3,
]
.iter()
.enumerate()
{
grid.write_string(
l,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
inner_area.skip_rows(i),
None,
);
let inner_area = create_box(grid, area.center_inside((max_len + 5, 5)));
grid.clear_area(inner_area, theme_default);
for (i, l) in [
stopped_message.as_str(),
stopped_message_2.as_str(),
STOPPED_MESSAGE_3,
]
.iter()
.enumerate()
{
grid.write_string(
l,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
inner_area.skip_rows(i),
None,
);
}
context.dirty_areas.push_back(area);
self.dirty = false;
}
return;
context.dirty_areas.push_back(area);
guard.grid.set_dirty(false);
self.dirty = false;
}
return;
}
} else {
self.embed_dimensions = (area.width(), area.height());
self.embedded_dimensions = (area.width(), area.height());
}
if self.pager.size().0 > body_area.width() {
@ -1038,7 +1037,7 @@ impl Component for Composer {
self.draw_attachments(grid, attachment_area, context);
//}
match self.mode {
ViewMode::Edit | ViewMode::Embed => {}
ViewMode::Edit | ViewMode::EmbeddedPty => {}
//ViewMode::EditAttachments { ref mut widget } => {
// let inner_area = create_box(grid, area);
// (EditAttachmentsRefMut {
@ -1502,40 +1501,33 @@ impl Component for Composer {
));
return true;
}
UIEvent::EmbedInput((Key::Ctrl('z'), _)) => {
self.embedded
.as_ref()
.unwrap()
.status
.lock()
.unwrap()
.stop();
match self.embedded.take() {
Some(Embedded {
status: EmbedStatus::Running(e, f),
})
| Some(Embedded {
status: EmbedStatus::Stopped(e, f),
}) => {
self.embedded = Some(Embedded {
status: EmbedStatus::Stopped(e, f),
});
}
_ => {}
UIEvent::EmbeddedInput((Key::Ctrl('z'), _)) => {
self.embedded_pty.as_ref().unwrap().lock().unwrap().stop();
if let Some(EmbeddedPty {
running: _,
terminal,
file,
}) = self.embedded_pty.take()
{
self.embedded_pty = Some(EmbeddedPty {
running: false,
terminal,
file,
});
}
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
self.set_dirty(true);
}
UIEvent::EmbedInput((ref k, ref b)) => {
if let Some(ref mut embed) = self.embedded {
let mut embed_guard = embed.status.lock().unwrap();
if embed_guard.write_all(b).is_err() {
match embed_guard.is_active() {
UIEvent::EmbeddedInput((ref k, ref b)) => {
if let Some(ref mut embedded) = self.embedded_pty {
let mut embedded_guard = embedded.lock().unwrap();
if embedded_guard.write_all(b).is_err() {
match embedded_guard.is_active() {
Ok(WaitStatus::Exited(_, exit_code)) => {
drop(embed_guard);
let embedded = self.embedded.take();
drop(embedded_guard);
let embedded_pty = self.embedded_pty.take();
if exit_code != 0 {
context.replies.push_back(UIEvent::Notification(
None,
@ -1547,9 +1539,11 @@ impl Component for Composer {
melib::error::ErrorKind::External,
)),
));
} else if let Some(Embedded {
status: EmbedStatus::Running(_, file),
}) = embedded
} else if let Some(EmbeddedPty {
running: true,
file,
..
}) = embedded_pty
{
self.update_from_file(file, context);
}
@ -1563,19 +1557,18 @@ impl Component for Composer {
#[cfg(any(target_os = "linux", target_os = "android"))]
Ok(WaitStatus::PtraceEvent(_, _, _))
| Ok(WaitStatus::PtraceSyscall(_)) => {
drop(embed_guard);
match self.embedded.take() {
Some(Embedded {
status: EmbedStatus::Running(e, f),
})
| Some(Embedded {
status: EmbedStatus::Stopped(e, f),
}) => {
self.embedded = Some(Embedded {
status: EmbedStatus::Stopped(e, f),
});
}
_ => {}
drop(embedded_guard);
if let Some(EmbeddedPty {
running: _,
terminal,
file,
}) = self.embedded_pty.take()
{
self.embedded_pty = Some(EmbeddedPty {
running: false,
terminal,
file,
});
}
self.mode = ViewMode::Edit;
context
@ -1585,19 +1578,18 @@ impl Component for Composer {
return true;
}
Ok(WaitStatus::Stopped(_, _)) => {
drop(embed_guard);
match self.embedded.take() {
Some(Embedded {
status: EmbedStatus::Running(e, f),
})
| Some(Embedded {
status: EmbedStatus::Stopped(e, f),
}) => {
self.embedded = Some(Embedded {
status: EmbedStatus::Stopped(e, f),
});
}
_ => {}
drop(embedded_guard);
if let Some(EmbeddedPty {
running: _,
terminal,
file,
}) = self.embedded_pty.take()
{
self.embedded_pty = Some(EmbeddedPty {
running: false,
terminal,
file,
});
}
self.mode = ViewMode::Edit;
context
@ -1609,13 +1601,13 @@ impl Component for Composer {
Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::StillAlive) => {
context
.replies
.push_back(UIEvent::EmbedInput((k.clone(), b.to_vec())));
drop(embed_guard);
.push_back(UIEvent::EmbeddedInput((k.clone(), b.to_vec())));
drop(embedded_guard);
self.set_dirty(true);
return true;
}
Ok(WaitStatus::Signaled(_, signal, _)) => {
drop(embed_guard);
drop(embedded_guard);
context.replies.push_back(UIEvent::Notification(
None,
format!("Subprocess was killed by {} signal", signal),
@ -1624,7 +1616,7 @@ impl Component for Composer {
)),
));
self.initialized = false;
self.embedded = None;
self.embedded_pty = None;
self.mode = ViewMode::Edit;
context
.replies
@ -1632,15 +1624,15 @@ impl Component for Composer {
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some("Embed editor crashed.".to_string()),
Some("Embedded editor crashed.".to_string()),
format!("Subprocess has exited with reason {}", &err),
Some(NotificationType::Error(
melib::error::ErrorKind::External,
)),
));
drop(embed_guard);
drop(embedded_guard);
self.initialized = false;
self.embedded = None;
self.embedded_pty = None;
self.mode = ViewMode::Edit;
context
.replies
@ -1745,53 +1737,43 @@ impl Component for Composer {
return true;
}
UIEvent::Input(ref key)
if self.embedded.is_some()
if self.embedded_pty.is_some()
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
{
self.embedded
.as_ref()
.unwrap()
.status
.lock()
.unwrap()
.wake_up();
match self.embedded.take() {
Some(Embedded {
status: EmbedStatus::Running(e, f),
})
| Some(Embedded {
status: EmbedStatus::Stopped(e, f),
}) => {
self.embedded = Some(Embedded {
status: EmbedStatus::Running(e, f),
});
}
_ => {}
if let Some(EmbeddedPty {
running: _,
terminal,
file,
}) = self.embedded_pty.take()
{
terminal.lock().unwrap().wake_up();
self.embedded_pty = Some(EmbeddedPty {
running: true,
terminal,
file,
});
}
self.mode = ViewMode::Embed;
self.mode = ViewMode::EmbeddedPty;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
.push_back(UIEvent::ChangeMode(UIMode::Embedded));
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Ctrl('c'))
if self.embedded.is_some()
&& self.embedded.as_ref().unwrap().status.is_stopped() =>
if self.embedded_pty.is_some()
&& self.embedded_pty.as_ref().unwrap().is_stopped() =>
{
match self.embedded.take() {
Some(Embedded {
status: EmbedStatus::Running(embed, file),
})
| Some(Embedded {
status: EmbedStatus::Stopped(embed, file),
}) => {
let guard = embed.lock().unwrap();
guard.wake_up();
guard.terminate();
self.update_from_file(file, context);
}
_ => {}
if let Some(EmbeddedPty {
running: _,
terminal,
file,
}) = self.embedded_pty.take()
{
let guard = terminal.lock().unwrap();
guard.wake_up();
guard.terminate();
self.update_from_file(file, context);
}
context.replies.push_back(UIEvent::Notification(
None,
@ -1857,30 +1839,31 @@ impl Component for Composer {
}
};
if *account_settings!(context[self.account_hash].composing.embed) {
match crate::terminal::embed::create_pty(
self.embed_dimensions.0,
self.embed_dimensions.1,
if *account_settings!(context[self.account_hash].composing.embedded_pty) {
match crate::terminal::embedded::create_pty(
self.embedded_dimensions.0,
self.embedded_dimensions.1,
[editor, f.path().display().to_string()].join(" "),
) {
Ok(embed) => {
self.embedded = Some(Embedded {
status: EmbedStatus::Running(embed, f),
Ok(terminal) => {
self.embedded_pty = Some(EmbeddedPty {
running: true,
terminal,
file: f,
});
self.set_dirty(true);
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
context.replies.push_back(UIEvent::Fork(ForkType::Embed(
self.embedded
.push_back(UIEvent::ChangeMode(UIMode::Embedded));
context.replies.push_back(UIEvent::Fork(ForkType::Embedded(
self.embedded_pty
.as_ref()
.unwrap()
.status
.lock()
.unwrap()
.child_pid,
)));
self.mode = ViewMode::Embed;
self.mode = ViewMode::EmbeddedPty;
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
@ -2157,7 +2140,14 @@ impl Component for Composer {
fn is_dirty(&self) -> bool {
match self.mode {
ViewMode::Embed => true,
ViewMode::EmbeddedPty => {
self.dirty
|| self
.embedded_pty
.as_ref()
.map(EmbeddedPty::is_dirty)
.unwrap_or(false)
}
//ViewMode::EditAttachments { ref widget } => {
// widget.dirty
// || widget.buttons.is_dirty()
@ -2206,7 +2196,14 @@ impl Component for Composer {
ViewMode::WaitingForSendResult(ref mut widget, _) => {
widget.set_dirty(value);
}
ViewMode::Edit | ViewMode::Embed => {}
ViewMode::Edit => {}
ViewMode::EmbeddedPty => {
if let Some(pty) = self.embedded_pty.as_ref() {
if let Ok(mut guard) = pty.try_lock() {
guard.grid.set_dirty(value);
}
}
}
}
//if let ViewMode::EditAttachments { ref mut widget } = self.mode {
// (EditAttachmentsRefMut {

@ -151,7 +151,7 @@ fn run_app(opt: Opt) -> Result<()> {
let signals = &[
/* Catch SIGWINCH to handle terminal resizing */
signal_hook::consts::SIGWINCH,
/* Catch SIGCHLD to handle embed applications status change */
/* Catch SIGCHLD to handle embedded applications status change */
signal_hook::consts::SIGCHLD,
];
@ -217,7 +217,7 @@ fn run_app(opt: Opt) -> Result<()> {
}
}
match r.unwrap() {
ThreadEvent::Input((Key::Ctrl('z'), _)) if state.mode != UIMode::Embed => {
ThreadEvent::Input((Key::Ctrl('z'), _)) if state.mode != UIMode::Embedded => {
state.switch_to_main_screen();
//_thread_handler.join().expect("Couldn't join on the associated thread");
let self_pid = nix::unistd::Pid::this();
@ -233,8 +233,8 @@ fn run_app(opt: Opt) -> Result<()> {
state.update_size();
state.render();
state.redraw();
if state.mode == UIMode::Embed {
state.rcv_event(UIEvent::EmbedInput(raw_input));
if state.mode == UIMode::Embedded {
state.rcv_event(UIEvent::EmbeddedInput(raw_input));
state.redraw();
}
},
@ -286,8 +286,8 @@ fn run_app(opt: Opt) -> Result<()> {
},
}
},
UIMode::Embed => {
state.rcv_event(UIEvent::EmbedInput((k,r)));
UIMode::Embedded => {
state.rcv_event(UIEvent::EmbeddedInput((k,r)));
state.redraw();
},
UIMode::Fork => {
@ -333,7 +333,7 @@ fn run_app(opt: Opt) -> Result<()> {
}
},
signal_hook::consts::SIGCHLD => {
state.rcv_event(UIEvent::EmbedInput((Key::Null, vec![0])));
state.rcv_event(UIEvent::EmbeddedInput((Key::Null, vec![0])));
state.redraw();
}

@ -301,7 +301,7 @@ impl Drop for State {
log::warn!("Failed to wait on subprocess {}: {}", child.id(), err);
}
}
if let Some(ForkType::Embed(child_pid)) = self.child.take() {
if let Some(ForkType::Embedded(child_pid)) = self.child.take() {
/* Try wait, we don't want to block */
if let Err(e) = waitpid(child_pid, Some(WaitPidFlag::WNOHANG)) {
log::warn!("Failed to wait on subprocess {}: {}", child_pid, e);

@ -437,7 +437,7 @@ impl Component for SVGScreenshotFilter {
} else if let UIEvent::CmdInput(Key::F(6)) = event {
self.save_screenshot = true;
true
} else if let UIEvent::EmbedInput((Key::F(6), _)) = event {
} else if let UIEvent::EmbeddedInput((Key::F(6), _)) = event {
self.save_screenshot = true;
false
} else {

@ -33,7 +33,7 @@ pub mod position;
pub mod cells;
#[macro_use]
pub mod keys;
pub mod embed;
pub mod embedded;
pub mod text_editing;
use std::io::{BufRead, Write};

@ -0,0 +1,192 @@
/*
* meli
*
* Copyright 2017-2020 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::{
ffi::{CString, OsStr},
os::unix::{
ffi::OsStrExt,
io::{AsRawFd, FromRawFd, IntoRawFd},
},
};
use melib::{error::*, log};
#[cfg(not(target_os = "macos"))]
use nix::{
fcntl::{open, OFlag},
pty::{grantpt, posix_openpt, ptsname, unlockpt},
sys::stat,
};
use nix::{
ioctl_none_bad, ioctl_write_ptr_bad,
libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
pty::Winsize,
unistd::{dup2, fork, ForkResult},
};
use smallvec::SmallVec;
pub mod escape_codes;
pub mod terminal;
#[cfg(not(target_os = "macos"))]
use std::path::Path;
use std::{
convert::TryFrom,
io::{Read, Write},
sync::{Arc, Mutex},
};
/// ioctl request code to "Make the given terminal the controlling terminal of
/// the calling process"
use libc::TIOCSCTTY;
/// ioctl request code to set window size of pty:
use libc::TIOCSWINSZ;
pub use terminal::{EmbeddedGrid, ScreenBuffer, Terminal};
ioctl_write_ptr_bad!(
/// Macro generated function that calls ioctl to set window size of backend
/// PTY end.
set_window_size,
TIOCSWINSZ,
Winsize
);
ioctl_none_bad!(
/// Set controling terminal fd for current session.
set_controlling_terminal,
TIOCSCTTY
);
/// Create a new pseudoterminal (PTY) with given width, size and execute
/// `command` in it.
pub fn create_pty(width: usize, height: usize, command: String) -> Result<Arc<Mutex<Terminal>>> {
#[cfg(not(target_os = "macos"))]
let (frontend_fd, backend_name) = {
// Open a new PTY frontend
let frontend_fd = posix_openpt(OFlag::O_RDWR)?;
// Allow a backend to be generated for it
grantpt(&frontend_fd)?;
unlockpt(&frontend_fd)?;
// Get the name of the backend
let backend_name = unsafe { ptsname(&frontend_fd) }?;
{
let winsize = Winsize {
ws_row: <u16>::try_from(height).unwrap(),
ws_col: <u16>::try_from(width).unwrap(),
ws_xpixel: 0,
ws_ypixel: 0,
};
let frontend_fd = frontend_fd.as_raw_fd();
unsafe { set_window_size(frontend_fd, &winsize)? };
}
(frontend_fd, backend_name)
};
#[cfg(target_os = "macos")]
let (frontend_fd, backend_fd) = {
let winsize = Winsize {
ws_row: <u16>::try_from(height).unwrap(),
ws_col: <u16>::try_from(width).unwrap(),
ws_xpixel: 0,
ws_ypixel: 0,
};
let ends = nix::pty::openpty(Some(&winsize), None)?;
(ends.master, ends.slave)
};
let child_pid = match unsafe { fork()? } {
ForkResult::Child => {
#[cfg(not(target_os = "macos"))]
/* Open backend end for pseudoterminal */
let backend_fd = open(Path::new(&backend_name), OFlag::O_RDWR, stat::Mode::empty())?;
// assign stdin, stdout, stderr to the pty
dup2(backend_fd, STDIN_FILENO).unwrap();
dup2(backend_fd, STDOUT_FILENO).unwrap();
dup2(backend_fd, STDERR_FILENO).unwrap();
/* Become session leader */
nix::unistd::setsid().unwrap();
match unsafe { set_controlling_terminal(backend_fd) } {
Ok(c) if c < 0 => {
log::error!(
"Could not execute `{command}`: ioctl(fd, TIOCSCTTY, NULL) returned {c}",
);
std::process::exit(c);
}
Ok(_) => {}
Err(err) => {
log::error!(
"Could not execute `{command}`: ioctl(fd, TIOCSCTTY, NULL) returned {err}",
);
std::process::exit(-1);
}
}
/* Find posix sh location, because POSIX shell is not always at /bin/sh */
let path_var = std::process::Command::new("getconf")
.args(["PATH"])
.output()?
.stdout;
for mut p in std::env::split_paths(&OsStr::from_bytes(&path_var[..])) {
p.push("sh");
if p.exists() {
if let Err(e) = nix::unistd::execv(
&CString::new(p.as_os_str().as_bytes()).unwrap(),
&[
&CString::new("sh").unwrap(),
&CString::new("-c").unwrap(),
&CString::new(command.as_bytes()).unwrap(),
],
) {
log::error!("Could not execute `{command}`: {e}");
std::process::exit(-1);
}
}
}
log::error!(
"Could not execute `{command}`: did not find the standard POSIX sh shell in PATH \
= {}",
String::from_utf8_lossy(&path_var),
);
// We are in a separate process, so doing exit(-1) here won't affect the parent
// process.
std::process::exit(-1);
}
ForkResult::Parent { child } => child,
};
let stdin = unsafe { std::fs::File::from_raw_fd(frontend_fd.as_raw_fd()) };
let mut embedded_pty = Terminal::new(stdin, child_pid);
embedded_pty.set_terminal_size((width, height));
let pty = Arc::new(Mutex::new(embedded_pty));
let pty_ = pty.clone();
std::thread::Builder::new()
.spawn(move || {
let frontend_fd = frontend_fd.into_raw_fd();
let frontend_file = unsafe { std::fs::File::from_raw_fd(frontend_fd) };
Terminal::forward_pty_translate_escape_codes(pty_, frontend_file);
})
.unwrap();
Ok(pty)
}

@ -19,179 +19,9 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::{
ffi::{CString, OsStr},
os::unix::{
ffi::OsStrExt,
io::{AsRawFd, FromRawFd, IntoRawFd},
},
};
use melib::{error::*, log};
#[cfg(not(target_os = "macos"))]
use nix::{
fcntl::{open, OFlag},
pty::{grantpt, posix_openpt, ptsname, unlockpt},
sys::stat,
};
use nix::{
ioctl_none_bad, ioctl_write_ptr_bad,
libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
pty::Winsize,
unistd::{dup2, fork, ForkResult},
};
use smallvec::SmallVec;
mod grid;
#[cfg(not(target_os = "macos"))]
use std::path::Path;
use std::{
convert::TryFrom,
io::{Read, Write},
sync::{Arc, Mutex},
};
pub use grid::{EmbedGrid, EmbedTerminal, ScreenBuffer};
// ioctl request code to "Make the given terminal the controlling terminal of the calling
// process"
use libc::TIOCSCTTY;
// ioctl request code to set window size of pty:
use libc::TIOCSWINSZ;
// Macro generated function that calls ioctl to set window size of slave pty end
ioctl_write_ptr_bad!(set_window_size, TIOCSWINSZ, Winsize);
ioctl_none_bad!(set_controlling_terminal, TIOCSCTTY);
pub fn create_pty(
width: usize,
height: usize,
command: String,
) -> Result<Arc<Mutex<EmbedTerminal>>> {
#[cfg(not(target_os = "macos"))]
let (master_fd, slave_name) = {
// Open a new PTY master
let master_fd = posix_openpt(OFlag::O_RDWR)?;
// Allow a slave to be generated for it
grantpt(&master_fd)?;
unlockpt(&master_fd)?;
// Get the name of the slave
let slave_name = unsafe { ptsname(&master_fd) }?;
{
let winsize = Winsize {
ws_row: <u16>::try_from(height).unwrap(),
ws_col: <u16>::try_from(width).unwrap(),
ws_xpixel: 0,
ws_ypixel: 0,
};
let master_fd = master_fd.as_raw_fd();
unsafe { set_window_size(master_fd, &winsize)? };
}
(master_fd, slave_name)
};
#[cfg(target_os = "macos")]
let (master_fd, slave_fd) = {
let winsize = Winsize {
ws_row: <u16>::try_from(height).unwrap(),
ws_col: <u16>::try_from(width).unwrap(),
ws_xpixel: 0,
ws_ypixel: 0,
};
let ends = nix::pty::openpty(Some(&winsize), None)?;
(ends.master, ends.slave)
};
let child_pid = match unsafe { fork()? } {
ForkResult::Child => {
#[cfg(not(target_os = "macos"))]
/* Open slave end for pseudoterminal */
let slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, stat::Mode::empty())?;
// assign stdin, stdout, stderr to the tty
dup2(slave_fd, STDIN_FILENO).unwrap();
dup2(slave_fd, STDOUT_FILENO).unwrap();
dup2(slave_fd, STDERR_FILENO).unwrap();
/* Become session leader */
nix::unistd::setsid().unwrap();
match unsafe { set_controlling_terminal(slave_fd) } {
Ok(c) if c < 0 => {
log::error!(
"Could not execute `{command}`: ioctl(fd, TIOCSCTTY, NULL) returned {c}",
);
std::process::exit(c);
}
Ok(_) => {}
Err(err) => {
log::error!(
"Could not execute `{command}`: ioctl(fd, TIOCSCTTY, NULL) returned {err}",
);
std::process::exit(-1);
}
}
/* Find posix sh location, because POSIX shell is not always at /bin/sh */
let path_var = std::process::Command::new("getconf")
.args(["PATH"])
.output()?
.stdout;
for mut p in std::env::split_paths(&OsStr::from_bytes(&path_var[..])) {
p.push("sh");
if p.exists() {
if let Err(e) = nix::unistd::execv(
&CString::new(p.as_os_str().as_bytes()).unwrap(),
&[
&CString::new("sh").unwrap(),
&CString::new("-c").unwrap(),
&CString::new(command.as_bytes()).unwrap(),
],
) {
log::error!("Could not execute `{command}`: {e}");
std::process::exit(-1);
}
}
}
log::error!(
"Could not execute `{command}`: did not find the standard POSIX sh shell in PATH \
= {}",
String::from_utf8_lossy(&path_var),
);
std::process::exit(-1);
}
ForkResult::Parent { child } => child,
};
let stdin = unsafe { std::fs::File::from_raw_fd(master_fd.as_raw_fd()) };
let mut embed_grid = EmbedTerminal::new(stdin, child_pid);
embed_grid.set_terminal_size((width, height));
let grid = Arc::new(Mutex::new(embed_grid));
let grid_ = grid.clone();
std::thread::Builder::new()
.spawn(move || {
let master_fd = master_fd.into_raw_fd();
let master_file = unsafe { std::fs::File::from_raw_fd(master_fd) };
forward_pty_translate_escape_codes(master_file, grid_);
})
.unwrap();
Ok(grid)
}
fn forward_pty_translate_escape_codes(pty_fd: std::fs::File, grid: Arc<Mutex<EmbedTerminal>>) {
let mut bytes_iter = pty_fd.bytes();
//log::trace!("waiting for bytes");
while let Some(Ok(byte)) = bytes_iter.next() {
//log::trace!("got a byte? {:?}", byte as char);
/* Drink deep, and descend. */
grid.lock().unwrap().process_byte(byte);
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub enum State {
ExpectingControlChar,
G0, // Designate G0 Character Set
@ -223,11 +53,12 @@ pub enum State {
SmallVec<[u8; 8]>,
),
CsiQ(SmallVec<[u8; 8]>),
#[default]
Normal,
}
/* Used for debugging */
struct EscCode<'a>(&'a State, u8);
/// Used for debugging escape codes.
pub struct EscCode<'a>(pub &'a State, pub u8);
impl<'a> From<(&'a mut State, u8)> for EscCode<'a> {
fn from(val: (&mut State, u8)) -> EscCode {

@ -19,6 +19,26 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! An `xterm`-compliant terminal, for running terminal applications inside
//! *meli*.
//!
//! ## API
//!
//! This module provides:
//!
//! - [`Terminal`]:
//! * a struct containing the PID of the child process that talks to this
//! pseudoterminal.
//! * a [`std::fs::File`] handle to the child process's standard input stream.
//! * an [`EmbeddedGrid`] which is a wrapper over
//! [`CellBuffer`](crate::CellBuffer) along with the properties needed to
//! maintain a proper state machine that keeps track of ongoing escape code
//! operations.
//!
//! ## Creation
//!
//! To create a [`Terminal`], see [`create_pty`](super::create_pty).
use melib::{
error::{Error, Result},
text_processing::wcwidth,
@ -26,7 +46,11 @@ use melib::{
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use super::*;
use crate::terminal::{cells::*, Area, Color, Screen, Virtual};
use crate::terminal::{
cells::*,
embedded::escape_codes::{EscCode, State},
Area, Color, Screen, Virtual,
};
#[derive(Debug, Default, Clone, Copy)]
#[repr(u8)]
@ -36,72 +60,33 @@ pub enum ScreenBuffer {
Alternate,
}
/// `EmbedGrid` manages the terminal grid state of the embed process.
///
/// The embed process sends bytes to the master end (see super mod) and
/// interprets them in a state machine stored in `State`. Escape codes are
/// translated as changes to the grid, eg changes in a cell's colors.
///
/// The main process copies the grid whenever the actual terminal is redrawn.
#[derive(Debug, Clone)]
pub struct EmbedGrid {
cursor: (usize, usize),
/// `[top;bottom]`
scroll_region: ScrollRegion,
pub alternate_screen: Box<Screen<Virtual>>,
pub state: State,
/// (width, height)
terminal_size: (usize, usize),
initialized: bool,
fg_color: Color,
bg_color: Color,
attrs: Attr,
/// Store the fg/bg color when highlighting the cell where the cursor is so
/// that it can be restored afterwards
prev_fg_color: Option<Color>,
prev_bg_color: Option<Color>,
prev_attrs: Option<Attr>,
cursor_key_mode: bool, // (DECCKM)
show_cursor: bool,
origin_mode: bool,
auto_wrap_mode: bool,
/// If next grapheme should be placed in the next line
/// This should be reset whenever the cursor value changes
wrap_next: bool,
/// Store state in case a multi-byte character is encountered
codepoints: CodepointBuf,
pub normal_screen: Box<Screen<Virtual>>,
screen_buffer: ScreenBuffer,
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CodepointBuf {
None,
TwoCodepoints(u8),
ThreeCodepoints(u8, Option<u8>),
FourCodepoints(u8, Option<u8>, Option<u8>),
}
#[derive(Debug)]
pub struct EmbedTerminal {
pub grid: EmbedGrid,
pub struct Terminal {
pub grid: EmbeddedGrid,
stdin: std::fs::File,
/// Pid of the embed process
/// Pid of the embedded process
pub child_pid: nix::unistd::Pid,
}
impl std::io::Write for EmbedTerminal {
impl std::io::Write for Terminal {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
/*
Key Normal Application
-------------+----------+-------------
Cursor Up | CSI A | SS3 A
Cursor Down | CSI B | SS3 B
Cursor Right | CSI C | SS3 C
Cursor Left | CSI D | SS3 D
-------------+----------+-------------
Key Normal Application
---------+----------+-------------
Home | CSI H | SS3 H
End | CSI F | SS3 F
---------+----------+-------------
*/
// Key Normal Application
// -------------+----------+-------------
// Cursor Up | CSI A | SS3 A
// Cursor Down | CSI B | SS3 B
// Cursor Right | CSI C | SS3 C
// Cursor Left | CSI D | SS3 D
// Home | CSI H | SS3 H
// End | CSI F | SS3 F
// -------------+----------+-------------
if self.grid.cursor_key_mode {
match buf {
&[0x1b, 0x5b, b'A']
@ -129,10 +114,10 @@ impl std::io::Write for EmbedTerminal {
}
}
impl EmbedTerminal {
impl Terminal {
pub fn new(stdin: std::fs::File, child_pid: nix::unistd::Pid) -> Self {
EmbedTerminal {
grid: EmbedGrid::new(),
Self {
grid: EmbeddedGrid::new(),
stdin,
child_pid,
}
@ -146,8 +131,8 @@ impl EmbedTerminal {
ws_xpixel: 0,
ws_ypixel: 0,
};
let master_fd = self.stdin.as_raw_fd();
let _ = unsafe { set_window_size(master_fd, &winsize) };
let frontend_fd = self.stdin.as_raw_fd();
let _ = unsafe { set_window_size(frontend_fd, &winsize) };
let _ = nix::sys::signal::kill(self.child_pid, nix::sys::signal::SIGWINCH);
}
@ -177,20 +162,72 @@ impl EmbedTerminal {
} = self;
grid.process_byte(stdin, byte);
}
#[inline]
/// Helper function to get every byte written to `pty_fd` and process it in
/// the terminal state machine.
pub fn forward_pty_translate_escape_codes(pty: Arc<Mutex<Self>>, pty_fd: std::fs::File) {
let mut bytes_iter = pty_fd.bytes();
//log::trace!("waiting for bytes");
while let Some(Ok(byte)) = bytes_iter.next() {
//log::trace!("got a byte? {:?}", byte as char);
/* Drink deep, and descend. */
pty.lock().unwrap().process_byte(byte);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CodepointBuf {
None,
TwoCodepoints(u8),
ThreeCodepoints(u8, Option<u8>),
FourCodepoints(u8, Option<u8>, Option<u8>),
/// `EmbeddedGrid` manages the terminal grid state of the embedded process.
///
/// The embedded process sends bytes to the frontend end (see super mod) and
/// interprets them in a state machine stored in `State`. Escape codes are
/// translated as changes to the grid, eg changes in a cell's colors.
///
/// The main process copies the grid whenever the actual terminal is redrawn.
#[derive(Debug, Clone)]
pub struct EmbeddedGrid {
cursor: (usize, usize),
/// `[top;bottom]`
scroll_region: ScrollRegion,
pub alternate_screen: Box<Screen<Virtual>>,
pub state: State,
/// (width, height)
terminal_size: (usize, usize),
initialized: bool,
fg_color: Color,
bg_color: Color,
attrs: Attr,
/// Store the fg/bg color when highlighting the cell where the cursor is so
/// that it can be restored afterwards
prev_fg_color: Option<Color>,
prev_bg_color: Option<Color>,
prev_attrs: Option<Attr>,
cursor_key_mode: bool, // (DECCKM)
show_cursor: bool,
origin_mode: bool,
auto_wrap_mode: bool,
/// If next grapheme should be placed in the next line
/// This should be reset whenever the cursor value changes
wrap_next: bool,
/// Store state in case a multi-byte character is encountered
codepoints: CodepointBuf,
pub normal_screen: Box<Screen<Virtual>>,
screen_buffer: ScreenBuffer,
dirty: bool,
}
impl EmbedGrid {
impl Default for EmbeddedGrid {
fn default() -> Self {
Self::new()
}
}
impl EmbeddedGrid {
#[inline]
pub fn new() -> Self {
let normal_screen = Box::new(Screen::<Virtual>::new());
EmbedGrid {
Self {
cursor: (0, 0),
scroll_region: ScrollRegion {
top: 0,
@ -216,9 +253,21 @@ impl EmbedGrid {
codepoints: CodepointBuf::None,
normal_screen,
screen_buffer: ScreenBuffer::Normal,
dirty: true,
}
}
#[inline]
pub fn set_dirty(&mut self, value: bool) {
self.dirty = value;
}
#[inline]
pub const fn is_dirty(&self) -> bool {
self.dirty
}
#[inline]
pub fn buffer(&self) -> &CellBuffer {
match self.screen_buffer {
ScreenBuffer::Normal => self.normal_screen.grid(),
@ -226,6 +275,7 @@ impl EmbedGrid {
}
}
#[inline]
pub fn buffer_mut(&mut self) -> &mut CellBuffer {
match self.screen_buffer {
ScreenBuffer::Normal => self.normal_screen.grid_mut(),
@ -250,20 +300,22 @@ impl EmbedGrid {
self.wrap_next = false;
}
#[inline]
pub const fn terminal_size(&self) -> (usize, usize) {
self.terminal_size
}
#[inline]
pub const fn area(&self) -> Area {
match self.screen_buffer {
ScreenBuffer::Normal => self.normal_screen.area(),
_ => self.alternate_screen.area(),
ScreenBuffer::Alternate => self.alternate_screen.area(),
}
}
pub fn process_byte(&mut self, stdin: &mut std::fs::File, byte: u8) {
let area = self.area();
let EmbedGrid {
let EmbeddedGrid {
ref mut cursor,
ref mut scroll_region,
ref mut terminal_size,
@ -284,6 +336,7 @@ impl EmbedGrid {
ref mut screen_buffer,
ref mut normal_screen,
initialized: _,
ref mut dirty,
} = self;
let mut grid = normal_screen.grid_mut();
@ -370,6 +423,7 @@ impl EmbedGrid {
//log::trace!("{}", EscCode::from((&(*state), byte)));
if cursor.1 == scroll_region.bottom {
grid.scroll_up(scroll_region, scroll_region.top, 1);
*dirty = true;
} else {
cursor.1 += 1;
}
@ -385,6 +439,7 @@ impl EmbedGrid {
grid[(x, y)] = Cell::default();
}
}
*dirty = true;
*state = State::Normal;
}
(b'K', State::ExpectingControlChar) => {
@ -393,6 +448,7 @@ impl EmbedGrid {
for x in cursor.0..terminal_size.0 {
grid[(x, cursor.1)] = Cell::default();
}
*dirty = true;
*state = State::Normal;
}
(_, State::ExpectingControlChar) => {
@ -432,6 +488,7 @@ impl EmbedGrid {
if cursor.1 + 1 < terminal_size.1 || !is_alternate {
if cursor.1 == scroll_region.bottom && is_alternate {
grid.scroll_up(scroll_region, cursor.1, 1);
*dirty = true;
} else {
increase_cursor_y!();
}
@ -549,6 +606,7 @@ impl EmbedGrid {
}
}
}
*dirty = true;
increase_cursor_x!();
}
(b'u', State::Csi) => {
@ -566,6 +624,7 @@ impl EmbedGrid {
grid[cursor_val!()].set_fg(Color::Default);
grid[cursor_val!()].set_bg(Color::Default);
grid[cursor_val!()].set_attrs(Attr::DEFAULT);
*dirty = true;
*state = State::Normal;
}
(b'C', State::Csi) => {
@ -599,6 +658,7 @@ impl EmbedGrid {
grid[cursor_val!()].set_fg(Color::Black);
grid[cursor_val!()].set_bg(Color::White);
grid[cursor_val!()].set_attrs(Attr::DEFAULT);
*dirty = true;
}
b"1047" | b"1049" => {
*screen_buffer = ScreenBuffer::Alternate;
@ -639,6 +699,7 @@ impl EmbedGrid {
} else {
grid[cursor_val!()].set_attrs(*attrs);
}
*dirty = true;
}
b"1047" | b"1049" => {
*screen_buffer = ScreenBuffer::Normal;
@ -670,6 +731,7 @@ impl EmbedGrid {
)),
Default::default(),
);
*dirty = true;
//log::trace!("{}", EscCode::from((&(*state), byte)));
*state = State::Normal;
}
@ -680,6 +742,7 @@ impl EmbedGrid {
for x in cursor.0..terminal_size.0 {
grid[(x, cursor.1)] = Cell::default();
}
*dirty = true;
*state = State::Normal;
}
(b'L', State::Csi) | (b'L', State::Csi1(_)) => {
@ -695,6 +758,7 @@ impl EmbedGrid {
grid.scroll_down(scroll_region, cursor.1, n);
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
*state = State::Normal;
}
(b'M', State::Csi) | (b'M', State::Csi1(_)) => {
@ -710,6 +774,7 @@ impl EmbedGrid {
grid.scroll_up(scroll_region, cursor.1, n);
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
*state = State::Normal;
}
(b'A', State::Csi) => {
@ -731,6 +796,7 @@ impl EmbedGrid {
for x in cursor.0..terminal_size.0 {
grid[(x, cursor.1)] = Cell::default();
}
*dirty = true;
*state = State::Normal;
}
(b'K', State::Csi1(buf)) if buf.as_ref() == b"1" => {
@ -740,6 +806,7 @@ impl EmbedGrid {
grid[(x, cursor.1)] = Cell::default();
}
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
*state = State::Normal;
}
(b'K', State::Csi1(buf)) if buf.as_ref() == b"2" => {
@ -756,6 +823,7 @@ impl EmbedGrid {
area.take_cols(terminal_size.0).take_rows(terminal_size.1),
Default::default(),
);
*dirty = true;
*state = State::Normal;
}
(b'J', State::Csi1(ref buf)) if buf.as_ref() == b"0" => {
@ -770,6 +838,7 @@ impl EmbedGrid {
Default::default(),
);
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
*state = State::Normal;
}
(b'J', State::Csi1(ref buf)) if buf.as_ref() == b"1" => {
@ -781,6 +850,7 @@ impl EmbedGrid {
Default::default(),
);
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
*state = State::Normal;
}
(b'J', State::Csi1(ref buf)) if buf.as_ref() == b"2" => {
@ -789,6 +859,7 @@ impl EmbedGrid {
grid.clear_area(area, Default::default());
//log::trace!("{}", EscCode::from((&(*state), byte)));
*dirty = true;
*state = State::Normal;
}
(b'X', State::Csi1(ref buf)) => {
@ -812,6 +883,7 @@ impl EmbedGrid {
ctr += 1;
}
//log::trace!("Erased {} Character(s)", ps);
*dirty = true;
*state = State::Normal;
}
(b't', State::Csi1(buf)) => {
@ -997,6 +1069,7 @@ impl EmbedGrid {
// "Delete {} Character(s) with cursor at {:?} ",
// offset, cursor
//);
*dirty = true;
*state = State::Normal;
}
(b'd', State::Csi1(_)) | (b'd', State::Csi) => {
@ -1133,6 +1206,7 @@ impl EmbedGrid {
grid[cursor_val!()].set_fg(*fg_color);
grid[cursor_val!()].set_bg(*bg_color);
grid[cursor_val!()].set_attrs(*attrs);
*dirty = true;
*state = State::Normal;
}
(b'm', State::Csi2(ref buf1, ref buf2)) => {
@ -1244,6 +1318,7 @@ impl EmbedGrid {
grid[cursor_val!()].set_fg(*fg_color);
grid[cursor_val!()].set_bg(*bg_color);
grid[cursor_val!()].set_attrs(*attrs);
*dirty = true;
*state = State::Normal;
}
(c, State::Csi1(ref mut buf)) if c.is_ascii_digit() || c == b' ' => {
@ -1346,6 +1421,7 @@ impl EmbedGrid {
Color::Default
};
grid[cursor_val!()].set_fg(*fg_color);
*dirty = true;
*state = State::Normal;
}
(b'm', State::Csi3(ref buf1, ref buf2, ref buf3))
@ -1361,6 +1437,7 @@ impl EmbedGrid {
Color::Default
};
grid[cursor_val!()].set_bg(*bg_color);
*dirty = true;
*state = State::Normal;
}
(c, State::Csi3(_, _, ref mut buf)) if c.is_ascii_digit() => {
@ -1429,6 +1506,7 @@ impl EmbedGrid {
_ => Color::Default,
};
grid[cursor_val!()].set_fg(*fg_color);
*dirty = true;
*state = State::Normal;
}
(
@ -1452,6 +1530,7 @@ impl EmbedGrid {
_ => Color::Default,
};
grid[cursor_val!()].set_bg(*bg_color);
*dirty = true;
*state = State::Normal;
}
(
@ -1474,6 +1553,7 @@ impl EmbedGrid {
_ => Color::Default,
};
grid[cursor_val!()].set_fg(*fg_color);
*dirty = true;
*state = State::Normal;
}
(
@ -1496,6 +1576,7 @@ impl EmbedGrid {
_ => Color::Default,
};
grid[cursor_val!()].set_bg(*bg_color);
*dirty = true;
*state = State::Normal;
}
(b'q', State::Csi1(buf))
@ -1612,9 +1693,3 @@ impl EmbedGrid {
}
}
}
impl Default for EmbedGrid {
fn default() -> Self {
Self::new()
}
}

@ -98,8 +98,8 @@ impl From<UIEvent> for ThreadEvent {
pub enum ForkType {
/// Already finished fork, we only want to restore input/output
Finished,
/// Embed pty
Embed(Pid),
/// Embedded pty
Embedded(Pid),
Generic(std::process::Child),
NewDraft(File, std::process::Child),
}
@ -131,7 +131,7 @@ pub enum UIEvent {
Input(Key),
CmdInput(Key),
InsertInput(Key),
EmbedInput((Key, Vec<u8>)),
EmbeddedInput((Key, Vec<u8>)),
Resize,
Fork(ForkType),
ChangeMailbox(usize),
@ -189,8 +189,8 @@ impl From<RefreshEvent> for UIEvent {
pub enum UIMode {
Normal,
Insert,
/// Forward input to an embed pseudoterminal.
Embed,
/// Forward input to an embedded pseudoterminal.
Embedded,
Command,
Fork,
}
@ -205,7 +205,7 @@ impl std::fmt::Display for UIMode {
UIMode::Insert => "INSERT",
UIMode::Command => "COMMAND",
UIMode::Fork => "FORK",
UIMode::Embed => "EMBED",
UIMode::Embedded => "EMBEDDED",
}
)
}

@ -1493,7 +1493,7 @@ impl Component for Tabbed {
_ => {}
}
let c = self.cursor_pos;
if let UIEvent::Input(_) | UIEvent::CmdInput(_) | UIEvent::EmbedInput(_) = event {
if let UIEvent::Input(_) | UIEvent::CmdInput(_) | UIEvent::EmbeddedInput(_) = event {
self.children[c].process_event(event, context)
} else {
self.children[c].process_event(event, context)

@ -22,7 +22,7 @@
use melib::text_processing::{LineBreakText, Truncate};
use super::*;
use crate::terminal::embed::EmbedGrid;
use crate::terminal::embedded::EmbeddedGrid;
/// A pager for text.
/// `Pager` holds its own content in its own `CellBuffer` and when `draw` is
@ -50,7 +50,7 @@ pub struct Pager {
/// total height? Used to decide whether to accept `scroll_down` key
/// events.
rows_lt_height: bool,
filtered_content: Option<(String, Result<EmbedGrid>)>,
filtered_content: Option<(String, Result<EmbeddedGrid>)>,
text_lines: Vec<String>,
line_breaker: LineBreakText,
movement: Option<PageMovement>,
@ -181,7 +181,7 @@ impl Pager {
}
pub fn filter(&mut self, cmd: &str) {
let _f = |bin: &str, text: &str| -> Result<EmbedGrid> {
let _f = |bin: &str, text: &str| -> Result<EmbeddedGrid> {
use std::{
io::Write,
process::{Command, Stdio},
@ -201,7 +201,7 @@ impl Pager {
.chain_err_summary(|| "Failed to wait on filter")?
.stdout;
let mut dev_null = std::fs::File::open("/dev/null")?;
let mut embedded = EmbedGrid::new();
let mut embedded = EmbeddedGrid::new();
embedded.set_terminal_size((80, 20));
for b in out {
@ -210,7 +210,7 @@ impl Pager {
Ok(embedded)
};
let buf = _f(cmd, &self.text);
if let Some((width, height)) = buf.as_ref().ok().map(EmbedGrid::terminal_size) {
if let Some((width, height)) = buf.as_ref().ok().map(EmbeddedGrid::terminal_size) {
self.width = width;
self.height = height;
}

190
tools/Cargo.lock generated

@ -44,6 +44,21 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "async-channel"
version = "1.9.0"
@ -289,6 +304,12 @@ dependencies = [
"tracing",
]
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "bytes"
version = "1.5.0"
@ -307,7 +328,6 @@ version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"jobserver",
"libc",
]
@ -329,7 +349,10 @@ version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets",
]
[[package]]
@ -444,6 +467,27 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "curl"
version = "0.4.44"
@ -926,6 +970,29 @@ dependencies = [
"itoa",
]
[[package]]
name = "iana-time-zone"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "idna"
version = "0.4.0"
@ -1058,12 +1125,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jobserver"
version = "0.1.27"
name = "js-sys"
version = "0.3.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
dependencies = [
"libc",
"wasm-bindgen",
]
[[package]]
@ -1100,6 +1167,7 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
dependencies = [
"cc",
"pkg-config",
]
@ -1140,6 +1208,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
@ -1208,8 +1277,6 @@ dependencies = [
[[package]]
name = "meli"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7948bdd90a81155b583bfcb92d57256939054d6ec6b74bdc3e4b2be14d677ee"
dependencies = [
"async-task",
"bitflags 2.4.1",
@ -1218,13 +1285,13 @@ dependencies = [
"futures",
"indexmap",
"libc",
"libz-sys",
"linkify",
"melib",
"nix",
"notify",
"notify-rust",
"num_cpus",
"pcre2",
"proc-macro2",
"quote",
"regex",
@ -1243,9 +1310,7 @@ dependencies = [
[[package]]
name = "melib"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f15b2a8e4ec0e4d9be20fd046346da8fb5737deff29bc1f881544d589cb3b860"
version = "0.8.3"
dependencies = [
"async-stream",
"base64 0.13.1",
@ -1541,6 +1606,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.1.6+3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.95"
@ -1549,6 +1623,7 @@ checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
@ -1559,28 +1634,6 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "pcre2"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9deb1d02d6a373ee392128ba86087352a986359f32a106e2e3b08cc90cc659c9"
dependencies = [
"libc",
"log",
"pcre2-sys",
]
[[package]]
name = "pcre2-sys"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae234f441970dbd52d4e29bee70f3b56ca83040081cb2b55b7df772b16e0b06e"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "percent-encoding"
version = "2.3.0"
@ -1803,11 +1856,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags 2.4.1",
"chrono",
"csv",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"serde_json",
"smallvec",
"time",
"url",
"uuid",
]
[[package]]
@ -2129,9 +2188,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [
"deranged",
"itoa",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
@ -2140,6 +2201,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@ -2308,6 +2378,60 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.39",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
[[package]]
name = "winapi"
version = "0.2.8"

@ -33,8 +33,8 @@ path = "src/smtp_conn.rs"
required-features = ["melib/smtp"]
[[bin]]
name = "embed"
path = "src/embed.rs"
name = "embedded"
path = "src/embedded.rs"
[[bin]]
name = "managesieve-client"
@ -43,8 +43,8 @@ required-features = ["melib/imap"]
[dependencies]
crossbeam = { version = "^0.8" }
meli = { version = "0.8.2" }
melib = { version = "0.8.2", features = ["debug-tracing", "unicode-algorithms"] }
meli = { path = "../meli", version = "0.8" }
melib = { path = "../melib", version = "0.8", features = ["debug-tracing", "unicode-algorithms"] }
nix = { version = "^0.24", default-features = false }
signal-hook = { version = "^0.3", default-features = false, features = ["iterator"] }
signal-hook-registry = { version = "1.2.0", default-features = false }

@ -6,7 +6,7 @@ use std::{
};
use meli::{
terminal::{embed::*, *},
terminal::{embedded::*, *},
*,
};
use nix::sys::wait::WaitStatus;
@ -53,20 +53,20 @@ fn notify(
}
#[derive(Debug)]
enum EmbedStatus {
Stopped(Arc<Mutex<EmbedTerminal>>),
Running(Arc<Mutex<EmbedTerminal>>),
enum EmbeddedPty {
Stopped(Arc<Mutex<Terminal>>),
Running(Arc<Mutex<Terminal>>),
}
impl EmbedStatus {
impl EmbeddedPty {
#[inline(always)]
fn is_stopped(&self) -> bool {
matches!(self, Self::Stopped(_))
}
}
impl std::ops::Deref for EmbedStatus {
type Target = Arc<Mutex<EmbedTerminal>>;
impl std::ops::Deref for EmbeddedPty {
type Target = Arc<Mutex<Terminal>>;
fn deref(&self) -> &Self::Target {
match self {
@ -75,7 +75,7 @@ impl std::ops::Deref for EmbedStatus {
}
}
impl std::ops::DerefMut for EmbedStatus {
impl std::ops::DerefMut for EmbeddedPty {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Stopped(ref mut e) | Self::Running(ref mut e) => e,
@ -84,21 +84,19 @@ impl std::ops::DerefMut for EmbedStatus {
}
#[derive(Debug)]
struct EmbedContainer {
struct EmbeddedContainer {
command: String,
embed_area: Area,
embed: Option<EmbedStatus>,
embedded_pty: Option<EmbeddedPty>,
id: ComponentId,
dirty: bool,
log_file: File,
}
impl EmbedContainer {
impl EmbeddedContainer {
fn new(command: String) -> Box<Self> {
Box::new(Self {
command,
embed: None,
embed_area: ((0, 0), (80, 20)),
embedded_pty: None,
dirty: true,
log_file: File::open(".embed.out").unwrap(),
id: ComponentId::default(),
@ -106,48 +104,32 @@ impl EmbedContainer {
}
}
impl std::fmt::Display for EmbedContainer {
impl std::fmt::Display for EmbeddedContainer {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "embed")
write!(fmt, "embedded_pty")
}
}
impl Component for EmbedContainer {
impl Component for EmbeddedContainer {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if let Some(ref mut embed_pty) = self.embed {
let embed_area = area;
if let Some(ref mut embedded_pty_pty) = self.embedded_pty {
let theme_default = crate::conf::value(context, "theme_default");
match embed_pty {
EmbedStatus::Running(_) => {
let mut guard = embed_pty.lock().unwrap();
grid.clear_area(embed_area, theme_default);
match embedded_pty_pty {
EmbeddedPty::Running(_) => {
let mut guard = embedded_pty_pty.lock().unwrap();
grid.clear_area(area, theme_default);
grid.copy_area(
guard.grid.buffer(),
embed_area,
((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))),
);
guard.set_terminal_size((embed_area.width(), embed_area.height()));
grid.copy_area(guard.grid.buffer(), area, guard.grid.area());
guard.set_terminal_size((area.width(), area.height()));
guard.grid.set_dirty(false);
context.dirty_areas.push_back(area);
self.dirty = false;
return;
}
EmbedStatus::Stopped(_) => {
let guard = embed_pty.lock().unwrap();
grid.copy_area(
guard.grid.buffer(),
embed_area,
((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))),
);
EmbeddedPty::Stopped(_) => {
let mut guard = embedded_pty_pty.lock().unwrap();
grid.change_theme(
embed_area,
ThemeAttribute {
fg: Color::Byte(8),
..theme_default
},
);
grid.copy_area(guard.grid.buffer(), area, guard.grid.buffer().area());
grid.change_colors(area, Color::Byte(8), theme_default.bg);
let stopped_message: String =
format!("Process with PID {} has stopped.", guard.child_pid);
let stopped_message_2: String = "-press 'e' to re-activate.".to_string();
@ -157,19 +139,7 @@ impl Component for EmbedContainer {
stopped_message.len(),
std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()),
);
let inner_area = create_box(
grid,
(
pos_inc(area.upper_left(), (1, 0)),
pos_inc(
area.upper_left(),
(
std::cmp::min(max_len + 5, area.width()),
std::cmp::min(5, area.height()),
),
),
),
);
let inner_area = create_box(grid, area.center_inside((max_len + 5, 5)));
grid.clear_area(inner_area, theme_default);
for (i, l) in [
stopped_message.as_str(),
@ -184,33 +154,34 @@ impl Component for EmbedContainer {
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(
pos_inc((0, i), inner_area.upper_left()),
inner_area.bottom_right(),
),
Some(get_x(inner_area.upper_left())),
inner_area.skip_rows(i),
None,
);
}
context.dirty_areas.push_back(area);
guard.grid.set_dirty(false);
self.dirty = false;
}
}
return;
} else {
let theme_default = crate::conf::value(context, "theme_default");
grid.clear_area(area, theme_default);
self.embed_area = (area.upper_left(), area.bottom_right());
match create_pty(
self.embed_area.width(),
self.embed_area.height(),
self.command.clone(),
) {
Ok(embed) => {
//embed.lock().unwrap().set_log_file(self.log_file.take());
self.embed = Some(EmbedStatus::Running(embed));
match create_pty(area.width(), area.height(), self.command.clone()) {
Ok(embedded_pty) => {
//embedded_pty.lock().unwrap().set_log_file(self.log_file.take());
self.embedded_pty = Some(EmbeddedPty::Running(embedded_pty));
self.set_dirty(true);
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
context.replies.push_back(UIEvent::Fork(ForkType::Embed(
self.embed.as_ref().unwrap().lock().unwrap().child_pid,
.push_back(UIEvent::ChangeMode(UIMode::Embedded));
context.replies.push_back(UIEvent::Fork(ForkType::Embedded(
self.embedded_pty
.as_ref()
.unwrap()
.lock()
.unwrap()
.child_pid,
)));
}
Err(err) => {
@ -228,11 +199,11 @@ impl Component for EmbedContainer {
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
match event {
UIEvent::EmbedInput((Key::Ctrl('z'), _)) => {
self.embed.as_ref().unwrap().lock().unwrap().stop();
match self.embed.take() {
Some(EmbedStatus::Running(e)) | Some(EmbedStatus::Stopped(e)) => {
self.embed = Some(EmbedStatus::Stopped(e));
UIEvent::EmbeddedInput((Key::Ctrl('z'), _)) => {
self.embedded_pty.as_ref().unwrap().lock().unwrap().stop();
match self.embedded_pty.take() {
Some(EmbeddedPty::Running(e)) | Some(EmbeddedPty::Stopped(e)) => {
self.embedded_pty = Some(EmbeddedPty::Stopped(e));
}
_ => {}
}
@ -241,18 +212,18 @@ impl Component for EmbedContainer {
.push_back(UIEvent::ChangeMode(UIMode::Normal));
self.set_dirty(true);
}
UIEvent::EmbedInput((ref k, ref b)) => {
UIEvent::EmbeddedInput((ref k, ref b)) => {
let _ = self
.log_file
.write_all(format!("{} bytes {:?}", k, b).as_bytes());
let _ = self.log_file.flush();
if let Some(ref mut embed) = self.embed {
let mut embed_guard = embed.lock().unwrap();
if embed_guard.write_all(b).is_err() {
match embed_guard.is_active() {
if let Some(ref mut embedded_pty) = self.embedded_pty {
let mut embedded_pty_guard = embedded_pty.lock().unwrap();
if embedded_pty_guard.write_all(b).is_err() {
match embedded_pty_guard.is_active() {
Ok(WaitStatus::Exited(_, exit_code)) => {
drop(embed_guard);
_ = self.embed.take();
drop(embedded_pty_guard);
_ = self.embedded_pty.take();
if exit_code != 0 {
context.replies.push_back(UIEvent::Notification(
None,
@ -273,11 +244,11 @@ impl Component for EmbedContainer {
#[cfg(any(target_os = "linux", target_os = "android"))]
Ok(WaitStatus::PtraceEvent(_, _, _))
| Ok(WaitStatus::PtraceSyscall(_)) => {
drop(embed_guard);
match self.embed.take() {
Some(EmbedStatus::Running(e))
| Some(EmbedStatus::Stopped(e)) => {
self.embed = Some(EmbedStatus::Stopped(e));
drop(embedded_pty_guard);
match self.embedded_pty.take() {
Some(EmbeddedPty::Running(e))
| Some(EmbeddedPty::Stopped(e)) => {
self.embedded_pty = Some(EmbeddedPty::Stopped(e));
}
_ => {}
}
@ -288,11 +259,11 @@ impl Component for EmbedContainer {
return true;
}
Ok(WaitStatus::Stopped(_, _)) => {
drop(embed_guard);
match self.embed.take() {
Some(EmbedStatus::Running(e))
| Some(EmbedStatus::Stopped(e)) => {
self.embed = Some(EmbedStatus::Stopped(e));
drop(embedded_pty_guard);
match self.embedded_pty.take() {
Some(EmbeddedPty::Running(e))
| Some(EmbeddedPty::Stopped(e)) => {
self.embedded_pty = Some(EmbeddedPty::Stopped(e));
}
_ => {}
}
@ -305,11 +276,11 @@ impl Component for EmbedContainer {
Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::StillAlive) => {
context
.replies
.push_back(UIEvent::EmbedInput((k.clone(), b.to_vec())));
.push_back(UIEvent::EmbeddedInput((k.clone(), b.to_vec())));
return true;
}
Ok(WaitStatus::Signaled(_, signal, _)) => {
drop(embed_guard);
drop(embedded_pty_guard);
context.replies.push_back(UIEvent::Notification(
None,
format!("Subprocess was killed by {} signal", signal),
@ -317,21 +288,21 @@ impl Component for EmbedContainer {
melib::error::ErrorKind::External,
)),
));
self.embed = None;
self.embedded_pty = None;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some("Embed editor crashed.".to_string()),
Some("Embedded editor crashed.".to_string()),
format!("Subprocess has exited with reason {}", &err),
Some(NotificationType::Error(
melib::error::ErrorKind::External,
)),
));
drop(embed_guard);
self.embed = None;
drop(embedded_pty_guard);
self.embedded_pty = None;
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Normal));
@ -345,26 +316,33 @@ impl Component for EmbedContainer {
UIEvent::Resize => {
self.set_dirty(true);
}
UIEvent::Input(Key::Char('e')) if self.embed.is_some() => {
self.embed.as_ref().unwrap().lock().unwrap().wake_up();
match self.embed.take() {
Some(EmbedStatus::Running(e)) | Some(EmbedStatus::Stopped(e)) => {
self.embed = Some(EmbedStatus::Running(e));
UIEvent::Input(Key::Char('e')) if self.embedded_pty.is_some() => {
self.embedded_pty
.as_ref()
.unwrap()
.lock()
.unwrap()
.wake_up();
match self.embedded_pty.take() {
Some(EmbeddedPty::Running(e)) | Some(EmbeddedPty::Stopped(e)) => {
self.embedded_pty = Some(EmbeddedPty::Running(e));
}
_ => {}
}
context
.replies
.push_back(UIEvent::ChangeMode(UIMode::Embed));
.push_back(UIEvent::ChangeMode(UIMode::Embedded));
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Ctrl('c'))
if self.embed.is_some() && self.embed.as_ref().unwrap().is_stopped() =>
if self.embedded_pty.is_some()
&& self.embedded_pty.as_ref().unwrap().is_stopped() =>
{
match self.embed.take() {
Some(EmbedStatus::Running(embed)) | Some(EmbedStatus::Stopped(embed)) => {
let guard = embed.lock().unwrap();
match self.embedded_pty.take() {
Some(EmbeddedPty::Running(embedded_pty))
| Some(EmbeddedPty::Stopped(embedded_pty)) => {
let guard = embedded_pty.lock().unwrap();
guard.wake_up();
guard.terminate();
}
@ -411,12 +389,12 @@ fn main() -> std::io::Result<()> {
let signals = &[
/* Catch SIGWINCH to handle terminal resizing */
signal_hook::consts::SIGWINCH,
/* Catch SIGCHLD to handle embed applications status change */
/* Catch SIGCHLD to handle embedded applications status change */
signal_hook::consts::SIGCHLD,
];
let quit_key: Key = Key::Char('q');
let window = EmbedContainer::new(command);
let window = EmbeddedContainer::new(command);
let signal_recvr = notify(signals, sender.clone())?;
let mut state = meli::State::new(Some(Default::default()), sender, receiver.clone()).unwrap();
let status_bar = Box::new(StatusBar::new(&state.context, window));
@ -444,7 +422,7 @@ fn main() -> std::io::Result<()> {
}
}
match r.unwrap() {
ThreadEvent::Input((Key::Ctrl('z'), _)) if state.mode != UIMode::Embed => {
ThreadEvent::Input((Key::Ctrl('z'), _)) if state.mode != UIMode::Embedded => {
state.switch_to_main_screen();
//_thread_handler.join().expect("Couldn't join on the associated thread");
let self_pid = nix::unistd::Pid::this();
@ -460,8 +438,8 @@ fn main() -> std::io::Result<()> {
state.update_size();
state.render();
state.redraw();
if state.mode == UIMode::Embed {
state.rcv_event(UIEvent::EmbedInput(raw_input));
if state.mode == UIMode::Embedded {
state.rcv_event(UIEvent::EmbeddedInput(raw_input));
state.redraw();
}
},
@ -508,8 +486,8 @@ fn main() -> std::io::Result<()> {
},
}
},
UIMode::Embed => {
state.rcv_event(UIEvent::EmbedInput((k,r)));
UIMode::Embedded => {
state.rcv_event(UIEvent::EmbeddedInput((k,r)));
state.redraw();
},
UIMode::Fork => {
@ -556,7 +534,7 @@ fn main() -> std::io::Result<()> {
}
},
signal_hook::consts::SIGCHLD => {
state.rcv_event(UIEvent::EmbedInput((Key::Null, vec![0])));
state.rcv_event(UIEvent::EmbeddedInput((Key::Null, vec![0])));
state.redraw();
}
Loading…
Cancel
Save