terminal/embedded: overhaul refactor
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>pull/312/head
parent
54d21f25fd
commit
0a74c7d0e5
|
@ -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);
|
||||
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(), 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(), 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(), 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"]
|
||||
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,
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
EmbeddedPty::Stopped(_) => {
|
||||
let mut guard = embedded_pty_pty.lock().unwrap();
|
||||
|
||||
grid.copy_area(
|
||||
guard.grid.buffer(),
|
||||
embed_area,
|
||||
((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))),
|
||||
);
|
||||
|
||||
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…
Reference in New Issue