From 0a74c7d0e5c318dd29c8ace01e588d441e0fcfb6 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Tue, 21 Nov 2023 18:44:16 +0200 Subject: [PATCH] terminal/embedded: overhaul refactor Signed-off-by: Manos Pitsidianakis --- meli/src/conf/composing.rs | 9 +- meli/src/conf/overrides.rs | 2 +- meli/src/mail/compose.rs | 401 +++++++++--------- meli/src/main.rs | 14 +- meli/src/state.rs | 2 +- meli/src/svg.rs | 2 +- meli/src/terminal.rs | 2 +- meli/src/terminal/embedded.rs | 192 +++++++++ .../{embed.rs => embedded/escape_codes.rs} | 177 +------- .../{embed/grid.rs => embedded/terminal.rs} | 235 ++++++---- meli/src/types.rs | 12 +- meli/src/utilities.rs | 2 +- meli/src/utilities/pager.rs | 10 +- tools/Cargo.lock | 190 +++++++-- tools/Cargo.toml | 8 +- tools/src/{embed.rs => embedded.rs} | 220 +++++----- 16 files changed, 838 insertions(+), 640 deletions(-) create mode 100644 meli/src/terminal/embedded.rs rename meli/src/terminal/{embed.rs => embedded/escape_codes.rs} (62%) rename meli/src/terminal/{embed/grid.rs => embedded/terminal.rs} (94%) rename tools/src/{embed.rs => embedded.rs} (76%) diff --git a/meli/src/conf/composing.rs b/meli/src/conf/composing.rs index 0162a5d3..85e4e412 100644 --- a/meli/src/conf/composing.rs +++ b/meli/src/conf/composing.rs @@ -46,9 +46,10 @@ pub struct ComposingSettings { alias = "editor_cmd" )] pub editor_command: Option, - /// 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(), diff --git a/meli/src/conf/overrides.rs b/meli/src/conf/overrides.rs index 62da184b..6ffad9f7 100644 --- a/meli/src/conf/overrides.rs +++ b/meli/src/conf/overrides.rs @@ -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 } } } diff --git a/meli/src/mail/compose.rs b/meli/src/mail/compose.rs index 4f1f79b8..686deff4 100644 --- a/meli/src/mail/compose.rs +++ b/meli/src/mail/compose.rs @@ -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>, File), - Running(Arc>, File), +struct EmbeddedPty { + running: bool, + terminal: Arc>, + 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>; +impl std::ops::Deref for EmbeddedPty { + type Target = Arc>; 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, - embed_dimensions: (usize, usize), + embedded_pty: Option, + 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
), #[cfg(feature = "gpgme")] SelectEncryptKey(bool, gpg::KeySelection), @@ -185,8 +185,8 @@ impl Composer { gpg_state: gpg::GpgComposeState::default(), dirty: true, has_changes: false, - embedded: None, - embed_dimensions: (80, 20), + embedded_pty: None, + embedded_dimensions: (80, 20), initialized: false, id: ComponentId::default(), } @@ -955,72 +955,71 @@ impl Component for Composer { self.form.draw(grid, header_area, context); - if let Some(ref mut embedded) = self.embedded { - let embed_pty = &mut embedded.status; - let embed_area = area; - match embed_pty { - EmbedStatus::Running(_, _) => { - if self.dirty { - let mut guard = embed_pty.lock().unwrap(); - grid.clear_area(embed_area, theme_default); - - grid.copy_area(guard.grid.buffer(), embed_area, guard.grid.area()); - guard.set_terminal_size((embed_area.width(), embed_area.height())); - context.dirty_areas.push_back(embed_area); - self.dirty = false; - } - return; - } - EmbedStatus::Stopped(_, _) => { - if self.dirty { - let guard = embed_pty.lock().unwrap(); - - grid.copy_area(guard.grid.buffer(), embed_area, guard.grid.buffer().area()); - grid.change_colors(embed_area, Color::Byte(8), theme_default.bg); - let our_map: ShortcutMap = - account_settings!(context[self.account_hash].shortcuts.composing) - .key_values(); - let mut shortcuts: ShortcutMaps = Default::default(); - shortcuts.insert(Shortcuts::COMPOSING, our_map); - let stopped_message: String = - format!("Process with PID {} has stopped.", guard.child_pid); - let stopped_message_2: String = format!( - "-press '{}' (edit shortcut) to re-activate.", - shortcuts[Shortcuts::COMPOSING]["edit"] - ); - const STOPPED_MESSAGE_3: &str = - "-press Ctrl-C to forcefully kill it and return to editor."; - let max_len = std::cmp::max( - stopped_message.len(), - std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()), + if let Some(ref mut embedded_pty) = self.embedded_pty { + let embedded_area = area; + if embedded_pty.is_dirty() { + if embedded_pty.running { + let mut guard = embedded_pty.lock().unwrap(); + grid.clear_area(embedded_area, theme_default); + + grid.copy_area(guard.grid.buffer(), embedded_area, guard.grid.area()); + guard.set_terminal_size((embedded_area.width(), embedded_area.height())); + guard.grid.set_dirty(false); + context.dirty_areas.push_back(embedded_area); + self.dirty = false; + } else { + let mut guard = embedded_pty.lock().unwrap(); + + grid.copy_area( + guard.grid.buffer(), + embedded_area, + guard.grid.buffer().area(), + ); + grid.change_colors(embedded_area, Color::Byte(8), theme_default.bg); + let our_map: ShortcutMap = + account_settings!(context[self.account_hash].shortcuts.composing) + .key_values(); + let mut shortcuts: ShortcutMaps = Default::default(); + shortcuts.insert(Shortcuts::COMPOSING, our_map); + let stopped_message: String = + format!("Process with PID {} has stopped.", guard.child_pid); + let stopped_message_2: String = format!( + "-press '{}' (edit shortcut) to re-activate.", + shortcuts[Shortcuts::COMPOSING]["edit"] + ); + const STOPPED_MESSAGE_3: &str = + "-press Ctrl-C to forcefully kill it and return to editor."; + let max_len = std::cmp::max( + stopped_message.len(), + std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()), + ); + let inner_area = create_box(grid, area.center_inside((max_len + 5, 5))); + grid.clear_area(inner_area, theme_default); + for (i, l) in [ + stopped_message.as_str(), + stopped_message_2.as_str(), + STOPPED_MESSAGE_3, + ] + .iter() + .enumerate() + { + grid.write_string( + l, + theme_default.fg, + theme_default.bg, + theme_default.attrs, + inner_area.skip_rows(i), + None, ); - let inner_area = create_box(grid, area.center_inside((max_len + 5, 5))); - grid.clear_area(inner_area, theme_default); - for (i, l) in [ - stopped_message.as_str(), - stopped_message_2.as_str(), - STOPPED_MESSAGE_3, - ] - .iter() - .enumerate() - { - grid.write_string( - l, - theme_default.fg, - theme_default.bg, - theme_default.attrs, - inner_area.skip_rows(i), - None, - ); - } - context.dirty_areas.push_back(area); - self.dirty = false; } - return; + context.dirty_areas.push_back(area); + guard.grid.set_dirty(false); + self.dirty = false; } + return; } } else { - self.embed_dimensions = (area.width(), area.height()); + self.embedded_dimensions = (area.width(), area.height()); } if self.pager.size().0 > body_area.width() { @@ -1038,7 +1037,7 @@ impl Component for Composer { self.draw_attachments(grid, attachment_area, context); //} match self.mode { - ViewMode::Edit | ViewMode::Embed => {} + ViewMode::Edit | ViewMode::EmbeddedPty => {} //ViewMode::EditAttachments { ref mut widget } => { // let inner_area = create_box(grid, area); // (EditAttachmentsRefMut { @@ -1502,40 +1501,33 @@ impl Component for Composer { )); return true; } - UIEvent::EmbedInput((Key::Ctrl('z'), _)) => { - self.embedded - .as_ref() - .unwrap() - .status - .lock() - .unwrap() - .stop(); - match self.embedded.take() { - Some(Embedded { - status: EmbedStatus::Running(e, f), - }) - | Some(Embedded { - status: EmbedStatus::Stopped(e, f), - }) => { - self.embedded = Some(Embedded { - status: EmbedStatus::Stopped(e, f), - }); - } - _ => {} + UIEvent::EmbeddedInput((Key::Ctrl('z'), _)) => { + self.embedded_pty.as_ref().unwrap().lock().unwrap().stop(); + if let Some(EmbeddedPty { + running: _, + terminal, + file, + }) = self.embedded_pty.take() + { + self.embedded_pty = Some(EmbeddedPty { + running: false, + terminal, + file, + }); } context .replies .push_back(UIEvent::ChangeMode(UIMode::Normal)); self.set_dirty(true); } - UIEvent::EmbedInput((ref k, ref b)) => { - if let Some(ref mut embed) = self.embedded { - let mut embed_guard = embed.status.lock().unwrap(); - if embed_guard.write_all(b).is_err() { - match embed_guard.is_active() { + UIEvent::EmbeddedInput((ref k, ref b)) => { + if let Some(ref mut embedded) = self.embedded_pty { + let mut embedded_guard = embedded.lock().unwrap(); + if embedded_guard.write_all(b).is_err() { + match embedded_guard.is_active() { Ok(WaitStatus::Exited(_, exit_code)) => { - drop(embed_guard); - let embedded = self.embedded.take(); + drop(embedded_guard); + let embedded_pty = self.embedded_pty.take(); if exit_code != 0 { context.replies.push_back(UIEvent::Notification( None, @@ -1547,9 +1539,11 @@ impl Component for Composer { melib::error::ErrorKind::External, )), )); - } else if let Some(Embedded { - status: EmbedStatus::Running(_, file), - }) = embedded + } else if let Some(EmbeddedPty { + running: true, + file, + .. + }) = embedded_pty { self.update_from_file(file, context); } @@ -1563,19 +1557,18 @@ impl Component for Composer { #[cfg(any(target_os = "linux", target_os = "android"))] Ok(WaitStatus::PtraceEvent(_, _, _)) | Ok(WaitStatus::PtraceSyscall(_)) => { - drop(embed_guard); - match self.embedded.take() { - Some(Embedded { - status: EmbedStatus::Running(e, f), - }) - | Some(Embedded { - status: EmbedStatus::Stopped(e, f), - }) => { - self.embedded = Some(Embedded { - status: EmbedStatus::Stopped(e, f), - }); - } - _ => {} + drop(embedded_guard); + if let Some(EmbeddedPty { + running: _, + terminal, + file, + }) = self.embedded_pty.take() + { + self.embedded_pty = Some(EmbeddedPty { + running: false, + terminal, + file, + }); } self.mode = ViewMode::Edit; context @@ -1585,19 +1578,18 @@ impl Component for Composer { return true; } Ok(WaitStatus::Stopped(_, _)) => { - drop(embed_guard); - match self.embedded.take() { - Some(Embedded { - status: EmbedStatus::Running(e, f), - }) - | Some(Embedded { - status: EmbedStatus::Stopped(e, f), - }) => { - self.embedded = Some(Embedded { - status: EmbedStatus::Stopped(e, f), - }); - } - _ => {} + drop(embedded_guard); + if let Some(EmbeddedPty { + running: _, + terminal, + file, + }) = self.embedded_pty.take() + { + self.embedded_pty = Some(EmbeddedPty { + running: false, + terminal, + file, + }); } self.mode = ViewMode::Edit; context @@ -1609,13 +1601,13 @@ impl Component for Composer { Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::StillAlive) => { context .replies - .push_back(UIEvent::EmbedInput((k.clone(), b.to_vec()))); - drop(embed_guard); + .push_back(UIEvent::EmbeddedInput((k.clone(), b.to_vec()))); + drop(embedded_guard); self.set_dirty(true); return true; } Ok(WaitStatus::Signaled(_, signal, _)) => { - drop(embed_guard); + drop(embedded_guard); context.replies.push_back(UIEvent::Notification( None, format!("Subprocess was killed by {} signal", signal), @@ -1624,7 +1616,7 @@ impl Component for Composer { )), )); self.initialized = false; - self.embedded = None; + self.embedded_pty = None; self.mode = ViewMode::Edit; context .replies @@ -1632,15 +1624,15 @@ impl Component for Composer { } Err(err) => { context.replies.push_back(UIEvent::Notification( - Some("Embed editor crashed.".to_string()), + Some("Embedded editor crashed.".to_string()), format!("Subprocess has exited with reason {}", &err), Some(NotificationType::Error( melib::error::ErrorKind::External, )), )); - drop(embed_guard); + drop(embedded_guard); self.initialized = false; - self.embedded = None; + self.embedded_pty = None; self.mode = ViewMode::Edit; context .replies @@ -1745,53 +1737,43 @@ impl Component for Composer { return true; } UIEvent::Input(ref key) - if self.embedded.is_some() + if self.embedded_pty.is_some() && shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) => { - self.embedded - .as_ref() - .unwrap() - .status - .lock() - .unwrap() - .wake_up(); - match self.embedded.take() { - Some(Embedded { - status: EmbedStatus::Running(e, f), - }) - | Some(Embedded { - status: EmbedStatus::Stopped(e, f), - }) => { - self.embedded = Some(Embedded { - status: EmbedStatus::Running(e, f), - }); - } - _ => {} + if let Some(EmbeddedPty { + running: _, + terminal, + file, + }) = self.embedded_pty.take() + { + terminal.lock().unwrap().wake_up(); + self.embedded_pty = Some(EmbeddedPty { + running: true, + terminal, + file, + }); } - self.mode = ViewMode::Embed; + self.mode = ViewMode::EmbeddedPty; context .replies - .push_back(UIEvent::ChangeMode(UIMode::Embed)); + .push_back(UIEvent::ChangeMode(UIMode::Embedded)); self.set_dirty(true); return true; } UIEvent::Input(Key::Ctrl('c')) - if self.embedded.is_some() - && self.embedded.as_ref().unwrap().status.is_stopped() => + if self.embedded_pty.is_some() + && self.embedded_pty.as_ref().unwrap().is_stopped() => { - match self.embedded.take() { - Some(Embedded { - status: EmbedStatus::Running(embed, file), - }) - | Some(Embedded { - status: EmbedStatus::Stopped(embed, file), - }) => { - let guard = embed.lock().unwrap(); - guard.wake_up(); - guard.terminate(); - self.update_from_file(file, context); - } - _ => {} + if let Some(EmbeddedPty { + running: _, + terminal, + file, + }) = self.embedded_pty.take() + { + let guard = terminal.lock().unwrap(); + guard.wake_up(); + guard.terminate(); + self.update_from_file(file, context); } context.replies.push_back(UIEvent::Notification( None, @@ -1857,30 +1839,31 @@ impl Component for Composer { } }; - if *account_settings!(context[self.account_hash].composing.embed) { - match crate::terminal::embed::create_pty( - self.embed_dimensions.0, - self.embed_dimensions.1, + if *account_settings!(context[self.account_hash].composing.embedded_pty) { + match crate::terminal::embedded::create_pty( + self.embedded_dimensions.0, + self.embedded_dimensions.1, [editor, f.path().display().to_string()].join(" "), ) { - Ok(embed) => { - self.embedded = Some(Embedded { - status: EmbedStatus::Running(embed, f), + Ok(terminal) => { + self.embedded_pty = Some(EmbeddedPty { + running: true, + terminal, + file: f, }); self.set_dirty(true); context .replies - .push_back(UIEvent::ChangeMode(UIMode::Embed)); - context.replies.push_back(UIEvent::Fork(ForkType::Embed( - self.embedded + .push_back(UIEvent::ChangeMode(UIMode::Embedded)); + context.replies.push_back(UIEvent::Fork(ForkType::Embedded( + self.embedded_pty .as_ref() .unwrap() - .status .lock() .unwrap() .child_pid, ))); - self.mode = ViewMode::Embed; + self.mode = ViewMode::EmbeddedPty; } Err(err) => { context.replies.push_back(UIEvent::Notification( @@ -2157,7 +2140,14 @@ impl Component for Composer { fn is_dirty(&self) -> bool { match self.mode { - ViewMode::Embed => true, + ViewMode::EmbeddedPty => { + self.dirty + || self + .embedded_pty + .as_ref() + .map(EmbeddedPty::is_dirty) + .unwrap_or(false) + } //ViewMode::EditAttachments { ref widget } => { // widget.dirty // || widget.buttons.is_dirty() @@ -2206,7 +2196,14 @@ impl Component for Composer { ViewMode::WaitingForSendResult(ref mut widget, _) => { widget.set_dirty(value); } - ViewMode::Edit | ViewMode::Embed => {} + ViewMode::Edit => {} + ViewMode::EmbeddedPty => { + if let Some(pty) = self.embedded_pty.as_ref() { + if let Ok(mut guard) = pty.try_lock() { + guard.grid.set_dirty(value); + } + } + } } //if let ViewMode::EditAttachments { ref mut widget } = self.mode { // (EditAttachmentsRefMut { diff --git a/meli/src/main.rs b/meli/src/main.rs index 47e7d1d4..02f97a48 100644 --- a/meli/src/main.rs +++ b/meli/src/main.rs @@ -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(); } diff --git a/meli/src/state.rs b/meli/src/state.rs index 38f536e2..2dfa9af2 100644 --- a/meli/src/state.rs +++ b/meli/src/state.rs @@ -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); diff --git a/meli/src/svg.rs b/meli/src/svg.rs index e7df5d8d..1c31d39e 100644 --- a/meli/src/svg.rs +++ b/meli/src/svg.rs @@ -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 { diff --git a/meli/src/terminal.rs b/meli/src/terminal.rs index 7203318d..d1f20414 100644 --- a/meli/src/terminal.rs +++ b/meli/src/terminal.rs @@ -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}; diff --git a/meli/src/terminal/embedded.rs b/meli/src/terminal/embedded.rs new file mode 100644 index 00000000..72524b05 --- /dev/null +++ b/meli/src/terminal/embedded.rs @@ -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 . + */ + +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>> { + #[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: ::try_from(height).unwrap(), + ws_col: ::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: ::try_from(height).unwrap(), + ws_col: ::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) +} diff --git a/meli/src/terminal/embed.rs b/meli/src/terminal/embedded/escape_codes.rs similarity index 62% rename from meli/src/terminal/embed.rs rename to meli/src/terminal/embedded/escape_codes.rs index 593486c5..6a5a4358 100644 --- a/meli/src/terminal/embed.rs +++ b/meli/src/terminal/embedded/escape_codes.rs @@ -19,179 +19,9 @@ * along with meli. If not, see . */ -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>> { - #[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: ::try_from(height).unwrap(), - ws_col: ::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: ::try_from(height).unwrap(), - ws_col: ::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>) { - 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 { diff --git a/meli/src/terminal/embed/grid.rs b/meli/src/terminal/embedded/terminal.rs similarity index 94% rename from meli/src/terminal/embed/grid.rs rename to meli/src/terminal/embedded/terminal.rs index 291e15d7..1aadedf6 100644 --- a/meli/src/terminal/embed/grid.rs +++ b/meli/src/terminal/embedded/terminal.rs @@ -19,6 +19,26 @@ * along with meli. If not, see . */ +//! 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>, - 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, - prev_bg_color: Option, - prev_attrs: Option, - - 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_buffer: ScreenBuffer, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CodepointBuf { + None, + TwoCodepoints(u8), + ThreeCodepoints(u8, Option), + FourCodepoints(u8, Option, Option), } #[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 { - /* - - 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>, 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), - FourCodepoints(u8, Option, Option), +/// `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>, + 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, + prev_bg_color: Option, + prev_attrs: Option, + + 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_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::::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() - } -} diff --git a/meli/src/types.rs b/meli/src/types.rs index 7aca12d8..b9d14a9e 100644 --- a/meli/src/types.rs +++ b/meli/src/types.rs @@ -98,8 +98,8 @@ impl From 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)), + EmbeddedInput((Key, Vec)), Resize, Fork(ForkType), ChangeMailbox(usize), @@ -189,8 +189,8 @@ impl From 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", } ) } diff --git a/meli/src/utilities.rs b/meli/src/utilities.rs index 50104178..e9d6e540 100644 --- a/meli/src/utilities.rs +++ b/meli/src/utilities.rs @@ -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) diff --git a/meli/src/utilities/pager.rs b/meli/src/utilities/pager.rs index 116f8b15..ea1c843f 100644 --- a/meli/src/utilities/pager.rs +++ b/meli/src/utilities/pager.rs @@ -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)>, + filtered_content: Option<(String, Result)>, text_lines: Vec, line_breaker: LineBreakText, movement: Option, @@ -181,7 +181,7 @@ impl Pager { } pub fn filter(&mut self, cmd: &str) { - let _f = |bin: &str, text: &str| -> Result { + let _f = |bin: &str, text: &str| -> Result { 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; } diff --git a/tools/Cargo.lock b/tools/Cargo.lock index 7973ffbe..2733ed28 100644 --- a/tools/Cargo.lock +++ b/tools/Cargo.lock @@ -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" diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 91fc9f19..24fdfa53 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -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 } diff --git a/tools/src/embed.rs b/tools/src/embedded.rs similarity index 76% rename from tools/src/embed.rs rename to tools/src/embedded.rs index 0385da00..340319eb 100644 --- a/tools/src/embed.rs +++ b/tools/src/embedded.rs @@ -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>), - Running(Arc>), +enum EmbeddedPty { + Stopped(Arc>), + Running(Arc>), } -impl EmbedStatus { +impl EmbeddedPty { #[inline(always)] fn is_stopped(&self) -> bool { matches!(self, Self::Stopped(_)) } } -impl std::ops::Deref for EmbedStatus { - type Target = Arc>; +impl std::ops::Deref for EmbeddedPty { + type Target = Arc>; 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, + embedded_pty: Option, id: ComponentId, dirty: bool, log_file: File, } -impl EmbedContainer { +impl EmbeddedContainer { fn new(command: String) -> Box { Box::new(Self { command, - embed: None, - embed_area: ((0, 0), (80, 20)), + embedded_pty: None, dirty: true, log_file: File::open(".embed.out").unwrap(), id: ComponentId::default(), @@ -106,48 +104,32 @@ impl EmbedContainer { } } -impl std::fmt::Display for EmbedContainer { +impl std::fmt::Display for EmbeddedContainer { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(fmt, "embed") + write!(fmt, "embedded_pty") } } -impl Component for EmbedContainer { +impl Component for EmbeddedContainer { fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) { - if let Some(ref mut embed_pty) = self.embed { - let embed_area = area; + if let Some(ref mut embedded_pty_pty) = self.embedded_pty { let theme_default = crate::conf::value(context, "theme_default"); - match embed_pty { - EmbedStatus::Running(_) => { - let mut guard = embed_pty.lock().unwrap(); - grid.clear_area(embed_area, theme_default); + match embedded_pty_pty { + EmbeddedPty::Running(_) => { + let mut guard = embedded_pty_pty.lock().unwrap(); + grid.clear_area(area, theme_default); - grid.copy_area( - guard.grid.buffer(), - embed_area, - ((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))), - ); - guard.set_terminal_size((embed_area.width(), embed_area.height())); + grid.copy_area(guard.grid.buffer(), area, guard.grid.area()); + guard.set_terminal_size((area.width(), area.height())); + guard.grid.set_dirty(false); context.dirty_areas.push_back(area); self.dirty = false; - return; } - EmbedStatus::Stopped(_) => { - let guard = embed_pty.lock().unwrap(); - - grid.copy_area( - guard.grid.buffer(), - embed_area, - ((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))), - ); + EmbeddedPty::Stopped(_) => { + let mut guard = embedded_pty_pty.lock().unwrap(); - grid.change_theme( - embed_area, - ThemeAttribute { - fg: Color::Byte(8), - ..theme_default - }, - ); + grid.copy_area(guard.grid.buffer(), area, guard.grid.buffer().area()); + grid.change_colors(area, Color::Byte(8), theme_default.bg); let stopped_message: String = format!("Process with PID {} has stopped.", guard.child_pid); let stopped_message_2: String = "-press 'e' to re-activate.".to_string(); @@ -157,19 +139,7 @@ impl Component for EmbedContainer { stopped_message.len(), std::cmp::max(stopped_message_2.len(), STOPPED_MESSAGE_3.len()), ); - let inner_area = create_box( - grid, - ( - pos_inc(area.upper_left(), (1, 0)), - pos_inc( - area.upper_left(), - ( - std::cmp::min(max_len + 5, area.width()), - std::cmp::min(5, area.height()), - ), - ), - ), - ); + let inner_area = create_box(grid, area.center_inside((max_len + 5, 5))); grid.clear_area(inner_area, theme_default); for (i, l) in [ stopped_message.as_str(), @@ -184,33 +154,34 @@ impl Component for EmbedContainer { theme_default.fg, theme_default.bg, theme_default.attrs, - ( - pos_inc((0, i), inner_area.upper_left()), - inner_area.bottom_right(), - ), - Some(get_x(inner_area.upper_left())), + inner_area.skip_rows(i), + None, ); } + context.dirty_areas.push_back(area); + guard.grid.set_dirty(false); + self.dirty = false; } } + return; } else { let theme_default = crate::conf::value(context, "theme_default"); grid.clear_area(area, theme_default); - self.embed_area = (area.upper_left(), area.bottom_right()); - match create_pty( - self.embed_area.width(), - self.embed_area.height(), - self.command.clone(), - ) { - Ok(embed) => { - //embed.lock().unwrap().set_log_file(self.log_file.take()); - self.embed = Some(EmbedStatus::Running(embed)); + match create_pty(area.width(), area.height(), self.command.clone()) { + Ok(embedded_pty) => { + //embedded_pty.lock().unwrap().set_log_file(self.log_file.take()); + self.embedded_pty = Some(EmbeddedPty::Running(embedded_pty)); self.set_dirty(true); context .replies - .push_back(UIEvent::ChangeMode(UIMode::Embed)); - context.replies.push_back(UIEvent::Fork(ForkType::Embed( - self.embed.as_ref().unwrap().lock().unwrap().child_pid, + .push_back(UIEvent::ChangeMode(UIMode::Embedded)); + context.replies.push_back(UIEvent::Fork(ForkType::Embedded( + self.embedded_pty + .as_ref() + .unwrap() + .lock() + .unwrap() + .child_pid, ))); } Err(err) => { @@ -228,11 +199,11 @@ impl Component for EmbedContainer { fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool { match event { - UIEvent::EmbedInput((Key::Ctrl('z'), _)) => { - self.embed.as_ref().unwrap().lock().unwrap().stop(); - match self.embed.take() { - Some(EmbedStatus::Running(e)) | Some(EmbedStatus::Stopped(e)) => { - self.embed = Some(EmbedStatus::Stopped(e)); + UIEvent::EmbeddedInput((Key::Ctrl('z'), _)) => { + self.embedded_pty.as_ref().unwrap().lock().unwrap().stop(); + match self.embedded_pty.take() { + Some(EmbeddedPty::Running(e)) | Some(EmbeddedPty::Stopped(e)) => { + self.embedded_pty = Some(EmbeddedPty::Stopped(e)); } _ => {} } @@ -241,18 +212,18 @@ impl Component for EmbedContainer { .push_back(UIEvent::ChangeMode(UIMode::Normal)); self.set_dirty(true); } - UIEvent::EmbedInput((ref k, ref b)) => { + UIEvent::EmbeddedInput((ref k, ref b)) => { let _ = self .log_file .write_all(format!("{} bytes {:?}", k, b).as_bytes()); let _ = self.log_file.flush(); - if let Some(ref mut embed) = self.embed { - let mut embed_guard = embed.lock().unwrap(); - if embed_guard.write_all(b).is_err() { - match embed_guard.is_active() { + if let Some(ref mut embedded_pty) = self.embedded_pty { + let mut embedded_pty_guard = embedded_pty.lock().unwrap(); + if embedded_pty_guard.write_all(b).is_err() { + match embedded_pty_guard.is_active() { Ok(WaitStatus::Exited(_, exit_code)) => { - drop(embed_guard); - _ = self.embed.take(); + drop(embedded_pty_guard); + _ = self.embedded_pty.take(); if exit_code != 0 { context.replies.push_back(UIEvent::Notification( None, @@ -273,11 +244,11 @@ impl Component for EmbedContainer { #[cfg(any(target_os = "linux", target_os = "android"))] Ok(WaitStatus::PtraceEvent(_, _, _)) | Ok(WaitStatus::PtraceSyscall(_)) => { - drop(embed_guard); - match self.embed.take() { - Some(EmbedStatus::Running(e)) - | Some(EmbedStatus::Stopped(e)) => { - self.embed = Some(EmbedStatus::Stopped(e)); + drop(embedded_pty_guard); + match self.embedded_pty.take() { + Some(EmbeddedPty::Running(e)) + | Some(EmbeddedPty::Stopped(e)) => { + self.embedded_pty = Some(EmbeddedPty::Stopped(e)); } _ => {} } @@ -288,11 +259,11 @@ impl Component for EmbedContainer { return true; } Ok(WaitStatus::Stopped(_, _)) => { - drop(embed_guard); - match self.embed.take() { - Some(EmbedStatus::Running(e)) - | Some(EmbedStatus::Stopped(e)) => { - self.embed = Some(EmbedStatus::Stopped(e)); + drop(embedded_pty_guard); + match self.embedded_pty.take() { + Some(EmbeddedPty::Running(e)) + | Some(EmbeddedPty::Stopped(e)) => { + self.embedded_pty = Some(EmbeddedPty::Stopped(e)); } _ => {} } @@ -305,11 +276,11 @@ impl Component for EmbedContainer { Ok(WaitStatus::Continued(_)) | Ok(WaitStatus::StillAlive) => { context .replies - .push_back(UIEvent::EmbedInput((k.clone(), b.to_vec()))); + .push_back(UIEvent::EmbeddedInput((k.clone(), b.to_vec()))); return true; } Ok(WaitStatus::Signaled(_, signal, _)) => { - drop(embed_guard); + drop(embedded_pty_guard); context.replies.push_back(UIEvent::Notification( None, format!("Subprocess was killed by {} signal", signal), @@ -317,21 +288,21 @@ impl Component for EmbedContainer { melib::error::ErrorKind::External, )), )); - self.embed = None; + self.embedded_pty = None; context .replies .push_back(UIEvent::ChangeMode(UIMode::Normal)); } Err(err) => { context.replies.push_back(UIEvent::Notification( - Some("Embed editor crashed.".to_string()), + Some("Embedded editor crashed.".to_string()), format!("Subprocess has exited with reason {}", &err), Some(NotificationType::Error( melib::error::ErrorKind::External, )), )); - drop(embed_guard); - self.embed = None; + drop(embedded_pty_guard); + self.embedded_pty = None; context .replies .push_back(UIEvent::ChangeMode(UIMode::Normal)); @@ -345,26 +316,33 @@ impl Component for EmbedContainer { UIEvent::Resize => { self.set_dirty(true); } - UIEvent::Input(Key::Char('e')) if self.embed.is_some() => { - self.embed.as_ref().unwrap().lock().unwrap().wake_up(); - match self.embed.take() { - Some(EmbedStatus::Running(e)) | Some(EmbedStatus::Stopped(e)) => { - self.embed = Some(EmbedStatus::Running(e)); + UIEvent::Input(Key::Char('e')) if self.embedded_pty.is_some() => { + self.embedded_pty + .as_ref() + .unwrap() + .lock() + .unwrap() + .wake_up(); + match self.embedded_pty.take() { + Some(EmbeddedPty::Running(e)) | Some(EmbeddedPty::Stopped(e)) => { + self.embedded_pty = Some(EmbeddedPty::Running(e)); } _ => {} } context .replies - .push_back(UIEvent::ChangeMode(UIMode::Embed)); + .push_back(UIEvent::ChangeMode(UIMode::Embedded)); self.set_dirty(true); return true; } UIEvent::Input(Key::Ctrl('c')) - if self.embed.is_some() && self.embed.as_ref().unwrap().is_stopped() => + if self.embedded_pty.is_some() + && self.embedded_pty.as_ref().unwrap().is_stopped() => { - match self.embed.take() { - Some(EmbedStatus::Running(embed)) | Some(EmbedStatus::Stopped(embed)) => { - let guard = embed.lock().unwrap(); + match self.embedded_pty.take() { + Some(EmbeddedPty::Running(embedded_pty)) + | Some(EmbeddedPty::Stopped(embedded_pty)) => { + let guard = embedded_pty.lock().unwrap(); guard.wake_up(); guard.terminate(); } @@ -411,12 +389,12 @@ fn main() -> std::io::Result<()> { let signals = &[ /* Catch SIGWINCH to handle terminal resizing */ signal_hook::consts::SIGWINCH, - /* Catch SIGCHLD to handle embed applications status change */ + /* Catch SIGCHLD to handle embedded applications status change */ signal_hook::consts::SIGCHLD, ]; let quit_key: Key = Key::Char('q'); - let window = EmbedContainer::new(command); + let window = EmbeddedContainer::new(command); let signal_recvr = notify(signals, sender.clone())?; let mut state = meli::State::new(Some(Default::default()), sender, receiver.clone()).unwrap(); let status_bar = Box::new(StatusBar::new(&state.context, window)); @@ -444,7 +422,7 @@ fn main() -> std::io::Result<()> { } } match r.unwrap() { - ThreadEvent::Input((Key::Ctrl('z'), _)) if state.mode != UIMode::Embed => { + ThreadEvent::Input((Key::Ctrl('z'), _)) if state.mode != UIMode::Embedded => { state.switch_to_main_screen(); //_thread_handler.join().expect("Couldn't join on the associated thread"); let self_pid = nix::unistd::Pid::this(); @@ -460,8 +438,8 @@ fn main() -> std::io::Result<()> { state.update_size(); state.render(); state.redraw(); - if state.mode == UIMode::Embed { - state.rcv_event(UIEvent::EmbedInput(raw_input)); + if state.mode == UIMode::Embedded { + state.rcv_event(UIEvent::EmbeddedInput(raw_input)); state.redraw(); } }, @@ -508,8 +486,8 @@ fn main() -> std::io::Result<()> { }, } }, - UIMode::Embed => { - state.rcv_event(UIEvent::EmbedInput((k,r))); + UIMode::Embedded => { + state.rcv_event(UIEvent::EmbeddedInput((k,r))); state.redraw(); }, UIMode::Fork => { @@ -556,7 +534,7 @@ fn main() -> std::io::Result<()> { } }, signal_hook::consts::SIGCHLD => { - state.rcv_event(UIEvent::EmbedInput((Key::Null, vec![0]))); + state.rcv_event(UIEvent::EmbeddedInput((Key::Null, vec![0]))); state.redraw(); }